6.3 Bloom Filter 與空間節省技巧#
概念說明#
Bloom Filter(1970)是「有假陽性、無假陰性」的成員測試結構:它可能說「在」(其實不在),但絕不會說「不在」(其實在)。結構是 m 個位元的位元陣列 + k 個雜湊函數:
- 插入 x:對 k 個雜湊值 h₁(x)…h_k(x) 的位置都設 1。
- 查詢 x:k 個位置全為 1 → 「可能在」;任一為 0 → 「一定不在」。
磁碟上的殺手級應用:LSM 樹的每個 SSTable 附一個 Bloom Filter(常駐記憶體)。查詢一個鍵時,先問各層的 Bloom Filter——「一定不在」的 SSTable 完全不必讀磁碟。以 1% 假陽性率,每鍵只需約 9.6 bits,一百萬鍵的索引約 1.2MB 記憶體,卻能省下數百萬次無謂的磁碟讀取。
誤判率公式:p ≈ (1 − e^{−kn/m})^k,最佳 k = (m/n) ln 2。位元/鍵為 10 時 p ≈ 0.8%,為 15 時 p ≈ 0.03%。
同族的空間節省技巧:
- 計數型 Bloom Filter(CBF):每槽存 4-bit 計數器,支援刪除。
- Cuckoo Filter:支援刪除且空間更省,誤判 3% 時約每鍵 4~6 bits。
- 壓縮稀疏列(CSR)/ 差分編碼:SSTable 的索引鍵有序,存「前綴差值」可壓縮 5~10 倍。
Python 範例#
import math
import hashlib # 避免外部依賴,改用內建雜湊
def h_i(key, i, m):
return int(hashlib.md5(f"{i}:{key}".encode()).hexdigest(), 16) % m
class BloomFilter:
def __init__(self, n, p=0.01):
m = math.ceil(-n * math.log(p) / (math.log(2) ** 2))
k = max(1, round((m / n) * math.log(2)))
self.m, self.k = m, k
self.bits = [0] * m
def add(self, key):
for i in range(self.k):
self.bits[h_i(key, i, self.m)] = 1
def may_contain(self, key):
return all(self.bits[h_i(key, i, self.m)] for i in range(self.k))
n = 10000
bf = BloomFilter(n, p=0.01)
print(f"m={bf.m} bits({bf.m/n:.1f} bits/鍵), k={bf.k}")
for i in range(n):
bf.add(f"key{i}")
false_pos = sum(bf.may_contain(f"ghost{i}") for i in range(n))
print(f"10000 個不存在的鍵 → 誤判 {false_pos} 個({false_pos/n:.2%})")
print("key123(存在):", bf.may_contain("key123"))
# 應用:LSM 的 SSTable 探測,模擬省下的 I/O
class FakeSSTable:
def __init__(self, keys):
self.keys = set(keys)
self.bf = BloomFilter(len(keys), p=0.01)
for k in keys: self.bf.add(k)
self.disk_reads = 0
def probe(self, k):
if not self.bf.may_contain(k):
return None # 免讀磁碟
self.disk_reads += 1
return k in self.keys
tables = [FakeSSTable([f"key{i}" for i in range(lo, lo+1000)])
for lo in range(0, 10000, 1000)]
misses = 0
for i in range(10000, 20000): # 全部不存在的查詢
for t in tables:
if t.probe(f"key{i}"): misses += 1
reads = sum(t.disk_reads for t in tables)
print(f"10000 次不存在查詢 → 實際磁碟探測 {reads} 次(省下 {10000-reads} 次)")
複雜度與 I/O 成本分析#
- 插入/查詢:O(k) CPU,0 次磁碟 I/O(位元陣列常駐記憶體)。
- 空間:O(n log(1/p)) bits;1% 誤判約 9.6 bits/鍵。
- LSM 讀取放大:無 Bloom 時每查詢需讀各層的索引頁(數次 I/O);有 Bloom 後,不存在鍵的查詢幾乎 0 I/O——寫多讀少且常有「查無此鍵」的負載(如 UUID 主鍵)受益最大。
- 注意:Bloom Filter 不能刪除(會誤傷其他鍵);需要刪除用 CBF 或 Cuckoo Filter。
練習#
- 用 p=0.05 與 p=0.001 分別建 10000 鍵的 filter,比較 bits/鍵與實測誤判率。
- 把範例的 SSTable 探測接入 6.2 節的 LSM 樹,重新測量讀取 I/O。
- 說明為何 Bloom Filter 的「假陽性」在 LSM 場景是安全的,而「假陰性」是致命的。