AlgoMaster Logo

Lab: Watch the Metadata Quorum Work

24 min readUpdated September 13, 2026
Listen to this chapter
Unlock Audio

In this lab, you run a three-node KRaft cluster and inspect its metadata quorum. You observe leader election, follower catch-up, broker registration and fencing, and the effects of losing a majority. You use only the tools bundled in the apache/kafka:4.3.1 image.

Use this fixed lab setup: the cluster has voters 1, 2, and 3, every node runs both roles, and the only topic you create before the failure phases is orders.placed with three partitions and replication factor 3. That gives one topic record, three partition records, one leader epoch before the failover and a larger one after it, and log end offsets that must agree across the voters once lag drains.

Learning Objectives

  • Read LeaderId, LeaderEpoch, HighWatermark, and each voter's log end offset and lag, and explain how they relate.
  • Decode the __cluster_metadata-0 log and locate the records a topic creation appends.
  • Stop the active controller, record the election result, and verify that record traffic works after recovery.
  • Observe a restarted voter’s role and verify that it catches up.
  • Trace a broker through registration, fencing, and unfencing in the controller's log.
  • Show that a single surviving voter cannot commit a topic creation, and record the client's error.
  • Locate a metadata snapshot on disk, decode it, and relate it to the log.

Time and Environment

  • Setup and Phase 1: 15 minutes
  • Phases 2 and 3: 40 minutes
  • Phases 4 and 5: 35 minutes
  • Phase 6 and report: 30 minutes

Environment: E2, the three combined broker-and-controller nodes kafka-1, kafka-2, and kafka-3. Docker and Docker Compose run on your host. Every Kafka tool runs inside a container through docker exec with a bootstrap address of kafka-1:9092, kafka-2:9092, or kafka-3:9092; always bootstrap through a node that is running. Keep the Compose file, decoded logs, observations, and report on the host. Run docker compose commands from kafka-labs/cluster/ and everything else from kafka-labs/metadata-quorum/.

Safety Boundaries

  • Work only in the kafka-labs/cluster/ compose project. Do not point any command at another cluster.
  • Stop nodes with docker compose stop and restart them with docker compose start. Do not run docker compose down -v until Cleanup; it deletes the metadata logs you are studying.
  • Never run kafka-storage.sh format, and never delete or edit files under /tmp/kraft-combined-logs. Read them only.
  • Stop two nodes at once only in Phase 5, and restore both before continuing.
  • Save observations before each stop; a stopped node cannot answer commands.

Project Layout

Create:

Setup: Start the Cluster

Reuse the cluster compose file from the brokers module lab after completing that lab’s cleanup. Start with fresh lab storage and only the three original services. Before the first startup, add KAFKA_METADATA_LOG_MAX_SNAPSHOT_INTERVAL_MS: 60000 under each service’s environment to shorten the snapshot wait for Phase 6. Create the observations/ and decoded/ directories before saving outputs. From kafka-labs/cluster/, start all three nodes:

Run the topic-list command below against kafka-1:9092. With fresh lab storage, expect an empty result and exit status 0. A successful listing confirms that this endpoint answers requests; inspect the quorum and brokers before the failure phases.

Host clients would use localhost:19092,localhost:29092,localhost:39092; this lab needs none.

Phase 1: Read the Baseline Quorum

Describe the quorum status, then the per-replica replication view, and save both outputs as observations/01-baseline.txt:

Write down, in your own table: the leader id, the leader epoch, the high watermark, and for each of nodes 1, 2, and 3 the LogEndOffset, Lag, and Status columns.

What you should see: CurrentVoters lists exactly 1, 2, and 3. One node has Status Leader and the other two are Follower; that node's id matches LeaderId. Once the followers have caught up, all three LogEndOffset values are equal and every Lag is 0. The high watermark is close to those offsets; if it is smaller, re-run the command and watch it catch up. The log is not empty on a fresh cluster, because the quorum committed broker registrations and feature records during startup. Record whether CurrentObservers lists anything.

Phase 2: Create a Topic and Find Its Records

Create the topic:

Immediately re-run describe --replication and describe --status, then describe the topic. Save all three outputs as observations/02-after-create.txt:

Now read the metadata log itself. Each node stores the metadata partition under its log directory. Decode the available segments on kafka-1. Find the TOPIC_RECORD for orders.placed, note its topicId, then copy that record and the three PARTITION_RECORD entries with the same ID into decoded/topic-records.txt:

Run the same decode on kafka-2, saving it under a different filename, and compare the matching topic records. Filtering by record type alone would also include other topics. On fresh lab storage these segments should still contain the creation records; if they do not, record that retention removed the history rather than guessing a segment filename.

What you should see: Compare each controller’s log end and the high watermark with Phase 1. Decode the topic-creation records to identify their offsets. Other metadata work can also advance the log, so the total offset change need not equal the number of creation records. decoded/topic-records.txt holds one TOPIC_RECORD naming orders.placed with a topicId, and three PARTITION_RECORD entries with partitionId 0, 1, and 2, each carrying replicas, isr, and leader. The topicId matches the TopicId in the topic description, and each partition's leader and isr match its row there. The decode from kafka-2 contains the same records at the same offsets, because followers copy the leader's log rather than composing their own. Confirm those offsets sit below the new high watermark.

Phase 3: Stop the Active Controller

Take the LeaderId from Phase 2 and call that node L. Note the leader epoch. Then stop L and record the time:

Replace L with the actual id. From a surviving node, describe the status until it reports a leader, and note when it does. The examples use kafka-2; substitute if kafka-2 was L:

Publish three keyed events, then read them back:

Describe the topic through kafka-2 as well. Then bring L back and watch it rejoin:

Run describe --replication through kafka-2 several times over the next minute. Save every output from this phase, with the time you ran each command, as observations/03-failover.txt. You are timing the response to a graceful stop, the election, and the old leader’s return. The optional abrupt-stop experiment lets you compare failure detection without a graceful resignation.

What you should see: the status reports a different LeaderId and a LeaderEpoch larger than the one you noted; record the exact value, since it can grow by more than one if an election attempt failed. The producer exits without error and the consumer prints three records with keys ord-1042, ord-1043, and ord-1044. The topic description shows one of the two running nodes leading each partition with an Isr of two ids, because L also carried a broker role in this combined-mode cluster; that ISR change is partition metadata, not quorum leadership. After L starts, its row normally returns with Status Follower, a LogEndOffset that catches up with the leader’s and Lag that reaches 0. You may miss the brief catch-up interval; record what you observe. Restarting L does not force leadership back to it; note any further elections.

Phase 4: Watch a Broker Register, Get Fenced, and Unfence

Use kafka-3 unless it is the current leader, in which case use kafka-2 and substitute throughout. The current leader's container log is where the controller records its decisions.

Stop kafka-3, wait at least 60 seconds, then describe the topic and the replication view through kafka-1:

Start it again and record the time, then pull the controller's log lines about broker 3, with kafka-L replaced by the current leader's container:

After a minute, re-run describe --replication and the topic description. Save everything as observations/04-fencing.txt.

What you should see: while kafka-3 is stopped, every partition's Isr drops node 3 while Replicas still lists it; assignment and synchronization are separate facts. In the replication view, node 3's LastFetchTimestamp stops advancing and its Lag grows as the remaining quorum commits ISR shrink records without it. After the restart, the controller log shows, in order, a line registering broker 3 again, its fenced state, and a line granting its unfence request because the broker caught up with the offset of its own registration record. Then Isr returns to three ids on every partition. Record both timestamps: unfencing is a broker-level milestone, and rejoining the ISR is a per-partition milestone that can arrive later.

The unfence waited for evidence carried in the broker's own heartbeats: that it had applied the metadata log at least as far as the record that registered it, and that its local startup work was done. A reachable port did not count.

Phase 5: Lose the Majority

Keep only kafka-1 running; start it first if needed.

Wait 30 seconds, then attempt a metadata change through the survivor. Record the full error text and how long the command took:

Also run describe --status through kafka-1 and record whatever it returns. Then restore the quorum:

First verify that a leader exists and all three voters have caught up. Then list topics again, allowing for broker metadata propagation. If orders.cancelled exists, describe it and verify its partition count and replication factor. If it remains absent, retry creation and verify the result. Save all outputs as observations/05-quorum-loss.txt.

What you should see: with one voter of three, the creation does not succeed. The error names a timeout, because the request waits for a controller that cannot commit. The status command may also time out or show no usable leader; record the exact behavior. After the two nodes return, verify quorum recovery and inspect the topic list. A timeout does not establish whether creation committed. Allow for metadata propagation; if the topic exists, inspect its definition, and if it remains absent, retry creation.

Phase 6: Find and Decode a Snapshot

List the metadata directory on kafka-1 and look for files ending in .checkpoint; each name carries the snapshot’s end offset and epoch. Kafka 4.3 defaults to a one-hour snapshot interval when committed metadata lies beyond the latest snapshot; Setup reduces it to one minute. A size threshold can trigger one sooner. If no checkpoint exists, confirm the Setup override and wait after a committed metadata change. Decode an existing checkpoint by replacing SNAPSHOT_FILE.checkpoint with its filename, then read the quorum state file next to it. See the Kafka snapshot configuration for the two triggers.

If a later .checkpoint exists, decode it too and compare record counts. Save the listing and the quorum state as observations/06-snapshot.txt.

What you should see: the directory holds the .log, .index, and .timeindex files of the metadata log plus the .checkpoint files written so far. Its decoded contents are state records such as the topic, partition, and feature-level records, not the full sequence of earlier changes; a snapshot describes the metadata image at its end offset, and the replica next fetches log records starting at that end offset. The quorum state file names the current leader epoch, which equals the latest LeaderEpoch, and the vote this node persisted. That file is why a restarted controller cannot vote twice in one epoch.

Required Deliverables

  • The six observation files, each containing raw command output with the time each command ran.
  • decoded/topic-records.txt and decoded/snapshot.txt.
  • A failover timeline for Phase 3: stop, first status with the new leader, produce and consume success, restart, and lag reaching 0.
  • A fencing timeline for Phase 4: stop, ISR shrink, restart, registration line, unfence line, ISR restored.
  • report.md answering these questions:
    1. Why did the leader epoch increase in Phase 3, and what does the exact size of the increase tell you?
    2. What does the successful produce-and-consume check after the election establish? Why does it not prove uninterrupted service during the election?
    3. What was the unfence in Phase 4 waiting for, and why is a reachable port insufficient?
    4. In Phase 5, why could the surviving node not commit the topic creation even though it held a full copy of the metadata log?
    5. What would have happened in Phase 3 and Phase 5 with only one controller in the voting set?
    6. How do the snapshot's end offset and the log's offsets fit together when a replica catches up?

Acceptance Checks

  • 01-baseline.txt shows CurrentVoters of 1, 2, and 3, one Leader row, and two Follower rows.
  • 02-after-create.txt records the log-end and high-watermark changes. The identified creation offsets lie below the later high watermark; explain any additional metadata records.
  • decoded/topic-records.txt contains one TOPIC_RECORD naming orders.placed and three PARTITION_RECORD entries.
  • 03-failover.txt shows a new LeaderId, a larger LeaderEpoch, three consumed records, and a final replication view with the returned node caught up. Record its role and explain any further election.
  • 04-fencing.txt shows a two-id ISR while the selected node remains offline, a log line granting its unfence, and a three-id ISR afterwards.
  • 05-quorum-loss.txt contains the failed request and a post-recovery topic description, showing either the existing topic or a successful retry.
  • 06-snapshot.txt lists at least one .checkpoint file, and decoded/snapshot.txt is not empty.
  • All six report questions cite evidence from the observation files.

Grading Rubric

AreaPoints
Baseline and post-creation quorum readings15
Decoded topic and partition records matched to the topic description15
Failover timeline with epoch and leader evidence20
Fencing timeline with controller log evidence15
Quorum loss error and safe retry15
Snapshot and quorum state interpretation10
Report answers10
Total100

Optional Extensions

  • Stop L with docker compose kill instead of stop and compare how long the new leader takes to appear without a graceful resignation.
  • Decode the metadata log after Phase 4 and find the registration and fencing records for broker 3.
  • Alter retention.ms on orders.placed with kafka-configs.sh and locate the resulting configuration record in the decoded log.
  • In a fresh run after Cleanup, omit the snapshot-interval override and compare the default interval with this lab’s one-minute setting. Set the environment before creating containers; recreating these lab containers removes their writable-layer logs.

Cleanup

From kafka-labs/cluster/, stop and remove the cluster containers and any lab volumes:

Verify that nothing remains:

The listing must contain no containers from this lab’s Compose project. Keep kafka-labs/metadata-quorum/ with your observations and report.

Summary

The quorum status and replication views turned the module's vocabulary into numbers you could check: one voter led, the others fetched its log, and the leader advanced the high watermark after learning that replication met the commitment rules. Creating a topic appended a topic record and three partition records that you read back from disk on two nodes at the same offsets, which is what a replicated log means in practice.

After stopping the combined node, you observed a new metadata leader and a higher epoch. You then verified that producers and consumers could work after recovery. You checked the returning node’s role and verified that it caught up. You also traced startup fencing and unfencing, then observed how losing the voter majority prevents new metadata commits. The snapshot and quorum state files are the durable pieces that let those guarantees survive a restart.