ZAB (ZooKeeper Atomic Broadcast) — concept page covering leader election → discovery → synchronization → broadcast phases. Shows ZooKeeper ensemble with 5 nodes (1 leader + 4 followers), client. zxid (epoch, counter) transaction IDs, quorum writes, primary-order semantics. Multi-scenario: normal broadcast (PROPOSAL/ACK/COMMIT), leader crash + recovery, follower SNAP/DIFF catch-up.
Total order broadcast с primary-order semantics. Сердце Apache ZooKeeper, а значит — исторически сердце Kafka (≤3.x), HBase, Hadoop NameNode HA, SolrCloud, Storm и Mesos. Близкий родственник Multi-Paxos, прямой современник Raft, но со своей идентичностью: explicit recovery + FIFO порядок per-client.
ZooKeeper — это «system-of-record для метаданных»: leader election, membership, конфигурация, distributed locks. Чтобы такой service не врал клиентам и не разваливался при крэше ноды, он держит state в реплицируемом логе, а лог реплицируется через consensus. Этот протокол — Zab.
Понимать Zab надо когда:
dataLogDir, tickTime, syncLimit — всё это про Zab.См. также consensus-overview и raft — современная альтернатива.
«Один primary (leader). Все writes идут через него. Primary рассылает PROPOSAL followers; на quorum acks → COMMIT всем. Если primary помер — explicit recovery phase синхронизирует ensemble, выбирает нового primary с самым свежим логом, потом возобновляет broadcast. Главное обещание: client-perceived order сохраняется навсегда — если я как клиент написал A потом B на одной сессии, никто никогда не увидит B без A.»
Близкий родственник Multi-Paxos, но с упором на primary-order: порядок операций, как видел его конкретный клиент через конкретного primary, фиксируется навсегда. Это качественно отличает Zab от Multi-Paxos и сближает его с Raft.
ZK ensemble из 5 нод (leader + 4 followers, quorum=3) и клиент с активной сессией. Между всеми followers — discovery edges (полносвязный граф для recovery). От лидера к каждому follower — PROPOSAL/COMMIT канал. Клиент общается только с лидером (writes; reads могут идти на followers, но это уже не про Zab).
Три сценария:
zxid=(epoch, counter), рассылает PROPOSAL, ждёт quorum acks (включая свой fsync), рассылает COMMIT, отвечает клиенту. Late acks от оставшихся followers приходят после ответа — нормально, leader их учитывает но игнорирует «по существу».zxid выше всех (это follower-1, успевший получить in-flight PROPOSAL). Новый лидер инкрементирует epoch, делает Sync (replay zxid=(2,18)), переходит в Broadcast. Клиент при reconnect видит свой write committed — primary-order сохранён.WRITE create /lock/x.zxid = (epoch=2, counter=17) — монотонный счётчик в рамках эпохи.PROPOSAL(zxid, txn) параллельно всем.ACK(zxid).COMMIT(zxid), followers применяют к in-memory DataTree.OK.Похоже на Raft AppendEntries, но Zab разделяет PROPOSAL и COMMIT в разные RPC, тогда как Raft объединяет их в одном AppendEntries через commitIndex. Zab «болтливее» (2× round trip), но даёт более чёткие точки наблюдения для мониторинга и forensic анализа.
Тонкость, противоречащая наивной интуиции про «uncommitted = должно откатиться». Если follower-1 единственный, кто получил PROPOSAL zxid=(2,18), и становится новым лидером — этот PROPOSAL переезжает в новую эпоху. Логика: «у нового лидера самый свежий лог; всё что в нём есть — авторитативно». При Sync остальные followers догоняются до (2,18), транзакция получает COMMIT уже в эпохе 3.
Альтернативный исход: лидером становится follower-2, у которого нет (2,18). Тогда follower-1 при Sync получит TRUNC — лишний хвост обрезан. Election гарантированно выбирает того, у кого zxid выше — поэтому если follower-1 успел сделать fsync, он почти всегда станет лидером.
Для клиента, не получившего ACK, результат неопределён до завершения recovery. ZK-клиенты должны переподключаться и читать свой last write, чтобы понять «прошло или нет». Этот паттерн — основа Curator's RetryPolicy.
Replaying 14500 PROPOSAL для отставшего follower'а — 14500 round trips с fsync. Snapshot — один большой transfer + последние ~200 транзакций для tail. Threshold переключения регулируется snapCount (default 100000 entries). Этот же путь — для нового follower'а, впервые присоединяющегося к ensemble.
Контекст. ZooKeeper — сервис координации с total order broadcast, primary-order semantics и predictable recovery. Выбор протокола фиксируется на десятилетия (см. Kafka KRaft — 5 лет от KIP-500 до GA).
Альтернативы. Multi-Paxos, Raft (не существовал в 2008), Zab. Раннее ZK прототипировалось на Paxos, но: (1) Multi-Paxos не гарантирует per-client FIFO — клиент, переподключившийся к другой реплике, мог видеть свои writes в случайном порядке; (2) implicit recovery («каждый slot догоняется независимо») сложно дебажить в production.
Решение. Свой протокол Zab, оптимизированный под три свойства: (a) primary-order — клиент в одной сессии всегда видит writes в порядке отправки; (b) explicit recovery phase (Discovery + Synchronization) — детерминирована и легко reason-about при крэше лидера; (c) раздельные PROPOSAL/ACK/COMMIT — упрощают мониторинг и forensic анализ.
Trade-offs vs Raft. Zab сложнее Raft (две выделенные фазы recovery vs один AppendEntries + commitIndex), и recovery — традиционное место багов в реализациях. Raft (2014) выиграл как «modern default»: etcd, Consul, CockroachDB, TiKV, Kafka KRaft — все взяли Raft. Для нового проекта сегодня — выбирать Raft. Zab релевантен только при необходимости совместимости с ZooKeeper API (ClickHouse Keeper заявляет Raft внутри, но сохраняет ZK wire protocol).
Контекст. Zab quorum = N/2 + 1. Размеры: 1 (только dev), 3 (минимум для HA, staging), 5 (production default), 7 (multi-DC или критичные среды), 9+ (редко полезно).
Решение. Production default — 5 нод (quorum=3, tolerates 2 failures). Чётное N никогда: с N=4 quorum=3, tolerance=1 — тот же 1 крэш как при N=3, но latency = max из 3 acks вместо 2. С N=6 — то же что N=5, дороже на каждом write. С N=7+ риск попасть на slow disk или GC pause растёт больше чем выигрыш в fault tolerance.
Multi-DC. Не растягивать ensemble через регионы — quorum write через 500ms RTT убивает throughput. Лучше per-region ensembles + application-level cross-region replication. Observers (read-only ноды, не участвуют в quorum) частично помогают в reads, но writes всё равно идут на voting members в одном регионе.
Мониторинг. Трекать fsyncTime, outstandingRequests, GC pauses. Slow disk или long STW пауза каскадно ломают ensemble через flapping leader: лидер не шлёт heartbeats → followers триггерят re-election → лидерство мечется → клиенты получают ConnectionLossException шторм.
Motivation из KIP-500 — полезный список того, как ZK болит в operational reality:
KRaft решил это встроенным Raft'ом в controllers: один артефакт, одна сборка, scale до миллионов partitions, failover за секунды.
tickTime / syncLimit. Default tickTime=2000ms, syncLimit=5 = 10s до detection отвалившегося follower'а. Тюнить под workload, понимая trade-off с false positives при GC pause.fsyncTime / outstandingRequests. Будешь debugger'ить «slow Kafka», а причина — насыщенный диск ZK.myid=1 на двух машинах → quorum'у конец. ZK не имеет автоматической защиты — нужна дисциплина в config management.dataLogDir. Transaction log fsync конкурирует со snapshot writes и application IO → миллисекундные fsync'и становятся секундными, leader rejection storm.