Explain how --initial-controllers and the bootstrap.checkpoint mechanism work for first-boot quorum bootstrap in newer Kafka versions, and how that differs from controller.quorum.voters.
answer
- Static = controller.quorum.voters (frozen)
- KIP-853 dynamic = bootstrap.servers + log-based membership
- --standalone / --initial-controllers at format time
- bootstrap.checkpoint = genesis snapshot at offset -1
- directory.id identifies a voter's storage
- kafka-metadata-quorum add/remove-controller at runtime
basics
~20 sOlder KRaft uses a static controller.quorum.voters list. Newer Kafka (KIP-853) supports dynamic quorums: at format time you pass --initial-controllers (or --standalone) so the formatter writes a bootstrap.checkpoint snapshot recording the initial voter set, and the quorum bootstraps from that instead of a static config line.
solid answer
~50 sClassic KRaft pins the voter set with a static config: controller.quorum.voters=id@host:port,.... It's simple but the membership is frozen — you can't safely add or remove controllers at runtime. KIP-853 (dynamic quorums, GA in Kafka 3.9+/4.0) replaces that with controller.quorum.bootstrap.servers for discovery plus an on-disk record of who the initial voters are. At format time you choose the seed: --standalone for a single-controller start, or --initial-controllers "id@host:directory.id,..." listing each controller's id, endpoint, and its directory.id. The formatter writes a bootstrap.checkpoint file (a Raft snapshot at offset -1) into the metadata log dir encoding that initial voter set. On first boot the controllers read this checkpoint to form the genesis quorum; thereafter membership lives in the metadata log itself and is changed with kafka-metadata-quorum add-controller/remove-controller. directory.id (in meta.properties) uniquely identifies each controller's storage so voters are tracked by storage identity, enabling safe reassignment.
go deeper
Awareness only: there's a static voters list and a newer dynamic mode; not expected to detail it.
Know that newer Kafka can add/remove controllers and that format takes --standalone/--initial-controllers.
Explain bootstrap.checkpoint, directory.id, discovery vs membership, and static-vs-dynamic trade-offs.
Advise on choosing static vs dynamic, migration paths, version gating (KIP-853, metadata.version), and operational quorum reconfiguration.
## The problem with static quorums In the original KRaft design every node lists the controllers explicitly: ``` controller.quorum.voters=1@host1:9093,2@host2:9093,3@host3:9093 ``` This is a **static** membership: the set of voters is baked into config on every node. It works, but it's rigid — you cannot cleanly **add or remove a controller** at runtime, because all nodes must agree on the voter set and there is no protocol record of membership changes. Replacing a dead controller meant a careful, error-prone reconfiguration. ## KIP-853: dynamic quorums **KIP-853** introduces **dynamic controller membership**, generally available around Kafka **3.9 / 4.0**. Two pieces change: 1. **Discovery vs. membership are separated.** Instead of `controller.quorum.voters`, dynamic clusters use: ``` controller.quorum.bootstrap.servers=host1:9093,host2:9093,host3:9093 ``` This is only a list of **endpoints to contact** to find the quorum — it is *not* the authoritative voter set. The real voter set lives **inside the metadata log** as records and can change over time. 2. **The genesis voter set is recorded at format time** via a snapshot rather than config. ## directory.id With dynamic quorums, each storage directory gets a **`directory.id`** (a UUID written into `meta.properties` by format). A controller voter is identified by the pair **(node id, directory id)**. Tracking storage identity — not just node id — lets the quorum distinguish a controller that was rebuilt on fresh disks from the original, which is essential for safe add/remove. ## --standalone and --initial-controllers At format time you declare the seed quorum: - **`--standalone`** (`-s`): format a single controller as a one-node initial quorum (you grow it later with add-controller). - **`--initial-controllers`** (`-I`): provide the full initial voter set explicitly, e.g. ``` bin/kafka-storage.sh format -t $CLUSTER_ID -c controller.properties \ --initial-controllers "0@host0:9093:<dir-uuid-0>,1@host1:9093:<dir-uuid-1>,2@host2:9093:<dir-uuid-2>" ``` Each entry is `id@host:port:directory.id`. You format every initial controller with the **same** `--initial-controllers` string so they all agree on the genesis set. - **`--no-initial-controllers`**: format a node that will *join* an existing dynamic quorum later (it does not seed membership). ## bootstrap.checkpoint For a dynamic quorum, the formatter writes a **`bootstrap.checkpoint`** file into the metadata log directory. This is a **Raft snapshot at the special offset `-1`** (the "bootstrap" snapshot, sometimes shown as `00000000000000000000-0000000000.checkpoint`-style genesis) that encodes the **initial voter set** chosen by `--standalone`/`--initial-controllers`, plus a bootstrap metadata version. On **first boot**, the controllers read `bootstrap.checkpoint` to establish the genesis quorum and the initial **metadata.version** feature level. After that, all membership changes are appended to the metadata log and replicated by Raft; the checkpoint is no longer the source of truth for membership. ## Runtime membership changes Once bootstrapped dynamically you manage controllers with: ``` bin/kafka-metadata-quorum.sh --command-config ... add-controller bin/kafka-metadata-quorum.sh ... remove-controller --controller-id <id> --controller-directory-id <dir> ``` These append membership records to the metadata log — no rolling config edit across the fleet. ## Static vs dynamic — how to tell - **Static:** `controller.quorum.voters` set; format does **not** need `--initial-controllers`; no `bootstrap.checkpoint` voter set; membership frozen. - **Dynamic:** `controller.quorum.bootstrap.servers` set; format uses `--standalone`/`--initial-controllers`; `bootstrap.checkpoint` written; membership mutable via kafka-metadata-quorum. You should not mix both styles for the same cluster. ## Edge cases - Formatting initial controllers with mismatched `--initial-controllers` strings produces inconsistent genesis quorums that won't converge. - The directory.id must match what you list; if you wipe a controller's disk it gets a new directory.id and must be re-added as a new voter. - Migrating an existing static cluster to dynamic membership is a deliberate, version-gated operation, not automatic.
- What is the role of directory.id in a dynamic quorum?It's a UUID in meta.properties identifying a controller's specific storage. Voters are tracked by (node.id, directory.id), so a controller rebuilt on fresh disks is treated as a distinct voter, which makes runtime add/remove safe and unambiguous.
- After dynamic bootstrap, where does the authoritative voter set live?Inside the __cluster_metadata log as replicated membership records — not in config and no longer in bootstrap.checkpoint. controller.quorum.bootstrap.servers is only used for discovery; runtime changes go through kafka-metadata-quorum add/remove-controller.
- Why can't you add a controller at runtime with the classic static quorum?Static membership is fixed in controller.quorum.voters on every node with no protocol-level membership-change records, so the quorum has no safe way to agree on a new voter set without coordinated config changes and restarts.
saying these in an interview costs you the question
- Claiming --initial-controllers is required for all KRaft clusters (it's specific to dynamic/KIP-853).
- Saying controller.quorum.bootstrap.servers is the authoritative voter set (it's only for discovery).
- Confusing bootstrap.checkpoint with ordinary metadata snapshots (it encodes the genesis voter set at offset -1).
- Thinking you can freely add/remove controllers with a static controller.quorum.voters list.
- Ignoring directory.id and identifying voters by node.id alone.