Skip to content

Phase 2: Hierarchical DataTree, Znodes & Session Lifecycles

Audience: Principal Distributed Systems Architect & Core Systems Engineer
Source References: zookeeper-server/src/main/java/org/apache/zookeeper/server/DataTree.java, DataNode.java


1. Cellular Java Anatomy: DataTree & DataNode

ZooKeeper does not read or query from disk during runtime. The entire hierarchical filesystem tree is held 100% resident in JVM heap memory:

// Cellular layout of DataTree.java
public class DataTree {
    // Master registry of all znodes in the system: Path -> DataNode
    private final ConcurrentHashMap<String, DataNode> nodes = 
        new ConcurrentHashMap<String, DataNode>();

    // Fast tracking of ephemeral nodes per session
    private final Map<Long, HashSet<String>> ephemerals = 
        new ConcurrentHashMap<Long, HashSet<String>>();

    private final WatchManager dataWatches = new WatchManager();
    private final WatchManager childWatches = new WatchManager();
}

1.1 The DataNode C Struct / Java Layout

public class DataNode {
    DataNode parent;          // Pointer to parent node in tree
    byte[] data;              // Payload bytes (Capped by jute.maxbuffer, default 1MB)
    Long acl;                 // Long index mapping to global shared ACL table
    public StatPersisted stat;// Node metadata (czxid, mzxid, version, cversion, etc.)
    Set<String> children;     // Set of child znode path names
}

1.2 Memory Overhead Implication

Even an empty znode with zero payload consumes approximately 500 bytes of JVM heap memory due to Java object headers, references, StatPersisted, and ConcurrentHashMap node padding.
Therefore: A cluster with 2,000,000 znodes consumes over 1 GB of heap purely for node pointers!


2. Znode Types & Creation Modes

Znode Hierarchy Spectrum:
┌─────────────────────────┬────────────────────────────────────────────────────────┐
│ Mode                    │ Behavioral Lifecycle                                   │
├─────────────────────────┼────────────────────────────────────────────────────────┤
│ PERSISTENT              │ Node persists until explicitly deleted by a client     │
│ PERSISTENT_SEQUENTIAL   │ Appends a 10-digit monotonic counter: /path/node-00001 │
│ EPHEMERAL               │ Automatically wiped when the client session terminates │
│ EPHEMERAL_SEQUENTIAL    │ Ephemeral node with monotonic counter (Used for locks) │
│ CONTAINER (v3.5+)       │ Automatically deleted by server when last child dies   │
│ TTL (v3.5+)             │ Expired and deleted after X milliseconds of inactivity │
└─────────────────────────┴────────────────────────────────────────────────────────┘

The Invariant of Ephemeral Znodes

[!IMPORTANT] An Ephemeral znode CANNOT have children.
If ZooKeeper permitted child nodes under an ephemeral node, cascading deletions during session expirations would trigger unmanageable recursive watch cascades across the cluster.


3. Session Lifecycle & Heartbeat Expiration Wheel

When a client connects to ZooKeeper, the Leader allocates a unique 64-bit sessionId:

┌──────────────────────────────────────┬──────────────────────────────────────┐
│ Leader ID (High 8 Bits)              │ Timestamp + Random Seed (Low 56 Bits)│
└──────────────────────────────────────┴──────────────────────────────────────┘

Client Session State Machine:
[DISCONNECTED] ──► (TCP Connect) ──► [CONNECTING] ──► (Auth & Session Established) ──► [CONNECTED]
      ▲                                                                                     │
      │                                                                                     ▼
      └── (Heartbeat Loss > sessionTimeout) ◄── [EXPIRED] ◄── (Network Partition) ◄────────┘

3.1 The Expiration Wheel (SessionTrackerImpl)

To monitor 50,000 active sessions without iterating through thousands of timer threads, ZooKeeper organizes sessions into discrete Expiration Buckets rounded to multiples of tickTime (default: 2,000ms):

Session Expiration Wheel in RAM:
Bucket [t + 2000ms]: { Session 101, Session 205 }
Bucket [t + 4000ms]: { Session 308 }
Bucket [t + 6000ms]: { Session 412, Session 519, Session 621 }
  • When a client sends a PING heartbeat (every \(\frac{1}{3} \times \text{sessionTimeout}\)), ZooKeeper moves the session from its current bucket into the latest future bucket.
  • A single background thread wakes up every tickTime, grabs the oldest bucket, and invalidates all expired sessions in that bucket in \(O(1)\) time!

3.2 Ephemeral Node Cleanup Sequence

When a session expires: 1. SessionTracker posts an internal closeSession transaction to the Leader. 2. The Leader looks up ephemerals.get(sessionId). 3. It generates an atomic batch of delete transactions for every ephemeral node owned by that session. 4. Commits the batch via ZAB, cleaning up all distributed locks held by the dead client.