ふくふくHukuhuku LLC
EP.05Algorithms 9分公開: 更新:

Bloom Filter:Cloudflare のセキュリティと Bitcoin SPV を支える確率的フィルタ

「この URL は犯罪サイトか?」を 117KB のメモリで 99% 正答。Cloudflare・Bitcoin・Chrome の Safe Browsing で動く Bloom Filter を Python で実装。

#Bloom Filter#確率的アルゴリズム#Cloudflare#Bitcoin
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
CO📔 Google Colab で開く(上から順にセルを実行)
シェア

Cloudflare が「悪性 URL データベース 100 万件」を毎リクエスト全件確認するのは現実的ではない。Bloom Filter という確率的データ構造で、117KB のメモリで 99% を即時判定できる。「間違うけど、間違っても困らない方向に間違う」絶妙なアルゴリズム。

1. これで何が動いているか

  • Cloudflare の悪性 URL ブロックリスト判定
  • Bitcoin SPV (軽量ノード) の Merkle ブロック判定
  • Google Chrome の Safe Browsing
  • Cassandra / HBase / RocksDB の SSTable 前段フィルタ
  • Akamai / CDN のキャッシュ判定
  • スパムフィルタ の既知スパム URL 判定

2. 仕組みのざっくり

  • 長さ m の bit 配列 (初期は全 0)
  • k 個の独立ハッシュ関数
  • 追加: 要素を k 個のハッシュにかけ、対応する k 個の bit を 1 に
  • 判定: 要素を k 個のハッシュにかけ、全ての bit が 1 か チェック。1 つでも 0 なら 絶対に未追加、全部 1 なら 追加されている可能性あり (偽陽性あり)
「No は確実、Yes は確率的」

Bloom Filter の特性: 偽陰性 (No なのに実は含む) は 絶対に発生しない。偽陽性 (Yes なのに実は含まない) は 設計したパラメータ通りの確率で発生。これが「キャッシュ前段で No を即返す」のに最適。

3. Python 実装 (動作確認済)

10 万件・偽陽性率 1% の Bloom Filter
Python
import hashlibimport math
class BloomFilter:    def __init__(self, n=100_000, p=0.01):        '''n = 想定追加件数、p = 偽陽性率'''        self.m = int(-n * math.log(p) / (math.log(2) ** 2))        self.k = int(self.m / n * math.log(2))        self.bits = bytearray(math.ceil(self.m / 8))
    def _hashes(self, item):        # 2 つの基底ハッシュから k 個を合成 (double hashing)        b = str(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) % self.m for i in range(self.k)]
    def add(self, item):        for idx in self._hashes(item):            self.bits[idx // 8] |= (1 << (idx % 8))
    def __contains__(self, item):        return all(self.bits[idx // 8] & (1 << (idx % 8)) for idx in self._hashes(item))
# テストbf = BloomFilter(n=100_000, p=0.01)for i in range(100_000):    bf.add(f'item_{i}')
# 偽陽性率の確認: 100,000 件の未登録アイテムで判定fp = sum(1 for i in range(100_000, 200_000) if f'item_{i}' in bf)print(f'メモリ: {bf.m / 8 / 1024:.1f} KB ({bf.m:,} bits)')print(f'ハッシュ数 k: {bf.k}')print(f'偽陽性率: {fp / 100_000 * 100:.2f}% (target: 1.00%)')
# 実機実行結果:# メモリ: 117.0 KB (958,505 bits)# ハッシュ数 k: 6# 偽陽性率: 1.01% (target: 1.00%)

4. メモリ・偽陽性率の設計

想定 np (偽陽性率)m (bits)メモリk (ハッシュ数)
1 万10%47,9365.9 KB3
1 万1%95,85111.7 KB6
10 万1%958,505117 KB6
100 万1%9,585,0581.14 MB6
100 万0.1%14,377,5871.71 MB9
10 億1%9,585,058,7321.14 GB6

5. Counting Bloom Filter (削除可能版)

削除可能な拡張版
Python
class CountingBloomFilter:    def __init__(self, n=100_000, p=0.01):        self.m = int(-n * math.log(p) / (math.log(2) ** 2))        self.k = int(self.m / n * math.log(2))        self.counts = [0] * self.m  # bit ではなく counter
    def _hashes(self, item):        b = str(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) % self.m for i in range(self.k)]
    def add(self, item):        for idx in self._hashes(item):            self.counts[idx] += 1
    def remove(self, item):        # 削除可能だが「未追加要素を remove するとカウンタが負に」する注意        for idx in self._hashes(item):            if self.counts[idx] > 0:                self.counts[idx] -= 1
    def __contains__(self, item):        return all(self.counts[idx] > 0 for idx in self._hashes(item))

6. 実用ライブラリ

pybloom-live (本番運用向け)
Python
# pip install pybloom-livefrom pybloom_live import BloomFilter, ScalableBloomFilter
# 固定容量bf = BloomFilter(capacity=100_000, error_rate=0.01)for i in range(100_000):    bf.add(f'item_{i}')
# スケーラブル (容量超過時に自動拡張)sbf = ScalableBloomFilter(initial_capacity=10_000, error_rate=0.01)for i in range(1_000_000):    sbf.add(f'item_{i}')

7. 関連ツール

  • RedisBloom: Redis の BF.* コマンド
  • Cassandra / HBase: 内部で SSTable に Bloom 標準装備
  • RocksDB: 同上、無効化も可能
  • Apache DataSketches: Bloom + HLL + CMS 統合

8. 次の話

EP.06 では MinHash + LSH を扱います。「1 億件の文書から似た文書を発見」を高速にする確率的アルゴリズム。剽窃検査・レコメンドの実装。

よくある質問

Bloom Filter って何ができる?
**「ある要素が集合に含まれるかどうかを判定」** に特化したデータ構造。**「絶対に含まれない (No)」は確実、「含まれる (Yes)」は誤りあり (偽陽性)**。要素を取り出す・削除はできない。「**No** なら本物の DB を見ない、**Yes** なら本物の DB で確認する」というキャッシュ前段で力を発揮。
なぜ偽陽性は OK?
**「Yes ならフルの DB を見るので最終的には正しい」**。Bloom が出した Yes が偽陽性でも、本物の DB で No が確認できる。**No だけ確実なので、「DB アクセスを避ける」最適化として強力**。
削除できないって本当?
**標準の Bloom Filter は削除不可** (ビットを下げると他の要素も誤って消える)。**Counting Bloom Filter** ならビットの代わりにカウンタを持ち、削除も可能 (ただしメモリは数倍)。
本番で使われている例は?
**Cloudflare**: 既知の悪性 URL リストを Bloom Filter で持ち、99% のリクエストは即判定。**Bitcoin SPV (軽量ノード)**: 自分のトランザクションを含む可能性のあるブロックを判定。**Google Chrome の Safe Browsing**: 既知のフィッシング URL をチェック。**Cassandra / Bigtable**: SSTable に該当キーがあるかの前段フィルタ。
誤陽性率 1% は実用的?
**用途次第**。「キャッシュ前段」「DB アクセス削減」なら 1% で十分 (1% の偽陽性は本物 DB が捌くだけ)。「セキュリティの判定」「料金徴収」など正確性必須なら、Bloom 単独はダメ、Bloom + 正確な DB の二段構え。
メモリと精度の関係
**n 件・偽陽性率 p の最適サイズ: m = -n × ln(p) / (ln 2)²、ハッシュ数 k = m/n × ln 2**。例: n=10万、p=1% なら m=959,000 bits ≈ 117KB、k=7。**追加が増えるほど偽陽性率が上がる**ので、容量設計は事前に必要。
シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp