Cassandra が秒間 1 万 write/node を捌く / RocksDB が SSD の限界に近い書込み性能を出す / BigTable が Google の規模を支える ── これらすべての中核に LSM Tree (Log-Structured Merge Tree) がある。「ランダム書込みを順次書込みに変換する」 という発想が、ストレージ性能の壁を打ち破った。
1. これで何が動いているか
- Apache Cassandra / ScyllaDB
- RocksDB / LevelDB (Facebook / Google)
- Google Bigtable / HBase
- DynamoDB / Riak
- InfluxDB / TimescaleDB / VictoriaMetrics (時系列 DB)
- TiKV (TiDB) / CockroachDB
2. 仕組みのざっくり
- 1書込み: WAL (Write-Ahead Log) に append + MemTable (in-memory ソート済) に追加
- 2MemTable がフル: 新しい MemTable に切替、古い MemTable を SSTable (Disk) として書き出し
- 3SSTable: Immutable、ソート済、Bloom Filter + Sparse Index 付
- 4Compaction: 複数 SSTable をマージ、削除済 (tombstone) を物理削除
- 5読込み: MemTable → 新しい SSTable → 古い SSTable の順でキー検索 (Bloom Filter で skip)
3. Python 実装 (簡略版・動作確認済)
MemTable + SSTable の最小実装
Python
class LSMTree: def __init__(self, mem_limit=4): self.memtable = {} # in-memory dict (本物は Skip List) self.sstables = [] # [{k: v}, ...] (新しいものが末尾) self.mem_limit = mem_limit
def put(self, k, v): '''書込み: MemTable に追加、フルなら flush''' self.memtable[k] = v if len(self.memtable) >= self.mem_limit: self._flush()
def _flush(self): '''MemTable → SSTable''' sstable = dict(sorted(self.memtable.items())) self.sstables.append(sstable) self.memtable = {}
def get(self, k): '''読込み: MemTable → 新しい SSTable → 古い SSTable の順''' if k in self.memtable: return self.memtable[k] for sst in reversed(self.sstables): if k in sst: return sst[k] return None
def delete(self, k): '''削除は tombstone を書く (物理削除は Compaction 時)''' self.put(k, None) # None = tombstone
def compact(self): '''複数 SSTable をマージ + tombstone を物理削除''' merged = {} for sst in self.sstables: for k, v in sst.items(): if v is None: merged.pop(k, None) # tombstone は削除 else: merged[k] = v self.sstables = [merged]
# 動作確認lsm = LSMTree(mem_limit=4)for i in range(10): lsm.put(f'k{i}', f'v{i}')
print(f'SSTable 数: {len(lsm.sstables)}')print(f'MemTable サイズ: {len(lsm.memtable)}')print(f'k3 = {lsm.get("k3")}')
# 削除lsm.delete('k3')print(f'\n削除後 k3 = {lsm.get("k3")}')
# Compactionlsm.compact()print(f'Compaction 後 SSTable 数: {len(lsm.sstables)}')print(f'Compaction 後 k3 = {lsm.get("k3")}')4. Bloom Filter で読込み高速化
SSTable に Bloom Filter を付けて Read Amp を削減
Python
from typing import Optionalimport hashlib, math
class BloomedSSTable: def __init__(self, data: dict, p=0.01): self.data = data self.bloom = self._build_bloom(list(data.keys()), p)
def _build_bloom(self, keys, p): n = max(len(keys), 1) m = int(-n * math.log(p) / (math.log(2) ** 2)) bits = bytearray(math.ceil(m / 8)) k_h = max(int(m / n * math.log(2)), 1)
def idx(item): b = item.encode() h1 = int.from_bytes(hashlib.sha256(b).digest()[:8], 'big') h2 = int.from_bytes(hashlib.blake2b(b, digest_size=8).digest(), 'big') return [(h1 + i * h2) % m for i in range(k_h)]
for k in keys: for i in idx(k): bits[i // 8] |= (1 << (i % 8))
self._idx_func = idx self._bits = bits return None
def __contains__(self, k): '''Bloom 判定: No なら確実、Yes なら次に data を見る''' return all(self._bits[i//8] & (1 << (i%8)) for i in self._idx_func(k))
def get(self, k) -> Optional[str]: if k not in self: return None # Bloom が No なら data を見ない return self.data.get(k)5. Compaction Strategy 比較
| 戦略 | 代表 DB | Read Amp | Write Amp | Space Amp |
|---|---|---|---|---|
| Size-Tiered | Cassandra (default) | 中 | 低 | 高 (~2x) |
| Leveled | RocksDB / LevelDB | 低 | 高 (~10x) | 低 |
| Time-Window | Cassandra (時系列) | 低 | 低 | 中 |
| Universal | RocksDB | 中 | 低 | 中 |
6. メリットとデメリット
- メリット: 書込み性能が圧倒的 (B-Tree の 10-100x)
- メリット: 順次 I/O 主体で SSD / HDD ともに最適化
- メリット: Compaction で空間使用が最適化
- デメリット: 読込みが複雑 (Bloom Filter で軽減)
- デメリット: Write Amplification が大きい (~10-30x)
- デメリット: 範囲スキャンは複数 SSTable をマージ読込
7. 関連ツール
- RocksDB: Facebook 製の LSM、最も広く使われる組込み DB
- LevelDB: Google 製、RocksDB の元
- python-rocksdb: RocksDB の Python バインディング
- lsm.py: 学習用の Python LSM 実装
8. 次の話
EP.14 では MCTS (Monte Carlo Tree Search) を扱います。AlphaGo / AlphaZero / 将棋 AI が世界チャンピオンを倒した探索アルゴリズム ── ランダムプレイアウトと UCB の組合せの美しさ。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。