跳到主要内容

Apache Kafka 是一个分布式、可 replay 的 log。Producers append records。Consumers 按 offset pull。Partitions 把 log 分到多个 brokers,并保住一把 key 的顺序。

这篇 note 依 Evan 的 walkthrough。通用 queue pattern —— outbox、idempotent consumers、backpressure 与 broker choice —— 见 System Design 里的 Message Queues。Production 的 Hono/MSK 购票路径见 打造 Event-Driven 票务 Backend。这篇 note 讲的是 log。



Pattern Map

PatternKafka shapeReach for it when
Work queue一个 consumer group;每个 partition 由一个 member 持有Jobs 需要 buffering 与 per-key order;retry 由你负责
Event streamRetained log + offsetsContinuous processing、replay 与 ordered history 很重要
Pub/sub每个 subscriber 一个 consumer group多个独立 services 都要读到同一个 event

Kafka 三种都能表达,不代表三种都该默认用它。如果需求只有「执行这份 job 并 retry」,SQS 用更少 machinery 就给 delayed retries 与 DLQ。只有 retention、replay、high throughput 或多个独立 readers 是产品要求时,Kafka 的成本才值得。



1. Kafka 为什么存在

想象一个 World Cup 网站发布 goals、bookings 与 substitutions。一个 producer 写 events。一个 consumer 更新 live page。


text
Match feed → Producer → Queue → Consumer → Live site

现在让一千场比赛同时进行。一个 queue 与一个 consumer 成了 bottleneck。把 events 随机分到多个 queues 虽然加了 capacity,却丢了 causality:goal 可能出现在 kickoff 之前。

Kafka 的答案是按 game ID partition。一个 game 的每个 event 都 hash 到同一个 partition,所以比赛内的顺序保住了。Consumer group 可以加 workers,又不会让 group 内两个 workers 同时持有同一个 partition。Topics 把 soccer 与 basketball 分开。


text
topic: match-events

partition 0  game-17: kickoff → goal → booking
partition 1  game-42: kickoff → substitution
partition 2  game-91: kickoff → goal

Guarantee 刻意很窄:partition 内有序。Partition 0 与 partition 1 之间没有有用的 global order。


Failure: 说「Kafka 保证顺序」,却随机给每个 event 选 key。Kafka 只保住一个 partition 内的 append order。必须有序的 records 必须共用一把 key。



2. Brokers、topics 与 partitions

Kafka cluster 由 brokers 组成。Broker 是存 partition replicas、响应 producer 与 consumer requests 的 server。


text
Topic: match-events

Broker A              Broker B              Broker C
P0 leader             P1 leader             P2 leader
P1 follower           P2 follower           P0 follower

  • Topic 是 clients publish 与 subscribe 的 logical stream。
  • Partition 是一条有序、immutable、append-only 的 log。它是 storage、ordering、replication 与 consumer parallelism 的单位。
  • Record 是 log 里的一项。Kafka 在 consume 后仍保留它;read 不会 delete。
  • Offset 是 record 在一个 partition 内的位置。它不是 global,也不是 timestamp。

一个 consumer 可以持有多个 partitions。同一 consumer group 里,一个 partition 同一时间只能由一个 consumer 持有。十二个 consumers 配六个 partitions,最多只有六个 active consumers。


Failure: Topic 仍只有一个 partition,却一直加 consumers。十一个 workers 都在 idle。Partitions 决定 consumer group 内 parallelism 的上限。



3. Record shape 与 write path

一条 record 有四个实用 fields:keyvaluetimestampheaders。Value 是 payload。Headers 放 schema version、correlation ID 与 traceparent 等 metadata。Key 通常决定 partition。


ts
await producer.send({
  topic: "match-events",
  messages: [
    {
      key: matchId,
      value: JSON.stringify({ type: "goal", playerId }),
      headers: { "schema-version": "2", traceparent },
    },
  ],
})

常用的 mental model:


text
partition = hash(key) % partitionCount

producer
  → fetch cluster metadata
  → choose partition from key
  → send to that partition's leader
  → leader appends
  → followers replicate
  → consumer polls and advances its offset

没有 key 时,现代 producers 会把 batches 分散到多个 partitions。Distribution 变好;related-record ordering 消失。Custom partitioner 可以编码另一套 policy,但所有 producers 都必须同意它。

改变 partition count 也会改变 hash(key) % partitionCount。旧 key 的新 records 可能移到另一个 partition,所以加 partitions 会打破变更前后的 per-key order。


Failure: 把 timestamp 当 ordering guarantee,或在 live ordered topic 上加 partitions 却没有规划 key remapping。权威顺序是一个 partition 内的 offset。



4. 什么时候用 Kafka

当 log 本身解决一项 requirement 时,Kafka 才有价值。


RequirementExampleWhy Kafka fits
Async processingTranscode uploaded videoIngest 与 workers 独立 scale
Per-key order从 waiting room 放用户进入一把 key 留在一个 partition
Stream processing用 Flink aggregate ad clicksConsumers 处理 continuous retained flow
Pub/sub把 live comments 送给多个 services每个 consumer group 都拿到自己的 copy
Replay修 bug 后重建 projectionReset offsets,再读一次 retained history

做 video transcoding 时,Kafka 带一条小 event,里面只有 videoId 与 S3 URL。S3 才装 video。Worker poll 到 event 后再下载。


text
Upload → S3
       → Kafka { videoId, s3Url } → Transcoder → renditions

Kafka 不是 business row 的 source of truth,也不是 blob storage。如果 HTTP write 与 event 必须一致,就在一个 database transaction 里 persist business row 与 outbox row,再 relay event。Outbox 见 System Design 里的 Message Queues


Failure: 因为 transcoder 是 async,就把 1 GB video 放进 Kafka。Blob 放 object storage,log 里只放 pointer。也不要只因为 replay 听起来有用,就给一次性 email job 选 Kafka。



5. Scale 从 partition key 开始

Scale 前先估 records per second、average record size、retention、replication factor 与 consumer work。Evan 给的 interview baselines 刻意很粗:records 尽量低于约 1 MB,一台配置不错的 broker 大致按 1 TB10k messages per second 想。真实 capacity 会随 hardware、record size、replication、acknowledgements、compression 与 workload 巨幅变化。


text
ingress bytes/day
  = records/sec × average bytes × 86,400

stored bytes
  ≈ ingress bytes/day × retention days × replication factor

加 brokers 只增加潜在 storage 与 network capacity,不会神奇地拆开现有 topic。Topic 要有足够 partitions,replicas 也要 reassign,新 brokers 才能接住它的 load。

Partition key 是最主要的 design decision:

  • 它必须保住产品真正需要的 order:matchIdorderIdaccountId
  • 它应该 high-cardinality,而且 traffic distribution 均匀。
  • 它不能比 ordering boundary 更宽。如果只有每个 invoiceId 需要有序,用 tenantId 会把一个大 tenant 压在一个 partition 上。
  • 它是 data contract 的一部分。改 key 就会改 ordering 与 stateful consumer behavior。

Failure: 只说「我们会加 brokers」,却说不出 key、partition count、throughput 或 retention。再多机器也救不了一个 hot partition。



6. Hot partitions

Hot partition 收到远多于 peers 的 traffic。Ad-click topic 按 adId key 看似均匀,直到一个 campaign viral。一个 leader saturated,cluster average 却仍健康。


StrategyWhat it buysWhat it costs
No key一段时间后分布均匀没有 per-entity order
Random salt一把 hot key 变 N 把 keysConsumers 要 merge N 条 partial streams
Compound key按 region 等真实 dimension 分散Order 变成 per compound key
Backpressure保护 brokers 与 downstream systemsProducer latency 更高,或拒绝 work

如果一个 celebrity account 的 exact order 不能退让,它就是 serial workload。没有 partitioning trick 能让一条 ordered sequence 无限 parallel。减少每条 record 的工作、batch,或改变 requirement。

监控 per-partition bytes、requests 与 consumer lag。Cluster averages 会藏住 skew。


Failure: Salt key 后仍承诺 unsalted entity 的 total order。Load 之所以散开,是因为 order boundary 已经变了。



7. Durability 是 configuration

每个 partition 有一个 leader 与位于其他 brokers 的 follower replicas。Producers 写 leader。Followers fetch 它的 log。Controller 追踪 broker health,在 failure 后从 in-sync replicas (ISR) 里 elect 新 leader。

Replication factor 为 3,意思是三份总 copies:一个 leader、两个 followers。


Producer acksSuccess meansTrade-off
0Producer 发出了 requestLatency 最低;loss 可能 silent
1Leader append 完成Leader failure 可能丢 unreplicated record
all每个 required in-sync replica 都 ackDurability 最强;latency 更高

acks=all 不是等待每个 configured replica,无论它健不健康。它等的是 ISR requirement。要配一个有意义的 min.insync.replicas;否则「all」仍可能只有一个 live replica。Durability 重要时要 disable unclean leader election,否则 out-of-date replica 可能成为 leader,丢掉已 acknowledged data。

Kafka 不是魔法般 always available。Interview 里有用的回答是 failure domain:一个 broker 可以 fail,而 replicated partitions 继续工作。整个 cluster、region、坏 configuration 或 operator 仍可能让它 fail。


Failure: 说 replication factor 3 就一定安全承受任意两台 broker failures,却不看 ISR、replica placement、acksmin.insync.replicas。Write 没有等 copies,copies 就不是 guarantee。



8. Consumer failure、offsets 与 rebalancing

Consumers pull records。每个 consumer group 存自己的 committed offsets,所以两个 groups 能独立、以不同速度读同一个 topic。


text
poll record
  → perform durable side effect
  → commit offset

Side effect 前 commit,crash 就会丢 work。Side effect 后 commit,effect 与 commit 之间 crash 就会 reprocess record。因此 Kafka default 是 at-least-once。Consumer 必须用 event ID、unique inbox record 与 idempotent business write 让 duplicate 安全。

Consumer join、leave 或停止 polling 时,group 会 rebalance,在 members 之间移动 partitions。受影响 partitions 会暂停 processing。Cooperative rebalancing 减少 disruption,但没有消除保持 poll loop healthy 与 handler bounded 的要求。

  • 看每个 partition 的 consumer lag,不只看 average lag。
  • 保持 consumer unit of work 小。
  • 按真实 processing time 设置 poll 与 session timeouts。
  • Shutdown 前先停止 polling,finish 或 abandon in-flight work,只 commit 完成的 records。

Failure: Database write 还没完成就 auto-commit offsets。Dashboard 说 caught up;business event 已经消失。相反的 failure 是 commit 后做 non-idempotent side effect,recovery 时无法安全 replay。



9. Retry 是 design 的一部分

Producer request 会 ambiguously fail:Kafka 可能已经 append record,只是 acknowledgment 没回到 producer。启用 idempotence 与 retries,让 broker 能 deduplicate 同一 producer session 的 retransmissions。


ts
const producer = kafka.producer({
  idempotent: true,
  retry: { retries: 5, initialRetryTime: 100 },
})

Producer idempotence 不会让整个 business workflow exactly once。它不会 deduplicate 两个 application requests,也不会让 database side effect 与 offset commit atomic。

Kafka 没有 SQS 那种 queue-native delayed consumer retries。常见 design:


text
main topic
  → consumer fails
  → retry topic(s) with attempt and next-at metadata
  → retry consumer
  → dead-letter topic after the ceiling

使用 bounded attempts、带 jitter 的 exponential backoff,并观测 dead-letter topic。保留 original event ID 与 error context。Poison record 不能永远堵住 partition。


Failure: Catch exception 后 commit offset,再 log「will retry」,但根本没有 retry event。另一个是把 permanent schema error 永远 retry 在一个 ordered partition 的头部。



10. Throughput 与 retention

Kafka 靠 sequential appends、batching、compression 与 parallel partitions 拿 throughput。

  • Batch records,让一次 network request 与 disk append 带多个 events。更大 batches 提升 throughput,却增加 waiting latency。
  • Compress batch,可选 LZ4、Snappy、Zstd 或 Gzip。更少 network 与 disk,代价是 CPU。
  • Partition evenly. Batching 救不了一个 hot leader。
  • Keep payloads small. Large records 会同时吃 broker memory、network、replication bandwidth 与 consumer fetch capacity。

Kafka 通过 retention.msretention.bytes 按时间和/或 partition size 保留 records。常见 broker default 是七天。无论 consumers 有没有读过,retention 都照样执行。

更长 retention 能 replay 更久之前的 history,却会放大 storage、recovery time 与 cost。Log compaction 是另一种 policy:保留每把 key 的 latest value 与 tombstones,而不是永远保留每个 event。它适合 rebuildable current state,不适合 immutable audit history。


text
retention answers:   how much history can be replayed?
compaction answers:  what is the latest value for each key?

Failure: 承诺 90-day replay,disk 却只按一天来 size;或者给 audit stream 开 compaction,还以为每个 intermediate event 都会留下。



Recap Q&A