Kafka:分区、位移与交付边界
分区、键、位移与消费组
本轮仅静态审阅,以下命令与练习均未执行;预期结果不是本轮运行证据。早期真实服务验证仅为历史记录,不能代替当前环境验证。
topic 与 partition
前置:完成 起步。topic 是一类日志的名字,partition 是独立追加的有序日志。offset 是一条记录在某个分区中的位置;不同分区的 offset 不能拿来比较先后。消息不会因为消费成功就立即从 Kafka 删除。
本实验三个分区、一个副本。生产时明确 --partition 0/1/2 可以观察每个分区的 offset 从 0 增长;这用于测试分区覆盖,并非推荐业务把分区号硬编码。分区内有序不表示整个 topic 全局有序。并行处理同一分区的任务也可能使业务完成顺序变化。
key 与顺序
没有显式指定分区时,驱动按 key 选择分区。相同 key、相同分区数和相同分区算法通常会落在同一分区,因此 customer-1 的事件可以保持分区内顺序。增加分区数或改变客户端分区算法可能改变映射。跨 Python、Java 客户端迁移时不能只凭“都使用 key”就假设相同分区。
python examples/kafka/main.py --topic key-orders create
python examples/kafka/main.py --topic key-orders produce --key customer-1 --value created
python examples/kafka/main.py --topic key-orders produce --key customer-1 --value paid预期两条结果 partition 相同、offset 递增。value 是 UTF-8 字符串,本项目不添加 Schema Registry;真实演进应约定字段版本、编码和兼容策略,避免消费者突然无法解析新数据。
group 与提交位置
同一消费组中的成员分摊分区,同一时刻一个分区分给组内一个成员;成员多于分区会有空闲成员。不同 group.id 有独立位移,因此 accounting 和 audit 各读一遍同一日志,这是广播业务能力的基础。加入、退出和失去心跳会触发分区重分配,不能把一次 assignment 当永久归属。
消费组提交下一条的读取位置(即下一个要读取的 offset),而不是最后一条已处理记录的位置。例如处理分区 0 的 offset 7 后提交 8。我们的 commit(message=message) 由驱动计算加一,避免手工把 7 当恢复位置。enable.auto.commit=false 与 enable.auto.offset.store=false 都显式设置,处理结束后才同步提交。
未提交重放与恢复
未提交重放指同一消费组恢复时重新读取尚未提交的记录。新 topic 写入三条消息,各分区一条;同一新组先消费而不加 --commit,再消费并提交,第二次应该看到相同内容和分区 offset。随后再写入 after-commit,同组应只读取新记录。历史验证器设计为直接比较消息内容与分区 offset,不以进程退出作为提交成功的证明。
python examples/kafka/main.py --topic key-orders consume --group accounting --count 2
python examples/kafka/main.py --topic key-orders consume --group accounting --count 2 --commit这里沿用前节全新 key-orders 的两条记录,且 accounting 尚无提交,因此 count=2。若 topic 已有旧数据,应换新名重做生产,不能假定固定数量;跨分区返回列表也不保证全局顺序。没有有效提交才由 auto.offset.reset=earliest 决定从头开始,已有提交不会因 earliest 而重读。读取数量不足在期限结束返回非零,避免把空列表误当成功。
本篇的机制与失败分支可按综合练习书面推演;提示与参考答案分开提供,无需执行示例。
确认、重试与保留
本轮仅静态审阅,以下命令与练习均未执行;预期结果不是本轮运行证据。早期真实服务验证仅为历史记录,不能代替当前环境验证。
producer 的交付边界
produce 只是把消息排入客户端缓冲区。完整程序启用 enable.idempotence=true、acks=all,并在 delivery callback 收集 broker 结果,再检查 flush 剩余数量;任一交付错误或未排空都返回非零。acks=all 等待当前 ISR 参与确认,实际可靠性还取决于副本数和 min.insync.replicas;本实验单副本不具备节点失效冗余。
幂等生产者可以消除同一生产者会话内协议重试造成的部分重复,但客户端重启后重新提交同一订单仍可能是新消息。不能据此宣称业务端到端 exactly-once。订单号等业务键与外部数据库唯一约束仍有价值。
consumer 提交与副作用
消费提交必须以业务副作用的完成为边界。先提交再做业务,进程故障可能漏处理;先做业务再提交,故障后可能重复处理。这份 CLI 将完整消费批次的 JSON 输出并 flush 视为可观察实验效果,达到 count 且输出成功后才逐条同步提交;批次不足超时不输出且不提交。真实应用应先持久化业务结果,再提交位移,配合幂等键/去重表;外部数据库写入和 Kafka 提交通常不是同一个原子事务。输出之后提交失败时即使 stdout 有内容仍返回非零,下一次可能重放,接收结果的一方仍须处理重复。
批量提交可降低请求开销,却增加故障后的重放范围。不要异步启动业务任务后立刻提交尚未完成的消息,也不要对一个分区提交超过尚未处理成功的 offset。跨分区各有进度,不应把最大 offset 当成所有分区进度。
积压、保留与压缩
积压 lag 通常按分区观察日志末尾与组已提交位置之差,同时看记录速率、消息大小和处理时延。组重分配、提交间隔和事务可见性都会影响指标解释,lag=0 不证明下游业务结果正确。保留策略按时间/大小回收日志段,消费者落后太久可能失去待处理数据。
docker exec showdocs-m4b-kafka-broker /opt/kafka/bin/kafka-configs.sh --bootstrap-server broker:9092 --entity-type topics --entity-name orders --describe
docker exec showdocs-m4b-kafka-broker /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server broker:9092 --group accounting --describeretention.ms 是日志保留配置,不能当精确的单条消息定时器;实际删除还受 segment 滚动和清理调度影响。cleanup.policy=compact 保留每个 key 的最新状态,压缩异步运行,不等于队列消费确认,也不能用来保证所有历史事件长期可回放。不要为做练习随意缩短共享 topic 保留时间。
失败后的选择
临时网络错误由有界生产重试处理;永久业务解析失败则需要记录 topic/partition/offset、原始内容与原因,暂停该分区或发送独立失败 topic 并定义补偿规则。无限原地重试会卡住整个分区,跳过并提交又可能永久丢弃业务。本课程不虚构通用重试次数,也没有实现跨系统事务或自动死信框架。
失败练习把目标改成 127.0.0.1:1,预期在 message.timeout.ms=5000 消息到期后交付失败,进程返回 1;消息期限不保证进程精确五秒退出。不要因为 produce 没立即抛异常就判定消息成功写入。
继续阅读
- 完整实践
- 知识专题
- 知识专题