← Back to index

Apache ZooKeeper — Internal Architecture

A developer's guide to how ZooKeeper actually works under the hood: the ensemble and quorums, the znode data model, the ZAB consensus protocol, the write and read paths, sessions and watches, and how it persists and recovers state.

Student edition · understand, apply, verify

Understand versioned coordination, session liveness, ordered writes, and possibly stale reads.

What you should be able to do: Use a session/namespace recipe without assuming timeout proves death or sync makes every read globally current.

Architecture tutorial for a student with basic programming knowledge. Defaults, guarantees, file layouts, and APIs are release/configuration-sensitive. Toy models and pseudocode do not implement the named product.

Apache ZooKeeper is a small, strongly-consistent coordination service — the kernel that other distributed systems build on for the hard problems they would rather not solve themselves: leader election, configuration, distributed locking, group membership, and service discovery. It deliberately does very little. It exposes a tiny filesystem-like API over a tree of small data nodes, guarantees that every server in the cluster sees updates in the same total order, and lets clients put watches on nodes to be notified of change. Everything else — locks, queues, barriers, leader election — is a recipe built on those primitives. Understanding ZooKeeper means understanding two things: the data model clients see, and the ZAB protocol that keeps every replica's copy of that data in agreement.

Contents

  1. Design Goals and Core Ideas
  2. Architecture: The Ensemble
  3. Quorums & Why Odd Numbers
  4. Data Model: znodes & the Namespace
  5. znode Types & Watches
  6. The ZAB Protocol
  7. Write Path
  8. Read Path & Consistency
  9. Sessions & Ephemeral Nodes
  10. Persistence & Recovery
  11. Classic Recipes
  12. Summary
  13. Concepts explained
  14. Worked example
  15. Practice and verification

Concepts explained in this guide

Read these definitions first, or use the linked terms as you go. The examples and limitations are local to this guide; companion pages are optional.

znodes, sessions, and watches

Small versioned namespace nodes hold coordination data. Ephemeral nodes belong to sessions; watches notify clients to reread state.

Example. A lock node disappears on session expiry while its owner is partitioned; fence that owner's external writes.

Boundary. Classic watches are one-shot; persistent modes differ. Sequence counters have finite scoped/recreation behavior, not eternal global nonreuse.

ZAB and ZooKeeper consistency

ZAB establishes a replicated leader-ordered history through quorum and recovery rules. Local reads can lag while following documented order/session guarantees.

Example. B can read an older replica after A writes. A sync/barrier is not an unconditional globally-current read guarantee.

Boundary. Intersection alone is not the recovery proof. Durable logging, safe membership, and external-effect fencing remain necessary; observers do not vote.

Quorum

A set of replicas whose responses are sufficient for an operation. In a fixed set of N replicas, read size R and write size W overlap whenever R+W>N. A majority is one particular quorum, not the definition of every quorum.

Example. With N=3, writes to {A,B} and reads from {B,C} share B. If the write completed durably and replicas preserve a comparable version, B carries that version forward.

Boundary. Overlap alone does not provide linearizability: concurrent or failed writes, sloppy replica sets, and version selection matter. Consensus adds protocol rules to preserve decisions.

Fencing tokens

A strictly increasing grant number attached to operations. The protected resource atomically compares it with its durable highest accepted token and rejects older operations, making superseded clients unable to overwrite newer effects.

Example. A has token 33 and pauses. B acquires 34 and writes. When A sends token 33 later, the resource rejects it because its fence is already 34.

Boundary. A new grant must be installed at the resource before it promises exclusion there. Token 33 can still pass until 34 is observed. Equal-token operations also need sequence/idempotency rules if they can arrive out of order.

Leases

Time-limited permission issued by an authority. Expiry lets new work proceed after a client stops renewing; it does not prove that the previous holder is dead or unable to act.

Example. A 30-second lease expires while A is paused for 45 seconds. B can acquire a new lease, but A can still wake up and send a delayed write.

Boundary. Heartbeats improve recovery time, not safety by themselves. Compare-and-renew/delete must verify a unique acquisition token atomically, and protected effects need fencing or equivalent serialization.

Optimistic concurrency and compare-and-set

Update state only if the expected version or status still matches, in the same atomic operation that changes it. A failed comparison means a conflicting operation won; reread or return a conflict.

Example. UPDATE seats SET status='held', version=version+1 WHERE id=? AND status='available' allows only one concurrent claimant to update the row.

Boundary. Application-level GET then unconditional SET is not equivalent. Include acquisition identity to reject stale callbacks, and couple related invariants in a transaction.

1. Design Goals and Core Ideas

ZooKeeper's design is shaped by a refusal to be a general-purpose database. It is a coordination kernel, and every decision serves that narrow goal. Keeping these in mind makes the rest predictable.

GoalHow ZooKeeper achieves it
Strong consistencyAll writes go through a single leader and are committed in a strict total order (the zxid sequence). Every server applies them in the same order.
Small, simple APIA hierarchical namespace of znodes with a handful of operations (create, delete, setData, getData, getChildren, exists) plus one-shot watches.
High read throughputReads are served entirely from each server's in-memory copy of the tree — no quorum, no disk. The whole dataset is kept in RAM.
High availabilityA replicated ensemble. As long as a majority of servers are up, the service stays available and survives a minority of failures.
Liveness primitivesEphemeral znodes tied to client sessions make it trivial to detect when a participant has gone away — the foundation of locks and membership.

ZooKeeper is a CP system in CAP terms: it favors consistency and partition tolerance. If a server cannot reach a quorum, it stops serving writes rather than risk divergence. It is built for small amounts of critical metadata read and watched by many clients — not for bulk data.

2. Architecture: The Ensemble

ZooKeeper uses leader/follower/observer roles under its protocol. Elections can temporarily leave no serving leader; stale prior leaders may remain unaware. Current authority and valid quorum/recovery rules govern commits.

The ZooKeeper ensemble
One leader serializes writes; followers replicate and vote; observers scale reads without voting. Each client holds a session with a single server.

The pieces that make this work:

3. Quorums and Why Odd Numbers

Majorities of one fixed voting membership intersect. With 2f+1 voters, f may be unavailable while a quorum remains. This arithmetic supports ZAB's durable election/recovery rules; it does not alone prove no conflicting decision or stop an unaware old leader.

Odd membership is an efficiency choice, not a way to avoid every partition or guarantee an active leader at every instant. Observers add read capacity without votes. Safe membership changes and surviving capacity are part of the operational contract.

4. Data Model: znodes and the Namespace

ZooKeeper presents data as a hierarchical namespace very much like a filesystem. Every node in the tree is called a znode and is addressed by an absolute, slash-separated path such as /app/servers/host-A. The crucial difference from a real filesystem is that every znode can hold both data and children — there is no distinction between files and directories.

The znode namespace
A hierarchical tree of znodes. Each holds a small payload plus a stat structure with versions and the zxids that created and last modified it.

Two properties of the model matter for how you use it:

5. znode Types and Watches

Persistent nodes last until deletion; ephemeral nodes are tied to sessions. Sequential creation appends a scoped sequence component, but counters/namespace lifetime are finite and parent recreation/wrap/recovery matter. Do not claim eternal global nonreuse from a sequence suffix alone.

Classic watches are one-shot notifications that tell a client to reread; supported persistent/recursive watches have different behavior. A watch is not a durable lossless event queue: reconnect, missed intermediate changes, and session expiry need a resync policy. Check state and install/re-arm the watch through supported atomic API semantics, handling the node disappearing between steps.

6. The ZAB Protocol

The engine that keeps every replica's copy of the tree identical is ZAB — the ZooKeeper Atomic Broadcast protocol. ZAB is not Paxos, though it solves a related problem; it is a primary-backup atomic broadcast designed specifically so that a recovered ensemble re-applies exactly the same prefix of transactions in exactly the same order. It runs in two modes that alternate: leader election / recovery, and broadcast.

The ZAB protocol
Election picks the server with the most up-to-date history; broadcast is a two-phase propose/commit. Order is defined by the 64-bit zxid.

The zxid

Everything in ZAB is ordered by the zxid (ZooKeeper transaction id), a 64-bit number split into two halves. The high 32 bits are the epoch — incremented every time a new leader is elected. The low 32 bits are a counter that the leader increments for each transaction it proposes within its epoch. Comparing zxids is just comparing 64-bit integers: a higher epoch always wins, and within an epoch the higher counter wins. This single number is how servers decide who has the most recent state and how they re-apply history deterministically.

Leader election (Fast Leader Election)

When a server starts, or loses contact with the current leader, the ensemble runs Fast Leader Election. Each server votes for a candidate and broadcasts that vote; servers update their own vote toward whoever has the most up-to-date history. The comparison key is (epoch, last-zxid, server-id) — the server with the highest last-committed zxid wins, because choosing the most current server guarantees no committed transaction is lost. Once a server sees a quorum agree on the same candidate, that candidate becomes leader.

Recovery and atomic broadcast

Before serving writes, a new leader synchronizes followers up to its own history (the recovery phase), establishing a clean starting point in a new epoch. Then it enters broadcast mode, where every write is a two-phase commit:

  1. The leader assigns the write the next zxid and sends a PROPOSE to all followers.
  2. Each follower writes the proposal to its transaction log (with an fsync) and replies ACK.
  3. Once a quorum has acked, the leader broadcasts COMMIT, and every server applies the change to its in-memory tree in zxid order.
# Leader broadcast loop (simplified)
function broadcast(txn):
  zxid = (current_epoch << 32) | (++counter)   # total order
  proposal = (zxid, txn)
  log.append(proposal); fsync()                # leader logs first
  for f in followers:
    send(f, PROPOSE, proposal)
  wait until acks >= quorum                     # majority of voters
  for f in followers:
    send(f, COMMIT, zxid)
  apply_to_memory(txn)                          # in zxid order, on every server

7. Write Path

Because there is exactly one leader, the write path always converges on it. A client may be connected to any server, but only the leader can originate a transaction, so a write received by a follower is forwarded.

The write path
A follower forwards the write to the leader, which proposes it, collects a quorum of acks, commits, and the result flows back to the client.

The end-to-end sequence for a create or setData:

  1. The client sends the write to its connected server.
  2. If that server is a follower, it forwards the request to the leader.
  3. The leader assigns the next zxid and sends a PROPOSE to all followers, logging it durably itself.
  4. Each follower fsyncs the proposal to its transaction log and returns an ACK.
  5. On a quorum of acks, the leader sends COMMIT; every server applies the change to its in-memory tree, and the originating server replies success to the client.
# From the client's connected server's perspective
function handle_write(req):
  if not is_leader:
    forward_to_leader(req)            # followers cannot originate writes
  zxid, ok = leader.propose_and_commit(req)   # PROPOSE -> quorum ACK -> COMMIT
  if ok:
    apply_to_memory(req)              # applied in zxid order, everywhere
    return SUCCESS(zxid)
  return FAILED                       # e.g. no quorum reachable

The key durability point: a follower must persist a proposal to disk (fsync) before acking it. Combined with the quorum rule, this guarantees that any committed transaction survives the failure of a minority of servers — it is already on stable storage on a majority.

8. Read Path and Consistency

Ordinary reads use the connected server's local state and can lag other clients' committed writes. ZooKeeper's documented write ordering/session guarantees are not an unconditional linearizable read service. A client's own ordered write/read behavior is distinct from a globally latest read.

sync requests a supported synchronization barrier before a subsequent read, but do not label it 'now guaranteed current' under all leader/failure/timing conditions. Use the actual release/client contract and coordination protocol for cross-client freshness. Critical external effects need version/ownership conditions and fences, not a stale local read as authorization.

9. Sessions and Ephemeral Nodes

A negotiated session is renewed through supported client traffic/heartbeats. Expiry is an authority decision committed under the ensemble's protocol; it is not proof a client process died. Disconnected/paused clients may still try old effects after a new holder proceeds.

Reconnect can preserve a still-valid session, but watch/data state must follow client resynchronization rules. Expiry removes ephemeral nodes through committed changes when the ensemble can progress. A permanent loss of quorum can delay cleanup. Namespace payloads are small coordination data, not a bulk store; packet/data limits and ACL/version fields must be checked for the deployed release.

10. Persistence and Recovery

Protocol logging must preserve proposal/election state under its configured durable-storage contract. Append without fsync/equivalent does not prove power-loss safety; settings and devices matter. ZAB recovery synchronizes the agreed history before serving, not arbitrary replay of every locally present uncommitted proposal.

Snapshots may be fuzzy while writes continue. Supported snapshot/log recovery uses version/order bookkeeping to reconstruct valid state; application create/delete operations are not universally idempotent by themselves. Retain compatible snapshot/log coverage and test restore; do not purge merely because any newer file exists.

11. Classic Recipes

Use reviewed ephemeral/sequential lock/election recipes with session/connection error handling and bounded deadlines/cancellation. On uncertain node creation, discover your own uniquely identified contender rather than creating unbounded duplicates. Compare child basenames with the created basename, not a full path versus a name.

Watch the relevant predecessor, then reread/recheck after notification or disappearance. Do not infer ownership just from one watch event; other contenders, deletion, reconnection, and session loss change state. Release only your own node. Install a durable nonreused fencing identity at external resources before trusting the new holder's effects.

Configuration watches need a current state reload and version guard. They reduce polling, not every possible missed intermediate update. Sequential naming and zxids have scope/lifecycle constraints; avoid promising globally eternal IDs.

Worked example: a session expires while its holder is paused

Use this example to check which promise each mechanism actually provides.

a session expires while its holder is pausedA owns an ephemeral lock node: Its session identity persists across eligible reconnects; a TCP connection is not the session.. A pauses beyond authority expiry: ZooKeeper can commit deletion and another holder proceeds. A may remain unaware.. B activates a newer resource fence: External storage/effects atomically record a nonreused newer token under the supported recipe.. A resumes: Its stale effects are rejected at the resource. A local read, watch, or missing heartbeat alone would not physically stop it.1A owns an ephemeral lock node2A pauses beyond authority expiry3B activates a newer resource fence4A resumes
Sequence for this example. The table explains the evidence and decision at each step.
StepEvidence, decision, and expected result
A owns an ephemeral lock nodeIts session identity persists across eligible reconnects; a TCP connection is not the session.
A pauses beyond authority expiryZooKeeper can commit deletion and another holder proceeds. A may remain unaware.
B activates a newer resource fenceExternal storage/effects atomically record a nonreused newer token under the supported recipe.
A resumesIts stale effects are rejected at the resource. A local read, watch, or missing heartbeat alone would not physically stop it.
Check your understanding: Is read-your-writes the same as a globally latest read?

No. Session ordering can preserve your own writes while other clients' newer writes have not reached the serving replica. sync/barrier behavior must follow the documented service contract.

Run and change the example

Download guide_labs.py. Python 3.10+, standard library only. In its directory, run:

python3 guide_labs.py coordination
python3 guide_labs.py test

The model demonstrates scoped version conditions, stale read snapshots, and installed-token rejection.

The program tests its stated finite contracts. It is an educational model, not an implementation of the named production product or a proof of unmodeled distributed faults.

Sources and further verification

12. Summary

ZooKeeper is small on purpose. A few reinforcing ideas account for the whole system:

ConcernMechanism
What does a client see?A hierarchical tree of small znodes with versioned stat metadata and one-shot watches.
How is consistency maintained?A single leader plus the ZAB atomic-broadcast protocol; every server applies transactions in zxid order.
What guarantees a single history?Quorum (majority) commit — any two majorities overlap, so no divergence. Odd ensemble sizes for efficient fault tolerance.
Why are reads fast?They are served from each server's in-memory tree; no leader, no quorum. sync() when you need read-your-writes.
How is liveness detected?Sessions with heartbeats; ephemeral znodes that vanish on expiry, firing watches.
How is data made durable?Write-ahead transaction log (fsync) plus periodic snapshots; replay on restart.
How are coordination problems solved?Recipes — locks, leader election, membership, config — built from ephemeral+sequential znodes and watches.
The recurring theme: ZooKeeper does one hard thing well — a strongly-consistent, totally-ordered, in-memory tree with change notification — and lets everything else be a recipe on top. It is the small, reliable kernel that bigger distributed systems lean on so they do not have to solve consensus themselves.

⌂