"Writing a Kafka Streams application is only the first step for your event driven applications. Eventually, you will deploy and tune your applications with different goals in mind (throughput, latency, robustness, high availability, you name it). To run and configure your applications efficiently, it is paramount to understand the internal architecture, and the dependencies and interactions of the different internal components. Otherwise, you end up with a trial-and-error approach that is cumbersome, potentially frustrating, and for sure not fun.
In this talk, we will explore the internal architecture of Kafka Streams to set you up for successfully running and tuning your applications. -- What does the internal threading model look like? How are partitions assigned and mapped to tasks? Why are there multiple internal consumers? What are the most important/interesting configurations and how do they interact with each other? -- Those are just a few questions we will answer in this session, enabling you to run and tune your applications in record time!"
Injustice - Developers Among Us (SciFiDevCon 2024)
The Nuts and Bolts of Kafka Streams---An Architectural Deep Dive
1. The Nuts and Bolts of Kafka Streams:
An Architectural Deep Dive
Matthias J. Sax
Software Engineer | Apache Kafka Committer and PMC member
Twitter/𝕏 @MatthiasJSax
11. 11
Main Processing Loop
while (running)
records = mainConsumer.poll() // config: poll.ms
// max.poll.records
// fetch configs
addRecordsToTask(records); // no deserialization
// config: buffered.records.per.partition
while (keepProcessing)
for each task:
if (task.isReady) // data available for all inputs?
task.process(nRecords) // -> ts-extraction
// -> (de)serialization + exception-handling
maybePunctuate()
maybeCommit() // config: commit.interval.ms [ctx.commit()]
adjust nRecords
12. 12
Main Processing Loop
while (running)
records = mainConsumer.poll() // config: poll.ms
// max.poll.records
// fetch configs
addRecordsToTask(records); // no deserialization
// config: buffered.records.per.partition
while (keepProcessing)
for each task:
if (task.isReady) // data available for all inputs?
task.process(nRecords) // -> ts-extraction
// -> (de)serialization + exception-handling
maybePunctuate()
maybeCommit() // config: commit.interval.ms [ctx.commit()]
adjust nRecords max.poll.interval.ms
13. 13
Main Processing Loop
while (running)
records = mainConsumer.poll() // config: poll.ms
// max.poll.records
// fetch configs
addRecordsToTask(records); // no deserialization
// config: buffered.records.per.partition
while (keepProcessing)
for each task:
if (task.isReady) // data available for all inputs?
task.process(nRecords) // -> ts-extraction
// -> (de)serialization + exception-handling
maybePunctuate()
maybeCommit() // config: commit.interval.ms [ctx.commit()]
adjust nRecords max.poll.interval.ms
17. 17
Main Processing Loop
while (running)
records = mainConsumer.poll() // config: poll.ms
// max.poll.records
// fetch configs
addRecordsToTask(records); // no deserialization
// config: buffered.records.per.partition
while (keepProcessing)
for each task:
if (task.isReady) // data available for all inputs?
task.process(nRecords) // -> ts-extraction
// -> (de)serialization + exception-handling
maybePunctuate()
maybeCommit() // config: commit.interval.ms [ctx.commit()]
adjust nRecords max.poll.interval.ms
18. 18
Main Processing Loop
while (running)
records = mainConsumer.poll() // config: poll.ms
// max.poll.records
// fetch configs
addRecordsToTask(records); // no deserialization
// config: buffered.records.per.partition
while (keepProcessing)
for each task:
if (task.isReady) // data available for all inputs?
task.process(nRecords) // -> ts-extraction
// -> (de)serialization + exception-handling
maybePunctuate()
maybeCommit() // config: commit.interval.ms [ctx.commit()]
adjust nRecords max.poll.interval.ms
20. Committing
20
At-least-once:
• flush state stores
• flush producer
• avoid data
• get offset / consumer position
• record buffers
• commitSync()
Exactly-once:
• flush state stores
• flush producer
• avoid data
• get offsets / consumer position
• record buffers
• addOffsetsToTransaction()
• commitTransaction()
• configs:
• transaction.timeout.ms
21. 21
Main Processing Loop
while (running)
records = mainConsumer.poll() // config: poll.ms
// max.poll.records
// fetch configs
addRecordsToTask(records); // no deserialization
// config: buffered.records.per.partition
while (keepProcessing)
for each task:
if (task.isReady) // data available for all inputs?
task.process(nRecords) // -> ts-extraction
// -> (de)serialization + exception-handling
maybePunctuate()
maybeCommit() // config: commit.interval.ms [ctx.commit()]
adjust nRecords max.poll.interval.ms
22. 22
Main Processing Loop
while (running)
records = mainConsumer.poll() // config: poll.ms
// max.poll.records
// fetch configs
addRecordsToTask(records); // no deserialization
// config: buffered.records.per.partition
while (keepProcessing)
for each task:
if (task.isReady) // data available for all inputs?
task.process(nRecords) // -> ts-extraction
// -> (de)serialization + exception-handling
maybePunctuate()
maybeCommit() // config: commit.interval.ms [ctx.commit()]
adjust nRecords max.poll.interval.ms
transaction.timeout.ms
28. Restore Consumer
28
KafkaConsumer (restore)
no consumer group
read changelog topics
used to restore state
no offset commits
while (running) {
mainConsumer.poll()
if (restoring)
restoreConsumer.poll()
restoreState()
continue;
processActiveTasks()
}
31. Instance A
31
Standby Tasks and High-Availability
Task 1_1
cnt
log-1
Instance B
Standby 1_1
restore
config: num.standby.tasks
32. Restore Consumer
32
KafkaConsumer (restore)
no consumer group
read changelog topics
used to restore state
no offset commits
while (running) {
mainConsumer.poll()
if (restoring)
restoreConsumer.poll()
restoreState()
continue;
else
restoreConsumer.poll()
updateStandbys()
processActiveTasks()
}
38. Instance B
Instance A
38
Global State Stores and GlobalStreamThread
builder.globalTable(…); // or .addGlobalStore(…)
KafkaStreams streamsClient = new KafkaStreams(…);
streamsClient.start()
GlobalStreamThread
40. GlobalStreamThread
40
KafkaConsumer (global)
no consumer group
reads all global topic partitions
no offset commits
Global StateStore updating
// bootstrap global state
start() {
while(!endOffsetsReached)
globalConsumer.poll()
updateGlobalState()
}
// start StreamThreads
// regular processing
while(running) {
globalConsumer.poll()
updateGlobalState()
}
43. Instance B
Instance A
43
Thread Refactoring – Phase 2
Decouple consumer and processing threads (WIP)
GlobalStreamThread ProcessingThread
StateUpdaterThread
44. The Nuts and Bolts of Kafka Streams:
An Architectural Deep Dive
Matthias J. Sax
Software Engineer | Apache Kafka Committer and PMC member
Twitter/𝕏 @MatthiasJSax