Series: Kafka 為什麼這麼快 (2/2)

← Why Is Kafka Fast? Sequential I/O and Zero-Copy Explained
Table of Contents

Part 1 covered single-node Kafka performance: sequential I/O, Page Cache, Zero-Copy. These allow one machine to handle very high throughput.

But Kafka’s real capability is horizontal scaling—add machines, throughput scales linearly. That depends on partitions.

TL;DR

  • Partition: Each topic is split into partitions, each an independent append-only log distributed across brokers
  • Consumer Group: Members each own different partitions, enabling true parallel consumption
  • Replication: Each partition has a leader and followers; writes wait for ISR confirmation; acks controls latency vs. durability tradeoff
  • ISR (In-Sync Replicas): Kafka’s reliability mechanism—only replicas keeping up with the leader count as in-sync

Design Philosophy: Partition Is the Unit of Performance

Kafka’s performance model rests on one principle: each partition is completely independent. Reads and writes across different partitions don’t interfere; they can run in parallel on different brokers.

This differs from RabbitMQ queues (pre-3.8): a single queue regardless of how many consumers attached was single-threaded on dispatch. Kafka has no such limit—if you want more parallelism, add more partitions.

Topic: orders
├── Partition 0 → Broker 1 (leader), Broker 2 (replica)
├── Partition 1 → Broker 2 (leader), Broker 3 (replica)
└── Partition 2 → Broker 3 (leader), Broker 1 (replica)

Three brokers, three partitions, load evenly distributed. Add a fourth broker and partitions rebalance automatically.

Partition Count Tradeoffs

More partitions raise the throughput ceiling but add overhead:

Benefits:

  • More partitions = more consumers can run in parallel
  • Near-linear performance scaling
  • Smaller individual partitions = faster leader elections

Costs:

  • More partitions = more file handles and OS threads per broker
  • Each partition has its own log segment; too many partitions increases file management pressure
  • End-to-end latency doesn’t necessarily improve with more partitions; depends where the bottleneck is

Practical starting point: no more than 100 partitions per broker—not more-is-better.

Consumer Groups: The Key to Parallel Consumption

A Consumer Group’s members share consumption of a topic, each owning different partitions:

Topic: orders (3 partitions)
Consumer Group: order-processors

Consumer A → Partition 0
Consumer B → Partition 1
Consumer C → Partition 2

One consumer in the group processes all 3 partitions. Three consumers, one each—throughput scales 3x (theoretically). More than 3 consumers and the extras sit idle; partition count is the parallelism ceiling.

Multiple Consumer Groups can independently consume the same topic, each maintaining its own offsets. This is Kafka’s event broadcasting capability: one event stream, multiple downstream services consuming at their own pace without blocking each other.

Replication: The Cost of Durability

Each partition has one leader and n followers. Producers talk only to the leader. The leader writes the message; followers asynchronously fetch and replicate.

The acks setting controls write confirmation behavior:

acksSemanticsLatencyData Loss Risk
0No confirmationLowestHigh (broker crash = lost)
1Leader confirmedMediumMedium (leader crash before replication)
all (-1)All ISR confirmedHighestLowest

acks=all with min.insync.replicas=2 is common in production: any single broker failure won’t lose data. The tradeoff is every write waits for follower confirmation, increasing latency. ISR synchronization is the core tuning knob in Kafka performance.

ISR Mechanism

ISR is the set of replicas “keeping up” with the leader. Kafka uses a time window (replica.lag.time.max.ms) to define “keeping up”—a follower that hasn’t fetched from the leader within this window gets dropped from ISR.

When a leader fails, Kafka’s Controller elects a new leader from ISR only. Choosing from ISR guarantees the new leader has all committed messages (no data loss).

If ISR shrinks to just the leader but min.insync.replicas requires 2, acks=all writes fail. This is an explicit design choice: refuse writes rather than lower consistency guarantees.

Comparison with RabbitMQ and Pulsar

FeatureKafkaRabbitMQPulsar
Storage modelAppend-only logQueue (delete after consume)Separate storage (BookKeeper)
Horizontal scalingLinear with partitionsFederation/shovelTopic sharding
Message retentionTime/size-basedRemoved after consumptionConfigurable
Consumer modelPullPushBoth
Ordering guaranteeWithin partitionWithin queueWithin partition
Best forHigh-throughput event streamsComplex routing, task queuesMulti-tenant, cloud-native

When to Use Kafka

  • Log/metrics ingestion pipelines (millions/second)
  • Event sourcing
  • Multiple downstream services consuming the same event stream
  • Historical replay (consumers can re-read from any offset)

When Not to Use Kafka

  • Task queues (each task done by exactly one worker)—SQS or RabbitMQ fits better
  • Complex message routing or filtering—RabbitMQ’s exchange + binding model is more flexible
  • Low-traffic, low-latency point-to-point delivery—Kafka’s overhead is non-trivial at low volumes

References

Ask this article

Answers come from this article only. Click any prompt below or open the chat at the bottom right.

🇺🇸 English

Kafka on a single machine is already fast—sequential writes, the page cache, zero-copy. If you caught part one, you know one box can push a serious amount of throughput. But here's the thing: single-node speed isn't really Kafka's superpower. The superpower is that when you add machines, throughput scales up almost linearly. And the whole trick behind that is one idea—partitions.

So let's start there, because partitions are the unit of performance in Kafka. The core principle is this: every partition is completely independent. A partition is just an append-only log—messages get tacked onto the end, in order. And because partitions don't talk to each other, reads and writes on different partitions never step on each other's toes. They can run in parallel, on different brokers, at the same time.

This is a real departure from how something like RabbitMQ worked, at least before version 3.8. Over there, a single queue was single-threaded on dispatch—didn't matter how many consumers you attached, that one queue was the bottleneck. Kafka just doesn't have that ceiling. You want more parallelism? Add more partitions.

Picture a topic called "orders" split into three partitions, spread across three brokers. Partition zero lives on broker one as its leader, with a copy on broker two. Partition one leads on broker two, backed up on broker three. Partition two leads on broker three, backed up on broker one. Three brokers, three partitions, load spread evenly. And if you bolt on a fourth broker, Kafka rebalances the partitions automatically to make use of it.

Now, you might be tempted to think more partitions is always better. It's not—there's a tradeoff. On the upside: more partitions means more consumers running in parallel, near-linear scaling, and since each partition is smaller, leader elections happen faster when something goes wrong. But there's a cost. Every partition eats file handles and OS threads on the broker. Every partition has its own log segment on disk, so pile up too many and you're drowning the broker in file management. And here's the subtle one—more partitions doesn't automatically mean lower end-to-end latency. That depends entirely on where your actual bottleneck is. A decent rule of thumb to start with: keep it under a hundred partitions per broker. This is not a "crank it to the max" situation.

Okay, so partitions give you the parallelism on the storage side. The thing that actually cashes in on that parallelism when reading is the consumer group. A consumer group is just a set of consumers that split the work of reading a topic, and the rule is each partition gets owned by exactly one consumer in the group.

Go back to our orders topic with three partitions. If you've got one consumer in the group, it handles all three partitions by itself. Add a second and third consumer, and now each one owns a single partition—your throughput roughly triples. But—and this is the catch people trip on—if you add a fourth consumer to that group, it just sits there idle. There's no fourth partition for it to own. Your partition count is the hard ceiling on parallelism. More consumers than partitions buys you nothing.

Here's the part that's genuinely elegant, though: multiple consumer groups can each read the same topic completely independently. Each group tracks its own position—its own offsets. So one stream of events can feed a bunch of different downstream services, and they all consume at their own pace without blocking each other. That's Kafka's broadcasting ability in a nutshell. Your analytics service and your billing service can both chew through the same order events, one racing ahead, one lagging behind, and neither one cares what the other's doing.

Now let's talk durability, because that's where replication comes in—and durability always costs something. Each partition has one leader and some number of followers. Producers only ever talk to the leader. The leader writes the message, and the followers asynchronously pull that data over and replicate it.

The knob that controls how careful you're being is a setting called "acks." Think of it as: how many confirmations do you wait for before you consider a write done? With acks set to zero, you don't wait for any confirmation—fire and forget. Lowest latency you can get, but if the broker crashes, that message is just gone. Set acks to one, and you wait for the leader to confirm it wrote the message. That's the middle ground—decent latency, but you can still lose data if the leader dies before the followers copied it over. And then acks equals "all," sometimes written as negative one, means you wait for all the in-sync replicas to confirm. Highest latency, lowest risk of losing anything.

In production, the common setup is acks equals all combined with a setting called min-insync-replicas set to two. What that combo buys you is: any single broker failure won't lose your data. The price you pay is that every write has to wait for a follower to confirm before it's acknowledged, so latency goes up. This synchronization is really the central tuning dial in Kafka performance—you're trading speed for safety, and you get to decide where on that dial you sit.

Which brings us to ISR—in-sync replicas—because that acks-equals-all guarantee is only as good as the ISR behind it. ISR is the set of replicas that are actually keeping up with the leader. And Kafka defines "keeping up" with a time window—there's a setting, replica-dot-lag-dot-time-dot-max-dot-ms. If a follower hasn't fetched from the leader within that window, it gets kicked out of the ISR. It's fallen behind, so it no longer counts.

Why does this matter? When a leader fails, the component called the controller elects a new leader—and it only ever picks from the ISR. That's the whole point. Because every replica in the ISR is caught up, whichever one gets promoted is guaranteed to have all the committed messages. No data loss on failover.

And here's a design decision I really like. Suppose your ISR shrinks down until only the leader is left—everybody else fell behind. But you told Kafka min-insync-replicas is two. Now an acks-equals-all write comes in. Kafka doesn't quietly relax its promise. It refuses the write. It would rather reject your data than silently lower the consistency guarantee it made you. That's an explicit, deliberate choice—fail loudly instead of lying about durability.

Let me quickly place Kafka against its neighbors, RabbitMQ and Pulsar, because they're not interchangeable. On storage, Kafka is an append-only log—messages stick around based on time or size, and consumers can re-read them. RabbitMQ is a queue: once a message is consumed, it's deleted. Pulsar separates storage out into a system called BookKeeper. On scaling, Kafka grows linearly by adding partitions; RabbitMQ leans on federation and shovel; Pulsar shards topics. On the consumer model, Kafka is pull-based—consumers ask for data. RabbitMQ pushes data to consumers. Pulsar does both. And on ordering, Kafka guarantees order within a partition, RabbitMQ within a queue, Pulsar within a partition. The one-liner version: Kafka's home turf is high-throughput event streams, RabbitMQ shines at complex routing and task queues, and Pulsar's story is multi-tenant, cloud-native workloads.

So when should you actually reach for Kafka? When you're ingesting logs or metrics at millions of events per second. When you're doing event sourcing. When multiple downstream services all need to consume the same event stream. Or when you need historical replay—the ability for a consumer to rewind and re-read from any point.

And when should you not? If it's a task queue where each task should be done by exactly one worker, something like SQS or RabbitMQ fits better. If you need complex routing or filtering, RabbitMQ's exchange-and-binding model is far more flexible. And if it's low-traffic, low-latency, point-to-point messaging, Kafka's overhead just isn't worth it at small volumes.

So let me leave you with the three things that really matter here. First: the partition is the atom of everything. It's the unit of parallelism, the unit of scaling, and the thing that caps how many consumers can work at once—choose your partition count thoughtfully, because it's your ceiling. Second: replication and the acks setting are a dial, not a switch. You're consciously trading latency for durability, and acks-equals-all with min-insync-replicas of two is the sweet spot most production systems land on. And third: the ISR is what makes the durability promise real—by only ever electing a caught-up replica as the new leader, and by refusing writes rather than quietly weakening its guarantees, Kafka keeps its word even when brokers fall over. Get those three ideas, and you understand why Kafka scales the way it does.

🇹🇼 中文

上一集,我們聊了 Kafka 效能的第一根支柱:sequential I/O,循序讀寫。這一集,來講第二個關鍵的設計選擇——效率。也就是說,Kafka 是怎麼在搬資料的過程中,盡可能砍掉那些多餘的複製。

先講一個很本質的觀察:Kafka 到底在幹嘛?說穿了,它就是一個搬資料的機器。把大量資料從網路搬到硬碟,再從硬碟搬回網路。當你每秒要搬動一頁又一頁的資料時,「在硬碟跟網路之間搬移的過程中,能不能少複製幾次」,這件事就變得超級重要。這,就是 zero copy,零複製原則登場的地方。

其實現代的 Unix 作業系統,本來就針對「把資料從硬碟送到網路、而且不做多餘複製」這件事做了很深的最佳化。但要體會 zero copy 有多值錢,我們得先看看——沒有它的時候,一份資料到底被折騰了幾次。

想像 Kafka 要把硬碟上的一頁資料送給 consumer,而且完全不用 zero copy。流程是這樣的:第一步,資料從硬碟載入到 OS cache。第二步,從 OS cache 複製進 Kafka 應用程式本身。第三步,再從 Kafka 複製到 socket buffer。第四步,從 socket buffer 複製到網卡的 buffer。最後才透過網路送出去。

你數數看,一份資料被複製了整整四次,還牽涉到兩次 system call。而且最扯的是中間那兩次——進出 Kafka 應用程式的複製,對「把硬碟上的位元組送到網路」這個目標,其實一點貢獻都沒有。資料只是原封不動地被搬進使用者空間、又被搬出來而已。純粹是白工。

那 zero copy 怎麼做?第一步一模一樣,資料還是從硬碟載入到 OS cache。差別在第二步。這次 Kafka 不再把資料撈進自己的應用程式了,它改用一個叫做 `sendfile` 的 system call,直接跟作業系統說:欸,你把資料從 OS cache 直接送到網卡 buffer 就好。

所以在這條最佳化過的路徑上,從頭到尾只剩下一次複製——就是 OS cache 到網卡 buffer 這一段。資料完全不用再繞進 Kafka、也不碰 socket buffer。

而且還有更漂亮的地方。在配備現代網卡的機器上,這唯一的一次複製,是透過 DMA,也就是直接記憶體存取來完成的。用 DMA 的話,CPU 根本不需要插手,整個搬移過程更省力、更高效。

把這兩集收攏起來看:sequential I/O 跟 zero copy,就是 Kafka 高效能的兩塊基石。sequential I/O 讓硬碟讀寫逼近它的物理極限;zero copy 讓資料從硬碟到網路的搬移,省下多餘的記憶體複製跟 system call,有 DMA 的時候連 CPU 都不用動。

在這兩塊基石之上,Kafka 還疊了不少其他技巧,把硬體的每一分效能都榨乾。這也是為什麼光是一台機器,就能撐起那種嚇人的吞吐量。

所以最後留三個重點給你。第一,Kafka 本質上就是個搬資料的機器,而搬資料時「少複製一次」就是效能。第二,沒有 zero copy 是四次複製、兩次 system call,其中兩次進出使用者空間根本是白費力氣;用了 sendfile 之後只剩一次複製。第三,配上現代網卡的 DMA,這一次複製連 CPU 都不必參與。說到底,Kafka 快,靠的不是什麼魔法,而是把「不做多餘的事」這件事,貫徹到底。

Tags

Related Articles

RAG's Five Stages: From Pipeline to Reasoning Retrieval, and the Naive RAG on My Own Site

Over the past two years RAG evolved from a 'linear pipeline' to 'loop-based reasoning'. It maps cleanly to five stages: Naive, Advanced, Modular, Graph, Agentic. The real inflection point is control moving from pipeline to agent — a System 1 → System 2 shift. Looking back at engineer-news's own RAG stack, it's stuck at the Naive edge — so this post also lays out what to fix next.

Building a Real RAG: 5 Infra Lessons from InfiniFlow's 2024 Year-in-Review

The previous post zoomed out for a five-stage panorama of RAG. This one zooms in on the five infra lessons any real RAG has to face: document ingestion, contextualized chunking, three-lane hybrid search, tensor reranker, and GraphRAG's semantic gap. Each lesson is checked against engineer-news's current stack, ending with a priority list for a personal site.