Failure-drill playbook
Break the lab cluster on purpose, read what Kafka tells you, repair it, and write it up. Each drill leaves you with one real story: what went wrong, what you decided, and what you changed.
These drills are assembled from commands run in the earlier labs. They were not run as timed drills. Outputs are copied from the modules. Where a step uses something no module shows, it carries the tag unverified and the words "not run in the labs".
How to use this page
- One drill takes 30 to 60 minutes. Do one a week, or several in a day. The times on each card are estimates, not measurements.
- Run the three health checks (from module 09) before you start and after you finish. Output means something is wrong. No output means healthy.
- Predict first. Write down what you expect, then break the cluster. A surprise is the useful part.
- Answer the questions before you repair. Then use the write-up builder at the bottom. It saves nothing and sends nothing.
- If the cluster is left broken, run
docker compose downand thendocker compose up -din the main folder. This removes the container data, so topics from earlier labs are gone and some drills need their setup again.
Helpers and health checks
Every drill uses these. Paste them into each new terminal.
k() { docker exec -i kafka-1 /opt/kafka/bin/"$@"; }
B=kafka-1:19092
d() { k kafka-topics.sh --bootstrap-server $B --describe --topic $1 | tail -1; }
# the three health checks (module 09, step 1)
k kafka-topics.sh --bootstrap-server $B --describe --unavailable-partitions
k kafka-topics.sh --bootstrap-server $B --describe --under-min-isr-partitions
k kafka-topics.sh --bootstrap-server $B --describe --under-replicated-partitionsModule 09 found a real leftover with the second check: the topic no-quorum, with one replica, appears under the minimum. You will see it after drill 5. If broker 1 is stopped, the helpers cannot reach the cluster: use another broker as shown in drill 5.
All drills
| # | Drill | Level | Modules | Min | Done |
|---|---|---|---|---|---|
| 1 | Stop a follower: clean stop or kill | 1 | 04 | 30 | 0/6 |
| 2 | Below the in-sync minimum with acks=all | 1 | 02, 04 | 25 | 0/6 |
| 3 | Leader failure and leader epochs | 2 | 04 | 35 | 0/6 |
| 4 | Unclean election and lost records | 3 | 04 | 45 | 0/6 |
| 5 | Lose the controller majority | 3 | 05 | 40 | 0/6 |
| 6 | Rebalance and a dead group member | 1 | 03 | 35 | 0/6 |
| 7 | Lag and a stuck partition | 2 | 03, 09 | 35 | 0/6 |
| 8 | Hanging transaction and read_committed | 2 | 06 | 45 | 0/6 |
| 9 | Connect task failure and a dead-letter topic | 2 | 08, 03 | 50 | 0/6 |
| 10 | An incompatible schema change | 1 | 08 | 35 | 0/6 |
| 11 | A noisy client and quotas | 2 | 09, 02 | 40 | 0/6 |
| 12 | Adding partitions moves a key | 1 | 09, 01, 02 | 30 | 0/6 |
| 13 | The disk fills | 2 | 09, 01 | 50 | 0/6 |
| 14 | MirrorMaker failover to the second cluster | 3 | 10, 06, 03 | 60 | 0/6 |
| 15 | Secured cluster: password, truststore, ACL | 2 | 11 | 45 | 0/6 |
Level 1 is easy. Level 2 needs two or three steps and some waiting. Level 3 changes more than one broker or needs earlier drills. Your progress is saved in this browser only.
The drills
1 Stop a follower: clean stop or kill
Builds on: Module 04.
Goal. See how one stopped follower changes the in-sync set, and measure how long Kafka takes to notice it.
Setup
k kafka-topics.sh --bootstrap-server $B --create --topic ha2 --replica-assignment 3:1:2
seq 1 100 | k kafka-console-producer.sh --bootstrap-server $B --topic ha2
d ha2Break it
# Part A: a clean stop. Start the stopwatch first.
docker stop kafka-2
d ha2unverified Not run in the labs: docker kill kafka-2 as the second part (an abrupt stop, with no goodbye to the controller), and the timings for a follower. The module shows a clean stop of a leader, and a clean stop that leaves the in-sync set after 12 seconds. Predict the kill timing from the 9 second and 30 second values in the cheat sheet, then measure it.
Watch
Before the stop, broker 3 leads and all three are in sync (module 04, step 1):
Topic: ha2 Partition: 0 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2 Elr: LastKnownElr:
Module 02 stopped a follower of another topic. After 12 seconds the describe line showed it had left the in-sync set and moved to the eligible list (Elr):
Topic: strict Partition: 0 Leader: 1 Replicas: 3,1,2 Isr: 1,2 Elr: 3 LastKnownElr:
Run the describe helper every few seconds and note when Isr changes. Also run the first health check from the top of the page.
Questions to answer before you repair
- Which broker leads
ha2now? Did the leader change when you stopped broker 2? Why not? - How many seconds passed before
Isrlost broker 2 after the clean stop? - What did the producer see while broker 2 was down? Try the producer command again.
- Which two timers decide how fast Kafka notices a dead broker, and which one applies to a clean stop?
Repair
docker start kafka-2
# wait 14 seconds
d ha2Expect Isr: 3,1,2 again. The module saw a returning broker copy what it missed and rejoin after about 14 seconds.
Write-up prompts
- What was the in-sync set before, during and after?
- How long did detection take for a clean stop, and for a kill if you ran it?
- Which alert would have told you, and how fast?
2 Below the in-sync minimum with acks=all
Builds on: Module 02, Module 04.
Goal. Make a topic refuse writes on purpose, then learn what acks=1 does in the same state.
Setup
k kafka-topics.sh --bootstrap-server $B --create --topic strict --partitions 1 \
--replication-factor 3 --config min.insync.replicas=3Break it
docker stop kafka-3
# wait 12 seconds
k kafka-topics.sh --bootstrap-server $B --describe --topic strict
echo hello | k kafka-console-producer.sh --bootstrap-server $B --topic strict \
--command-property delivery.timeout.ms=6000 --command-property request.timeout.ms=3000 \
--command-property linger.ms=0Watch
The describe line and the producer log (module 02, step 5):
Topic: strict Partition: 0 Leader: 1 Replicas: 3,1,2 Isr: 1,2 Elr: 3 LastKnownElr: WARN Got error produce response … on topic-partition strict-0, retrying (2 attempts left). Error: NOT_ENOUGH_REPLICAS WARN Got error produce response … on topic-partition strict-0, retrying (1 attempts left). Error: NOT_ENOUGH_REPLICAS WARN Got error produce response … on topic-partition strict-0, retrying (0 attempts left). Error: NOT_ENOUGH_REPLICAS ERROR Error when sending message to topic strict with key: null, value: 5 bytes with error: …
Now send with acks=1 and look at the end offset, before and after broker 3 returns:
echo hello | k kafka-console-producer.sh --bootstrap-server $B --topic strict \
--request-required-acks 1 --command-property enable.idempotence=false
k kafka-get-offsets.sh --bootstrap-server $B --topic strictstrict:0:0 ← while broker 3 is stopped strict:0:1 ← after broker 3 returns
Questions to answer before you repair
- Why did the leader refuse the write? State it with the numbers: how many are in sync, and how many does the topic need?
- The
acks=1producer got no error. Where is the record, and why can no consumer read it yet? - Which setting would let this topic survive one broker failure and still accept writes?
- How long did the producer retry, and which setting set that limit?
Repair
docker start kafka-3
# wait 12 seconds
k kafka-get-offsets.sh --bootstrap-server $B --topic strictThe end offset moves to 1 when the third broker is back in the in-sync set. The permanent fix is a topic setting, not a restart: lower min.insync.replicas to 2 on a 3-copy topic.
Write-up prompts
- What did the application see: errors, silence or both?
- What number made the broker say no?
- What would you change in the topic settings, and what do you give up?
3 Leader failure and leader epochs
Builds on: Module 04.
Goal. Stop a leader, write under the new leader, and read the epoch file that records who led which offsets.
Setup
If you ran drill 1, the topic ha2 already exists. Skip the create line, or use the name ha3 and change it in every command.
k kafka-topics.sh --bootstrap-server $B --create --topic ha2 --replica-assignment 3:1:2
seq 1 100 | k kafka-console-producer.sh --bootstrap-server $B --topic ha2
d ha2
docker exec kafka-1 cat /tmp/kraft-combined-logs/ha2-0/leader-epoch-checkpointBreak it
docker stop kafka-3
d ha2
seq 101 150 | k kafka-console-producer.sh --bootstrap-server $B --topic ha2
docker exec kafka-1 cat /tmp/kraft-combined-logs/ha2-0/leader-epoch-checkpointWatch
Module 04, steps 1 and 2. Before the stop, then after:
Topic: ha2 Partition: 0 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2 Elr: LastKnownElr: 0 1 0 0 ← epoch 0 started at offset 0 Topic: ha2 Partition: 0 Leader: 1 Replicas: 3,1,2 Isr: 1,2 Elr: LastKnownElr: 0 2 0 0 1 100 ← epoch 1 started at offset 100
The module saw broker 1 become leader about one second after the stop. Time yours with the stopwatch.
Questions to answer before you repair
- Which broker leads now, and which epoch is it? How can you tell from the file?
- Broker 3 is stopped. Which offsets does it have, and which does it lack?
- When broker 3 returns, how will it use the epoch file to decide where to start copying?
- Will broker 3 lead again by itself? What does the
preferredelection do?
Repair
docker start kafka-3
# wait 14 seconds
d ha2
k kafka-leader-election.sh --bootstrap-server $B --election-type preferred --topic ha2 --partition 0
d ha2Module 04 saw all 150 records present, Isr: 1,2,3, and leader 3 after the preferred election.
Write-up prompts
- What did clients see in the second after the stop?
- What do the two epoch lines say happened?
- What would you add to monitoring so a leader change is visible without a person running a describe?
4 Unclean election and lost records
Builds on: Module 04.
Goal. Cause a partition to go offline, force a leader that is missing records, and watch the data disappear.
Setup
This is the one drill where two brokers are down for a while. Keep broker 1 running so the controllers keep a majority.
k kafka-topics.sh --bootstrap-server $B --create --topic risky2 --replica-assignment 2:3 \
--config min.insync.replicas=1
seq 1 10 | k kafka-console-producer.sh --bootstrap-server $B --topic risky2Break it
docker stop kafka-3 # broker 3 now misses everything that follows
# wait 8 seconds
seq 11 20 | k kafka-console-producer.sh --bootstrap-server $B --topic risky2
k kafka-get-offsets.sh --bootstrap-server $B --topic risky2
docker stop kafka-2; docker start kafka-3 # the only in-sync broker is gone
# wait 20 seconds
d risky2Watch
The partition is offline although broker 3 runs (module 04, step 4):
risky2:0:20 Topic: risky2 Partition: 0 Leader: none Replicas: 2,3 Isr: Elr: 2 LastKnownElr: 2
Decide, then force the election and read the end offset:
k kafka-leader-election.sh --bootstrap-server $B --election-type unclean --topic risky2 --partition 0
d risky2
k kafka-get-offsets.sh --bootstrap-server $B --topic risky2Successfully completed leader election (UNCLEAN) for partitions risky2-0 Topic: risky2 Partition: 0 Leader: 3 Replicas: 2,3 Isr: 3 risky2:0:10
Write five records, start broker 2, and read its log:
seq 21 25 | k kafka-console-producer.sh --bootstrap-server $B --topic risky2
docker start kafka-2
# wait 16 seconds
docker exec kafka-2 /opt/kafka/bin/kafka-dump-log.sh --print-data-log \
--files /tmp/kraft-combined-logs/risky2-0/00000000000000000000.log | grep -o 'payload: .*'
docker logs kafka-2 2>&1 | grep -i 'risky2-0.*truncat'values on broker 2: 1 2 3 4 5 6 7 8 9 10 21 22 23 24 25 WARN [UnifiedLog partition=risky2-0] Truncating risky2-0 to offset 10 below high watermark 20
Questions to answer before you repair
- Why did Kafka refuse to make broker 3 the leader on its own?
- Which records were lost, and who had acknowledged them? Was the producer told?
- A consumer had committed offset 20 before the election. What does it see now?
- Which two settings would have prevented the loss, and what would each have cost in availability?
Repair
d risky2
k kafka-get-offsets.sh --bootstrap-server $B --topic risky2The lost records cannot be recovered from Kafka. Check that both brokers run and the in-sync set has both. The real repair is a decision: replay values 11 to 20 from the source of truth, and raise min.insync.replicas to 2 on a 3-copy topic.
Write-up prompts
- Write the timeline with offsets: what was acknowledged, when, and by whom?
- Who owns the decision to run an unclean election, and what do they need to know first?
- What is the exact data that was lost, in business terms?
5 Lose the controller majority
Builds on: Module 05.
Goal. Stop two of three controllers and see which operations stop, then restore and read the new epoch.
Setup
Read LeaderId first. The module had node 1 as leader. If yours differs, stop that node and run the tools from another one.
k kafka-metadata-quorum.sh --bootstrap-server $B describe --status
k kafka-metadata-quorum.sh --bootstrap-server $B describe --replicationBreak it
docker stop kafka-1
docker exec -i kafka-2 /opt/kafka/bin/kafka-metadata-quorum.sh --bootstrap-server kafka-2:19092 describe --status
docker exec -i kafka-2 /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-2:19092 \
--create --topic during-failover --partitions 1 --replication-factor 2
# now lose the majority
docker stop kafka-3
docker exec -i kafka-2 timeout 12 /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-2:19092 \
--describe --topic during-failover
docker exec -i kafka-2 timeout 14 /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-2:19092 \
--create --topic no-quorum --partitions 1 --replication-factor 1
docker logs --since 20s kafka-2 2>&1 | grep -i 'election' | tail -1Watch
With one node stopped (module 05, step 4): a new leader in about 1 second, and topic creation works.
LeaderId: 2 LeaderEpoch: 4 ← about 1 second after the stop NodeId LogEndOffset Lag Status 2 5875 0 Leader 1 -1 5876 Follower 3 5875 0 Follower Created topic during-failover.
With two nodes stopped (step 5): both commands ran into the time limit, and the log said:
INFO [RaftManager id=2] Election timed out before receiving sufficient vote responses to become candidate. …
Questions to answer before you repair
- Why did one failure change nothing for clients, and two failures stopped topic creation?
- Why did even
--describehang? Which part of the cluster did the tool ask? - Would a producer that was already writing to a healthy leader have failed? Check the module's mistakes table before you answer.
- Why is the epoch higher after the restore than after the first election?
Repair
docker start kafka-1 kafka-3
# wait 20 seconds
k kafka-metadata-quorum.sh --bootstrap-server $B describe --statusThe module saw LeaderId: 2, LeaderEpoch: 6 and MaxFollowerLag: 0. The failed election attempts each used up an epoch.
Write-up prompts
- What worked, and what did not, with two of three controllers down?
- How would you have found the cause from the logs alone?
- What would you change about controller placement (machines, racks, zones)?
6 Rebalance and a dead group member
Builds on: Module 03.
Goal. Watch partitions move when a member leaves, and compare a clean exit with a kill.
Setup
k kafka-topics.sh --bootstrap-server $B --create --topic events --partitions 6 --replication-factor 3
k kafka-producer-perf-test.sh --topic events --num-records 6000 --record-size 100 \
--throughput -1 --command-property bootstrap.servers=$B
docker exec -d kafka-1 /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server $B \
--topic events --group g-classic --from-beginning --command-property client.id=c1
docker exec -d kafka-2 /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server $B \
--topic events --group g-classic --from-beginning --command-property client.id=c2
# wait 12 seconds
k kafka-consumer-groups.sh --bootstrap-server $B --describe --group g-classic
k kafka-consumer-groups.sh --bootstrap-server $B --describe --group g-classic --stateBreak it
docker exec kafka-2 sh -c "pkill -f 'group g-classic'"
# wait 8 seconds
k kafka-consumer-groups.sh --bootstrap-server $B --describe --group g-classic --membersunverified Not run in the labs: pkill -9 on the consumer (a kill with no goodbye), so the group must wait for the 45 second session timeout. The module shows pkill -9 only for a transaction drill, and shows only the clean stop for a consumer. Restart c2 first, then time how long #MEMBERS takes to drop.
Watch
Before (module 03, step 2): each member owns three partitions, strategy range, state Stable, two members.
GROUP COORDINATOR (ID) ASSIGNMENT-STRATEGY STATE #MEMBERS g-classic kafka-1:19092 (1) range Stable 2
After the clean stop (step 4):
GROUP CONSUMER-ID HOST CLIENT-ID #PARTITIONS g-classic c1-7b00d4f1-… /172.23.0.4 c1 6
Questions to answer before you repair
- Which partitions did the surviving member gain? Did it have to wait for anything?
- Why did the clean stop not need the 45 second session timeout?
- What would a client see during the move: duplicates, a pause, or both?
- The module also ran a group with
group.protocol=consumer. What did the strategy name change to, and who now chooses it?
Repair
docker exec kafka-1 sh -c "pkill -f ConsoleConsumer"; docker exec kafka-2 sh -c "pkill -f ConsoleConsumer"
k kafka-consumer-groups.sh --bootstrap-server $B --describe --group g-classicThe group then shows no active members. Its lag is whatever was written since.
Write-up prompts
- How long was the group without a full assignment?
- What did you expect from a kill, and what did you measure?
- Which consumer settings would you tune for a long-running job per record?
7 Lag and a stuck partition
Builds on: Module 03, Module 09.
Goal. Read lag correctly, find the one partition that does not move, and skip it by moving the group's offset forward.
Setup
unverified Not run in the labs: a consumer that crashes on one bad record. No lab has a consumer with code in it. This drill stands in for the stuck record with lag on a stopped group, and teaches the skip. Drill 9 shows a real poison record in Connect.
# uses topic events and group g-classic from drill 6
docker exec kafka-1 sh -c "pkill -f ConsoleConsumer"; docker exec kafka-2 sh -c "pkill -f ConsoleConsumer"
k kafka-producer-perf-test.sh --topic events --num-records 3000 --record-size 100 \
--throughput -1 --command-property bootstrap.servers=$B
k kafka-consumer-groups.sh --bootstrap-server $B --describe --group g-classicBreak it
# the group is stopped, so the lag stays. Read it for every group:
k kafka-consumer-groups.sh --bootstrap-server $B --describe --all-groupsWatch
Module 03, step 5. The lag adds up to the 3,000 records written while nobody read:
GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG g-classic events 0 1361 1657 296 g-classic events 1 592 928 336 g-classic events 2 934 1230 296 g-classic events 3 592 1184 592 g-classic events 4 1480 2072 592 g-classic events 5 1041 1929 888
Look at the plan first, then move one partition. The module shows --dry-run, --execute and --shift-by -100; the last two lines are the module's forms applied to one partition:
k kafka-consumer-groups.sh --bootstrap-server $B --group g-classic --topic events \
--reset-offsets --to-earliest --dry-run
k kafka-consumer-groups.sh --bootstrap-server $B --group g-classic --topic events:3 \
--reset-offsets --shift-by 1 --dry-run
k kafka-consumer-groups.sh --bootstrap-server $B --group g-classic --topic events:3 \
--reset-offsets --shift-by 1 --executeGROUP TOPIC PARTITION NEW-OFFSET g-classic events 3 0 …
A positive --shift-by and the events:3 execute step were not run in the labs. The module also lists --to-latest, --to-datetime and --to-offset as other targets.
Questions to answer before you repair
- Which partition has the most lag? Is that the same as the partition that is stuck?
- Why must the group have no active members before a reset?
- What do you lose by skipping a record, and who must agree to that?
- Lag in records is not lag in time. What second number would you ask for?
Repair
k kafka-consumer-groups.sh --bootstrap-server $B --describe --group g-classicCheck that CURRENT-OFFSET moved on partition 3 only. Keep the skipped offset in your write-up: someone will ask for that record later. In production the better repair is a dead-letter topic (drill 9).
Write-up prompts
- How did you tell a slow group from a stuck one?
- Which offset did you skip and why was it safe?
- What alert on time since the last commit would have caught this?
8 Hanging transaction and read_committed
Builds on: Module 06.
Goal. Kill a transactional producer mid-transaction and see a read_committed consumer stop at the open transaction until the coordinator aborts it.
Setup
k kafka-topics.sh --bootstrap-server $B --create --topic payments --partitions 1 --replication-factor 3
k kafka-producer-perf-test.sh --topic payments --num-records 30 --record-size 20 --throughput 10 \
--transactional-id pay-1 --transaction-duration-ms 1000 --command-property bootstrap.servers=$B
k kafka-get-offsets.sh --bootstrap-server $B --topic paymentsBreak it
docker exec -d kafka-2 /opt/kafka/bin/kafka-producer-perf-test.sh --topic payments \
--num-records 100000 --record-size 20 --throughput 5 --transactional-id pay-2 \
--transaction-duration-ms 300000 --command-property bootstrap.servers=$B transaction.timeout.ms=40000
# wait 8 seconds
docker exec kafka-2 sh -c "pkill -9 -f 'transactional-id pay-2'"
k kafka-transactions.sh --bootstrap-server $B describe --transactional-id pay-2
for iso in read_uncommitted read_committed; do
k kafka-console-consumer.sh --bootstrap-server $B --topic payments --from-beginning \
--isolation-level $iso --timeout-ms 5000 2>&1 | tail -1
doneWatch
Module 06, step 4. The dead producer wrote 38 records:
CoordinatorId TransactionalId ProducerId ProducerEpoch TransactionState TransactionTimeoutMs … TopicPartitions 3 pay-2 2000 0 Ongoing 40000 payments-0 read_uncommitted: Processed a total of 68 messages read_committed: Processed a total of 30 messages
Wait until 40 seconds after the transaction began, then (step 5):
k kafka-transactions.sh --bootstrap-server $B describe --transactional-id pay-2
k kafka-dump-log.sh --print-data-log --files /tmp/kraft-combined-logs/payments-0/00000000000000000000.log | grep endTxnMarker | tail -1
k kafka-get-offsets.sh --bootstrap-server $B --topic payments… pay-2 2000 … CompleteAbort | offset: 71 … endTxnMarker: ABORT coordinatorEpoch: 0 payments:0:72
Questions to answer before you repair
- Why did the committed reader stop at 30, and not at 68?
- What would happen to a committed reader of this partition if other producers wrote after the open transaction?
- What decided the moment of cleanup, and where did you read that number?
- The 38 aborted records are still in the log. Who can still read them, and for how long?
Repair
k kafka-transactions.sh --bootstrap-server $B listNo manual repair is shown in the labs: you wait for the timeout (here 40 seconds, set by transaction.timeout.ms). The staff-level repair is a shorter timeout and a stable transactional id, so a restarted producer fences the old one.
Write-up prompts
- How long was the committed reader blocked, and by what?
- What is the transaction timeout in your applications, and is it right?
- Which consumers in your system read with the default isolation level?
9 Connect task failure and a dead-letter topic
Builds on: Module 08, Module 03.
Goal. Feed a bad record to a sink connector, watch the task fail with no data delivered, then route the bad record to a dead-letter topic.
Setup
docker exec kafka-1 sh -c 'cat > /tmp/connect.properties <<EOF
bootstrap.servers=kafka-1:19092
group.id=connect-lab
key.converter=org.apache.kafka.connect.storage.StringConverter
value.converter=org.apache.kafka.connect.storage.StringConverter
offset.storage.topic=connect-offsets
config.storage.topic=connect-configs
status.storage.topic=connect-status
plugin.path=/opt/kafka/libs/connect-file-4.3.1.jar
listeners=http://0.0.0.0:8083
EOF'
docker exec -d kafka-1 sh -c '/opt/kafka/bin/connect-distributed.sh /tmp/connect.properties > /tmp/connect.log 2>&1'
get() { docker exec kafka-1 wget -q -O - "http://localhost:8083$1"; }
post() { docker exec kafka-1 wget -q -O - --header 'Content-Type: application/json' --post-data "$2" "http://localhost:8083$1"; }
# wait about 20 seconds
get /Break it
k kafka-topics.sh --bootstrap-server $B --create --topic json-in --partitions 1 --replication-factor 3
printf '{"id":1}\nthis is not json\n{"id":2}\n' | k kafka-console-producer.sh --bootstrap-server $B --topic json-in
post /connectors '{"name":"json-strict","config":{"connector.class":"org.apache.kafka.connect.file.FileStreamSinkConnector","tasks.max":"1","file":"/tmp/strict.txt","topics":"json-in","value.converter":"org.apache.kafka.connect.json.JsonConverter","value.converter.schemas.enable":"false"}}'
# wait 10 seconds
get /connectors/json-strict/status
docker exec kafka-1 cat /tmp/strict.txtWatch
Module 08, part A, step 3. The connector says RUNNING, the task says FAILED, and the output file is empty:
{"name":"json-strict","connector":{"state":"RUNNING",…},
"tasks":[{"id":0,"state":"FAILED",…,"trace":"org.apache.kafka.connect.errors.ConnectException: Tolerance exceeded in error handler …
(strict.txt is empty)Questions to answer before you repair
- Which word in the status output tells you the real state: connector or task?
- The two good records were not delivered. Why not?
- Which consumer group tracks this sink? Which module 03 command shows its lag?
- Is a dead-letter topic always right? What does it hide?
Repair
post /connectors '{"name":"json-dlq","config":{"connector.class":"org.apache.kafka.connect.file.FileStreamSinkConnector","tasks.max":"1","file":"/tmp/dlq-ok.txt","topics":"json-in","value.converter":"org.apache.kafka.connect.json.JsonConverter","value.converter.schemas.enable":"false","errors.tolerance":"all","errors.deadletterqueue.topic.name":"json-in-dlq","errors.deadletterqueue.topic.replication.factor":"3","errors.deadletterqueue.context.headers.enable":"true"}}'
# wait 12 seconds
get /connectors/json-dlq/status
docker exec kafka-1 cat /tmp/dlq-ok.txt
k kafka-console-consumer.sh --bootstrap-server $B --topic json-in-dlq --from-beginning \
--formatter-property print.headers=true --timeout-ms 6000The module saw {id=1} and {id=2} delivered, and the bad record in the dead-letter topic with headers naming topic json-in, partition 0, offset 1, and the stage VALUE_CONVERTER. When done: docker exec kafka-1 sh -c "pkill -f ConnectDistributed".
Write-up prompts
- What did monitoring show: nothing, or a failed task?
- What do you do with the records in the dead-letter topic, and who owns that?
- Which alert would you add on task state?
10 An incompatible schema change
Builds on: Module 08.
Goal. Try to register a schema that old data cannot satisfy, read the refusal, and test changes without registering them.
Setup
Run this on your own machine, not inside a container: the registry listens on port 8086 and the module uses curl.
docker compose up -d schema-registry
R=http://localhost:8086
H='Content-Type: application/vnd.schemaregistry.v1+json'
curl -s $R/config
V1='{"schema":"{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"}]}"}'
V2='{"schema":"{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"},{\"name\":\"currency\",\"type\":\"string\",\"default\":\"CAD\"}]}"}'
V3='{"schema":"{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"},{\"name\":\"currency\",\"type\":\"string\",\"default\":\"CAD\"},{\"name\":\"customerId\",\"type\":\"string\"}]}"}'
curl -s -X POST -H "$H" -d "$V1" $R/subjects/orders-value/versions
curl -s -X POST -H "$H" -d "$V2" $R/subjects/orders-value/versionsBreak it
curl -s -X POST -H "$H" -d "$V3" $R/subjects/orders-value/versions
curl -s $R/subjects/orders-value/versionsWatch
Module 08, part B, step 2:
{"error_code":40901,"message":"Schema being registered is incompatible with an earlier schema for subject
"orders-value", details: [{errorType:'READER_FIELD_MISSING_DEFAULT_VALUE', description:'The field
'customerId' at path '/fields/3' in the new schema has no default value and is missing in the old schema' …
[1,2]Then test version 4 (removes amount) under BACKWARD, switch to FULL and test again (step 3):
V4='{"schema":"{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"currency\",\"type\":\"string\",\"default\":\"CAD\"}]}"}'
curl -s -X POST -H "$H" -d "$V4" "$R/compatibility/subjects/orders-value/versions/latest?verbose=true"
curl -s -X PUT -H "$H" -d '{"compatibility":"FULL"}' $R/config/orders-value
curl -s -X POST -H "$H" -d "$V4" "$R/compatibility/subjects/orders-value/versions/latest?verbose=true"{"is_compatible":true,"messages":[]}
{"compatibility":"FULL"}
{"is_compatible":false,"messages":["{errorType:'READER_FIELD_MISSING_DEFAULT_VALUE', description:'The field
'amount' at path '/fields/1' in the old schema has no default value and is missing in the new schema' …Questions to answer before you repair
- Which field did the registry object to, and what does "no default value" mean for an old record?
- Whose job is it to prevent this: the producer team, the registry, or the build pipeline?
- Why is removing
amountallowed underBACKWARDand refused underFULL? - What would have happened with no registry in the way?
Repair
k kafka-topics.sh --bootstrap-server $B --describe --topic _schemas | head -1Nothing was damaged: the registry refused the change. The repair is to give customerId a default and test it with the compatibility call. Registering that fixed version was not run in the labs. Then set the subject back with the same PUT to the mode you want.
Write-up prompts
- What did the refusal say, in your own words?
- Where in your release process would the test call run?
- Which compatibility mode does each of your subjects use, and why?
11 A noisy client and quotas
Builds on: Module 09, Module 02.
Goal. Put a byte-rate quota on one client, compare it with an unlimited one, and learn why a short test shows almost no throttling.
Setup
The topic perf comes from module 02. Skip the create line if it exists.
k kafka-topics.sh --bootstrap-server $B --create --topic perf --partitions 3 --replication-factor 3
k kafka-configs.sh --bootstrap-server $B --alter --add-config 'producer_byte_rate=5242880' \
--entity-type clients --entity-name slow-clientBreak it
k kafka-producer-perf-test.sh --topic perf --num-records 400000 --record-size 200 --throughput -1 \
--command-property bootstrap.servers=$B client.id=slow-client
k kafka-producer-perf-test.sh --topic perf --num-records 400000 --record-size 200 --throughput -1 \
--command-property bootstrap.servers=$B client.id=fast-clientWatch
Module 09, step 5. Read the last two lines of each run:
Client Quota MB each second Average latency slow-client 5 MiB each second 21.64 973 ms fast-client none 39.03 422 ms
The lesson. The limited client still averaged four times its quota. The test sent 80 MB in under 4 seconds, and the broker measures over an 11-second window, so most of it was an allowed burst. In the module, runs of 6 MB and 20 MB showed no slowing at all.
To see the steady limit you need a run that lasts a minute or more.
unverified Not run in the labs: a longer run. Raise --num-records until the run lasts a minute or more, then compare MB each second with the quota. The module only says this is the way to do it.
Questions to answer before you repair
- Why did the quota not reject anything? What does a quota do instead?
- Why did a short test mislead? Give the window length and the test length.
- Which numbers would you collect from a real noisy client before setting its quota?
- Who should be told when you throttle a client, and how will they find out?
Repair
k kafka-configs.sh --bootstrap-server $B --alter --delete-config producer_byte_rate \
--entity-type clients --entity-name slow-clientAlways remove the test quota. Quotas live in the cluster until someone deletes them.
Write-up prompts
- What did the first short test show, and why was that wrong?
- What quota would you set, and how long would you test it?
- What does the client team see when throttled?
12 Adding partitions moves a key
Builds on: Module 09, Module 01, Module 02.
Goal. Add partitions to a live topic and see one key land in a different partition, which breaks per-key order.
Setup
These are example records written for this drill. Module 01 builds the same topic; skip the create line if orders exists.
k kafka-topics.sh --bootstrap-server kafka-1:19092 --create --topic orders --partitions 3 --replication-factor 3
printf 'user-42:created\nuser-42:paid\nuser-42:shipped\nuser-17:created\nuser-1:created\n' | \
k kafka-console-producer.sh --bootstrap-server $B --topic orders \
--reader-property parse.key=true --reader-property key.separator=:Break it
k kafka-topics.sh --bootstrap-server $B --alter --topic orders --partitions 6
printf 'user-42:after-change\nuser-17:after-change\nuser-1:after-change\n' | \
k kafka-console-producer.sh --bootstrap-server $B --topic orders \
--reader-property parse.key=true --reader-property key.separator=:
k kafka-console-consumer.sh --bootstrap-server $B --topic orders --from-beginning \
--formatter-property print.key=true --formatter-property print.partition=true \
--formatter-property print.offset=true --timeout-ms 6000Watch
Module 09, step 4, sorted by key. Your offsets will differ. The partition for each key depends only on the key and the partition count, so the partition numbers should match.
Partition:2 Offset:0 user-1 created Partition:2 Offset:2 user-1 after-change Partition:0 Offset:0 user-17 created Partition:0 Offset:11 user-17 after-change Partition:1 Offset:0 user-42 created Partition:1 Offset:1 user-42 paid Partition:1 Offset:2 user-42 shipped Partition:4 Offset:0 user-42 after-change ← the same key, a different partition
Questions to answer before you repair
- Which key moved? Which two partitions now hold its story?
- What does that do to a consumer that needs all events for one customer in order?
- Why did the other keys not move?
- Can you now go back to 3 partitions? Try the alter command and read the error.
Repair
k kafka-topics.sh --bootstrap-server $B --alter --topic orders --partitions 3Module 09 saw: "The topic orders currently has 6 partition(s); 3 would not be an increase." You cannot undo it.
unverified Not run in the labs: the real repair. The usual plan is a new topic with the final partition count, then a copy and a switch for producers and consumers. The module does not run it.
Write-up prompts
- Which events could have been processed out of order, and what was the business effect?
- What rule would stop a team adding partitions to a keyed topic casually?
- How many partitions will you size for next time?
13 The disk fills
Builds on: Module 09, Module 01.
Goal. Find the topics that use the disk, then free space the supported way by lowering retention and waiting for the check. Do not delete files by hand.
Setup
Module 01 made five 1 MiB segments this way.
k kafka-topics.sh --bootstrap-server kafka-1:19092 --create --topic rolls \
--partitions 1 --replication-factor 3 --config segment.bytes=1048576
k kafka-producer-perf-test.sh --topic rolls --num-records 20000 --record-size 200 \
--throughput -1 --command-property bootstrap.servers=kafka-1:19092
docker exec kafka-1 ls /tmp/kraft-combined-logs/rolls-0Break it
# the filling step itself: not run in the labs
# (fill the disk of one broker; module 09 gives no command for it)unverified Not run in the labs: filling a broker's disk. Do not attempt it on a laptop shared with other work. The modules support only the steps before and after the fill.
Watch
Where did the disk go? (module 09, step 2). Output reduced to partition and size in bytes:
k kafka-log-dirs.sh --bootstrap-server $B --describe --broker-list 1 --topic-list perf,comp-none,orders,rollsperf-0 247910144 perf-2 241752474 perf-1 237756878 comp-none-0 1548818 orders-0 713 orders-2 159
Free space by lowering retention on the largest topic, then wait up to 6 minutes: 1 minute of retention plus the 5-minute check (module 01, step 7):
k kafka-configs.sh --bootstrap-server kafka-1:19092 --alter \
--entity-type topics --entity-name rolls --add-config retention.ms=60000
# wait up to 6 minutes
docker exec kafka-1 ls /tmp/kraft-combined-logs/rolls-0
k kafka-get-offsets.sh --bootstrap-server kafka-1:19092 --topic rolls --time earliest00000000000000000000.log.deleted 00000000000000004992.log.deleted … 00000000000000019968.log.deleted rolls:0:20000
Module 01 saw whole files marked .deleted for one minute before they disappeared, and the earliest offset moved from 0 to 20000. The active segment went too, because it had also expired.
Questions to answer before you repair
- Which topics use the most space on broker 1? What would you ask their owners?
- Why did the space not come back at once after you lowered retention?
- Why must you not delete segment files by hand?
- Which thresholds would you alert on before the disk is full? Check the module for the two values.
Repair
k kafka-configs.sh --bootstrap-server kafka-1:19092 --alter \
--entity-type topics --entity-name rolls --delete-config retention.msPut retention back to its normal value after the emergency. --delete-config returns a setting to its default. Module 09 uses it for a client quota (drill 11).
unverified Not run in the labs: --delete-config retention.ms on a topic. The module uses the flag only for quotas.
Write-up prompts
- What was the first sign, and how late was it?
- How much data did you give up by lowering retention, and who approved it?
- What limit would you set so no single topic can fill a disk?
14 MirrorMaker failover to the second cluster
Builds on: Module 10, Module 06, Module 03.
Goal. Start the copy to the second cluster, compare offsets, read translated group offsets, and find that aborted records were copied as normal records.
Setup
The group g-new comes from module 03, step 3. Without it, the last two commands show nothing.
# do drill 8 first, so payments has an aborted transaction
docker compose up -d kafka-dr
dr() { docker exec -i kafka-dr /opt/kafka/bin/"$@"; }
D=kafka-dr:19092
docker exec kafka-1 sh -c 'cat > /tmp/mm2.properties <<EOF
clusters = main, dr
main.bootstrap.servers = kafka-1:19092
dr.bootstrap.servers = kafka-dr:19092
main->dr.enabled = true
main->dr.topics = orders|payments|events
dr->main.enabled = false
groups = g-.*
replication.factor = 1
checkpoints.topic.replication.factor = 1
heartbeats.topic.replication.factor = 1
offset-syncs.topic.replication.factor = 1
offset.storage.replication.factor = 1
status.storage.replication.factor = 1
config.storage.replication.factor = 1
sync.group.offsets.enabled = true
emit.checkpoints.interval.seconds = 10
sync.group.offsets.interval.seconds = 10
refresh.topics.interval.seconds = 10
refresh.groups.interval.seconds = 10
EOF'
docker exec -d kafka-1 sh -c '/opt/kafka/bin/connect-mirror-maker.sh /tmp/mm2.properties > /tmp/mm2.log 2>&1'
# wait about 45 seconds
dr kafka-topics.sh --bootstrap-server $D --listBreak it
k kafka-get-offsets.sh --bootstrap-server $B --topic payments
dr kafka-get-offsets.sh --bootstrap-server $D --topic main.payments
k kafka-consumer-groups.sh --bootstrap-server $B --describe --group g-new
dr kafka-consumer-groups.sh --bootstrap-server $D --describe --group g-newunverified Not run in the labs: the failover itself: stop the copy with docker exec kafka-1 sh -c "pkill -f MirrorMaker; pkill -f ConnectDistributed", write records to orders on the main cluster, and count what is missing on main.orders. The module shows that command only as clean-up.
Watch
Module 10, part B, step 2. The offsets for payments differ by 5:
payments:0:78 main.payments:0:73
Step 4. The group never ran on the second cluster, yet has offsets there, behind the source:
on main: g-new events 0 CURRENT-OFFSET 1261 LOG-END-OFFSET 1657 LAG 396 on dr: g-new main.events 0 CURRENT-OFFSET 1112 LOG-END-OFFSET 1657 LAG 545
The module found that the source held 73 data records and 5 transaction markers. Of the 73, 38 belonged to the aborted transaction from module 06. They were copied as ordinary records, and the markers were not. Your counts depend on what you ran in drill 8.
Step 3 measured the delay between clusters at about 2 seconds:
main.orders:0:12 → main.orders:0:13 about 2 seconds later
Questions to answer before you repair
- If
g-newfailed over now, how many records of partition 0 would it read twice, and would it miss any? - Why do the offsets differ for
paymentsbut match fororders? - Why does a reader of
main.paymentssee records that were rolled back on the main cluster? - If the main cluster were lost now, which acknowledged records might not exist on the second cluster?
Repair
docker exec kafka-1 sh -c "pkill -f MirrorMaker; pkill -f ConnectDistributed"
docker compose down # stop every container; the labs keep no data after thisThe module names the fix for aborted records as setting the source consumer's isolation level to read_committed in the MirrorMaker 2 configuration for flows that carry transactional topics. The module marks it unverified.
unverified Not run in the labs: that setting. The exact property name is not shown in the module, and the lab was not rerun with it. Look it up before you use it.
Write-up prompts
- What is your recovery point: how many seconds of data could the second cluster lack?
- How would a consumer on the second cluster know where to start?
- What would you do about aborted records in the copy?
15 Secured cluster: password, truststore, ACL
Builds on: Module 11.
Goal. Meet the three common client failures on a secured cluster: wrong password, missing truststore, and missing permission. Read each one correctly.
Setup
This is the separate cluster in secure/, not the main one. It has its own health check: the list command below, run as admin.
cd ~/Desktop/kafka-knowledge-base/secure
sh make-certs.sh
docker compose up -d
docker logs kafka-sec 2>&1 | grep 'Kafka Server started'
s() { docker exec -i kafka-sec timeout 25 /opt/kafka/bin/"$@"; }
S=kafka-sec:9094
A=/clients/admin.properties
s kafka-topics.sh --bootstrap-server $S --command-config $A --create --topic payments-sec --partitions 1 --replication-factor 1
s kafka-acls.sh --bootstrap-server $S --command-config $A --add --allow-principal User:alice --producer --topic payments-sec
s kafka-acls.sh --bootstrap-server $S --command-config $A --add --allow-principal User:bob --consumer --topic payments-sec --group billingBreak it
s kafka-topics.sh --bootstrap-server $S --command-config /clients/plaintext.properties --list
s kafka-topics.sh --bootstrap-server $S --command-config /clients/no-truststore.properties --list
s kafka-topics.sh --bootstrap-server $S --command-config /clients/wrong-password.properties --list
s kafka-topics.sh --bootstrap-server $S --command-config /clients/alice.properties \
--create --topic alice-topic --partitions 1 --replication-factor 1
s kafka-console-consumer.sh --bootstrap-server $S --command-config /clients/alice.properties --topic payments-sec --group snoop --from-beginning --timeout-ms 6000Watch
Module 11, steps 3, 4 and 6. Read each error:
1. ERROR Uncaught exception in thread 'kafka-admin-client-thread' … java.lang.OutOfMemoryError: Java heap space 2. ERROR … failed authentication due to: SSL handshake failed org.apache.kafka.common.errors.SslAuthenticationException: SSL handshake failed 3. ERROR … failed authentication due to: Authentication failed: Invalid username or password alice create: Error while executing topic command : Authorization failed. alice read: GroupAuthorizationException: Not authorized to access group: snoop
The authorizer log names the user, the operation and the rule (step 7):
docker exec kafka-sec sh -c 'grep -i denied /opt/kafka/logs/kafka-authorizer.log | tail -3'INFO Principal = User:alice is Denied operation = DESCRIBE … on resource = Group:LITERAL:snoop for request = FindCoordinator … based on rule DefaultDeny
Questions to answer before you repair
- Which of the three failures is an authentication problem, and which is authorization? How do the messages differ?
- The plain client got an out-of-memory error. Why is that message misleading?
- An empty topic list was returned to alice. Is the list empty, or is alice not allowed to see it?
- Why is making the application a super user the wrong fix?
Repair
s kafka-acls.sh --bootstrap-server $S --command-config $A --list
s kafka-topics.sh --bootstrap-server $S --command-config $A --listFor the missing permission, add a rule with the same --consumer form as bob's, naming alice and group snoop. For the others, fix the client file, not the broker. When finished: docker compose down in the secure folder.
unverified Not run in the labs: the alice rule for snoop. It uses the same command form as module 11 with another principal and group.
Write-up prompts
- What did each failure look like to an application team, and how would they misread it?
- Which log line did you use to find the cause?
- What would a runbook line say for each of the three?
Write-up builder
Fill the fields and copy a post-incident report in Markdown. Nothing is sent anywhere. The text stays in this page until you copy it.