By the end of this lesson
  • Trace etcd's layered architecture: gRPC server facade, Raft consensus state machine, in-memory treeIndex, and Bbolt storage
  • Deconstruct the MVCC revision model: global 64-bit revision counters, generational histories, and compaction mechanics
  • Analyze linearizable read optimization: ReadIndex protocol vs LeaseRead vs full Raft consensus writes
  • Formulate failure modes: Bbolt freelist fragmentation, disk quota alarms (NOSPACE), and lease keep-alive starvation

1. Problem & Architecture Scope

Project Identity, Version & License Baseline

Project ComponentCanonical RepositoryPinned ReleaseGovernance / LicenseEdition Boundary
etcdetcd-io/etcdv3.5 (LTS)CNCF Graduated (Apache 2.0)Open source

etcd is the distributed, strongly consistent key-value store that serves as the single source of truth for Kubernetes, storing the entire cluster state: Pods, Services, CRDs, Deployments, and Secrets.

A control-plane data store for orchestrating distributed infrastructure faces unique engineering demands:

  1. Strict Linearizable Correctness: Two controllers attempting to acquire the same leader lease or allocate the same IP address must never both succeed.
  2. Deterministic Historical Revisions: Controllers need to watch resource changes continuously. If a network blip disconnects a controller for 30 seconds, it must be able to reconnect and request: “Give me all events since revision 104,291 without missing a single transition.”
  3. High-Throughput Watchers: Thousands of kubelet agents and API server components maintain long-running watch connections on specific key prefixes (e.g., /registry/pods/node-1/).
  4. Predictable Latency: Disk writes must complete in single-digit milliseconds; long compaction stalls or garbage collection pauses directly cascade into Kubernetes API server timeouts and worker node flapping.

In this architecture study, we deconstruct etcd v3’s layered internals:

  • The decoupled Raft Consensus Engine (etcd/raft).
  • The MVCC Architecture: Global 64-bit revisions, generations, and the in-memory treeIndex.
  • The persistent Bbolt Copy-on-Write Storage Backend.
  • High-performance read paths: ReadIndex vs LeaseRead.
  • Operational realities: Bbolt fragmentation, compaction debt, and disk quota alarms.

2. Mental Model & Architectural Abstraction

The architecture is cleanly layered into four decoupled tiers:

[ Client / Kube-API-Server ]
             │ gRPC / HTTP/2
             ▼
┌────────────────────────────────────────────────────────┐
│  Tier 1: gRPC API Server & Watch Hub                   │
│  - KVServer, LeaseServer, WatchServer                  │
│  - SyncedWatchers (in-memory) & UnsyncedWatchers (disk)│
└──────────────────────────┬─────────────────────────────┘
                           │ Write Proposal / Linearizable Read
                           ▼
┌────────────────────────────────────────────────────────┐
│  Tier 2: Raft Consensus Engine (etcd/raft)             │
│  - Monotonic Terms, Leader Election, Quorum Matching   │
│  - Persistent Append-Only WAL (`*.wal` on disk)        │
│  - Raft Snapshots (`*.snap` on disk)                   │
└──────────────────────────┬─────────────────────────────┘
                           │ Apply Committed Entry
                           ▼
┌────────────────────────────────────────────────────────┐
│  Tier 3: In-Memory treeIndex (B-Tree in RAM)           │
│  - Maps user key -> keyIndex{revisions, generations}   │
│  - Resolves (key, rev) -> revision(main, sub) in < 1µs │
└──────────────────────────┬─────────────────────────────┘
                           │ Fetch Value by Revision Key
                           ▼
┌────────────────────────────────────────────────────────┐
│  Tier 4: Persistent Bbolt Storage Engine (Disk)        │
│  - Copy-on-Write B+Tree over Memory-Mapped File (mmap) │
│  - Bucket "key": revision(main, sub) -> KeyValue proto │
│  - Freelist Page Allocator & Asynchronous Fsync        │
└────────────────────────────────────────────────────────┘

The fundamental design insight of etcd v3 is the separation of user keys from disk keys:

  • In the persistent disk storage (bbolt), the key is NOT the user string (/registry/pods/default/nginx).
  • The disk key is the composite monotonic revision tuple (main_rev, sub_rev) (: an 8-byte big-endian main_rev transaction counter + _ separator + an 8-byte big-endian sub_rev operation index within the transaction).
  • Because revisions only increase, all writes to bbolt are sequential appends to the right edge of the B+Tree.
  • The in-memory treeIndex maintains the mapping from user keys to their respective revision timelines.

3. Core Terminology & Guarantees

TermArchitectural DefinitionProduction Guarantee
RevisionA cluster-wide 64-bit integer incremented on every mutating transaction.Global logical clock providing strict total order across all keys.
GenerationThe lifecycle of a key between creation and deletion.Deleting a key writes a tombstone, ending the generation; recreating it starts generation .
treeIndexIn-memory Google B-Tree mapping user key strings to keyIndex structs holding revision histories.Sub-microsecond key lookup without disk seeks.
BboltAn embedded, ACID-compliant, copy-on-write B+Tree written in pure Go.Single-writer, multi-reader; reads bypass kernel boundaries via mmap.
ReadIndexProtocol allowing the Raft leader to serve linearizable reads without writing to disk.Verifies leader quorum via a single heartbeat round-trip.
LeaseA time-to-live (TTL) token granted by the cluster to bind key lifecycles.Keys attached to a lease are atomically deleted if the lease is not renewed.
CompactionPurging historical revisions older than revision from treeIndex and bbolt.Frees internal bbolt pages for future writes; prevents unbounded storage growth.
DefragmentationRewriting the physical bbolt file to reclaim free pages.Physically returns unused disk space to the host operating system.

4. System Architecture & Component Anatomy

SYSTEM ARCHITECTURE BLUEPRINTDISTRIBUTED SYSTEMS
etcd Internals and Architecture
ARCHITECTURAL FLOW · STEP 1 OF 3

1. gRPC Server and API Facade & gRPC Multiplexing

Establishes the ingress and structural boundary across gRPC Server and API Facade & gRPC Multiplexing: HTTP/2 Sub-Stream Per Watcher; ReadIndex RPC confirms leader quorum Bypasses Raft log write disk IO; Heartbeat TTL management Keys bound to lease expire together.

Architectural Invariant: Pure state machine design: zero network or disk IO inside core logic.
ARCHITECTURAL FLOW · STEP 2 OF 3

2. Persistent Raft WAL & Quorum Replication

Coordinates core data-plane execution and state transitions through Persistent Raft WAL & Quorum Replication: Sequential disk write Pre-allocated 64MB files O_SYNC / fsync flushed; Leader broadcasts to peers Majority (N/2 + 1) ACKs Advance commitIndex; Triggers snapshot when WAL exceeds 10,000 entries (configurable) Truncates older WAL files; fast recovery for lagging learner nodes Ensures constant bound on startup recovery time.

Architectural Invariant: Triggers snapshot when WAL exceeds 10,000 entries (configurable) Truncates older WAL files; fast recovery for lagging learner nodes Ensures constant bound on startup recovery time.
ARCHITECTURAL FLOW · STEP 3 OF 3

3. In-Memory treeIndex (B-Tree) & Google B-Tree in RAM

Enforces durability, state convergence, and output verification across In-Memory treeIndex (B-Tree) & Google B-Tree in RAM: Maps user key to revision history "foo" -> keyIndex{revisions}; keyIndex: generations and tombstones; main: increments per Raft transaction.

Architectural Invariant: Given key and rev, returns Bbolt rev key Range scans walk RAM B-tree leaves Zero disk seek during key lookup Resolves target revisions in sub-microsec.

Deep Component Breakdown:

  1. The Pure State Machine Raft Library (etcd/raft):
    • Unlike many Raft implementations that entangle networking, disk I/O, and timers inside consensus logic, etcd/raft is a pure deterministic state machine.
    • It performs zero disk writes and zero network system calls.
    • The application feeds messages (raft.Step()) and ticks (raft.Tick()) into the engine. The engine emits a Ready struct containing:
      • Entries: Uncommitted log entries to write to the WAL.
      • Messages: Outbound network RPCs to send to peers.
      • CommittedEntries: Entries agreed upon by quorum ready to be applied to the MVCC state machine.
  2. Persistent Raft WAL (wal/wal.go):
    • Pre-allocates 64 MB disk files to eliminate filesystem metadata allocation stalls.
    • Appends entries sequentially and issues an explicit fsync before acknowledging consensus.
  3. The MVCC Storage Subsystem (mvcc/):
    • Maintains the dual index: RAM-resident treeIndex and disk-resident bbolt.
    • Manages the WatchableKV: Separates watchers into synced (up to date with latest revision, notified directly from memory) and unsynced (catching up on historical revisions, reading from bbolt).
  4. Bbolt Backend (mvcc/backend/):
    • Maps the entire database file into the process’s virtual address space via mmap(2).
    • Read transactions execute concurrently without locks; write transactions are strictly serialized via a single mutex.

5. Detailed Request Flows & Protocols

5.1 Mutating Write Path (Put / Txn)

Client             KVServer             Raft Core              WAL (Disk)            MVCC / Bbolt
  │                   │                     │                      │                       │
  ├── Put(k, v) ─────>│                     │                      │                       │
  │                   ├── Step(MsgProp) ───>│                      │                       │
  │                   │                     ├── Ready: Entries ───>│ (Append & fsync)      │
  │                   │                     ├── Ready: Messages ──────────────────────────>│ (Broadcast to Peers)
  │                   │                     │<── Quorum ACKs ──────────────────────────────┤
  │                   │                     │                      │                       │
  │                   │<── CommittedEntries ┤                      │                       │
  │                   ├── Apply Entry ────────────────────────────────────────────────────>│
  │                   │                                                                    ├── 1. Rev = ++curRev
  │                   │                                                                    ├── 2. Bbolt.Put(Rev, KV)
  │                   │                                                                    ├── 3. treeIndex.Put(k, Rev)
  │                   │                                                                    └── 4. Notify Watchers
  │<── OK (Rev) ──────┤                                                                    │
  1. Client Proposal: Client sends a gRPC Put(key, value) request to the leader.
  2. Raft Replication: The leader wraps the command in an internal protobuf proposal and passes it to etcd/raft. The entry is written to the local WAL file (fsync) and broadcast to followers via Netty/HTTP2.
  3. Quorum Commitment: Once a majority of nodes acknowledge writing the entry to disk, the leader marks the entry committed.
  4. Application to MVCC:
    • The global revision counter is incremented: currentRev++.
    • A mvccpb.KeyValue protobuf is constructed and written to bbolt bucket "key" using the composite big-endian revision tuple (main_rev, sub_rev) as the primary key.
    • The user key string is inserted into treeIndex in RAM, appending the new revision to the key’s active generation.
    • Active watchers monitoring this key prefix are immediately notified.
  5. Response: The committed revision is returned to the client.

5.2 Linearizable Read Path (ReadIndex)

Client             KVServer              Raft Core              Peers (Followers)       Local Bbolt / RAM
  │                   │                      │                         │                        │
  ├── Get(key) ──────>│                      │                         │                        │
  │                   ├── ReadIndex() ──────>│                         │                        │
  │                   │   (Records commitIdx)├── Heartbeat (Quorum) ──>│                        │
  │                   │                      │<── Majority ACKs ───────┤                        │
  │                   │<── ReadIndex OK ─────┤                         │                        │
  │                   ├── Wait applied >= commitIndex                  │                        │
  │                   ├── Read from local storage ─────────────────────────────────────────────>│
  │                   │   1. treeIndex.Get(key) -> targetRev                                   │
  │                   │   2. bbolt.Get(targetRev) -> Value                                     │
  │<── OK (Value) ────┤                                                                        │
  1. Read Request: Client requests Get(key, WithSerializable=false).
  2. Quorum Verification: The leader captures its current commitIndex and sends a lightweight heartbeat round to peers. If a majority responds, the leader confirms it remains the valid leader.
  3. Applied Index Barrier: The leader waits until its state machine has finished applying all entries up to commitIndex.
  4. Local Execution: The leader resolves the user key in treeIndex in RAM, retrieving the latest revision , and reads the value directly from bbolt’s memory-mapped pages without writing to disk.

6. Mathematical Modeling & Hardware Physics

6.1 Bbolt Storage Layout & Page Allocation Math

A bbolt database consists of contiguous disk pages:

  • Meta Pages (Pages 0 & 1): Hold database root pointers, page size, and alternating transaction IDs for copy-on-write consistency.
  • Branch Pages: Internal B+Tree nodes storing child page IDs and separator keys.
  • Leaf Pages: Store key-value pairs. In etcd, on-disk keys are composite revision tuples (main_rev, sub_rev) ( big-endian main_rev + big-endian sub_rev, plus a 1-byte _ separator in revToBytes):

If a leaf page has and each key-value entry has average value size :

6.2 Write Amplification during Compaction

When writing at a sustained rate of with values:

In a 2 GB database, reaching the quota takes:

Without frequent automated compaction (--auto-compaction-retention=1h), etcd will exhaust its storage quota and lock into a read-only state.

6.3 Memory Overhead of treeIndex

Each distinct key in etcd is maintained in RAM by treeIndex:

  • keyIndex struct:
  • Key string:
  • Historical revisions (average 5 versions):
  • Total RAM per key:

For a cluster with active Kubernetes keys: This lightweight memory footprint explains why etcd easily holds millions of keys in memory while delegating payload storage to disk.


7. Production TypeScript Implementation

This self-contained TypeScript simulation implements:

  1. Revision: 64-bit monotonically increasing (main, sub) revisions.
  2. TreeIndex: In-memory multi-version revision mapping.
  3. BboltBackend: Simulated copy-on-write key-value page storage.
  4. EtcdServer: Complete transaction coordinator supporting Put, Get with ReadIndex, Compaction, and Watches.

Execute with: npx tsx etcd-internals-capstone.ts

/**
 * ARCHITECTURE STUDY: etcd Internals — MVCC, Bbolt & Raft Consensus
 * Simulation of etcd v3 MVCC Engine: treeIndex, Bbolt Backend,
 * Monotonic Revisions, and Compaction Mechanics.
 */

// ==========================================
// 1. Revision & Generation Data Structures
// ==========================================

export interface Revision {
  main: number; // Incremented on each transaction
  sub: number;  // Order within multi-op transaction
}

export function revToString(rev: Revision): string {
  return `${rev.main.toString().padStart(10, '0')}_${rev.sub.toString().padStart(5, '0')}`;
}

export interface KeyGeneration {
  createdRev: Revision;
  revisions: Revision[];
}

export class KeyIndex {
  public key: string;
  public generations: KeyGeneration[] = [];

  constructor(key: string, initialRev: Revision) {
    this.key = key;
    this.generations.push({
      createdRev: initialRev,
      revisions: [initialRev],
    });
  }

  put(rev: Revision): void {
    if (this.generations.length === 0) {
      this.generations.push({ createdRev: rev, revisions: [rev] });
    } else {
      const activeGen = this.generations[this.generations.length - 1];
      activeGen.revisions.push(rev);
    }
  }

  tombstone(rev: Revision): void {
    const activeGen = this.generations[this.generations.length - 1];
    activeGen.revisions.push(rev);
    // End current generation
    this.generations.push({ createdRev: rev, revisions: [] });
  }

  /**
   * Finds the latest revision <= targetRev
   */
  findRevision(targetRev: number): Revision | null {
    for (let i = this.generations.length - 1; i >= 0; i--) {
      const gen = this.generations[i];
      for (let j = gen.revisions.length - 1; j >= 0; j--) {
        if (gen.revisions[j].main <= targetRev) {
          return gen.revisions[j];
        }
      }
    }
    return null;
  }

  /**
   * Prunes revisions strictly older than compactRev
   */
  compact(compactRev: number): boolean {
    for (const gen of this.generations) {
      gen.revisions = gen.revisions.filter((r) => r.main >= compactRev);
    }
    this.generations = this.generations.filter((g) => g.revisions.length > 0);
    return this.generations.length > 0;
  }
}

// ==========================================
// 2. Persistent Bbolt Storage Simulation
// ==========================================

export interface KeyValueProto {
  key: string;
  value: string | null; // null represents Tombstone
  createRevision: Revision;
  modRevision: Revision;
  version: number;
}

export class BboltBackend {
  // Simulates B+Tree where primary key is revToString(rev)
  private diskStore: Map<string, KeyValueProto> = new Map();
  public freePageCount: number = 0;

  put(rev: Revision, kv: KeyValueProto): void {
    this.diskStore.set(revToString(rev), kv);
  }

  get(rev: Revision): KeyValueProto | undefined {
    return this.diskStore.get(revToString(rev));
  }

  compact(compactRev: number): number {
    let purged = 0;
    for (const [revKey] of this.diskStore) {
      const mainRev = parseInt(revKey.split('_')[0], 10);
      if (mainRev < compactRev) {
        this.diskStore.delete(revKey);
        this.freePageCount++;
        purged++;
      }
    }
    return purged;
  }

  size(): number {
    return this.diskStore.size;
  }
}

// ==========================================
// 3. Complete etcd v3 MVCC Server
// ==========================================

export class EtcdV3Server {
  private currentMainRev: number = 0;
  private treeIndex: Map<string, KeyIndex> = new Map(); // In-memory B-Tree
  private bbolt: BboltBackend = new BboltBackend();     // On-disk B+Tree
  private compactRev: number = 0;

  put(key: string, value: string): { revision: Revision; prevKv: KeyValueProto | null } {
    this.currentMainRev++;
    const rev: Revision = { main: this.currentMainRev, sub: 0 };

    let keyIdx = this.treeIndex.get(key);
    let prevKv: KeyValueProto | null = null;
    let createRev = rev;
    let version = 1;

    if (!keyIdx) {
      keyIdx = new KeyIndex(key, rev);
      this.treeIndex.set(key, keyIdx);
    } else {
      const latestRev = keyIdx.findRevision(this.currentMainRev);
      if (latestRev) {
        prevKv = this.bbolt.get(latestRev) || null;
        if (prevKv && prevKv.value !== null) {
          createRev = prevKv.createRevision;
          version = prevKv.version + 1;
        }
      }
      keyIdx.put(rev);
    }

    const proto: KeyValueProto = {
      key,
      value,
      createRevision: createRev,
      modRevision: rev,
      version,
    };

    this.bbolt.put(rev, proto);
    return { revision: rev, prevKv };
  }

  get(key: string, targetRev?: number): KeyValueProto | null {
    const queryRev = targetRev ?? this.currentMainRev;
    if (queryRev < this.compactRev) {
      throw new Error(`ErrCompacted: Revision ${queryRev} has been compacted (current compactRev: ${this.compactRev})`);
    }

    const keyIdx = this.treeIndex.get(key);
    if (!keyIdx) return null;

    const target = keyIdx.findRevision(queryRev);
    if (!target) return null;

    const record = this.bbolt.get(target);
    if (!record || record.value === null) {
      return null; // Tombstone or not found
    }

    return record;
  }

  delete(key: string): { revision: Revision } {
    this.currentMainRev++;
    const rev: Revision = { main: this.currentMainRev, sub: 0 };
    const keyIdx = this.treeIndex.get(key);

    if (keyIdx) {
      keyIdx.tombstone(rev);
      const proto: KeyValueProto = {
        key,
        value: null, // Tombstone marker
        createRevision: rev,
        modRevision: rev,
        version: 0,
      };
      this.bbolt.put(rev, proto);
    }

    return { revision: rev };
  }

  compact(revision: number): { purgedRevisions: number } {
    this.compactRev = revision;
    const purged = this.bbolt.compact(revision);

    // Prune in-memory treeIndex
    this.treeIndex.forEach((keyIdx, key) => {
      const alive = keyIdx.compact(revision);
      if (!alive) this.treeIndex.delete(key);
    });

    return { purgedRevisions: purged };
  }
}

// ==========================================
// 4. Execution & Verification Harness
// ==========================================

export function runEtcdDemo() {
  console.log("=== ARCHITECTURE STUDY: etcd v3 MVCC & Bbolt Engine ===");

  const server = new EtcdV3Server();

  console.log("\n[1] Executing Sequential Mutations (Incrementing Revisions):");
  const r1 = server.put("/registry/pods/default/nginx", '{"status":"Pending"}');
  console.log(`  Put v1 -> Revision: ${revToString(r1.revision)}`);

  const r2 = server.put("/registry/pods/default/nginx", '{"status":"Running","ip":"10.244.0.5"}');
  console.log(`  Put v2 -> Revision: ${revToString(r2.revision)}`);

  const r3 = server.put("/registry/services/default/web", '{"port":8080}');
  console.log(`  Put v3 -> Revision: ${revToString(r3.revision)}`);

  console.log("\n[2] Executing Point-in-Time Historical Reads (Time Travel):");
  const currentVal = server.get("/registry/pods/default/nginx");
  console.log(`  Latest Value: ${currentVal?.value} (modRev: ${revToString(currentVal!.modRevision)})`);

  const historicalVal = server.get("/registry/pods/default/nginx", 1);
  console.log(`  Historical Value at Rev 1: ${historicalVal?.value} (modRev: ${revToString(historicalVal!.modRevision)})`);

  console.log("\n[3] Deleting Key (Tombstone Generation):");
  const delRes = server.delete("/registry/pods/default/nginx");
  console.log(`  Deleted nginx at Revision: ${revToString(delRes.revision)}`);
  console.log(`  Read after deletion: ${server.get("/registry/pods/default/nginx")}`);

  console.log("\n[4] Executing etcdctl Compact at Revision 3:");
  console.log(`  Total Bbolt records before compaction: ${server['bbolt'].size()}`);
  const compRes = server.compact(3);
  console.log(`  Purged ${compRes.purgedRevisions} historical records. Bbolt freelist pages: ${server['bbolt'].freePageCount}`);

  try {
    server.get("/registry/pods/default/nginx", 1);
  } catch (err: any) {
    console.log(`  Attempt to read Rev 1 after compact: "${err.message}" (Expected Error)`);
  }

  console.log("\n=== etcd MVCC Simulation Complete. Invariants Verified. ===");
}

runEtcdDemo();

8. Real-World Case Studies

8.1 Kubernetes Control Plane

  • Every Kubernetes API resource is stored in etcd under a hierarchical key path (e.g., /registry/pods/{namespace}/{name}).
  • Kubernetes uses etcd’s resourceVersion parameter—which maps 1:1 to etcd’s 64-bit revision—for Optimistic Concurrency Control (OCC). If two controllers update a Deployment simultaneously, the second write is rejected with an HTTP 409 Conflict if its resourceVersion is stale.

8.2 Rook-Ceph and CoreOS Fleet

  • Uses etcd distributed locks and leases to orchestrate zero-downtime rolling node updates across bare-metal server fleets.

8.3 Vitess (YouTube / Slack)

  • Leverages etcd as a lock server and topology manager, registering tablet locations, shard boundaries, and active primary databases.

9. Edge Cases, Failure Modes & Antipatterns

9.1 The Database Quota Alarm (NOSPACE)

  • Failure Mode: When the bbolt database exceeds quota-backend-bytes (default 2 GB), etcd raises a NOSPACE alarm. All write operations fail with etcdserver: mvcc: database space exceeded.
  • Operational Antipattern: Running etcdctl compact frees revisions in treeIndex but does not shrink the file on disk. Operators must follow with etcdctl defrag to rewrite the bbolt file and then manually execute etcdctl alarm disarm.

9.2 Lease KeepAlive Starvation under Network Jitter

  • Failure Mode: When high network load or CPU starvation delays client heartbeat renewals beyond the lease TTL, etcd revokes the lease, instantly deleting all attached keys (e.g., Kubernetes master leader election locks).
  • Remediation: Configure client lease renewal to send keepalive heartbeats at intervals of (e.g., a 15-second lease sends keepalives every 5 seconds).

9.3 In-Flight Watcher Lag and Memory Inflation

  • Failure Mode: If a client opens a watch from revision 1 on a busy cluster, etcd moves the watcher to the unsynced queue, reading thousands of events from disk. If the client socket reads slowly, etcd buffers events in memory, triggering worker OOM.
  • Remediation: Set --max-request-bytes and configure clients to reject watches requesting revisions older than the cluster’s compacted revision window.

10. Observability, Metrics & Production Telemetry

Metric IdentifierMetric TypeTarget SLODiagnostic Meaning
etcd_server_has_leaderGaugeCritical health signal; 0 indicates split-brain or quorum loss.
etcd_disk_wal_fsync_duration_secondsHistogramDisk I/O latency for WAL writes. Spikes cause leader flapping.
etcd_disk_backend_commit_duration_secondsHistogramBbolt transaction commit latency to NVMe SSDs.
etcd_mvcc_db_total_size_in_bytesGaugeTriggers alert when approaching database quota threshold.
etcd_network_client_grpc_received_bytes_totalCounterDynamicMonitors ingress traffic spikes from misbehaving controllers.

11. Guided Hands-On Lab / Engineering Exercises

Lab 1: Implement an MVCC Range Scanner with Compact Guards

  • Objective: Extend EtcdV3Server with rangeScan(startKey, endKey, revision).
  • Deliverable: Verify that scanning across a key range returns only the latest non-tombstone revision for each distinct key, throwing ErrCompacted if the requested revision is older than compactRev.

Lab 2: Simulate Kubernetes Optimistic Concurrency Control (OCC)

  • Objective: Implement a conditional transaction: Txn().If(ModRev(key) == expectedRev).Then(Put(key, newVal)).Else(Fail).
  • Deliverable: Simulate two competing workers attempting to update the same pod status; prove that exactly one succeeds while the second receives an OCC failure.

Lab 3: Simulate Bbolt Freelist Fragmentation under Churn

  • Objective: Populate 10,000 keys. Update them 10 times each. Compact to latest revision.
  • Deliverable: Measure simulated file page count before and after compaction versus defragmentation.
View Reference Architecture & Worked Solutions (CORE-M35-002)

1. Worked Solution — Lab 1: MVCC Range Scanner with Compaction Guards (rangeScan)

// Extension method on EtcdV3Server:
public rangeScan(startKey: string, endKey: string, targetRev?: number): KeyValueProto[] {
  const queryRev = targetRev ?? this.currentMainRev;
  if (queryRev < this.compactRev) {
    throw new Error(
      `ErrCompacted: Revision ${queryRev} has been compacted (current compactRev: ${this.compactRev})`
    );
  }

  // 1. Traverse in-memory treeIndex keys in sorted lexicographical order [startKey, endKey)
  const matchingKeys = Array.from(this.treeIndex.keys())
    .filter((k) => k >= startKey && (endKey === '' || k < endKey))
    .sort((a, b) => a.localeCompare(b));

  const results: KeyValueProto[] = [];
  for (const key of matchingKeys) {
    const keyIdx = this.treeIndex.get(key)!;
    // 2. Resolve the highest revision <= queryRev in RAM
    const rev = keyIdx.findRevision(queryRev);
    if (!rev) continue;

    // 3. Fetch KeyValueProto from bbolt by composite revision key and skip tombstones
    const kv = this.bbolt.get(rev);
    if (kv && kv.value !== null) {
      results.push(kv);
    }
  }
  return results;
}
  • Verification Invariant: Because treeIndex tracks generations and marks deleted keys with an empty trailing generation after a tombstone revision, findRevision(queryRev) returns the tombstone revision if the key was deleted at or before queryRev (kv.value === null, skipped) or returns the latest live revision prior to deletion when time-traveling to an earlier queryRev.

2. Worked Solution — Lab 2: Kubernetes Optimistic Concurrency Control (OCC) via Txn

// Atomic Compare-And-Swap Transaction on EtcdV3Server:
public txnCompareAndSwap(
  key: string,
  expectedModMainRev: number,
  newValue: string
): { succeeded: boolean; revision: Revision; currentKv: KeyValueProto | null } {
  const currentKv = this.get(key);
  const actualModRev = currentKv ? currentKv.modRevision.main : 0;

  // If(ModRev(key) == expectedModMainRev) -> Then(Put)
  if (actualModRev === expectedModMainRev) {
    const { revision } = this.put(key, newValue);
    return { succeeded: true, revision, currentKv: this.get(key) };
  }

  // Else -> Return current state without mutating (Kubernetes API returns HTTP 409 Conflict)
  return {
    succeeded: false,
    revision: { main: this.currentMainRev, sub: 0 },
    currentKv,
  };
}
  • Two-Worker Contention Trace:
    1. Both Controller Worker A and Controller Worker B read /registry/pods/default/nginx at modRevision.main = 2 (resourceVersion: "2").
    2. Worker A submits txnCompareAndSwap("/registry/pods/default/nginx", 2, '{"phase":"Running","node":"node-1"}'). Because actualModRev (2) === 2, the Txn succeeds and commits at modRevision.main = 3.
    3. Worker B concurrently submits txnCompareAndSwap("/registry/pods/default/nginx", 2, '{"phase":"Running","node":"node-2"}'). Because actualModRev is now 3 !== 2, the Else branch executes with zero disk mutation, returning succeeded: false and the updated modRevision: 3 so kube-apiserver returns 409 Conflict and Worker B re-fetches before retrying.

3. Worked Solution — Lab 3: Bbolt Freelist Fragmentation vs. Defragmentation Math

  • Step 1 — Populate Keys & Apply Updates per Key ( values):
    • Initial load ( Puts) + updates per key ( Puts) generates distinct MVCC revisions in bbolt.
    • Using Section 6.1’s leaf-page packing ( entries of per leaf page):
  • Step 2 — After etcdctl compact 110000 (Before Defrag):
    • Compaction purges the superseded revisions (), leaving only the latest live revisions ().
    • Freelist Count: pages () are added to bbolt’s internal freelist.
    • OS File Size: Still ! Because bbolt memory-maps the file (mmap) and retains free pages for future Copy-on-Write page allocations, stat(2) / etcd_mvcc_db_total_size_in_bytes does not drop after compaction alone.
  • Step 3 — After etcdctl defrag:
    • Defragmentation opens a fresh temporary bbolt file, copies only the live key-value revisions sequentially into leaf pages, flushes, and atomically renames the file:

12. Comprehensive Summary & Decision Framework

Coordination Technology Comparison Matrix

Architectural Featureetcd v3Apache ZooKeeperHashiCorp Consul
Consensus AlgorithmRaftZab (Atomic Broadcast)Raft
Data ModelFlat key-value with revisionsHierarchical filesystem (znodes)Key-value + Service Catalog
Historical Revision MVCCNative Built-in (64-bit rev)None (Current state only)None
Read Consistency ModeReadIndex / LeaseReadRead from follower (may lag)Stale / Consistent / ConsistentRead
Recommended DeploymentCloud-native K8s clustersHadoop / Kafka clustersMulti-cloud service mesh

Production Operations Runbook

  1. Dedicated NVMe SSD: Always place the etcd WAL directory (--data-dir) on a dedicated physical NVMe SSD with sustained .
  2. Automated Compaction: Set --auto-compaction-retention=1h to ensure historical revisions are pruned continuously.
  3. Scheduled Defragmentation: Run etcdctl defrag during maintenance windows on one member at a time to reclaim physical disk space without cluster downtime.
  4. Cluster Sizing: Standardize on 3 or 5 members. A 3-node cluster tolerates 1 node failure; a 5-node cluster tolerates 2 node failures. Never deploy an even number of voting nodes.

13. Authoritative References & Version Context

MASTERY GATE · STEP 70 OF 94

Knowledge check & lesson completion

Score 4 / 4 (100%) on this check to mark this lesson Completed ✓, advance your position on the Master Spine, and add these concepts to your Spaced-Repetition Review Deck.

1. Why does etcd v3 use an in-memory B-Tree (treeIndex) in combination with an on-disk B+Tree (bbolt) rather than storing user keys directly as primary keys in bbolt?
2. In etcd's watchableStore engine, how does the server handle a lagging gRPC Watch client whose requested start_revision has fallen behind the current MVCC revision without blocking active write transactions?
3. What occurs in an etcd cluster when the database file size exceeds the configured storage quota (typically 2 GB or 8 GB)?
4. Why does running 'etcdctl compact ' not immediately shrink the physical size of the etcd database file on the host file system?
PERSONAL NOTEBOOK

Chapter Notes

Personal engineering insights and observations saved to this browser.

0 notes
Tip: You can also highlight text in the article above to quote directly.

Loading personal notes...