Probabilistic Data Structures Overview — concept page covering five families of sketches (Bloom, Cuckoo, HyperLogLog, Count-Min Sketch, t-digest) with comparison vs exact baselines (HashSet, HashMap, sorted array). Six animated scenarios: family overview, Bloom dedup, HLL count distinct, CMS heavy hitters, t-digest percentile, and trade-offs comparison. One ADR on probabilistic vs exact decision criteria.
Когда поток событий мерится миллиардами в день, а вопрос про эти события сводится к «сколько уникальных», «видели ли X», «как часто встречается» или «какой p99» — точные структуры (HashSet, HashMap, sorted array) перестают помещаться в RAM ни одной разумной ноды.
Дальше начинаются грязные паллиативы: шардировать RAM на сто нод, переливать в Redis cluster, поднимать ClickHouse на все запросы. Дорого, медленно, оперативно нагружено.
Probabilistic data structures (PDS, sketches) меняют точность на память на порядки: десятки KB на структуру вместо GB, с известной теоретической ошибкой (FPR 1%, ε relative error 0.81%). Дополнительно — почти все sketches streaming (single-pass, поток можно не перечитывать) и mergeable (склеить два sketch с разных нод в один союз).
Главный сигнал, что нужен sketch: «approximate is fine» + «размер растёт быстрее, чем RAM». Если оба true — берите sketch и экономьте порядок-два памяти и инфраструктуры.
Sketch — это lossy compression для конкретного типа запроса. Ты не хранишь сырьё; ты хранишь ответ на «сколько уникальных», «видели ли», «как часто», «какой percentile» с известной погрешностью.
Выбор sketch начинается не с данных, а с вопроса. «Уникальные» → HyperLogLog. «Видели ли» → Bloom. «Сколько раз» / «top-K» → Count-Min Sketch. «Percentile» → t-digest. Структуру не подбирают под dataset — её подбирают под query.
Ключевые свойства, которые делают семейство практичным:
merge(A, B) даёт sketch союза множеств без re-aggregation сырья. Это критично для distributed compute: shard локально, merge на координаторе.На канвасе — family overview: один клиент (event stream / query) подключён ко всем пяти каноническим sketches (Bloom, Cuckoo, HyperLogLog, Count-Min Sketch, t-digest) в одной группе и к трём exact baselines (HashSet, HashMap counter, sorted array) в другой. Цель раскладки — показать вопрос-в-вопрос: для каждого sketch внизу лежит exact-эквивалент, который пришлось бы заменить.
Edges подписаны интерфейсами: add / contains? для Bloom/Cuckoo, PFADD для HLL (Redis-style), inc(key) для CMS, add(value) для t-digest. Это API на стороне клиента — то, что разработчик реально пишет в job-е.
На ноде bloom висит ADR-001 — развёрнутое решение «Probabilistic vs Exact: когда менять точность на память». Это якорь страницы: переключение между семью сценариями player-а наполняет ADR конкретикой.
Family overview одной картинкой. Каждая структура отвечает на свой тип вопроса — пять разных вопросов, пять разных sketches. Закрепляем правило: выбор начинается с вопроса, не с данных. И финальный штрих — все пять mergeable, поэтому в distributed compute (Flink, Spark, ClickHouse coordinator) каждый shard строит локальный sketch, а координатор делает дешёвый merge без пересылки raw events.
Bloom перед expensive lookup — самый канонический use case. Naive HashSet на 1B URLs × 32 байта = 32 GB, нерентабельно. Bloom при m = 10n, k = 7 укладывается в ~1.2 GB на 1B URLs с FPR 1%. Алгоритм lookup: 7 hash + 7 bit reads ~100ns. Если хоть один бит = 0 → «точно нет» (no false negative — гарантия). Если все 1 → «вероятно есть, проверь диск». Win — 99% reads отсеяны за 100ns probe вместо 150μs disk seek. Используется в Cassandra/RocksDB Bloom per SSTable, Chrome Safe Browsing, BigTable, Akamai cache. Если нужен delete (TTL/eviction) → Cuckoo filter: та же FPR, плюс deletion, ~25% меньше памяти на низких FPR.
HyperLogLog для cardinality — count distinct в постоянных 12 KB. Naive HashSet на 1B unique users = 16 GB на metric per day per dim — нерентабельно даже на жирных инстансах. HLL: hash(x) → первые p бит выбирают bucket, в остатке считаем leading zeros (rho); registers[bucket] = max(registers[bucket], rho). Update O(1). Estimate: α_m × m² × harmonic_mean(2^registers), O(m) на query. Размеры: m = 4096 → 3 KB → ε ≈ 1.6%; m = 16384 → 12 KB → ε ≈ 0.81%. Главное свойство: 100 событий, 1M, 1B — HLL остаётся constant 12 KB. Mergeable через element-wise max(A.registers, B.registers) → distributed rollup без raw events. Production: Redis PFCOUNT, BigQuery APPROX_COUNT_DISTINCT, ClickHouse uniqHLL12, Snowflake/Redshift, Druid.
Count-Min Sketch для top-K и frequency. Hot keys в кэше, top trending hashtags, DDoS top IPs. Naive HashMap{key → count} на 1B distinct keys × 32 байта = десятки GB. CMS: 2D матрица d rows × w columns; обычно d = 4, w = 2¹⁷ → ~2 MB. Update: для каждой row i инкрементируем matrix[i][hash_i(key) % w]. Query: count(key) = min(matrix[i][hash_i(key) % w]). CMS только overestimate — никогда не underestimate из-за collisions; для honest top-K это терпимо. Trick для top-K: CMS + min-heap размером K хранит текущие лидеры без точного хранения всех counters. Production: Cloudflare DDoS top-K queries, hot key detection в Redis cluster, trending hashtags. Современные варианты — HeavyKeeper / SpaceSaving — дают лучшую точность.
t-digest для percentile (p50/p95/p99/p999). Naive sorted array всех samples — O(N) memory, O(log N) insert; не масштабируется на streaming. t-digest: иерархические centroid clusters с variable resolution (плотно на хвостах, разреженно в середине). Add: insert sample → merge в ближайший centroid если capacity позволяет. Query: quantile(q) = интерполяция centroids по cumulative weight. Размер ~5 KB per sketch (compression=100), bounded relative error 1-2%. Killer feature: точнее на p99/p999, чем в середине — ровно то, что нужно для SLA monitoring. Mergeable: sum centroids → service mesh aggregation per region. Production: Datadog APM percentiles, Elastic, ClickHouse quantileTDigest, AWS CloudWatch, Druid. Альтернатива — DDSketch (Datadog): bounded relative error guarantee.
Trade-offs всех пяти одной таблицей: space, time, accuracy, mergeable. Правило выбора: «видели ли» → Bloom/Cuckoo; «сколько уникальных» → HLL; «сколько раз / top-K» → CMS; «p99» → t-digest. Anti-patterns: точность критична (финансы) → НЕ probabilistic; маленький dataset → overhead не оправдан. И финал: эти структуры стакаются. Реальный Flink job для site analytics держит Bloom для dedup + HLL для unique visitors + CMS для top hot keys + t-digest для percentile latency — всё в одной machine state, всё mergeable между shards.
ADR-001 на ноде bloom фиксирует решение «probabilistic vs exact». Распакуем компоненты.
Контекст. Точные структуры (HashSet, HashMap, sorted index) дают exact answer, но требуют память O(N). На масштабах billions per day:
Точные структуры не mergeable в общем случае — distributed aggregation требует пересылки raw данных между shards. Streaming use case (Kafka → Flink job) физически не может re-read поток для второго прохода.
Решение. Использовать probabilistic data structures когда: (1) кардинальность велика (>10M элементов на dimension), (2) допустима известная ошибка (1% FPR / 0.81% relative error), (3) нужен mergeable property для distributed compute, (4) streaming single-pass. Выбор внутри family строго по вопросу:
| Вопрос | Sketch | Размер | Ошибка | Mergeable |
|---|---|---|---|---|
| «Видели ли X?» | Bloom | ~10 bits/element для ε=1% | FPR, no false negative | OR |
| «Видели X? + delete» | Cuckoo | ~7 bits/element для ε=1% | FPR + occasional evict fail | union (сложнее) |
| «Сколько уникальных?» | HyperLogLog | 12 KB const | ε ≈ 1.04/√m ≈ 0.81% | element-wise max |
| «Сколько раз X встречался?» | Count-Min Sketch | ~2 MB (d=4, w=2¹⁷) | only overestimate | element-wise sum |
| «Какой p99?» | t-digest | ~5 KB per sketch | 1-2% relative, точнее на хвостах | merge centroids |
Что получаем. Память сокращается на 2-3 порядка: 32 GB HashSet → 12 KB HLL (500 000×), 30 GB HashMap counter → 2 MB CMS (15 000×). Update в hot path — O(1) на event. Distributed rollup — дешёвый merge вместо передачи raw events. Streaming single-pass работает «бесплатно».
Что платим. Известная ошибка (1% FPR / 0.81% relative) — это дизайн-параметр, не баг. Нужно осознанно положить её в SLO: «counter может быть ±1%». Для finance/medical/regulatory это запрещено. Некоторые структуры one-sided (Bloom only false positive, CMS only overestimate) — нужно правильно интерпретировать «NO»/«небольшое значение». Параметры (m, k, d, w, compression) выбираются заранее, ошибка в n (ожидаемая мощность) на 2× раздувает реальную ε.
Production референсы. Cassandra и RocksDB — Bloom per SSTable. Redis — HLL/Bloom/Cuckoo first-class. ClickHouse — uniqHLL12 + quantileTDigest + bloom_filter index. BigQuery — APPROX_COUNT_DISTINCT / APPROX_QUANTILES / APPROX_TOP_COUNT. Datadog / Prometheus exemplars — t-digest для APM percentiles. Druid — sketches first-class в столбцах (HLL, Theta, Quantiles). Apache DataSketches (Yahoo) — каноническая production library.
bloom_filter_fp_chance per table.PFADD/PFCOUNT/PFMERGE), Bloom filter (BF.RESERVE/BF.ADD/BF.EXISTS), Cuckoo filter, Count-Min Sketch, Top-K (HeavyKeeper) — всё через RedisBloom module.uniqHLL12, quantileTDigest, quantileTDigestWeighted, bloom_filter index, tokenbf_v1 для full-text-prefilter.APPROX_COUNT_DISTINCT (HLL++), APPROX_QUANTILES (KLL), APPROX_TOP_COUNT (SpaceSaving).approx_count_distinct, frequency counting).n/m/k. Промах в 2× по ожидаемой мощности раздувает FPR/ε в разы. Например, Bloom рассчитан на 100M, а реально вставили 200M — FPR с 1% уезжает к 10-30%.bloom_filter_fp_chance = 0 в Cassandra. Это выключает bloom (FPR 100%, всегда seek), а не делает его идеальным. Частая ошибка.m, k, hash functions, seeds). Попытка merge HLL с разным p или CMS с разной w даст мусор.n_actual >> n_design или CMS при сильно превышенной нагрузке деградируют. Без алертов на FPR / ε ошибка незаметно уезжает.Sketch — не серебряная пуля. Не берите его, если:
Концепты и кейсы в курсе:
Foundational papers:
Production docs и libraries:
Книги:
Talks и tutorials: