Cuckoo filter (Fan, Andersen, Kaminsky, Mitzenmacher 2014) — probabilistic membership с поддержкой DELETE, в отличие от Bloom. Hash table из buckets с короткими fingerprints (8-12 bits); каждый element имеет 2 candidate bucket; lookup читает 1-2 cache lines (cache-friendly vs Bloom k random reads). Insert при коллизии — partial-key cuckoo eviction до MAX_KICKS. Сравнение vs Bloom: для FPR ≤3% Cuckoo меньше по памяти, поддерживает delete, лучше cache locality, но может FAIL при load >95% и нет dynamic resize. Production: RedisBloom CF.*, Snowflake micro-partition pruning, Apache Kudu, CDN edge cache invalidation, TiKV. Сценарии: insert-happy, insert с kick chain, lookup hit/miss, delete (главное преимущество), сравнение vs Bloom, real production uses.
Bloom filter не поддерживает delete. Если вы попробуете «убрать» элемент, обнулив его k bits, то сломаете все другие keys, которые делят эти bits — нарушите святое правило фильтра: no false negatives. Counting Bloom (4-bit счётчики вместо одиночных bits) решает проблему за 4x памяти — дорого.
А workload часто требует именно удаления: CDN-edge выкидывает items по TTL, allow/deny lists меняются, rolling membership в стриминговых данных. Для таких задач нужен фильтр, который умеет всё — add / exists / delete — и при этом не жирнее Bloom.
Cuckoo filter (Fan, Andersen, Kaminsky, Mitzenmacher, CoNEXT 2014) решает три задачи сразу:
Цена — сложнее реализация, insert может fail при load >95%, и нет dynamic resize.
Cuckoo filter = hash table из buckets, каждый bucket держит несколько коротких fingerprints (хэшей элемента, 8-12 bits). Каждый element имеет два candidate-bucket'а. Lookup проверяет оба. Insert при конфликте «выселяет» существующий fingerprint в его альтернативный bucket — отсюда «cuckoo» (как кукушка выталкивает яйца из чужого гнезда).
Ключевой трюк — partial-key cuckoo hashing: альтернативный bucket вычисляется как i2 = i1 XOR h(fingerprint). Это значит, что мы можем «переключаться» между bucket'ами зная только fingerprint, без оригинального key. В классическом cuckoo hashing нужен сам key для recompute — здесь хватает fingerprint, что и даёт фиксированный размер storage.
Insert(x):
fp = fp(x)
i1 = h(x) mod m
i2 = i1 XOR h(fp) mod m
if bucket[i1] свободен: insert; return
if bucket[i2] свободен: insert; return
# оба заняты → cuckoo eviction
for kick in 1..500:
swap fp с random slot в выбранном bucket
bucket = bucket XOR h(evicted_fp)
if bucket свободен: insert; return
return FAIL # filter «полный», нужен rebuild
Lookup(x): fp в bucket[i1] OR fp в bucket[i2]
Delete(x): убрать одно вхождение fp в bucket[i1] или bucket[i2]
На диаграмме — три слоя: Client → App (RAM, query engine) → CF (cuckoo filter) → Buckets. Cuckoo filter сидит в RAM рядом с приложением: типичная сцена — Snowflake-style query coordinator, который перед чтением 5MB micro-partition спрашивает CF «есть ли тут id=X?» и пропускает партиции, где CF говорит «definitely no».
Bucket-слой показан тремя нодами: bucket[i1] (primary), bucket[i2] (alternate), bucket[alt] (куда улетит выселенный fingerprint в сценарии с kick chain). В реальности m таких bucket'ов десятки тысяч и более — это просто визуализация двух candidate-позиций для одного элемента.
Edges: CF.ADD / EXISTS / DEL от клиента, потом fingerprint и индексы внутрь CF, дальше к buckets — h(x) mod m для primary, i1 XOR h(fp) для alternate, и kick chain для каскадного выселения.
Самый частый путь: один из двух candidate bucket'ов имеет свободный slot, fingerprint туда записывается, готово. Одна cache-line write, ~100ns. Cuckoo filter старается жить именно здесь — пока load factor ≤70%, kicks почти не случаются.
Когда оба bucket'а full, начинается «cuckoo dance»: выбираем random bucket, выселяем оттуда random fingerprint, кладём наш на его место. Выселенный fingerprint летит в свой alternate (i XOR h(fp)), и если там тоже full — выселяет кого-то ещё. Каскад продолжается до MAX_KICKS (типично 500). Это amortized O(1), но worst-case может стать миллисекундами при load >95% — и в худшем случае insert fail.
Read fingerprint, вычисли i1 и i2, прочитай bucket[i1] — match, return TRUE. Одна cache line, ~50ns. Это и есть главное преимущество в latency: Bloom читает k=7 random bits, разбросанных по всему bit-array, что выливается в 7 cache-line misses под нагрузкой.
Если в i1 не нашли — обязательно проверяем i2 (partial-key cuckoo гарантирует, что элемент мог быть выселен в alternate). Обе пусты → return FALSE. Гарантия: если CF говорит «нет», то нет (no false negatives). Если говорит «да» — может быть false positive (~3% при b=4, f=8).
Главный сценарий «зачем мы вообще это взяли вместо Bloom». Найдём fingerprint в одном из двух bucket'ов → обнулим slot → готово. Никакой коррупции других keys. Bloom такое сделать не может — потому что bits в Bloom share между keys, и unset любого bit испортит все keys, которые этот bit устанавливали.
Важный нюанс: удалять можно только если элемент действительно был вставлен. Если вы попробуете delete элемента, которого не было, и его fingerprint случайно совпадёт с чужим (FP probability) — вы удалите чужой fingerprint. Поэтому delete без предварительного EXISTS — рискованно.
Сравнительная таблица в одном сценарии: где Cuckoo, где Bloom. Память почти одинакова при ε=1%, но CF выигрывает при ε≤0.1% и проигрывает при ε>3%. Lookup latency — CF лучше всегда (cache locality). Insert — Bloom никогда не fails, CF может. Delete — только CF. Resize — никто.
Production-показ: RedisBloom (CF.RESERVE / ADD / EXISTS / DEL), Snowflake (micro-partition pruning, сейчас уже Xor filters в новых версиях), Apache Kudu (column predicate filtering), CDN edge (TTL-based eviction), TiKV (transactional metadata), Meta hot key cache (Xor filter, 2019).
ADR-001 — Cuckoo filter вместо Bloom: deletion + cache locality.
Другие важные trade-offs (без отдельных ADR):
CF.RESERVE, CF.ADD, CF.ADDNX, CF.EXISTS, CF.DEL, CF.COUNT. De facto «as-a-service» Cuckoo filter, используется в продакшене многими.CF.EXISTS перед CF.DEL, либо отдельный source of truth (БД).CF.ADDNX (add if not exists) или предварительный EXISTS.Не берите Cuckoo, если:
Концепты-предшественники:
Foundational papers:
Книга:
Production blog posts:
Reference implementations: