<?xml version="1.0" encoding="utf-8"?>
<ul>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。A.1 核心配置速查Broker 端            配置      默认      说明                  broker.id / node.id      -      节点唯一 ID              log.dirs      /tmp/kafka-logs      数据目录，逗号分隔多目录              num.partitions      1      新 topic 默认分区数              default.replication.factor      1      新 topic 默认副本数              min.insync.replicas      1      写入所需最小同步副本              unclean.leader.election.enable      false      是否允许非 ISR 副本当选              log.retention.hours      168      日志保留时间              log.retention.bytes      -1      每分区保留大小              log.segment.bytes      1GB      单 segment 大小              log.index.interval.bytes      4096      稀疏索引间隔              num.network.threads      3      网络线程数              num.io.threads      8      请求处理线程数              num.replica.fetchers      1      副本拉取线程数              message.max.bytes      ~1MB      单条消息上限              auto.create.topics.enable      true      自动建 topic（生产建议关）              auto.leader.rebalance.enable      true      自动 Leader 均衡              replica.lag.time.max.ms      30000      ISR 判定超时      Producer            配置      默认      说明                  acks      all      0/1/all              enable.idempotence      true      幂等生产者              retries      MAX      重试次数              delivery.timeout.ms      120000      发送总期限              linger.ms      0      攒批等待              batch.size      16KB      单批大小              buffer.memory      32MB      发送缓冲总量              compression.type      none      zstd/lz4/snappy/gzip              max.in.flight.requests.per.connection      5      未确认请求数              max.request.size      1MB      单请求上限              transactional.id      -      事务 ID，需稳定      Consumer            配置      默认      说明                  group.id      -      消费组              enable.auto.commit      true      自动提交（生产建议关）              auto.commit.interval.ms      5000      自动提交间隔              auto.offset.reset      latest      无位移时策略              session.timeout.ms      45000      心跳超时              heartbeat.interval.ms      3000      心跳间隔              max.poll.interval.ms      300000      两次 poll 上限              max.poll.records      500      单次 poll 条数              fetch.min.bytes      1      返回最小数据量              fetch.max.wait.ms      500      攒批最长等待              max.partition.fetch.bytes      1MB      单分区拉取上限              isolation.level      read_uncommitted      事务场景 read_committed              partition.assignment.strategy      Range+CoopSticky      分配策略              group.instance.id      -      静态成员 ID      A.2 常用命令速查# 主题kafka-topics.sh --create --topic t --partitions 6 --replication-factor 3kafka-topics.sh --listkafka-topics.sh --describe --topic tkafka-topics.sh --alter --topic t --partitions 12kafka-topics.sh --delete --topic tkafka-topics.sh --describe --under-replicated-partitionskafka-topics.sh --describe --unavailable-partitions# 控制台kafka-console-producer.sh --topic tkafka-console-consumer.sh --topic t --from-beginningkafka-console-consumer.sh --topic t --group g \  --property print.key=true --property print.partition=true# 消费组kafka-consumer-groups.sh --listkafka-consumer-groups.sh --describe --group gkafka-consumer-groups.sh --group g --topic t \  --reset-offsets --to-earliest --execute# 位移与日志kafka-get-offsets.sh --topic tkafka-dump-log.sh --files &lt;segment.log&gt; --print-data-log# 集群与运维kafka-broker-api-versions.sh --bootstrap-server bskafka-metadata-quorum.sh --bootstrap-server bs describe --statuskafka-leader-election.sh --bootstrap-server bs \  --election-type preferred --all-topic-partitionskafka-reassign-partitions.sh --bootstrap-server bs \  --topics-to-move-json x.json --broker-list 1,2,3 --generatekafka-configs.sh --bootstrap-server bs \  --alter --entity-type topics --entity-name t \  --set-config retention.ms=86400000# 压测kafka-producer-perf-test.sh --topic t --num-records 1000000 \  --record-size 1024 --throughput -1 \  --producer-props bootstrap.servers=bs acks=allkafka-consumer-perf-test.sh --bootstrap-server bs --topic t \  --messages 1000000A.3 可靠性黄金配置# Broker / Topicdefault.replication.factor=3min.insync.replicas=2unclean.leader.election.enable=falseoffsets.topic.replication.factor=3# Produceracks=allenable.idempotence=truedelivery.timeout.ms=120000消费端：手动提交、先处理后提交、失败进重试/死信、下游幂等。A.4 学习资源  Apache Kafka 官方文档：https://kafka.apache.org/documentation/  Kafka 源码：https://github.com/apache/kafka  KIP 列表：https://kafka.apache.org/ips  Confluent Blog：工程实践与深度解析  《Kafka 权威指南》《数据密集型应用系统设计》</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。最后一章回答三个问题：如何读 Kafka 源码、如何跟进社区演进、如何从“会用”走向“专家”。28.1 为什么要读源码  文档没写清的行为，源码是唯一真相；  故障排查时能从“猜配置”升级到“看路径”；  深度面试与架构评审需要机制级解释；  Kafka 是分布式系统工程的教科书：日志、副本、共识、网络、存储都有可借鉴的实现。28.2 源码工程结构Kafka 主仓库（github.com/apache/kafka）核心模块：            模块      内容      关键点                  clients/      Java 客户端      Producer/Consumer/Admin 的完整实现              server/      Broker 服务端      请求处理、副本管理、协调器              storage/      存储层      日志、segment、索引、清理              metadata/      元数据      KRaft 元数据与快照              raft/      Raft 实现      仲裁、日志复制、选举              network/      网络层      Acceptor/Processor/请求队列              coordinator/      各类协调器      group、transaction              core/      Scala 兼容层/工具      历史遗留与命令工具              connect/, streams/      生态组件      Connect 与 Streams 框架      构建与运行：git clone https://github.com/apache/kafka.gitcd kafka./gradlew jar           # 编译./gradlew check         # 测试用 IDEA 打开后，重点给 clients、server、storage 加源码索引，调试时跟着请求走。28.3 第一条阅读路线：Producer 发送链路目标：把第 5 章的流程在代码里走一遍。KafkaProducer.send()  -&gt; doSend()     -&gt; interceptor.onSend()     -&gt; key/value serialize     -&gt; partition()     -&gt; accumulator.append()            # RecordAccumulator  -&gt; Sender 线程 run()     -&gt; drain batches by node     -&gt; NetworkClient.send()     -&gt; handleResponse / completeBatch  # 重试、回调重点类：  KafkaProducer：入口与配置；  RecordAccumulator：攒批、buffer 管理、阻塞控制；  Sender：IO 线程主循环；  NetworkClient：请求/响应与元数据刷新；  TransactionManager：事务状态机（进阶）。带着问题读：max.block.ms 在哪里生效？重试如何保序？batch 何时被拆分？28.4 第二条阅读路线：Consumer 与协调器KafkaConsumer.poll()  -&gt; subscribe / assignment  -&gt; pollForFetchMessages()     -&gt; fetcher.collectFetchedData()     -&gt; coordinator heartbeat / autocommit  -&gt; ConsumerCoordinator     -&gt; JoinGroup / SyncGroup / Heartbeat重点类：  KafkaConsumer：单线程模型与锁；  SubscriptionState：订阅与分区状态；  Fetcher：拉取与位置管理；  ConsumerCoordinator + AbstractCoordinator：组协议；  RangeAssignor / StickyAssignor：分配算法，适合作为第一组“可独立读懂”的类。实验：给 CooperativeStickyAssignor 打断点，观察两轮 rebalance 的分配差异。28.5 第三条阅读路线：Broker 请求处理SocketServer.accept -&gt; Processor -&gt; requestQueueKafkaRequestHandler.run()  -&gt; KafkaApis.handleProduceRequest / handleFetchRequest  -&gt; ReplicaManager.appendRecords()  -&gt; UnifiedLog.append()               # storage 层  -&gt; DelayedProduce (Purgatory)重点类：  SocketServer：网络线程模型；  KafkaApis：所有请求的入口分发，读它是“按请求索引源码”的最佳地图；  ReplicaManager：副本、ISR、延迟操作；  UnifiedLog / LogSegment：日志与分段；  DelayedOperationPurgatory：延迟请求完成机制；  GroupCoordinator：消费组状态机。28.6 第四条阅读路线：KRaft 元数据RaftClient / KafkaRaftServer  -&gt; __cluster_metadata 复制与提交  -&gt; MetadataRecordSerde  -&gt; BrokerMetadataListener 应用元数据  -&gt; KRaftRaftServer / QuorumController重点看：  元数据记录如何被序列化并按序应用；  快照（KRaftSnapshot）的生成与加载；  voter/learner 的差别在代码里的体现；  Controller 切换时哪些状态需要恢复。这条线最抽象，建议放在最后，且结合 kafka-metadata-quorum.sh 的输出对照理解。28.7 调试技巧  用小集群复现：单机三节点伪集群，行为真实且可断点；  按请求断点：在 KafkaApis.handle*Request 上按 request type 条件断点；  日志开 DEBUG：客户端 org.apache.kafka=DEBUG，服务端按包名调整 log4j；  对比行为：改一个参数（如 acks、linger.ms），观察指标与日志差异；  读测试：单元测试是最小可运行用例，比生产代码更能说明意图；  版本对照：读 3.x 理解过渡，再看 4.x 的纯 KRaft 实现，能看清演化动机。28.8 跟进社区：KIP 是地图Kafka 的重大变更通过 KIP（Kafka Improvement Proposal） 讨论。值得关注的方向：  KRaft 与元数据快照演进；  队列语义（共享组、workload 重平衡）；  分层存储（tiered storage）；  事务与 exactly-once 增强；  Streams 与 Connect 的新能力；  性能与大规模集群运维（百万分区方向）。阅读方法：先读 Motivation 与 Public Interfaces，理解“为什么改、影响什么”；有精力再读实现讨论。订阅 dev@kafka.apache.org 或关注 GitHub Discussions。28.9 成长路线图L1 会用：  建主题、发消费消息、Spring 集成、看 lagL2 稳：  手动提交、幂等、重试/死信、静态成员、压测调参、监控告警L3 懂：  存储/副本/协调器/事务/rebalance 内部机制，能讲清每个配置的副作用L4 治：  容量规划、故障演练、迁移升级、多租户治理、SOP 与工具化L5 破：  源码级定位、KIP 跟进、给社区提 issue/PR、输出设计模式与最佳实践每升一级的关键不是“知道更多名词”，而是能解释边界条件与失败模式：什么情况下配置会失效、什么场景下设计会崩、代价由谁承担。28.10 推荐深入资源  官方文档与配置说明：最权威的参数语义来源；  Kafka 源码仓库的 docs/ 与每个模块的测试；  KIP 列表：理解设计动机的第一手材料；  《Kafka 权威指南》：工程视角的经典；  《数据密集型应用系统设计》（DDIA）：复制、一致性、日志模型的底层理论；  Raft 论文与 KRaft 设计文档：元数据共识基础；  内部复盘：你自己的每一次事故都是最好的教材。28.11 写在最后从第一次敲下 kafka-console-producer，到能读懂 ReplicaManager 的一行代码，中间隔着无数次实验、事故与追问。Kafka 的优雅在于它把复杂的分布式问题压缩成了几个朴素概念：分区日志、副本同步、消费位移、元数据日志。把这四个概念在不同场景下的边界想透，你就不再是在“背 Kafka”，而是在用分布式系统的思维方式审视任何消息与存储系统。愿你既有高吞吐的系统，也有低延迟的生活。完结。本章小结Kafka 的进阶路线从可靠使用走向机制理解，再到源码定位和架构治理。掌握分区日志、副本同步、消费位移和元数据日志四个核心概念，并通过实验、事故和 KIP 持续验证边界，才是长期有效的学习方式。思考题  你当前处于 L1 到 L5 中的哪一层，瓶颈是什么？  如何为自己设计一个可重复的 Kafka 故障实验？  读源码时为什么建议从 KafkaApis.handle*Request 入手？  KIP 的 Motivation 和 Public Interfaces 能分别回答什么问题？  下一个季度你打算深入哪个 Kafka 子系统？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章按“基础 -&gt; 原理 -&gt; 性能运维 -&gt; 场景设计”组织 50 个高频问题，答案追求面试可直接使用的深度与结构。建议先遮住答案自己回答，再对照补充。27.1 基础篇Q1. Kafka 是什么？能解决什么问题？分布式事件流平台，兼具发布订阅、持久化存储与流处理三类能力。典型用途：系统解耦、异步削峰、日志/埋点管道、流计算、CDC 同步、事件驱动架构。回答时可强调它以“分区日志”为核心，消费不删数据，因此可回放、可多订阅。Q2. Kafka 为什么快？六件套：磁盘顺序写、页缓存、零拷贝（sendfile）、批量+压缩、分区水平扩展、消费者拉模式背压。再补一句：V2 消息格式用增量编码与 batch 级 CRC，进一步降低解析与存储成本。Q3. Topic、Partition、Replica 的关系？Topic 是逻辑类别；Partition 是并行与顺序的物理单位，每分区是一个只追加日志；Replica 是分区的副本，分 Leader/Follower，读写只走 Leader，Follower 只同步。Q4. 为什么分区只能增加不能减少？减少要处理已删除分区上的消息归属、key 哈希重分布、消费者位移迁移、副本删除时序等一致性问题，工程收益低风险高。Kafka 官方不支持；若有必要，走“新 topic 迁移 + 切流”。Q5. Kafka 和 RabbitMQ 怎么选？Kafka：海量日志/事件流、高吞吐、长期保留、回放、流处理生态。RabbitMQ：复杂路由、低延迟传统消息、请求回复型任务队列。定位上 Kafka 更像数据中枢，RabbitMQ 更像消息代理。Q6. delete 和 compact 两种清理策略的区别？delete 按 retention 滚动删除 segment，适合事件/日志。compact 按 key 保留最新值，适合状态快照、配置、CDC 最新值。可组合 compact,delete。Q7. 什么是 ISR？与 Leader 保持同步的副本集合，判定标准是 replica.lag.time.max.ms 内追上 Leader。acks=all 等待的是 ISR 而非全部副本；min.insync.replicas 限制 ISR 最小数量。Q8. LEO 和 HW 分别是什么？LEO 是日志下一条待写入的 offset；HW 是所有 ISR 已同步的位置，消费者只能读到 HW 之前。Leader 切换与副本截断依赖它们（更精确的是 leader epoch）。Q9. 消费组内外的消费语义？组内一个分区只给一个消费者（竞争、负载均衡）；组间各自独立消费全量（广播）。消费者数超过分区数时多余实例空闲。Q10. 消费位移存在哪里？内部 compact topic __consumer_offsets，默认 50 分区，key 为 group+topic+partition，value 为 offset 等元数据。27.2 原理篇Q11. 描述生产者发送流程。主线程：拦截器 -&gt; 序列化 -&gt; 分区器 -&gt; RecordAccumulator 攒批；Sender IO 线程按 Broker 合并发送；Broker 返回后执行回调。参数上 batch.size 控批大小，linger.ms 控等待，delivery.timeout.ms 控总期限。Q12. 消息如何选分区？显式指定优先；否则 key 非空用 murmur2(key) % 分区数；key 为空用粘性分区（先填满一个分区的 batch 再切换），兼顾均衡与批效率。Q13. acks=0/1/all 的语义？0：不等确认，可能丢；1：Leader 写入即返回，Leader 闪退未同步会丢；all：等 min.insync.replicas 个 ISR 副本确认，配合 RF=3/min ISR=2 最可靠。all 在 ISR=1 时退化为 1。Q14. 幂等生产者的原理？PID + epoch + 每分区单调 sequence。Broker 缓存最近 5 个 batch 序号：重复丢弃并返回成功，缺口报乱序。范围限单会话单分区，进程重启后不能防业务层重复。Q15. 事务的原理？transactional.id 定位事务协调器；initTransactions 注册并 fence 旧 epoch；提交两阶段（PrepareCommit + 分区 marker）；read_committed 消费者由 LSO 控制可见性。适合跨分区原子写与 consume-transform-produce。Q16. Kafka 的 exactly once 范围是什么？默认幂等防发送重试重复；事务保证 Kafka 到 Kafka 链路的原子性；涉及外部系统要业务幂等（唯一键/状态机/本地事务）配合。不能说“开了 EOS 就万事大吉”。Q17. Rebalance 的触发条件与流程？触发：成员变化、订阅变化、分区数变化。流程：FindCoordinator -&gt; JoinGroup（选组 leader）-&gt; 组 leader 算分配 -&gt; SyncGroup 下发 -&gt; Heartbeat 维持。generation 递增隔离旧成员。Q18. 四种分配策略的区别？Range 按 topic 切块可能不均；RoundRobin 全局轮询更均匀但要求订阅一致；Sticky 均衡且保留原分配减少迁移；CooperativeSticky 在 Sticky 上支持增量式，只停顿迁移分区，推荐新系统。Q19. Rebalance 风暴怎么排查？看被踢成员与时间点，对因处理：max.poll.interval 超时（降 max.poll.records/优化处理）、GC 停顿、心跳超时、发布频繁（静态成员）、订阅不一致。加协作式策略与告警。Q20. Kafka 如何保证顺序？分区内有序是底线；业务上同实体同 key；生产端开幂等且 in-flight&lt;=5；消费端单线程或按 key 路由线程；避免扩分区破坏 key 连续性；必要时版本号拒绝旧事件。Q21. 描述存储结构。每分区一个目录，多个 segment（.log/.index/.timeindex），文件名为起始 offset。索引是稀疏的（每 4KB 日志一条），查找用二分索引 + 小范围顺序扫描；leader-epoch-checkpoint 支持一致性。Q22. 零拷贝是怎么回事？消费 fetch 时用 sendfile，数据从页缓存直达网卡，跳过用户态拷贝与上下文切换；条件是无需服务端格式转换且通道未做用户态加密。Q23. Leader epoch 解决什么问题？解决基于 HW 截断在极端时序下的副本分叉：Follower 重启/追随时先按 epoch 向 Leader 查询权威位移，精确截断后再同步。Q24. KRaft 与 ZooKeeper 模式的区别？KRaft 把元数据放进 __cluster_metadata Raft 日志，多数派 commit 后各节点按序回放；不再有 ZK 双写窗口，运维一套系统，Controller 切换更快。Q25. __consumer_offsets 的 key/value 是什么？为什么 compact？key=group+topic+partition，value=offset+metadata 等；每个位置只需最新值，compact 让体积可控，同时保留回溯能力。27.3 性能与运维篇Q26. 消息丢失怎么系统排查？分层：Broker 是否写入（get-offsets/dump-log）、生产是否确认（回调/acks/flush）、消费是否跳过（committed offset/reset 记录）、保留期是否覆盖。修复靠 acks=all+RF3+minISR2+先处理后提交。Q27. 重复消费的常见原因？处理完未来得及提交就宕机；rebalance 后重复处理；commitAsync 失败未兜底；业务自己超时重发。处理：手动提交+同步兜底+下游唯一键/状态机。Q28. 消息堆积怎么办？先定位：写入突增、消费变慢、消费者减少、分区倾斜。手段：加消费者（&lt;=分区数）、批量写下游、降级重逻辑、扩分区（注意 key 顺序）、业务确认后跳过过期数据。Q29. 乱序怎么处理？查 key 设计（同实体是否同 key）、生产重试乱序（幂等+in-flight&lt;=5）、消费并发、扩分区、上游乱序。修复按根因对症。Q30. ISR 频繁收缩的原因？Follower 机器慢（CPU/磁盘/GC）、num.replica.fetchers 不足、Broker 间网络抖动、大迁移占 IO、replica.lag.time 过小。处置：调 fetcher、限流迁移、修硬件。Q31. 怎么监控 consumer lag？客户端 records-lag + 外部采集器（Burrow/kafka-lag-exporter，覆盖消费者挂掉场景）+ 命令巡检兜底。告警看 lag 趋势与追平时间（lag/消费速率）。Q32. 分区数怎么定？目标吞吐/单分区吞吐，向上取整并留余量；同时看消费并行度（消费者实例&lt;=分区数）。分区不是越多越好：文件句柄、元数据、请求碎片、再平衡时间都会变差。Q33. 生产者怎么调优？吞吐：linger.ms 5-20、batch.size 32-64KB、zstd/lz4、buffer.memory 足够。延迟：linger=0、压缩 lz4。可靠：acks=all+幂等+回调。大消息拆引用。Q34. 消费者怎么调优？实例数&lt;=分区数、批量处理、fetch.min.bytes 适度放大、max.poll.records 与处理时长匹配、静态成员+协作式 rebalance、下游异步化。Q35. Broker 怎么调优？看指标调：NetworkProcessorAvgIdle&lt;30% 加网络线程；RequestHandlerAvgIdle&lt;30% 加 IO 线程；ISR 落后加 replica fetchers；磁盘瓶颈加盘/条带化。页缓存优先于大 JVM 堆。Q36. 为什么 Kafka 不建议开超大 JVM 堆？性能核心在页缓存；堆大 GC 停顿风险高。通常 6-8GB 堆，剩余内存留给页缓存；网络/压缩用堆外内存，容器 limit 要留余量。Q37. 扩容 Broker 后如何均衡数据？kafka-reassign-partitions 生成-&gt;审查-&gt;执行-&gt;验证，可 throttle 限速分批迁移；之后 preferred leader 选举均衡 Leader；大规模用 Cruise Control。Q38. Kafka 安全怎么做？三层：TLS 加密、SASL/SCRAM 认证、ACL 授权（默认拒绝+最小权限）。凭据用 Secret/Vault，审计 ACL 变更。Q39. 怎么迁移 Kafka 集群？MirrorMaker 2 复制（注意 replication policy 与 offset checkpoint）、数据一致性校验、生产灰度切流、消费位移同步、观察期后停旧集群；全程保留回滚窗口。Q40. 磁盘快满了怎么办？应急：扩容、缩短低价值 topic 保留、清理僵尸 topic；根治：容量水位告警、生命周期治理、压缩策略、容量规划 review。磁盘 100% 会影响写入与元数据，优先级最高。27.4 场景设计篇Q41. 设计一个日志采集平台。采集端（Filebeat/Fluent-bit）-&gt; Kafka（按日志类型分 topic，分区按吞吐规划，zstd 压缩，保留 3-7 天）-&gt; 批量消费者写 ClickHouse/ES + 归档 S3。要点：acks 可 1 换吞吐、批量写库、lag 与端到端延迟监控。Q42. 订单系统如何不丢消息？Outbox 本地事务 + relay 发送（acks=all、幂等、回调）；topic RF=3/minISR=2；key=orderId 保序；消费先处理后提交 + 状态机幂等 + 重试/DLT；每日对账。Q43. Kafka 怎么实现延迟队列？原生不支持。常见方案：按延迟级别建 topic + 定时转发；时间轮库自研消费者；或用支持延迟的中间件承接该场景。说明权衡即可，别硬造。Q44. 大消息（视频/大文本）怎么处理？消息只放引用（对象存储 URL+摘要），内容放外部存储；必要时调 message.max.bytes/producer max.request.size/fetch 参数，但大消息会破坏批效率与页缓存命中率。Q45. 如何实现广播和单播？广播：不同消费组各自独立消费全量。单播（竞争）：同一消费组内分区独占。注意组 id 管理：新组=新广播实例，同组扩容=负载均衡。Q46. 消费端幂等怎么设计？唯一事件表（event_id 唯一键 + 业务写同事务）、状态机拒绝非法迁移、upsert by 业务主键、乐观锁版本号。按业务后果选择强度。Q47. 写库和发消息如何保证一致？不要双写。用 Outbox：业务与 outbox 同一本地事务，relay 至少一次投递，消费端幂等。CDC 方案（Debezium 读 outbox/binlog）是变体。Q48. 多租户 Kafka 怎么隔离？共享 topic（字段/header 标识，弱）、独立 topic（ACL/quota，中）、独立集群（强）。配 quota 防大租户、header 带租户、大租户独立分区/topic 防热点。Q49. Kafka 能当缓存或数据库用吗？不能完全替代。它是为追加日志与顺序读优化的，点查/随机更新弱。可作物化状态源（KTable/compact topic）支撑流处理，但业务主存储仍应是 DB。Q50. 什么场景不适合 Kafka？强事务同步返回、低延迟 RPC 语义、复杂路由优先的小规模任务队列、超小消息量的简单系统（上 Kafka 运维成本不划算）、需要每条消息随机更新删除的场景。27.5 回答技巧  先给结论，再给机制，最后给边界：例如“acks=all 一定不丢吗？不一定，当 ISR=1 时退化为…”；  带上参数与证据：能说出配置名、指标名、命令，可信度立刻不同；  结合事故或实验：讲一个你亲手复现的场景，胜过背十条概念；  主动提权衡：Kafka 的设计几乎都是取舍，展示你知道代价。本章小结面试的高分答案 = 准确概念 + 内部机制 + 参数证据 + 权衡意识。把第 10-20 章的原理章节吃透，这 50 个问题就能自然生长出你自己的版本。思考题  面试中如何证明自己真的处理过 Kafka 生产问题？  为什么回答高可用问题时必须同时说明 acks、min.insync.replicas 和副本数？  消费端乱序、重复和丢失分别对应哪些机制？  如何解释 Kafka 页缓存与 JVM 堆内存的分配权衡？  设计题应按什么顺序展开容量、可靠性、治理和演进？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。会用 API 只是开始，把 Kafka 放进架构需要处理一致性、顺序、依赖与演进。本章讲事件驱动架构中的核心模式。26.1 事件驱动 vs 请求驱动            维度      请求驱动（RPC/REST）      事件驱动（Kafka）                  耦合      调用方必须知道下游      生产者只发布事实              时间      下游必须在线      异步，可落后再追              扩展      新下游要改调用方      新订阅者零侵入              一致性      强（同步返回）      最终一致              排查      链路追踪直观      需要 event id + 对账      判断标准：“这个动作是否是已经发生的事实？” 是则发布事件；“希望对方做什么”更适合命令/请求。26.2 三种事件风格1. Event Notification（事件通知）OrderCreated { orderId }只告诉“发生了”，下游需要回查。优点：事件小、敏感数据少；缺点：下游强依赖上游查询接口，形成隐式耦合。2. Event-Carried State Transfer（携带状态）OrderCreated { orderId, amount, userId, status }事件自带数据，下游可本地物化，真正解耦。代价：数据冗余与 schema 演进治理（第 8 章）。3. Event Sourcing（事件溯源）事件是唯一事实源，当前状态由事件回放得到：事件流: Created -&gt; ItemAdded -&gt; Paid -&gt; Shipped当前状态: 由 1..n 折叠(fold)得到适合审计、回放、复杂状态机；成本高（schema 稳定性、快照、版本迁移），不要无脑上。26.3 Outbox 模式：终结双写问题：业务写库和发 Kafka 是两个系统，无法放进一个事务。错误做法：  BEGIN; 写订单; COMMIT;   sendKafka(...);   // 中间宕机 -&gt; 丢事件  sendKafka(...); BEGIN; 写订单; COMMIT;      // 发了事件但库没写 -&gt; 幽灵事件Outbox 解法：本地事务：  INSERT orders;  INSERT outbox(event);COMMIT;异步投递：  relay 读 outbox -&gt; 发 Kafka（至少一次）  消费端按 event_id 幂等实现细节：  outbox 表加 status/created_at/id，Relay 按 id 顺序投递并标记；  投递用 acks=all + 回调确认；  允许重复发送，幂等在消费端兜底；  表数据量大时按时间分区/定期归档。26.4 Saga：跨服务最终一致没有分布式事务时，用“本地事务 + 补偿”编排长流程：下单 Saga：1. 订单服务: 创建订单(PENDING)         [本地事务]2. 库存服务: 预扣库存                   [本地事务 + 消费事件]3. 支付服务: 创建支付单4. 成功: 订单 PAID, 库存 CONFIRMED   失败: 订单 CANCELLED, 库存 RELEASE(补偿)两种编排方式：            方式      特点                  编排（Orchestration）      中央 Saga 协调器发命令、监听结果，流程清晰              协同（Choreography）      服务互相订阅事件，去中心化，链路难追踪      实践建议：超过 3-4 步、有超时回滚的流程用编排；简单链路用协同 + 全链路 event id。26.5 CQRS 与 Kafka命令写主库，查询读物化视图：Command -&gt; MySQL -&gt; outbox -&gt; Kafka -&gt;  Projector(消费者) -&gt; ES/Redis/ClickHouse 读模型要点：  读模型按查询场景建（宽表、索引、物化列）；  投影是幂等的，重启可从 offset 重放；  最终一致窗口要监控（主库 vs 读模型延迟）；  业务要能接受“写后读可能读旧值”（或读主库兜底）。26.6 背压Kafka 天然是缓冲区，但不是无限缓冲：洪峰 -&gt; Kafka 堆积(lag) -&gt; 消费者按能力拉取 -&gt; 下游稳定背压治理三问：  堆积多久可接受？（业务 SLA）  保留期能否覆盖最长堆积？（防位移过期）  下游能否弹性扩容？（消费者数 &lt;= 分区数是硬上限）超过分区数还想加速：分区扩容（注意 key 顺序）、下游再并行、或拆分 topic。26.7 多租户隔离常见三级隔离：            级别      做法      隔离度                  共享 topic      租户 ID 做消息字段/header      弱，易互相影响              独立 topic      每租户一组 topic + ACL      中，配额与监控友好              独立集群      物理隔离      强，成本高      配套手段：  quota 按用户/客户端限流，防止单租户打挂集群；  ACL 严格隔离读写；  header 放租户 ID，消息体可加密；  大租户独立 topic/分区，避免热点 key 倾斜。26.8 Schema 与契约演进事件是跨团队契约，演进要有流程：1. schema 入 Git，PR 评审2. CI 跑兼容性检查（FULL 推荐）3. 消费者先兼容旧版，再发布新版4. 破坏性变更 -&gt; 新 topic / 新版本事件5. 双写过渡期 -&gt; 验证 -&gt; 下线旧版配套治理：  event id 全局唯一，贯穿链路追踪与对账；  事件带 occurred_at（业务时间）与 version；  建立 schema registry 与消费关系登记，知道“谁在用这个事件”。26.9 事件驱动架构的坑  把事件当命令用：到处发布 DoSomething，职责倒挂；  无幂等：重复消费直接写脏数据；  无对账：最终一致变成“最终不知道一不一致”；  过度事件溯源：普通业务上了全套，复杂度爆炸；  共享主题热 key：大客户流量把一个分区打满；  无死信治理：失败消息进黑洞；  版本随意变更：下游大规模反序列化失败。本章小结  事件表达“已发生的事实”，命令表达“要求执行的动作”，两者别混；  Outbox 解决双写，Saga 解决跨服务长事务，CQRS 解决读写模型差异；  幂等、对账、死信治理是事件系统的三大基础设施；  背压是 Kafka 的天赋，但保留期与分区数是硬边界；  schema 是团队间契约，演进必须流程化。思考题  “PaymentRequested” 和 “PaymentCompleted” 哪个是事件？哪个更像命令？为什么？  Saga 中补偿动作本身失败怎么办？设计重试与人工介入机制。  你的系统里哪些查询适合走 CQRS 读模型？数据延迟窗口要求是多少？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Kafka 是实时数据管道的事实标准。本章讲它与 Flink、Spark、ClickHouse、数据湖、CDC 的集成方式与关键取舍。25.1 整体链路业务库 --CDC(Debezium)--&gt; Kafka --Flink/Spark--&gt;  ├-&gt; 实时看板(ClickHouse/Doris)  ├-&gt; 数据湖(Iceberg/Hudi)  └-&gt; Kafka(回流/宽表)Kafka 在其中的职责：缓冲、解耦、回放、多订阅。一旦数据进了 Kafka，就可以被任意多个引擎按需消费，互不影响。25.2 Kafka + FlinkFlink 是流处理生态中事件时间、状态管理与 exactly-once 能力最强的引擎之一，Kafka 是它最常见的 source/sink。Maven 依赖&lt;dependency&gt;  &lt;groupId&gt;org.apache.flink&lt;/groupId&gt;  &lt;artifactId&gt;flink-streaming-java&lt;/artifactId&gt;  &lt;version&gt;1.19.0&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;  &lt;groupId&gt;org.apache.flink&lt;/groupId&gt;  &lt;artifactId&gt;flink-connector-kafka&lt;/artifactId&gt;  &lt;version&gt;3.1.0-1.19&lt;/version&gt;&lt;/dependency&gt;典型作业StreamExecutionEnvironment env =        StreamExecutionEnvironment.getExecutionEnvironment();env.enableCheckpointing(60_000, CheckpointingMode.EXACTLY_ONCE);env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30_000);KafkaSource&lt;String&gt; source = KafkaSource.&lt;String&gt;builder()        .setBootstrapServers("broker1:9092")        .setTopics("user-behaviors")        .setGroupId("flink-behavior-job")        .setStartingOffsets(OffsetsInitializer.committedOffsets(                OffsetResetStrategy.EARLIEST))        .setValueOnlyDeserializer(new SimpleStringSchema())        .build();DataStream&lt;Event&gt; events = env.fromSource(                source, WatermarkStrategy                        .forBoundedOutOfOrderness(Duration.ofSeconds(5)),                "kafka-source")        .map(this::parseEvent);events.keyBy(e -&gt; e.userId)      .window(TumblingEventTimeWindows.of(Time.minutes(5)))      .aggregate(new SessionAgg())      .sinkTo(buildClickHouseSink());env.execute("behavior-agg");Exactly Once 的两条线1. Kafka -&gt; Flink 内部:   checkpoint 把消费 offset 记入状态，故障恢复时从 checkpoint 位点重放2. Flink -&gt; Kafka 输出:   KafkaSink 配置 DELIVERY_GUARANTEE_EXACTLY_ONCE,   依赖 Kafka 事务 + 两阶段提交KafkaSink 示例：KafkaSink&lt;AggResult&gt; sink = KafkaSink.&lt;AggResult&gt;builder()        .setBootstrapServers("broker1:9092")        .setRecordSerializer(new SimpleStringSchema(), "result-topic")        .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)        .setTransactionalIdPrefix("flink-agg-")        .build();注意：  exactly-once sink 会拉长端到端延迟（受 checkpoint 与事务提交影响）；  下游 Kafka 消费者要用 read_committed；  watermark 决定事件时间语义，乱序窗口要配 allowed lateness 与侧输出。25.3 Kafka + Spark Structured Streaming已有 Spark 技术栈时，Structured Streaming 也能消费 Kafka：val df = spark.readStream  .format("kafka")  .option("kafka.bootstrap.servers", "broker1:9092")  .option("subscribe", "user-behaviors")  .option("startingOffsets", "latest")  .load()val events = df.selectExpr(  "CAST(key AS STRING) key",  "CAST(value AS STRING) json").select(from_json($"json", schema).as("e")).select("e.*")val agg = events  .withWatermark("event_time", "10 minutes")  .groupBy(window($"event_time", "5 minutes"), $"itemId")  .count()agg.writeStream  .format("console")  .outputMode("update")  .start()  .awaitTermination()对比：            维度      Flink      Spark Streaming                  延迟      毫秒-秒级      微批（默认百毫秒-秒级）              状态      RocksDB 大状态成熟      基于 checkpoint              事件时间/watermark      强      支持              生态      纯流      批流一体、队列生态      25.4 Kafka -&gt; ClickHouse / Doris实时看板常用组合。三种摄入方式：            方式      特点                  自研消费者批量 INSERT      灵活、可控，需处理幂等与批次              ClickHouse Kafka 表引擎      数据库内拉取，部署简单，运维边界模糊              Doris/StarRocks Routine Load      声明式任务，原生支持 group 与重试      ClickHouse 批量插入建议：  单批 1 万-10 万行或 10-100MB；  按 ORDER BY 键排序写入，减少后台 merge；  用 ReplacingMergeTree / 协处理器处理迟到重复；  消费者按分区并行，写库失败不提交位移。25.5 Kafka -&gt; 数据湖（Iceberg）近实时数仓路径：Kafka -&gt; Flink Iceberg Sink -&gt;  ODS 表 -&gt; 批/流加工 -&gt; DWD/DWS -&gt; 查询引擎(Trino/Spark)Flink 写 Iceberg 的关键点：  exactly-once 依赖 checkpoint 提交快照（不同版本实现细节不同）；  小文件问题：调大 checkpoint 间隔与文件滚动条件，定期 compaction；  到达时间 vs 事件时间：湖表分区按事件时间列，避免迟到数据写错分区；  回溯重建：Kafka 保留期内可重放，超过则需从湖表或归档补。25.6 CDC：DebeziumCDC 把数据库变更变成流：MySQL binlog -&gt; Debezium(Connect) -&gt; Kafka topic(每表一个)价值：  缓存失效、搜索索引更新准实时；  数仓同步替代双写与定时拉取；  审计流水（完整变更历史）。关键实践：  topic 按 server.schema.table 命名（默认规则），key 为主键；  snapshot 模式选择 initial / schema-only / never；  Debezium offset 与 schema history topic 必须高可靠（RF=3）；  下游用 upsert 语义（Flink upsert-kafka connector、CH ReplacingMergeTree）；  敏感列在 connector 配置里脱敏或加密。25.7 反压与资源大数据链路中常见“Kafka lag 告警，但 Broker 很闲”：排查顺序：1. 计算引擎 slot/并行度是否不足2. 算子是否有数据倾斜（keyBy 热点）3. 外部 sink 是否慢（DB 写入、湖提交）4. checkpoint 是否过大过慢，拖住作业处理：  并行度对齐 Kafka 分区数（source 并行 &lt;= 分区数）；  热点 key 加盐或预聚合；  sink 攒批/异步化；  状态清理（TTL）避免 checkpoint 膨胀。本章小结  Kafka 是大数据链路的缓冲与回放层，多引擎共享同一份流；  Flink 与 Kafka 组合可做到端到端 exactly-once，注意 checkpoint、事务与 read_committed；  Spark 适合批流一体场景，纯低延迟大状态优先 Flink；  ClickHouse/Doris 消费要攒批，湖表要管理小文件与分区；  CDC 是数仓与缓存同步的正确姿势，offset/schema 主题必须高可靠。思考题  Flink checkpoint 间隔从 30s 调到 5 分钟，对吞吐、延迟、恢复各有什么影响？  为什么 Kafka -&gt; Flink -&gt; Kafka 的 exactly-once 在链路外还要 read_committed？  设计一个“MySQL 订单表实时同步到 ClickHouse”方案，说明迟到与重复如何处理。</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章把前面所有知识拼成两个完整项目：一个是高吞吐日志采集平台（偏数据管道），一个是电商订单事件系统（偏业务事件驱动）。给出架构、分区设计、关键代码与上线清单。24.1 项目一：日志采集平台需求  2000 台机器，Nginx/应用日志峰值 50 万条/s，均单条 500B；  写入 ClickHouse 供查询，写 S3/HDFS 归档；  允许极少丢失，但不允许堆积压垮采集端；  端到端延迟 P99 &lt; 10s。架构应用/Nginx   │ Filebeat/Fluent-bit 采集   vKafka 集群   topic: logs.app  (分区 24, RF=3)          logs.nginx (分区 24, RF=3)   │                        │   │ Consumer A (批量)       │ Consumer B   v                        vClickHouse              S3 归档任务   │   └--&gt; 查询/看板容量估算峰值流量 = 500,000 条/s * 500B = 250MB/s单分区经验写入 15MB/s -&gt; 至少 17 个分区考虑消费并行与增长 -&gt; 规划 24 个分区保留 3 天(未压缩) = 250 * 86400 * 3 ≈ 64.8TB(单副本)RF=3 + 压缩比 0.6 -&gt; 64.8 * 3 * 0.6 ≈ 116TBTopic 设计bin/kafka-topics.sh --create --topic logs.app \  --partitions 24 --replication-factor 3 \  --config retention.ms=259200000 \  --config min.insync.replicas=2 \  --config compression.type=producer \  --config segment.bytes=1073741824key 用 host + 服务名 均衡写入；日志不需要严格顺序，也可无 key 让粘性分区攒批。采集端要点（Filebeat 示例）filebeat.inputs:  - type: log    paths:      - /var/log/app/*.log    fields:      service: order-api      env: prod    fields_under_root: trueoutput.kafka:  hosts: ["broker1:9092", "broker2:9092", "broker3:9092"]  topic: "logs.app"  required_acks: 1  compression: zstd  bulk_max_size: 4096  worker: 2采集端对可靠性要求略低于业务事件，required_acks=1 换取吞吐；核心审计日志则用 all。ClickHouse 消费端批量拉取、攒批插入是关键：@KafkaListener(topics = "logs.app", groupId = "log-to-clickhouse", batch = "true")public void onBatch(List&lt;ConsumerRecord&lt;String, String&gt;&gt; records, Acknowledgment ack) {    List&lt;LogRow&gt; rows = records.stream().map(this::parse).toList();    int chunk = 5000;    for (int i = 0; i &lt; rows.size(); i += chunk) {        clickhouseDao.insertBatch(rows.subList(i, Math.min(i + chunk, rows.size())));    }    ack.acknowledge();}配置要点：spring:  kafka:    consumer:      max-poll-records: 2000      fetch-min-size: 1MB      fetch-max-wait: 2s    listener:      concurrency: 12   # &lt; 分区数 24，可后续扩      ack-mode: manualClickHouse 写入失败时不要 ack，进入本地重试；连续失败写入本地磁盘队列并告警（降级路径）。监控  采集端：Filebeat 发送失败率、背压；  Kafka：BytesIn/Out、lag、ISR；  消费端：批大小分布、写库耗时、死信量；  业务：端到端延迟 = 日志时间戳 - 入库时间。24.2 项目二：电商订单事件系统需求  下单成功后：扣库存、加积分、发通知、更新数仓；  订单事件绝不能丢，允许重复但必须幂等；  同一订单事件必须有序处理；  处理失败要有重试与死信，可人工重放。架构订单服务  ├─ MySQL: orders 表 + outbox 表 (同一本地事务)  └─ Relay 进程读 outbox -&gt; Kafka                          │                          v                topic: order.events (分区 12, RF=3)                          │        ┌─────────────┬───┴────────┬──────────────┐        v             v            v              v    库存消费者      积分消费者    通知消费者       数仓消费者   (group=stock)  (group=points) (group=notify)  (group=dwh)        │        └--失败--&gt; retry topic --&gt; DLT --&gt; 人工/自动补偿Topic 与顺序设计bin/kafka-topics.sh --create --topic order.events \  --partitions 12 --replication-factor 3 \  --config min.insync.replicas=2 \  --config retention.ms=604800000  key = orderId，保证同一订单的事件进同一分区、严格有序；消费端按 key 做幂等（订单状态机）；  分区 12 &gt; 下游最慢消费者的实例数上限，预留扩容。Outbox：解决“双写不一致”直接在业务代码里“写库 + 发 Kafka”会出现一个成功一个失败的分裂。Outbox 把这两步变成“一个本地事务 + 一个可靠投递”：BEGIN;  INSERT INTO orders(...) VALUES (...);          -- 业务数据  INSERT INTO outbox(      aggregate_id, event_type, payload, created_at  ) VALUES (      'order-1001', 'OrderCreated', '{"amount":99}', NOW()  );COMMIT;Relay 进程（或 Debezium）读取 outbox 表并发布到 Kafka，成功后标记：@Scheduled(fixedDelay = 200)public void relay() {    List&lt;OutboxEvent&gt; batch = outboxRepo.lockPendingBatch(500);    if (batch.isEmpty()) return;    for (OutboxEvent e : batch) {        kafkaTemplate.send("order.events", e.aggregateId(), e.payload())                .get(3, TimeUnit.SECONDS);   // 同步确认    }    outboxRepo.markPublished(batch);}发送可能重复（标记前宕机），所以消费端仍然幂等——这是“至少一次”的正确姿势。消费者骨架@KafkaListener(topics = "order.events", groupId = "points-service")public void onOrderEvent(ConsumerRecord&lt;String, String&gt; record, Acknowledgment ack) {    OrderEvent event = parse(record);    // 1. 去重表/状态机保证幂等    if (dedupService.seen(event.eventId())) {        ack.acknowledge();        return;    }    // 2. 业务处理 + 去重记录放同一事务    pointsService.grantWithDedup(event);    // 3. 处理成功才提交    ack.acknowledge();}重试与死信order.events   -&gt; 处理失败   -&gt; order.events.retry (delay 1m, 重试 3 次, 指数退避)   -&gt; 仍失败   -&gt; order.events.DLT (人工处理/自动补偿)Spring Kafka 配置见第 9 章。死信消息必须带：  原始 topic/partition/offset；  异常类型与堆栈摘要；  业务主键（orderId），便于按单查询与重放。幂等的三种实现            方式      适用                  唯一事件表（event_id 唯一键）      通用，强一致              状态机（CREATED-&gt;PAID-&gt;SHIPPED）      状态流转型业务              upsert by 业务主键      最终状态型（用户资料、积分余额）      端到端校验每日对账：  orders 表  vs  Kafka order.events (按天 count/sum)  Kafka      vs  下游结果表 (库存流水/积分流水)差异处理：  缺事件 -&gt; 检查 outbox 卡住的记录  缺结果 -&gt; 检查消费者 lag、DLT  多结果 -&gt; 检查幂等键是否失效24.3 上线检查清单Topic:  [ ] 分区数覆盖峰值吞吐与消费并行  [ ] RF=3, min.insync.replicas=2  [ ] 保留期 &gt; 故障恢复窗口生产:  [ ] acks=all + 幂等 + 回调  [ ] Outbox 而非业务代码直发  [ ] key 设计与顺序要求匹配消费:  [ ] 手动提交，先处理后提交  [ ] 幂等键 + 状态机  [ ] 重试/死信闭环 + 告警  [ ] 静态成员 + 协作式 rebalance观测:  [ ] lag、端到端延迟、DLT 数量、对账差异  [ ] 生产失败率、outbox 积压本章小结  日志管道优先吞吐：批量、压缩、分区预留、消费端攒批写库；  业务事件优先可靠：Outbox + 同 key 有序 + 幂等 + 死信闭环；  双写问题不要硬扛，用 Outbox 把跨系统一致性转化为本地事务；  对账是事件驱动系统的最后防线，必须自动化。思考题  日志 topic 为什么可以 acks=1，订单 topic 为什么必须 acks=all？  Outbox Relay 挂掉 10 分钟，系统数据会不一致吗？恢复后如何追平？  给订单系统加“取消订单”事件，如何保证 CREATED-&gt;CANCELLED 与并发支付的顺序正确？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Kafka 的强大一半来自生态。本章介绍三个官方系工具：Connect 解决“数据进出 Kafka”，Streams 解决“流处理”，ksqlDB 让你用 SQL 写流式应用。23.1 Kafka Connect是什么Connect 是一个数据集成框架，以插件方式运行 connector，无需写代码即可在 Kafka 与外部系统之间搬运数据：Source Connector: 外部系统 -&gt; Kafka  JDBC / Debezium(CDC) / File / S3 ...Sink Connector: Kafka -&gt; 外部系统  JDBC / Elasticsearch / S3 / ClickHouse ...核心概念            概念      说明                  Connector      逻辑任务定义（连接哪个库、读哪些表）              Task      实际并行工作的执行单元              Worker      运行 connector/task 的进程              Converter      消息格式转换（JSON/Avro/Protobuf）              Offset      source 端进度（如表的 binlog 位点），存于内部 topic              REST API      管理 connector 的 HTTP 接口      两种模式standalone：单进程，适合开发测试distributed：多 worker，任务自动均衡与容错，生产必选启动（distributed）bin/connect-distributed.sh config/connect-distributed.properties关键配置：bootstrap.servers=localhost:9092group.id=connect-clusteroffset.storage.topic=connect-offsetsconfig.storage.topic=connect-configsstatus.storage.topic=connect-statusoffset.storage.replication.factor=3config.storage.replication.factor=3status.storage.replication.factor=3key.converter=org.apache.kafka.connect.json.JsonConvertervalue.converter=org.apache.kafka.connect.json.JsonConverter用 REST 创建 JDBC Sourcecurl -X POST http://localhost:8083/connectors \  -H 'Content-Type: application/json' -d '{  "name": "jdbc-orders-source",  "config": {    "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector",    "tasks.max": "4",    "connection.url": "jdbc:mysql://db:3306/shop",    "connection.user": "reader",    "connection.password": "***",    "table.whitelist": "orders",    "mode": "incrementing",    "incrementing.column.name": "id",    "topic.prefix": "cdc-"  }}'管理命令：curl localhost:8083/connectors                 # 列出curl localhost:8083/connectors/jdbc-orders-source/statuscurl -X DELETE localhost:8083/connectors/jdbc-orders-source选型建议  CDC 场景优先 Debezium（binlog 精确、对库压力小）；  JDBC source 的 incrementing/timestamp 模式对删除与更新历史支持有限；  sink 端要考虑幂等（upsert by key），否则重平衡时可能重复写；  生产环境 connector 配置进版本库，REST 变更走 CI/CD。23.2 Kafka Streams是什么Streams 是一个 Java 库（不是独立服务），嵌入应用即可做有状态流计算：StreamsBuilder builder = new StreamsBuilder();KStream&lt;String, String&gt; lines = builder.stream("input-topic");KTable&lt;String, Long&gt; counts = lines        .flatMapValues(v -&gt; Arrays.asList(v.toLowerCase().split("\\W+")))        .groupBy((k, word) -&gt; word)        .count(Materialized.as("word-counts"));counts.toStream().to("output-topic", Produced.with(Serdes.String(), Serdes.Long()));KafkaStreams streams = new KafkaStreams(builder.build(), props);streams.start();配置：props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-app");props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE_V2);props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());application.id 同时充当消费组 ID 与状态存储前缀，同一应用的所有实例必须一致。KStream vs KTable            抽象      语义      类比                  KStream      事件流，每条都重要      append 日志              KTable      按 key 聚合的最新状态      可更新的表              GlobalKTable      全实例广播的表      小维度表      经典组合：订单流（KStream）join 用户表（KTable），实时补全维度。窗口clicks.groupByKey()      .windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofSeconds(10)))      .count();  Tumbling：固定不重叠；  Hopping：按步长滑动，可能重叠；  Session：按活动间隔动态聚合（用户会话）；  Grace period：允许迟到数据，超期进入弃用流。状态与容错状态存本地 RocksDB，并后台备份到 changelog topic：实例宕机 -&gt; 新实例接管分区 -&gt; 从 changelog 重建本地状态 -&gt; 继续处理因此 Streams 应用会自动创建 &lt;app-id&gt;-&lt;store&gt;-changelog 主题，不要手工删除。适用场景  实时聚合、监控规则、风控特征；  流表 join 补维；  数据清洗与路由；需要 exactly-once 的 Kafka-to-Kafka 管道。复杂拓扑、事件时间语义、大规模状态管理时，再评估 Flink（第 25 章）。23.3 ksqlDBksqlDB 把流处理变成 SQL，适合快速验证与轻量场景。-- 建流CREATE STREAM orders (  order_id VARCHAR KEY,  user_id VARCHAR,  amount DECIMAL(12, 2)) WITH (  KAFKA_TOPIC = 'order-events',  VALUE_FORMAT = 'JSON');-- 每 5 分钟统计各用户消费CREATE TABLE user_spend AS  SELECT user_id,         COUNT(*) AS order_cnt,         SUM(amount) AS total  FROM orders  WINDOW TUMBLING (SIZE 5 MINUTES)  GROUP BY user_id  EMIT CHANGES;-- 持续查询（推模式）SELECT * FROM user_spend  WHERE total &gt; 10000  EMIT CHANGES;优点：上手快、声明式、内置服务化。注意：复杂逻辑、精细状态控制与大型作业，仍建议 Streams/Flink。23.4 选型对比            需求      推荐                  数据库/日志/对象存储进出 Kafka      Connect              库表级 CDC      Debezium + Connect              Java 团队的轻中量流处理      Streams              SQL 快速分析/原型      ksqlDB              重状态、事件时间、大规模作业      Flink              复杂图计算/批流一体      Spark/Flink      本章小结  Connect 用 connector + worker + 内部 topic 实现高可用数据集成；  Streams 是库形态的流处理框架，KTable/窗口/changelog 是三大核心；  ksqlDB 用 SQL 降低流处理门槛，适合轻量与服务化查询；  生态选型先看团队栈与运维能力，再谈功能上限。思考题  Connect 的 source offset 存在哪里？为什么这比存在内存里可靠？  Streams 的 changelog topic 删除后会发生什么？如何恢复？  用 Streams 实现一个“每 1 分钟统计各接口 5xx 次数并告警”的拓扑，写出伪代码。</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。集群不是搭完就完事。本章覆盖日常最重的几类运维操作：扩容与数据均衡、跨集群迁移、滚动升级、备份与容灾。22.1 扩容 Broker步骤1. 准备新节点：装同版本、配置 log.dirs/监听器/安全2. 分配 node.id 并注册（KRaft combined 模式需评估 controller 容量）3. 启动并确认注册成功4. 观察元数据同步、无告警验证：bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 \  | grep -E '^newbroker'bin/kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --replication注意：新 Broker 上线后不会自动获得任何数据。 Kafka 不会自动迁移分区，必须执行分区再分配。22.2 分区再分配生成分配方案cat &gt; topics-to-move.json &lt;&lt;'EOF'{  "topics": [    {"topic": "order-events"}  ],  "version": 1}EOFbin/kafka-reassign-partitions.sh \  --bootstrap-server localhost:9092 \  --topics-to-move-json topics-to-move.json \  --broker-list 1,2,3,4 \  --generate输出两部分：当前分配（Current partition assignment）与建议方案（Proposed partition assignment）。审查建议方案（副本是否跨机架、是否过于集中）后保存为 reassignment.json。执行bin/kafka-reassign-partitions.sh \  --bootstrap-server localhost:9092 \  --reassignment-json-file reassignment.json \  --execute# 查看进度bin/kafka-reassign-partitions.sh \  --bootstrap-server localhost:9092 \  --reassignment-json-file reassignment.json \  --verify运维要点  迁移会占用大量磁盘 IO 与网络，错峰执行，分批 topic 进行；大 topic 一次迁完可能拖垮集群；  期间会出现副本复制流量上涨、ISR 短暂抖动，属预期；  throttle 参数可以限速（如 --throttle 52428800 表示 50MB/s，完成后记得置 0 清除限流）；  迁移完成后可执行 preferred leader 选举，让 Leader 均匀分布。22.3 Leader 均衡再分配只改副本位置，Leader 可能仍集中在老节点：# 查看分布bin/kafka-leader-election.sh --bootstrap-server localhost:9092 \  --election-type preferred --all-topic-partitions配合 Broker 配置：auto.leader.rebalance.enable=trueleader.imbalance.per.broker.percentage=10leader.imbalance.check.interval.seconds=300规模化运维通常引入 Cruise Control：它基于负载模型（CPU、磁盘、网络、副本数）自动生成均衡方案，支持增量目标（balance goals）与异常检测，是大型集群的标配。22.4 跨集群迁移：MirrorMaker 2MM2 基于 Kafka Connect 实现 topic 复制，支持：  主-&gt;备、主-&gt;主（双活）复制；  offset 转换（checkpoint）；  ACL 与 topic 配置同步（可配置）；  循环复制防护（replication policy）。最小配置 mm2.properties：clusters = src, dstsrc.bootstrap.servers = src-broker1:9092dst.bootstrap.servers = dst-broker1:9092src-&gt;dst.enabled = truesrc-&gt;dst.topics = .*dst-&gt;src.enabled = falsereplication.factor = 3checkpoints.topic.replication.factor = 3heartbeats.topic.replication.factor = 3# 新集群的 topic 会加前缀 src.（DefaultReplicationPolicy）# topics = order-events -&gt; src.order-events启动：bin/connect-mirror-maker.sh mm2.properties切流流程1. 开启 MM2，让 dst 持续追 src2. 校验数据一致性（抽样 offset/内容比对）3. 生产端灰度切到 dst（或切 DNS/负载入口）4. 消费组按 checkpoint 同步位移5. 观察稳定后停写 src，保留回滚窗口注意 DefaultReplicationPolicy 会给目标 topic 加 src. 前缀；若想保留原名，用 IdentityReplicationPolicy，但要非常小心双活场景的循环复制。22.5 滚动升级标准流程：1. 阅读目标版本 release notes 与升级路径（是否支持跨版本）2. 备份配置与元数据3. 在测试集群演练4. 按批次（如每批 1 台或 10%）升级：     a. 下线前确认该 Broker 上的分区有其他 ISR     b. 停进程、替换二进制、改配置     c. 启动、观察入组/ISR 恢复/无告警     d. 进入下一批5. 全部完成后跑功能与性能回归关键观察点：UnderReplicatedPartitions 回到 0、Controller 仲裁正常、客户端错误率无异常、请求延迟恢复基线。22.6 备份与容灾两层含义            层      手段      恢复目标                  集群内      多副本（RF=3、跨机架）      节点故障              集群间      MM2 异地复制、云厂商跨 AZ      机房/区域故障              误操作      定期导出/对象存储归档、版本化配置      误删 topic、错误配置      同城双活与异地灾备同城双活：  两个机房各部署集群，MM2 双向复制  按业务分片写入，避免同 key 双写冲突异地灾备：  主集群单向复制到备集群  RPO = 复制延迟，RTO = 切流时间  定期演练切换，否则预案只是纸面安全明确 RPO/RTO：复制延迟 10s、切流 5 分钟，和复制延迟 1 小时、切流半天，是完全不同的架构投入。22.7 日常运维节奏每日：  巡检 lag / ISR / 磁盘 / 告警每周：  容量趋势 review、慢查询 topic 分析  配置漂移对比每月：  故障演练（kill broker、切 Controller）  僵尸 topic 清理、ACL 审计每季度：  灾备切换演练、大版本升级评估把节奏写成 checklist，比“等出事再查”可靠得多。本章小结  扩 Broker 只是第一步，数据均衡要靠分区再分配；  reassign 分“生成-审查-执行-验证”四步，可限速、可分批；  Leader 均衡与副本均衡是两件事，必要时用 Cruise Control 自动化；  跨集群迁移用 MirrorMaker 2，核心是复制策略与切流流程；  升级走小批次滚动，容灾要演练，RPO/RTO 必须量化。思考题  为什么 Kafka 不在新 Broker 上线时自动迁移分区？自动迁移有什么风险？  MM2 用 IdentityReplicationPolicy 保留原名时，如何防止双活循环复制？  设计一个 RPO&lt;30s、RTO&lt;10min 的异地容灾方案，列出关键组件与演练计划。</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。默认的 PLAINTEXT 监听等于“裸奔”：任何能连通端口的人都能读写所有 topic。本章讲清楚三层安全模型，并给出可落地的配置。21.1 安全的三个层次加密（Encryption）   数据在网络上不可被窃听      SSL/TLS认证（Authentication） 你是谁                     SASL / mTLS授权（Authorization）  你能做什么                 ACL三者独立且都要做：只加密不认证，等于只防窃听不防冒充；只认证不加密，凭据和数据仍可能被嗅探。21.2 监听器与协议Broker 通过监听器组合不同协议：listeners=SSL://:9093,SASL_SSL://:9094advertised.listeners=SSL://broker1:9093,SASL_SSL://broker1:9094listener.security.protocol.map=SSL:SSL,SASL_SSL:SASL_SSL            协议      含义                  PLAINTEXT      无加密无认证（仅内网可信环境）              SSL      TLS 加密，可选 mTLS 双向认证              SASL_PLAINTEXT      认证但不加密              SASL_SSL      认证 + 加密（推荐）      实践建议：内部服务用 SASL_SSL 或 SSL；管理端口可用单独监听器加 ACL 收敛权限。21.3 SASL/SCRAM：最常用的认证SCRAM 的用户与凭据存在 ZooKeeper/KRaft 元数据中（而非静态 JAAS 文件），支持动态增删，运维友好。以 SCRAM-SHA-256 为例。1. 创建用户（在任意 Broker 机器执行）bin/kafka-configs.sh --bootstrap-server localhost:9092 \  --alter --add-config 'SCRAM-SHA-256=[iterations=8192,password=app-secret]' \  --entity-type users --entity-name app-producer查看：bin/kafka-configs.sh --bootstrap-server localhost:9092 \  --describe --entity-type users --entity-name app-producer2. Broker 端配置listeners=SASL_SSL://:9094advertised.listeners=SASL_SSL://broker1:9094listener.security.protocol.map=SASL_SSL:SASL_SSLsasl.enabled.mechanisms=SCRAM-SHA-256sasl.mechanism.inter.broker.protocol=SCRAM-SHA-256# Broker 之间互联也需要账号（这里用 admin）listener.name.sasl_ssl.scram-sha-256.sasl.jaas.config=\org.apache.kafka.common.security.scram.ScramLoginModule required \  username="admin" \  password="admin-secret";3. 客户端配置security.protocol=SASL_SSLsasl.mechanism=SCRAM-SHA-256sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \  username="app-producer" \  password="app-secret";SCRAM-SHA-512 更强，PLAIN 只建议配合 SSL 在测试环境使用。21.4 SSL/TLS：加密与双向认证证书准备（生产用公司 CA，测试可用自签）# 1. 生成 CAopenssl req -new -x509 -keyout ca-key -out ca-cert -days 3650 \  -subj "/CN=TestCA"# 2. 每台 Broker 生成密钥与 CSR，并用 CA 签发keytool -keystore broker1.keystore.p12 -storetype PKCS12 \  -alias broker1 -genkey -keyalg RSA -validity 3650 \  -dname "CN=broker1.example.com"keytool -keystore broker1.keystore.p12 -alias broker1 \  -certreq -file broker1.csropenssl x509 -req -CA ca-cert -CAkey ca-key \  -in broker1.csr -out broker1-signed.pem -days 3650 -CAcreateserial# 3. 导入 CA 与签发证书到 keystore，导入 CA 到 truststorekeytool -keystore broker1.keystore.p12 -alias CARoot \  -import -file ca-certkeytool -keystore broker1.keystore.p12 -alias broker1 \  -import -file broker1-signed.pemkeytool -keystore truststore.p12 -alias CARoot \  -importcert -file ca-cert -nopromptBroker 配置listeners=SSL://:9093ssl.keystore.location=/etc/kafka/ssl/broker1.keystore.p12ssl.keystore.password=changeitssl.key.password=changeitssl.truststore.location=/etc/kafka/ssl/truststore.p12ssl.truststore.password=changeitssl.client.auth=required        # mTLS 双向认证；none 表示仅加密客户端配置security.protocol=SSLssl.truststore.location=/path/truststore.p12ssl.truststore.password=changeit# mTLS 时客户端也需要证书ssl.keystore.location=/path/client.keystore.p12ssl.keystore.password=changeitTLS 会增加 CPU 与延迟，大流量集群建议评估硬件加速，并保持长连接复用。21.5 ACL 授权认证解决“你是谁”，ACL 决定“你能干什么”。前提：authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer   # KRaft# ZK 模式: kafka.security.authorizer.AclAuthorizerallow.everyone.if.no.acl.found=false    # 默认拒绝super.users=User:admin;User:broker1     # 管理与 Broker 互联账号常用 ACL 操作BOOTSTRAP=localhost:9094CMD=(bin/kafka-acls.sh --bootstrap-server $BOOTSTRAP \     --command-config admin.properties)   # admin.properties 含管理员认证# 生产者：写 order-events"${CMD[@]}" --add --allow-principal User:app-producer \  --producer --topic order-events# 消费者：读 order-events + 读消费组"${CMD[@]}" --add --allow-principal User:order-consumer \  --consumer --topic order-events --group order-service# 只读某前缀的所有 topic"${CMD[@]}" --add --allow-principal User:report \  --operation READ --resource-pattern-type prefixed --topic report-# 列出 ACL"${CMD[@]}" --list# 删除"${CMD[@]}" --remove --allow-principal User:app-producer \  --producer --topic order-events最小权限示例矩阵：            主体      topic 权限      组权限      集群权限                  producer-app      WRITE 特定 topic      无      无              consumer-app      READ 特定 topic      READ 特定组      无              connect-worker      读写内部 topic      组权限      Describe              admin      全部      全部      全部      21.6 Spring Boot 客户端配置spring:  kafka:    bootstrap-servers: broker1:9094    security:      protocol: SASL_SSL    properties:      sasl.mechanism: SCRAM-SHA-256      sasl.jaas.config: &gt;        org.apache.kafka.common.security.scram.ScramLoginModule required        username="app-producer"        password="app-secret";      ssl.truststore.location: /etc/app/truststore.p12      ssl.truststore.password: changeit凭据不要进 Git：用环境变量、Vault、K8s Secret 注入。21.7 安全最佳实践清单网络：  [ ] Kafka 只在内网/私网监听，必要时才经网关暴露  [ ] Broker 间与 Controller 通道启用 TLS/SASL认证：  [ ] 禁用 PLAINTEXT 或仅限运维白名单  [ ] 不同应用使用不同主体（不要一个账号走天下）  [ ] 凭据集中管理，定期轮换授权：  [ ] allow.everyone.if.no.acl.found=false  [ ] 最小权限：读写分离、按 topic/组授权  [ ] ACL 变更走审批与审计审计与加密：  [ ] 开启审计日志（谁读写了什么）  [ ] 敏感数据在业务层额外加密（Kafka 不理解内容）本章小结  安全是加密、认证、授权三层叠加，缺一不可；  SASL/SCRAM 支持动态用户管理，是最常用的认证方案；  SSL 提供传输加密，mTLS 还能同时完成客户端认证；  ACL 以“主体 + 操作 + 资源”建模，默认拒绝 + 最小权限；  凭据用 Secret/Vault 管理，永远不要提交进仓库。思考题  只启用 ACL 不启用认证，安全模型还成立吗？为什么？  Broker 之间也需要账号吗？如果配置错误会出现什么现象？  设计多租户 Kafka 的权限模型：租户 A 的服务如何绝对无法读租户 B 的 topic？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章是“急救包”：按症状组织，给出排查路径、关键命令与常见根因。遇到问题时可以直接对照执行。20.1 排查总原则1. 先看范围：单 topic？单分区？单消费者组？全集群？2. 再看时间：什么时候开始？有没有变更（发布/扩容/配置）？3. 分层定位：客户端 -&gt; 网络 -&gt; Broker -&gt; 下游4. 保留现场：日志、指标截图、配置快照5. 最小干预：先恢复（扩容/限流/切流），再根因万能三件套：kafka-topics.sh --describe --topic &lt;t&gt;kafka-consumer-groups.sh --describe --group &lt;g&gt;kafka-broker-api-versions.sh --bootstrap-server &lt;bs&gt;   # 确认集群可达20.2 消息丢失排查路径1. Broker 端确认写入了吗？   kafka-dump-log.sh / kafka-get-offsets.sh 查看分区末位移是否增长2. 生产端真的发成功了吗？   检查回调/日志；检查是否 acks=0；是否未 flush 退出；   是否异步 send 后进程崩溃3. 消费端跳过了吗？   查 committed offset 与 log end offset；   查 auto.offset.reset、位移重置记录、rebalance 时间点4. 数据被删了吗？   retention 是否过短；是否误操作 reset；磁盘清理是否异常高频根因            层      根因      证据                  生产      send() 后直接退出      无回调，日志缺失              生产      acks=0 或 1 且 Leader 闪退      producer 配置              Broker      RF=1 + 磁盘损坏      topic describe              Broker      unclean election      controller.log              消费      先 commit 后 process      代码审查              消费      reset 到 latest/未来 offset      OffsetReset 日志      20.3 消息重复排查路径1. 判断重复发生在哪一层：   - broker 日志里物理上两条？ -&gt; 生产端重试/业务重发   - 日志一条但业务处理两次？ -&gt; 消费端重复消费（rebalance/提交失败）2. 生产端：   关闭幂等？transactional.id 不稳定？业务层超时后自己重发？3. 消费端：   处理完但 commit 失败？再平衡导致重复处理？   异步提交丢失（commitAsync 未等待确认）？4. 兜底：   下游是否有幂等键（requestId / offset / 唯一约束）？处置  物理重复无法事后删除，只能下游去重或重放修正；  预防：稳定 transactional.id、手动同步提交、幂等键设计；  对账任务：按业务主键统计多处理记录，发现重复模式。20.4 消息堆积（Lag 告警）定位瓶颈1. 写入速率是否突增？   MessagesInPerSec、BytesInPerSec 按 topic 查看2. 消费速率是否下降？   消费者处理耗时日志、下游 DB/接口指标3. 消费者数量是否减少？   rebalance 记录、实例存活、空闲消费者4. 是否分区倾斜？   按 partition 看 lag；某分区 key 热点或数据倾斜恢复手段            手段      适用                  扩消费者实例      实例数 &lt; 分区数              批量化下游写库      单条处理慢              降级非核心逻辑      处理链路太重              临时扩分区 + 消费者      长期容量不足（注意 key 顺序影响）              跳过历史（慎用）      业务允许丢弃过期数据      跳过堆积只适合明确可丢弃的数据（如过期告警），执行前必须业务确认并留审计。20.5 消息乱序常见原因：  未用相同 key，或 key 设计不对（同一实体被路由到不同分区）；  生产端重试导致乱序（未开幂等，in-flight &gt; 1）；  消费端多线程/并发处理同一分区的消息；  增加分区导致 key 落点变化；  上游本身乱序产生（多线程写库后发消息）。排查：# 消费时打印分区与 offset，确认同一实体的消息是否同分区连续kafka-console-consumer.sh ... --property print.key=true \  --property print.partition=true --property print.offset=true修复：  key = 需要顺序的实体 ID；  开启幂等 + in-flight &lt;= 5；  消费端按 key 哈希到固定工作线程；  版本号/时间戳拒绝旧事件。20.6 ISR 频繁收缩现象：UnderReplicatedPartitions 抖动，IsrShrinksPerSec 高。排查顺序：  Follower 所在 Broker 资源：CPU、磁盘 util、GC；  num.replica.fetchers 是否不足；  网络是否抖动（Broker 间延迟）；  是否有大分区迁移/均衡占满 IO；  replica.lag.time.max.ms 是否设得过小。处置：加大 fetcher 线程、限流迁移任务、错峰均衡、修复慢盘。20.7 Rebalance 风暴现象：消费反复停顿，日志出现 heartbeat failed / max poll interval / rebalancing。排查：1. 找出被踢的成员与时间点2. 对应实例 GC 日志 / CPU / 处理耗时3. 检查订阅是否一致（同一组订阅不同 topic 集合）4. 检查是否有实例频繁发布（无静态成员）处置参考第 15 章治理清单：调小单次处理量、放宽超时、静态成员、协作式策略、统一订阅。20.8 磁盘满与日志损坏磁盘满应急：  1. 扩容磁盘（云盘在线扩）  2. 缩短低价值 topic 的 retention  3. 删除已确认可删的僵尸 topic长期：  1. 容量水位告警（80%）  2. topic 生命周期治理  3. 压缩策略启用评估注意：磁盘 100% 时 Broker 可能无法写日志/元数据，处置优先级最高。段文件损坏症状：启动报 CorruptIndexException、Unkown magic value 等。处理：  停 Broker，备份整个分区目录；  删除对应 .index/.timeindex（会自动重建）；  若 .log 损坏，评估删除损坏 segment 的数据损失；  依赖副本恢复：把损坏副本下线，让其他副本重新同步。20.9 速查表            症状      第一条命令                  集群不可达      kafka-broker-api-versions.sh              分区不可用      kafka-topics.sh --describe --unavailable-partitions              副本落后      kafka-topics.sh --describe --under-replicated-partitions              消费堆积      kafka-consumer-groups.sh --describe --group              消息没写入      kafka-get-offsets.sh --topic + dump-log              KRaft 异常      kafka-metadata-quorum.sh describe --status              配置疑议      kafka-configs.sh --describe --all      本章小结  排查先定范围与时间线，再分层定位，保留现场；  丢失要区分“没写入”与“没消费”，重复要区分“物理重复”与“逻辑重复”；  堆积先看写入增速、消费降速、消费者数量与分区倾斜；  乱序通常出在 key 设计、生产端重试与消费端并发；  磁盘满与段损坏是最高优先级的基础设施故障。思考题  业务说“丢消息”，但你查到日志里有这条记录，问题出在哪个环节？下一步查什么？  为什么删除损坏的 .index 文件相对安全，删除 .log 却有真实数据损失？  为你们团队写一份“Kafka 故障应急预案”，包含哪些角色和动作？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Kafka 集群的稳定性 = 指标 + 告警 + 定期演练。本章给出指标体系、部署方案与阈值建议，让问题在用户感知前被发现。19.1 监控什么五个层面：            层面      关注点                  Broker      存活、请求延迟、线程池、磁盘、网络              分区/副本      ISR 健康、Leader 均衡、离线分区              Controller/KRaft      仲裁状态、元数据滞后              消费组      lag、rebalance 次数、处理耗时              主机      CPU、内存、页缓存、磁盘 IO、网络、时钟      19.2 Broker 关键指标            指标      含义      告警建议                  ActiveControllerCount      活跃 Controller 数      !=1 立即告警              OfflinePartitionsCount      无 Leader 分区数      &gt;0 严重告警              UnderReplicatedPartitions      ISR 不足的分区数      &gt;0 持续 5 分钟告警              UnderMinIsrPartitionCount      低于 min ISR 的分区      严重告警（影响写入）              RequestHandlerAvgIdlePercent      IO 线程空闲率      &lt;0.3 关注              NetworkProcessorAvgIdlePercent      网络线程空闲率      &lt;0.3 关注              BytesInPerSec / BytesOutPerSec      流量      突增突降告警              MessagesInPerSec      消息速率      趋势监控              RequestLatencyAvg/Max      请求延迟      按 P99 阈值              LogDirSize / 磁盘使用率      容量      &gt;80% 告警，&gt;90% 严重              IsrShrinksPerSec / Expands      ISR 变化频率      频繁收缩告警      JMX 名称示例（Broker 端）：kafka.server:type=ReplicaManager,name=UnderReplicatedPartitionskafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercentkafka.network:type=SocketServer,name=NetworkProcessorAvgIdlePercentkafka.server:type=BrokerTopicMetrics,name=BytesInPerSec19.3 消费者与 Lag 监控Lag 是业务最敏感的指标：lag = log_end_offset - committed_offset三种采集方式：  客户端指标：消费者进程暴露 records-lag-max（准确、实时，但消费者挂了就没有数据）；  Burrow / kafka-lag-exporter：外部独立采集，覆盖消费者已宕机的场景，推荐；  命令行巡检：kafka-consumer-groups.sh --describe，适合兜底脚本。告警不要只看绝对值。同样 100 万 lag：  每秒消费 50 万的组：2 秒追平，正常；  每秒消费 100 的组：2.7 小时，严重。推荐同时监控 lag 趋势（是否持续增长） 与 追平时间估算（lag / 消费速率）。19.4 用 Prometheus + Grafana 搭建启用 JMXexport JMX_PORT=9999export KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote=true \  -Dcom.sun.management.jmxremote.authenticate=false \  -Dcom.sun.management.jmxremote.ssl=false"bin/kafka-server-start.sh config/server.propertiesJMX Exporter 配置（片段）startDelaySeconds: 10hostPort: 127.0.0.1:9999rules:  - pattern: "kafka.server&lt;type=(.+), name=(.+)&gt;&lt;&gt;Value"    name: kafka_server_$1_$2    type: GAUGE  - pattern: "kafka.network&lt;type=(.+), name=(.+)&gt;&lt;&gt;Value"    name: kafka_network_$1_$2    type: GAUGE采集与服务发现scrape_configs:  - job_name: kafka-jmx    static_configs:      - targets:          - broker1:7071          - broker2:7071          - broker3:7071  - job_name: kafka-lag    static_configs:      - targets: ['lag-exporter:8080']Grafana 社区有现成 Kafka 面板（搜索 “Kafka Exporter” / “Kafka JMX”），建议至少包含四块：  集群健康：Controller、离线分区、ISR；  流量：BytesIn/Out、MessagesIn 按 Broker/Topic；  延迟与线程：RequestLatency、Idle Percent；  消费：lag 趋势、追平时间、rebalance 频率。19.5 告警策略建议            级别      条件示例                  P0      OfflinePartitionsCount &gt; 0；ActiveControllerCount != 1；磁盘 &gt; 90%；Broker down              P1      UnderReplicated 持续 &gt; 5min；UnderMinIsr &gt; 0；lag 追平时间 &gt; 30min              P2      磁盘 &gt; 80%；请求 P99 翻倍；ISR 频繁抖动；rebalance 频率异常              提醒      流量异常波动；topic 增长异常；配置变更事件      告警原则：  每条告警都要有明确的处置动作（runbook 链接）；  抑制抖动（持续 N 分钟才触发）；  分级通知，避免“告警疲劳”后大家集体屏蔽。19.6 日志与事件除了指标，还要保留：  server.log / controller.log：切主、分区迁移、副本剔除；  客户端日志：rebalance、超时、重试；  审计事件：topic/ACL 配置变更（谁在什么时候改了什么）；  kafka-configs.sh --describe --all 定期快照，用于配置漂移对比。19.7 巡检脚本示例#!/usr/bin/env bashBS=localhost:9092echo "== 集群描述 =="bin/kafka-metadata-quorum.sh --bootstrap-server $BS describe --statusecho "== 离线/落后分区 =="bin/kafka-topics.sh --bootstrap-server $BS --describe --unavailable-partitionsbin/kafka-topics.sh --bootstrap-server $BS --describe --under-replicated-partitionsecho "== 消费组 lag =="bin/kafka-consumer-groups.sh --bootstrap-server $BS --list | while read g; do  bin/kafka-consumer-groups.sh --bootstrap-server $BS --describe --group "$g" |    awk -v g="$g" 'NR&gt;1 &amp;&amp; $NF ~ /^[0-9]+$/ { if ($NF &gt; 100000) print g, $1, $2, "lag="$NF }'done定时执行并输出到群机器人，就是最朴素的“可观测性兜底”。本章小结  监控覆盖 Broker、分区副本、Controller、消费组、主机五层；  UnderReplicated / Offline / Controller / Disk / Lag 是五大生命线；  lag 告警看趋势与追平时间，不只看绝对值；  Prometheus + Grafana + lag-exporter 是标准组合；  每条告警配套 runbook，否则只是噪音。思考题  为什么消费者自带 lag 指标还不够，需要外部 lag 采集器？  磁盘使用率告警应该设几级？每级对应的动作是什么？  如果只能保留 5 个 Kafka 指标，你选哪 5 个？为什么？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。“Kafka 会不会丢消息？”的正确回答是：配置得当就不丢，配置不当每一层都能丢。本章按端到端路径给出工程清单。18.1 可靠性是端到端属性业务库 -&gt; Producer -&gt; 网络 -&gt; Broker集群 -&gt; 消费者 -&gt; 下游存储任何一环出问题都可能丢：  业务代码发送前进程崩溃；  Producer 异步缓冲未 flush 就退出；acks=0/1 且 Leader 副本未同步；  ISR 收缩到 1 时发生 unclean 选举；  消费者先提交位移后处理；  消费者 auto.offset.reset=latest 且位移过期。下面逐层堵漏。18.2 生产端清单            检查项      正确做法                  发送确认      acks=all              重试      enable.idempotence=true（含自动重试）              超时预算      delivery.timeout.ms 覆盖重试总时长，按业务 SLA 设置              回调      必须实现，失败落补偿表/告警，禁止静默吞掉              退出      close() 或 flush()，确保缓冲区清空              消息大小      不超过 max.request.size 与 Broker message.max.bytes              本地事务      业务库 + 消息发送用 Outbox 模式（第 26 章）      典型错误代码：producer.send(record); // 无回调、无检查System.exit(0);       // 缓冲区消息可能尚未发出正确姿势：producer.send(record, (md, ex) -&gt; {    if (ex != null) saveToOutboxRetry(record, ex); // 或直接本地补偿});producer.flush();18.3 Broker 端清单            检查项      推荐配置                  副本因子      replication.factor=3（重要业务）              最小同步副本      min.insync.replicas=2              脏选举      unclean.leader.election.enable=false              机架分布      broker.rack 设置，副本跨机架/可用区              内部主题      __consumer_offsets RF=3              磁盘      RAID10 或多副本替代；监控坏盘              保留期      覆盖最长故障恢复时间，防止位移过期      为什么是 3 + 2 + false：  一台宕机：ISR 剩 2，仍满足 min ISR，写入继续且已提交消息双副本持久；  两台同时宕机：写入拒绝（NotEnoughReplicasException），但不丢；  若开 unclean election：落后副本上位会丢已提交数据，与“不丢”目标冲突。18.4 消费端清单            检查项      正确做法                  位移提交      手动提交，先处理后提交              提交失败      commitSync 捕获后重试/记录，必要时暂停消费              再平衡      onPartitionsRevoked 中同步提交              起点策略      明确 auto.offset.reset（重要业务用 earliest）              处理失败      不 ack，交给重试/死信，禁止 catch 后吞掉              幂等      唯一键/去重表/状态机，容忍重复              堆积      监控 lag，防止位移超过保留期被删除      危险代码：for (var r : records) {    try { process(r); }    catch (Exception e) { log.error("ignore", e); } // 吞掉 = 消息丢失}consumer.commitSync();失败消息必须进入显式的重试/死信流程，并保留可观测性。18.5 位移过期：容易被忽视的丢失场景：消费者组故障超过 7 天（默认保留期），位移记录被删除，重启后 auto.offset.reset=latest -&gt; 跳过所有堆积消息。防护：  关键 topic 的保留期大于最长可接受停机时间；  严格监控 lag，不允许长期堆积；  auto.offset.reset 明确设置并写入团队规范；  长期不消费的组显式记录，避免“复活时踩坑”。18.6 顺序与重复的权衡  要不丢：至少一次 + 幂等；  要顺序：同 key 同分区 + 幂等生产者 + in-flight&lt;=5；  要精确一次（Kafka 内）：事务 + read_committed；  涉及外部系统：本地事务 + 去重表。不要试图用“关闭重试”换取不重复——那是用丢消息换不重复，方向反了。18.7 上线前检查清单Topic:  [ ] replication.factor = 3  [ ] min.insync.replicas = 2  [ ] retention 覆盖故障恢复窗口  [ ] 分区数满足吞吐与并行需求Producer:  [ ] acks=all, idempotence=true  [ ] 发送失败有回调与补偿  [ ] 停机 flush/close  [ ] 压缩与大消息策略确认Consumer:  [ ] 手动提交，先处理后提交  [ ] auto.offset.reset 明确  [ ] 失败进重试/死信  [ ] 幂等设计评审  [ ] rebalance 参数与静态成员确认运维:  [ ] unclean.leader.election.enable=false  [ ] 副本跨机架  [ ] lag/ISR/Controller 告警  [ ] 容量与磁盘增长预警18.8 一个真实事故复盘模板现象：   支付回调消息丢失 3000 条时间线： 14:00 Broker2 宕机 -&gt; 14:05 恢复 -&gt; 15:00 业务发现根因：   topic RF=1，Broker2 上的分区数据未同步副本修复：   RF 扩到 3 + min ISR 2，补发丢失区间改进：   上线 topic 配置校验（禁止 RF&lt;3），增加 ISR 告警复盘要点：不只修配置，还要让“同类错误无法再次发生”（配置准入、告警、演练）。本章小结  不丢消息 = 生产端可靠发送 + Broker 多副本 + 消费端正确提交，缺一不可；  黄金组合：acks=all + RF=3 + min.insync.replicas=2 + unclean=false；  消费端铁律：先处理后提交，失败必须显式处理（重试/死信）；  位移过期 + latest 是隐蔽的丢失来源；  把清单工具化：CI 校验配置、告警兜底、事故复盘闭环。思考题  每一层都“看似可靠”，为什么拼起来仍可能丢消息？举一个跨层时序的例子。  为什么 min.insync.replicas=2 在两台 Broker 宕机时反而“保护”了你？  为你的系统写一份“消息可靠性 SLA”：允许丢吗？允许重吗？允许乱序吗？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。调优的第一原则：没有测量就没有调优。本章给出一套从压测定位、分层调参到容量规划的完整方法。17.1 先建立基线任何改动前，先回答三个问题：  当前吞吐是多少（条/秒、MB/秒）？  端到端延迟是多少（P99）？  瓶颈在哪：生产者、网络、Broker 磁盘、还是消费者？Producer -&gt; 网络 -&gt; Broker(磁盘/页缓存/副本) -&gt; 网络 -&gt; Consumer -&gt; 下游逐段测量，找出最长的那块板，避免“凭感觉调参”。17.2 官方压测工具生产者压测bin/kafka-producer-perf-test.sh \  --topic perf-test \  --num-records 1000000 \  --record-size 1024 \  --throughput -1 \  --producer-props bootstrap.servers=localhost:9092 \      acks=all linger.ms=10 compression.type=zstd输出关注：records/sec、MB/sec、avg latency、max latency、percentile。消费者压测bin/kafka-consumer-perf-test.sh \  --bootstrap-server localhost:9092 \  --topic perf-test \  --messages 1000000 \  --reporting-interval 1000压测建议：  用与生产一致的消息大小与压缩算法；  分组测试不同 linger.ms、batch.size、线程数，每次只改一个变量；  观察服务端 BytesInPerSec/BytesOutPerSec，确认瓶颈不在客户端；  压测 topic 的分区数覆盖多台 Broker，避免单盘打满掩盖集群能力。17.3 操作系统层            项      建议                  文件句柄      ulimit -n 100000+，分区多时按 分区数*3 估算              swappiness      vm.swappiness=1，尽量避免页缓存被换出              文件系统      XFS（首选）或 ext4，挂载 noatime              磁盘      SSD/NVMe，多盘条带化 log.dirs；避免与 OS、日志混盘              网络      万兆起步，txqueuelen、缓冲区按需调整              时间同步      NTP/chrony，时间戳语义依赖时钟      Kafka 是少数“吃页缓存比吃 JVM 堆更划算”的服务：机器 64GB 内存：  Broker JVM 堆 6-8GB（足够）  剩余 50GB+ 留给页缓存（关键！）17.4 Broker 调优            参数      默认      建议                  num.network.threads      3      看 NetworkProcessorAvgIdlePercent，低于 30% 加到 6-8              num.io.threads      8      看 RequestHandlerAvgIdlePercent，磁盘快时适当加大              num.replica.fetchers      1      ISR 频繁收缩/写入大时调到 2-4              num.partitions      1      新 topic 默认分区数，显式规划覆盖              log.flush.interval.messages      MAX      保持默认，依赖 OS              socket.send/receive.buffer.bytes      100KB      大流量可调大到 1MB              replica.fetch.max.bytes      1MB      与生产端大消息匹配              message.max.bytes      ~1MB      大消息需同步调客户端      核心指标驱动：NetworkProcessorAvgIdlePercent  &lt; 0.3 -&gt; 加网络线程RequestHandlerAvgIdlePercent    &lt; 0.3 -&gt; 加 IO 线程UnderReplicatedPartitions       &gt; 0 持续 -&gt; 副本追赶能力不足RequestLatencyAvg               高 -&gt; 磁盘/页缓存/请求堆积17.5 生产者调优高吞吐模板：linger.ms=20batch.size=65536compression.type=zstdbuffer.memory=67108864acks=1            # 若业务允许；可靠场景保持 all低延迟模板：linger.ms=0acks=1compression.type=lz4enable.idempotence=true取舍关系：  linger.ms 调大 -&gt; 批更大 -&gt; 吞吐升、延迟升；  compression=zstd -&gt; CPU 升、网络/磁盘降，通常净赚；  acks=0/1/all -&gt; 吞吐降、可靠性升；  大消息会破坏批次效率与页缓存命中率，能拆小就拆小，大内容放对象存储、消息只放引用。17.6 消费者调优fetch.min.bytes=1fetch.max.wait.ms=500max.partition.fetch.bytes=2097152max.poll.records=500吞吐优先时：  增加消费者实例直到 = 分区数；  批量处理（攒批写库）；  调大 fetch.min.bytes（如 1KB）减少小包；  下游异步化/线程池化，但要自己管理位移。延迟优先时：fetch.min.bytes=1、小 max.poll.records、处理逻辑轻量化。17.7 JVM 调优推荐起点：-Xms6g -Xmx6g-XX:+UseG1GC-XX:MaxGCPauseMillis=20-XX:InitiatingHeapOccupancyPercent=35要点：  堆不要盲目开大：6-8GB 通常足够，剩余内存留给页缓存；  GC 停顿会同时影响心跳与 poll（rebalance 风暴诱因）；  堆外内存也要算：网络缓冲、压缩缓冲在 native memory，容器里 memory limit 要留余量。17.8 容量规划磁盘日数据量 = 峰值MB/s * 86400 (按均值更准)保留7天 * 副本3 * 压缩比(约0.7) * 1.2(索引/元数据)例：均值 100MB/s -&gt; 100*86400=8.64TB/天(单副本)    保留7天、RF=3 -&gt; 8.64*7*3*1.2 ≈ 217TB网络入流量 = 写入出流量 = 写入 * 消费副本数(消费者数) + 副本同步例：写入 1Gbps、3个独立消费组、RF=3    Broker 间副本 ≈ 2x 写入，出向消费 ≈ 3x 写入    万网卡可能接近打满，需评估分区与机器  单分区吞吐经验值：写约 10-30MB/s（取决于消息大小/盘/acks）；  目标吞吐 / 单分区吞吐 = 最少分区数，再留 30-50% 余量；  Broker 数量由磁盘容量 + 网络吞吐 + 副本分布共同决定。17.9 一个调优案例现象：生产端 P99 从 50ms 涨到 2s，吞吐只有 3 万条/s。排查：  Broker RequestHandlerAvgIdlePercent=0.9（不忙）；  磁盘 util 95%（写入瓶颈）；  消息未压缩、linger.ms=0，请求碎片化严重。动作：开启 zstd、linger.ms=15、batch.size=64KB；两块新盘加入 log.dirs。结果：磁盘 util 降到 65%，吞吐 12 万条/s，P99 80ms。这个案例说明：调优常常不是“加线程”，而是减少无效 IO 与请求碎片化。本章小结  先测量再调优：压测工具 + 服务端指标定位瓶颈段；  页缓存是 Kafka 性能核心，JVM 堆不要贪大；  线程参数由 idle 指标驱动，盲目加线程无用；  生产端吞吐靠攒批与压缩，消费端吞吐靠并行与批量处理；  容量规划要同时算磁盘、网络、分区与副本。思考题  为什么“给 Kafka 开 32GB 大堆”通常反而会伤害性能？  linger.ms 从 0 调到 20ms，为什么吞吐和延迟可能同时变得更可控？  规划一个日均 10TB、保留 3 天、RF=3 的集群，需要多少裸容量？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。KRaft（Kafka Raft）是 Kafka 4.x 唯一的运行模式。本章深入它的动机、架构、元数据日志与运维要点，并说明如何从 ZooKeeper 集群迁移。16.1 为什么要去掉 ZooKeeperZooKeeper 模式下的痛点：  两套系统：Broker 依赖 ZooKeeper 选举 Controller、存元数据；运维要同时维护两个分布式系统的安全、扩容、监控；  元数据双写：Controller 变更先写 ZooKeeper，再推送给 Broker，路径长、窗口内有不一致；  扩展瓶颈：分区数达到十万级时，Controller 故障切换要重放大量状态，收敛可能需要分钟级；  安全与网络复杂：客户端、Broker、ZooKeeper 三方网络与认证都要打通。KRaft 的思路：既然 Kafka 本身就是高吞吐、高可用的分布式日志，那就用 Kafka 存 Kafka 的元数据。16.2 架构与角色              ┌──────────────────────────────────┐              │   __cluster_metadata (Raft log)  │              │  topic 配置 / broker 注册 / 分区   │              │  Leader 选举 / ACL / 配额 ...     │              └───────────────┬──────────────────┘                              │ 复制        ┌─────────────┬───────┴──────┬─────────────┐        v             v              v             v   controller1    controller2    controller3     broker...   (voter)        (voter)        (voter)        (learner)角色由 process.roles 定义：            取值      含义      场景                  controller      只参与仲裁，不存业务数据      大规模分离部署              broker      只存数据，作为 learner 同步元数据      分离部署的数据节点              broker,controller      兼具两种角色      中小集群合并部署      核心概念：  Voter：有投票权的 Controller（配置在 controller.quorum.voters），数量为奇数；  Learner：Broker，只拉取元数据日志，不投票；  元数据日志：一个特殊的内部 topic __cluster_metadata，提交即生效；  Snapshot：元数据日志定期快照，新节点/落后节点先加载快照再追增量。16.3 元数据变更如何发生以“创建 topic”为例：1. 客户端向任意 Broker 发 CreateTopics 请求2. Broker 转发给 Leader Controller3. Leader 把变更作为记录追加到 __cluster_metadata4. Raft 多数派确认 -&gt; commit5. 所有 Broker 的元数据日志推进到该 offset6. 客户端收到创建成功对比 ZooKeeper 模式：不再存在“先写 ZK 再通知”的两阶段窗口，元数据的真相只有一份日志，所有节点按同一顺序回放。这从机制上消除了元数据分叉。16.4 配置速览# 节点角色与 IDprocess.roles=broker,controllernode.id=1# 仲裁者列表：id@host:controller端口controller.quorum.voters=1@broker1:9093,2@broker2:9093,3@broker3:9093# 监听器listeners=PLAINTEXT://:9092,CONTROLLER://:9093advertised.listeners=PLAINTEXT://broker1:9092controller.listener.names=CONTROLLERlistener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT# 数据目录log.dirs=/data/kafka运维要点：  controller.quorum.voters 集群生命周期内基本不变；扩 Controller 是“加 voter + 滚动重配”的严肃操作，规划时直接定 3 或 5；  controller.listener.names 指向的监听器专用于仲裁，不要暴露给客户端；生产环境建议对 Controller 通道启用 SSL 或 SASL；  node.id 全集群唯一，且与 controller.quorum.voters 中的 id 严格一致。16.5 常用运维命令# 仲裁状态：谁是 Leader、HW、voter 列表bin/kafka-metadata-quorum.sh \  --bootstrap-server localhost:9092 describe --status# 每个 voter 的同步详情bin/kafka-metadata-quorum.sh \  --bootstrap-server localhost:9092 describe --replication# 查看元数据日志（排障）bin/kafka-dump-log.sh \  --files /data/kafka/__cluster_metadata-0/00000000000000000000.log \  --cluster-metadata-decoderdescribe --replication 里的 LastFetchTimestamp、LastCaughtUpTimestamp 能判断哪些 voter 落后。16.6 故障场景            场景      影响                  Leader Controller 宕机      多数派重新选出 Leader，期间元数据变更短暂不可用，数据读写基本不受影响              一个 Follower Controller 宕机      3 节点仲裁仍有多数派，无感知              两个 Controller 宕机（3 节点）      丢失多数派，元数据变更不可用；已有分区读写通常继续              Broker learner 落后      该 Broker 可能用旧元数据应答，客户端重试后纠正；持续落后需查网络/磁盘      和 Kafka 数据面一样：元数据面也是少数派服从多数派。3 节点 Controller 可以容忍 1 台故障，5 节点容忍 2 台。16.7 从 ZooKeeper 迁移到 KRaftKafka 3.4-3.9 提供在线迁移工具，思路是“双写过渡”：阶段1 ZK 模式：  Controller &lt;- ZooKeeper阶段2 迁移中：  ZK 里的元数据被迁移工具搬运到 __cluster_metadata  新 KRaft Controller 作为 DualWriter 同时服务  Broker 逐台切换为 KRaft 配置阶段3 KRaft 模式：  ZooKeeper 不再被依赖，可下线操作要点：  先升级到支持迁移的 3.x 版本，做全量备份；  搭建 KRaft Controller 仲裁（通常 3 节点）；  按 runbook 执行格式化与迁移准备；  触发 ZkMigrationState 切换，观察元数据一致性；  滚动重启 Broker 切到 KRaft 配置；  观察 kafka-metadata-quorum.sh 正常后，摘除 ZooKeeper。迁移是高风险变更，务必在预发演练并准备好回退窗口。Kafka 4.0 已不支持 ZK 模式，老集群终将走上这条路。16.8 监控要点关键 JMX 指标：  kafka.controller:type=KafkaRaftServer / raft-metrics：current-state、leader-id、commit-offset、high-watermark；  ActiveControllerCount：应为 1（KRaft 下表示有唯一 Leader）；  MetadataFencedCount、MetadataLoadErrorCount：出现非零要立刻排查；  Follower voter 的 LastCaughtUpTimestamp：仲裁健康度。告警建议：Controller Leader 长期缺失、voter 长期未追上、Broker 元数据 offset 滞后 Leader 过大。本章小结  KRaft 用 Raft 日志 __cluster_metadata 取代 ZooKeeper，元数据单一路径、按序回放；  process.roles 决定节点角色，voter 是奇数的 Controller，broker 是 learner；  3 Controller 容忍 1 故障，5 容忍 2；Controller 通道应独立且加密；  快照让新节点快速加载状态，kafka-metadata-quorum.sh 是核心巡检命令；  ZK -&gt; KRaft 迁移是双写过渡的严肃变更，需演练与回滚预案。思考题  为什么 Broker 宕机不影响 KRaft 仲裁？为什么 Controller 宕机一般也不影响数据读写？  元数据日志和普通 topic 日志有什么相同与不同？  如果公司还在 ZK 模式，你会如何制定迁移评估清单？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。再平衡（Rebalance）是消费组弹性的来源，也是生产事故的高发区：一次大规模 rebalance 可能让整个组停止消费数十秒。本章讲清楚触发条件、协议流程、分配策略与如何避免 rebalance 风暴。15.1 什么是 Rebalance消费组的成员或分区发生变化时，协调器把分区在消费者之间重新分配，这个过程叫 rebalance。触发条件只有三类：  成员变化：新消费者加入、旧消费者退出/崩溃；  订阅变化：消费者订阅的 topic 集合改变（含正则匹配到新 topic）；  分区数变化：topic 增加分区。注意：Broker 宕机本身不触发消费组 rebalance（分区 Leader 切换对客户端透明），除非它同时是协调器且导致组会话超时。15.2 协议流程消费者A  消费者B  消费者C          Coordinator(Broker)   │        │        │                 │   │--- JoinGroup -------------------&gt;│  所有成员报名   │        │        │                 │ 选出组 leader（这里是 A）   │&lt;-- JoinGroup 响应(你是leader) ----│   │&lt;------ 你是普通成员 --------------│   │                                │   │ A 本地运行分配策略:   │   A: p0,p1  B: p2,p3  C: p4,p5   │                                │   │--- SyncGroup(分配方案) ---------&gt;│   │&lt;------ SyncGroup(A: p0,p1) ------│   │        &lt;------ (B: p2,p3) -------│   │                 &lt;---- (C: p4,p5)-│   │                                │   │--- Heartbeat / OffsetCommit ---&gt;│  进入稳定态再次强调：分配方案由组 leader 消费者计算（第 12 章），协调器只是组织者。generation（代数）随每次 rebalance 递增，用于隔离旧成员的过期请求。15.3 Eager vs Cooperative：两种再平衡协议Eager（全部回收再分配）传统模式。触发 rebalance 时，所有消费者撤销全部分区，进入 JoinGroup，等全组达成一致后再重新分配。代价：即使只是新增一个消费者，也会造成全组短暂停止消费。Cooperative / Incremental（增量式）只撤销“需要移动的分区”，其他分区继续消费：Eager:       A[p0,p1] B[p2,p3] -&gt; 全部撤销 -&gt; A[p0] B[p1,p2] C[p3]Cooperative: A[p0,p1] B[p2,p3] -&gt; A继续p0, B继续p2 -&gt; 只迁移 p1,p32.4+ 版本支持。新系统建议直接使用 CooperativeStickyAssignor，把 rebalance 的停顿范围降到最小。15.4 四种分配策略配置项：partition.assignment.strategy。RangeAssignor（默认之一）按 topic 逐个计算：分区排序、消费者按名称排序，再连续切块。topicX 6分区, topicY 3分区, 2个消费者topicX: C1=[0,1,2]  C2=[3,4,5]topicY: C1=[0,1]    C2=[2]-&gt; C1 共 5 个，C2 共 4 个多 topic 时前面的消费者容易分到更多分区，可能不均。RoundRobinAssignor把所有 topic 的分区放在一起轮询分配，通常更均匀。但要求组内订阅一致才语义明确。StickyAssignor尽量均匀，且再平衡时尽量保留原分配（粘性），减少分区迁移。CooperativeStickyAssignor在 Sticky 基础上支持协作式增量再平衡，是当前推荐的默认选择：partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor15.5 静态成员：免打扰的重启滚动发布时，每个实例重启都会触发 rebalance。配置静态成员后，实例以固定身份加入组：group.instance.id=pod-1session.timeout.ms=60000行为：  重启时间小于 session.timeout.ms：协调器认为成员只是短暂离线，不触发 rebalance，分区保留；新实例以同一身份回来，拿着原分区继续消费；  超时未归：才按普通成员失效处理。这是 K8s/滚动升级场景的标配，能把发布对消费的影响降到最低。注意 group.instance.id 必须保证唯一且与实例绑定（如 Pod 序号），否则会互相 fence。15.6 Rebalance 风暴：症状、原因与治理典型症状消费 lag 突增、日志出现 Attempt to heartbeat failed / session timeout / max poll interval、rebalancing 频繁、消费延迟周期性抖动。常见原因  处理太慢：两次 poll 间隔超过 max.poll.interval.ms，被踢出组；  GC 停顿：长时间 STW，心跳/poll 都停；  网络抖动：心跳超时；  频繁发布：无静态成员 + 部署波次太密；  消费者订阅不一致：有的订阅 topic A，有的订阅 A+B，不断触发 rebalance。治理清单            手段      配置/做法                  降单次处理量      max.poll.records 调小（如 100）              放宽处理时限      max.poll.interval.ms 调大（如 600000）              心跳更宽容      session.timeout.ms=60000，heartbeat.interval.ms=15000              滚动发布免扰      group.instance.id + 保持 session 内回归              协作式再平衡      CooperativeStickyAssignor              统一订阅      同组所有消费者订阅完全相同              排查 GC      缩短停顿、调整堆、看 GC 日志      15.7 观察 Rebalance客户端日志是最直接的证据，关注：(Re)join group ... as memberSuccessfully synced groupAttempt to heartbeat failed because group is rebalancingMember ... sending LeaveGroup requestJMX 指标：  kafka.consumer:type=consumer-coordinator-metrics：sync-rate、assigned-partitions；  协调器端：kafka.server:type=group-coordinator-metrics。把“1 分钟内 rebalance 次数”做成告警，是发现风暴的第一道防线。15.8 动手实验  建一个 6 分区 topic，启动 2 个消费者（控制台或代码），观察分区分配；  再启动第 3 个消费者，观察 rebalance 与新分配；  kill 一个消费者，观察分区转移与短暂停顿；  在业务处理里 Thread.sleep(310000)，复现 max.poll.interval.ms 超时导致的踢组；  切换为 CooperativeStickyAssignor，重复第 2 步，对比停顿范围。本章小结  Rebalance 由成员、订阅、分区数三类变化触发，Broker 宕机不直接触发；  分配方案由组 leader 消费者计算，generation 隔离旧请求；  Cooperative 增量式再平衡只迁移必要分区，是新系统默认选择；  静态成员让滚动重启不打扰消费组，是容器环境的必备配置；  rebalance 风暴的根源多为处理超时、GC 停顿与订阅不一致，治理要对症下药。思考题  为什么 max.poll.records 调小反而能缓解 rebalance？  Eager 策略下新增一个消费者，为什么没发生迁移的分区也暂停消费了？  两个消费者订阅的 topic 不一致会怎样？什么时候这反而是合理的？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。“消息会不会丢、会不会重”是 Kafka 工程的灵魂问题。本章把三种交付语义的定义、实现手段与适用边界彻底讲清楚，并给出可以落地的代码。14.1 三种交付语义            语义      含义      典型实现      风险                  At most once      至多一次      先提交位移，后处理消息      可能丢，不会重              At least once      至少一次      先处理消息，后提交位移      可能重，不会丢              Exactly once      精确一次      幂等 + 事务 + 消费端幂等存储      实现复杂，范围有限制      注意一个常见误解：Exactly once 不是魔法，而是若干机制组合出的端到端属性，而且它的“端”有明确边界。Kafka 事务保证的是“Kafka 到 Kafka”的处理链路精确一次；一旦涉及外部系统（MySQL、Redis），仍需要业务侧幂等或事务配合。14.2 至少一次：默认的工程底线标准做法（第 6 章）：poll -&gt; 处理 -&gt; commit offset处理完成但未来得及提交时宕机，重启后会重复消费，所以是“至少一次”。重复交给下游幂等解决：  数据库唯一键 INSERT ... ON DUPLICATE KEY UPDATE；  Redis SETNX key requestId；  状态机检查（订单已是 PAID 则忽略再次支付事件）；  业务版本号 / 乐观锁。绝大多数业务系统应该停在这里：至少一次 + 业务幂等。 简单、可控、可观测。14.3 幂等生产者：解决发送端重复问题：为什么发送会重复Producer 发消息给 Broker，Broker 写入成功，但响应在网络中丢了。Producer 超时重试，Broker 收到两条相同消息。网络层面无法区分“没写进去”和“写进去了但响应丢了”。原理：PID + Sequence Number开启幂等（enable.idempotence=true，3.0 起默认开启）后：  Producer 启动时从 Broker 获取一个 PID（producer id）；  对每个 (PID, partition) 维护单调递增的 sequence number；  消息（batch）带着 PID + epoch + sequence 写入 Broker；  Broker 为每个分区缓存最近 5 个 batch 的序号：          序号正好是预期值：正常写入；      序号小于预期（重复）：丢弃并返回成功；      序号大于预期（乱序缺口）：返回 OutOfOrderSequenceException。      Producer 重试:  seq=7 -&gt; Broker 已有 seq=7 -&gt; 丢弃，返回成功               客户端视角：发送成功，且日志中只有一条约束与注意：  幂等范围是当前 Producer 会话 + 单分区，进程重启后 PID 改变，不能防止跨重启的重复；  max.in.flight.requests.per.connection &lt;= 5 且幂等开启时，Broker 能重排序号，仍保持分区内有序；  若关闭幂等又想要顺序，只能把 in-flight 设为 1，吞吐大幅下降。14.4 事务：跨分区原子写幂等解决“单分区单会话不重”，事务解决“多条消息原子写入多个分区”。完整 APIProperties props = new Properties();props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "order-tx-producer-1");props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);// 其余配置同普通生产者KafkaProducer&lt;String, String&gt; producer = new KafkaProducer&lt;&gt;(props);producer.initTransactions();      // 1. 注册到事务协调器，获取 epoch（会 fence 旧实例）try {    producer.beginTransaction();  // 2. 开启事务    producer.send(new ProducerRecord&lt;&gt;("order-events", "o1", "created"));    producer.send(new ProducerRecord&lt;&gt;("audit-events", "o1", "audit"));    producer.commitTransaction(); // 3. 提交：两条要么都可见，要么都不可见} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {    producer.close();             // 不可恢复，直接关闭} catch (KafkaException e) {    producer.abortTransaction();  // 可恢复错误则回滚}要点：  transactional.id 必须稳定且唯一（如“服务名-分区号”）。重启后新实例用同一 id 初始化，会 fence 掉旧实例（旧 epoch 的请求被拒绝），这是防止僵尸生产者重复写的关键；  提交分两阶段：协调器先写 PrepareCommit 到 __transaction_state，再向相关分区写入控制消息（COMMIT/ABORT marker），最后完成；  消费者用 isolation.level=read_committed，只读已提交事务的消息。14.5 LSO 与 read_committed事务消息写入后并非立即可见。Broker 为每个分区维护 LSO（Last Stable Offset）：日志:  [普通消息] [事务T1消息] [普通消息] [事务T2消息] ...                          ^                 ^                        LSO 在这里         T2 未提交read_committed 消费者最多读到 LSO。事务提交前，T1 的消息不可见；一旦提交，marker 之后的 LSO 前移，消费者才能读到。监控事务相关指标：  transaction-coordinator-metrics 下的 active-transaction-count、prepare-transaction-completion-time；  消费端 records-lag 突增且 isolation-level=read_committed 时，检查是否有长时间未提交/未中止的事务。14.6 Consume-Transform-Produce：流式精确一次最常见的 exactly-once 形态：消费上游 topic，处理后写下游 topic，把“输入位移提交”和“输出写入”放进同一个事务。props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "etl-job-1");consumer.subscribe(List.of("input-topic"));producer.initTransactions();while (true) {    var records = consumer.poll(Duration.ofMillis(500));    if (records.isEmpty()) continue;    producer.beginTransaction();    try {        for (var r : records) {            producer.send(new ProducerRecord&lt;&gt;(                    "output-topic", r.key(), transform(r.value())));        }        // 把消费位移也纳入事务        Map&lt;TopicPartition, OffsetAndMetadata&gt; offsets = buildOffsets(records);        producer.sendOffsetsToTransaction(offsets, consumer.groupMetadata());        producer.commitTransaction();    } catch (Exception e) {        producer.abortTransaction();        // 重新 poll 会从上一次提交位移开始    }}这段代码保证了：输出消息与位移提交原子生效。要么下游看到结果且位移前移，要么两者都回滚，重放后再次写入。配合输入输出都在 Kafka，就构成了 Kafka 端到端的 exactly-once。Kafka Streams 的 exactly_once_v2 本质就是把这套机制框架化，业务代码无需手写事务边界。14.7 消费端的精确一次如果下游是外部系统，事务管不到那里，需要自己设计幂等：数据库方案-- messages 表主键 = topic + partition + offset 或业务 request_idINSERT INTO consumed_messages(message_key, topic_name, partition_id, offset_id)VALUES (?, ?, ?, ?)ON CONFLICT DO NOTHING;插入成功才处理业务；插入冲突说明已处理过，直接跳过。把业务写和去重表放同一个本地事务，就是完整的端到端精确一次。状态机方案CREATED -&gt; PAID -&gt; SHIPPED重复的 PAID 事件到来时，当前状态已是 SHIPPED -&gt; 直接忽略代价与选择事务会带来吞吐下降（两阶段提交与 marker 写入）和延迟上升。问自己：  重复会造成资金/库存错误吗？必须严格幂等；  重复只是多记一条日志/多推一次通知吗？至少一次 + 去重可能就够；  输入输出是否都在 Kafka 内？是则优先用事务/Streams。14.8 常见误区  开了幂等就不丢消息：幂等只防重，不丢消息靠 acks=all + 副本配置；  事务保证外部系统原子性：不能。MySQL 写入失败不会被 Kafka 事务回滚；  read_committed 能去重业务消息：它只过滤未提交/已中止事务，不处理业务层重试造成的逻辑重复；  换个 transactional.id 重启：必须保持稳定，否则失去 fence 能力；  把所有业务都上事务：事务有真实成本，大多数场景“至少一次 + 幂等”更划算。本章小结  至少一次 + 业务幂等是大多数系统的最佳平衡点；  幂等生产者用 PID+sequence 在 broker 端去重重试，但仅限单会话单分区；  事务提供跨分区原子写与 consume-transform-produce 的位移原子提交；  消费端用 read_committed 只读已提交数据，边界由 LSO 控制；  涉及外部系统时，精确一次要靠“去重表/状态机 + 本地事务”自己完成。思考题  为什么幂等生产者不能防止“进程重启后重发同一业务消息”？该用什么解决？  sendOffsetsToTransaction 如果漏掉，会发生什么？  设计一个“Kafka -&gt; Redis -&gt; MySQL”链路的端到端精确一次方案，指出每一段由谁保证。</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Broker 每秒要处理几十万请求，却用一套非常经典的 Reactor 线程模型做到高吞吐低延迟。理解这套模型，num.network.threads、num.io.threads 这些参数才调得有依据。13.1 线程模型总览                      ┌────────────────┐      客户端请求 -----&gt; │  Acceptor      │  监听端口，接收连接                      └───────┬────────┘                              │ 轮询分配              ┌───────────────┼───────────────┐              v               v               v        ┌──────────┐    ┌──────────┐    ┌──────────┐        │Processor1│    │Processor2│    │Processor3│   网络线程        │ (NIO)    │    │ (NIO)    │    │ (NIO)    │   num.network.threads        └────┬─────┘    └────┬─────┘    └────┬─────┘             │               │               │             +-------+-------+------+--------+                     v              v              ┌────────────┐  ┌────────────┐              │ 请求队列    │  │ 响应队列    │              └─────┬──────┘  └─────^──────┘                    v               │        ┌─────────────────────────────────┐        │  RequestHandler 线程池          │   IO 线程        │  num.io.threads                 │   真正处理请求        │  (KafkaApis -&gt; ReplicaManager)  │        └─────────────────────────────────┘各角色职责：  Acceptor：接受 TCP 连接，注册到 Processor；  Processor（网络线程）：读写 Socket、解析/序列化协议，不做业务逻辑；  请求队列：待处理请求的缓冲，默认 queued.max.requests=500；  RequestHandler（IO 线程）：执行 KafkaApis.handleProduceRequest/handleFetchRequest 等，读写日志；  Purgatory：存放“暂时无法完成”的请求，稍后重试检查。这套模型让网络 IO 与请求处理解耦：慢的磁盘操作不会阻塞网络线程收包，网络突发也不会让 IO 线程无上限堆积。13.2 Purgatory：延迟操作的“候车室”有两类典型请求需要等待条件满足：  Produce 请求 acks=all：Leader 写入本地后，必须等 ISR 其他副本 Fetch 追上，才能给客户端返回成功；  Fetch 请求 fetch.min.bytes：消费者要求“攒够 N 字节再返回”，不足时先挂起，等新数据到达。如果让 IO 线程自旋等待，吞吐会瞬间崩塌。于是有了 Purgatory：请求到达 -&gt; 条件不满足 -&gt; 封装成 DelayedOperation 放入 Purgatory                        -&gt; 线程立即释放事件发生（副本追上/新数据写入）-&gt; 尝试 complete 该请求超时（request.timeout.ms 等）    -&gt; 按 timeout 完成/失败purgatory.size 相关指标过大，说明“等待条件”长期无法满足，常见于副本落后、消费者 min.bytes 过大、acks=all 写入压力大等场景。13.3 副本拉取线程Follower 同步走的是独立的 ReplicaFetcherThread：num.replica.fetchers=1   # 每 Broker 的副本拉取线程数，繁忙集群可调大replica.fetch.max.bytes=1048576replica.fetch.wait.max.ms=500replica.fetch.response.max.bytes=10485760集群写入流量大、出现 UnderReplicatedPartitions 告警时，适当增大 num.replica.fetchers（如 2-4）常能有效提升副本追赶速度。13.4 关键 Broker 参数            参数      默认      说明                  num.network.threads      3      网络线程数，CPU 密集              num.io.threads      8      请求处理线程数，通常给足              queued.max.requests      500      请求队列上限，满则拒绝新请求              queued.max.bytes      -1      请求队列字节数限制              socket.send.buffer.bytes      102400      Socket 发送缓冲              socket.receive.buffer.bytes      102400      Socket 接收缓冲              num.replica.fetchers      1      副本拉取线程数              message.max.bytes      1048588      单条消息上限              replica.lag.time.max.ms      30000      ISR 判定超时      调优经验：  NetworkProcessorAvgIdlePercent 低于 30%：加大 num.network.threads；  RequestHandlerAvgIdlePercent 低于 30%：加大 num.io.threads；  两者都很空闲但延迟高：问题多半在磁盘/页缓存/客户端，而不是线程数。13.5 一次 Produce 请求的完整旅程把前面所有章节串起来：1. Producer Sender 线程把某 Broker 上多个分区的 batch 合并成 ProduceRequest2. 网络 -&gt; Broker Acceptor -&gt; Processor 解析 -&gt; 请求队列3. IO 线程 KafkaApis.handleProduceRequest4. ReplicaManager 追加到各分区 Leader 本地日志5. acks=all：请求进入 Purgatory，等待 ISR 副本 Fetch 追上6. Follower 的 ReplicaFetcherThread 拉取并写入本地7. Leader 推进 HW，DelayedProduce 完成8. 响应经响应队列 -&gt; Processor -&gt; 网络 -&gt; Producer 回调消费 Fetch 类似，但多了零拷贝路径与 fetch.min.bytes 等待。本章小结  Broker 是 Reactor 模型：Acceptor + Processor(网络线程) + 请求队列 + IO 线程池；  Purgatory 让 acks=all 与 fetch.min.bytes 这类延迟请求不占用线程；  副本同步由 ReplicaFetcherThread 独立完成，落后时可加线程数；  线程参数调优要看 idle 指标，而不是盲调；  一条消息的成功确认，横跨网络、日志、副本、协调多条链路。思考题  为什么网络线程不直接处理请求，而要经过请求队列交给 IO 线程？  fetch.min.bytes 调大后，延迟和吞吐分别怎么变？它与 fetch.max.wait.ms 如何配合？  如果 Purgatory 中积压大量 acks=all 的 Produce 请求，你会优先排查什么？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Kafka 集群里有两类“管理者”：Controller 管集群元数据与分区 Leader，Group Coordinator 管消费组成员与位移。理解它们，rebalance、选主、位移提交这些行为就不再神秘。12.1 Controller 是谁，做什么Controller 是集群元数据的仲裁者，负责：  Broker 上下线感知；  分区 Leader 选举（第 11 章）；  topic 的创建、删除、配置变更；  分区副本重分配（reassignment）；  向所有 Broker 广播元数据更新。在 ZooKeeper 时代，Controller 通过抢占临时节点选举，元数据存 ZooKeeper；KRaft 时代，Controller 是一个 Raft 仲裁组，元数据就是 __cluster_metadata 这条日志本身。12.2 KRaft 下的 ControllerKRaft 集群中常见两种部署形态：combined（合并模式）：  broker1(broker+controller)  broker2(broker+controller)  broker3(broker+controller)  适合中小集群，省机器separated（分离模式）：  controller1  controller2  controller3   &lt;- 只跑仲裁  broker1 ... brokerN                      &lt;- 只存数据  适合大规模或元数据敏感场景由 process.roles 决定：process.roles=broker,controller   # combined# 或process.roles=broker              # 分离模式的数据节点Controller 数量必须是奇数（3 或 5），因为 Raft 需要多数派（quorum）达成共识。元数据变更流程：  客户端/Broker 发起变更（建 topic、Broker 注册）；  Leader Controller 把变更追加到元数据日志；  Raft 多数派确认后 commit；  所有 Broker（作为 learner）拉取并应用新元数据。查看仲裁状态：bin/kafka-metadata-quorum.sh \  --bootstrap-server localhost:9092 describe --status输出包含 LeaderId、LeaderEpoch、HighWatermark，可以直观看到 Raft 进度。12.3 Group Coordinator 的职责每个消费组在某个 Broker 上有一个 Group Coordinator，负责：  处理 JoinGroup / SyncGroup（加入组、接收分配方案）；  维护成员与心跳（判断谁掉线）；  触发与管理 Rebalance；  接收并存储位移提交（写入 __consumer_offsets）。哪个 Broker 协调哪个组？由组名的哈希决定：coordinator 分区 = abs(group.hashCode()) % 50该分区的 Leader 所在 Broker = 协调器（50 是 offsets.topic.num.partitions 默认值。）所以组名固定，协调器位置也固定；该分区 Leader 切换时，协调器随之切换，客户端会自动重新发现。12.4 消费组协议：一次完整的组生命周期1. FindCoordinator   消费者问：我的协调器在哪台 Broker？2. JoinGroup         所有成员向协调器报名，选出一个 leader 消费者3. SyncGroup         组 leader 把分配方案发给协调器，协调器下发给成员4. Heartbeat         成员定期心跳维持成员资格5. OffsetCommit/OffsetFetch  提交与拉取位移6. LeaveGroup        成员主动退出（优雅停机）注意第 2、3 步中的“组 leader”是消费者 leader（不是 Broker）：它运行分配策略，决定哪个成员拿哪些分区。协调器只负责组织流程与存储结果。这就是为什么切换分配策略只需要改客户端配置（第 15 章）。12.5 __consumer_offsets 内部结构位移提交写入内部 compact topic __consumer_offsets，key 是 (group, topic, partition)：key:   group.id + topic + partitionvalue: offset + metadata + timestamp配合 compact 清理，每个 key 只保留最新提交，因此该主题体积可控。读取它需要专用 formatter（第 4 章命令）。两点实践提醒：  生产集群该主题 RF 必须是 3（默认配置项 offsets.topic.replication.factor），否则协调器所在 Broker 宕机会影响大量消费组；  重置位移命令本质上也是往这个主题写新记录，这就是为什么执行前要停止消费者。12.6 事务协调器事务生产者初始化时，会根据 transactional.id 定位到某个 Broker 上的 TransactionCoordinator：  事务状态持久化在 __transaction_state；  协调器负责分配 producer epoch、两阶段提交（PrepareCommit -&gt; 写 marker -&gt; CompleteCommit）；  Broker 端用 LSO（Last Stable Offset）控制 read_committed 消费者能读到哪（第 14 章）。12.7 元数据如何到达客户端生产/消费客户端会缓存集群元数据（分区 Leader 位置）。刷新时机：  定期：metadata.max.age.ms（默认 5 分钟）；  被动：请求收到 NOT_LEADER_OR_PARTITION、UNKNOWN_TOPIC_OR_PARTITION 等错误时立刻刷新。所以 Leader 切换后的短暂窗口内，客户端可能仍把请求发给旧 Leader，收到错误后自动纠正。这就是“切主瞬间偶发报错又自愈”的原因。本章小结  Controller 管元数据与分区 Leader；KRaft 用 Raft 日志 __cluster_metadata 承载元数据；  Group Coordinator 管消费组成员、rebalance 与位移，位置由 group 哈希定位；  消费组里的“组 leader”（一个消费者）负责计算分区分配方案；  位移存储在 compact 的 __consumer_offsets，事务状态在 __transaction_state；  客户端元数据缓存 + 错误触发刷新，让 Leader 切换对业务基本透明。思考题  为什么 KRaft Controller 数量要配成奇数？2 个 Controller 会发生什么？  __consumer_offsets 为什么用 compact 而不是普通 delete？  消费组 rebalance 过程中，协调器和“组 leader 消费者”各做什么？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。多副本是 Kafka 不丢消息的根基。本章讲清楚副本如何同步、什么算“同步”、Leader 挂了如何选举，以及经典的一致性问题如何被 leader epoch 解决。11.1 副本的角色每个分区有一个 Leader 和若干 Follower：  Leader：处理该分区所有生产与消费请求；  Follower：唯一工作是向 Leader 发送 Fetch 请求，拉取数据写进本地日志。注意这个反直觉的设计：Follower 拉，而不是 Leader 推。好处是每个 Follower 按自己的节奏同步，慢的 Follower 不会拖累 Leader 的写入性能。11.2 LEO 与 HW两个核心指针：Leader 日志:  [0][1][2][3][4][5]   LEO=6                              ^                              HW=5  (已被 ISR 全部同步的位置)Follower A 日志: [0][1][2][3][4][5]  LEO=6  (已请求到 6)Follower B 日志: [0][1][2][3][4]     LEO=5  (落后 1 条)  LEO（Log End Offset）：日志末端位移，下一条待写入消息的 offset；  HW（High Watermark）：所有 ISR 副本都已同步到的位移。消费者只能读到 HW 之前的消息。HW 的推进过程：  Follower 发送 Fetch，请求 fetchOffset = 自身 LEO；  Leader 处理 Fetch 时更新“该 Follower 已追上到哪”的视图；  若 ISR 全部追上某位移，Leader 推进 HW；  Follower 从 Fetch 响应中获知新 HW，更新本地 HW。11.3 ISR：什么算“同步”ISR = 与 Leader 保持同步的副本集合（含 Leader）。 判定标准由 replica.lag.time.max.ms（默认 30 秒）决定：  Follower 持续向 Leader 发 Fetch，且每次都能在超时时间内追上 Leader 的 LEO，就留在 ISR；  落后超过 replica.lag.time.max.ms，被 Leader 移出 ISR（收缩）；  重新追上后，再加入 ISR（扩张）。相关命令观察：kafka-topics.sh --describe --topic order-events# Isr 字段少了某个副本 = 收缩kafka-run-class.sh kafka.tools.IsrChangeDetector   # 旧版工具# 新版直接看 JMX 指标 UnderReplicatedPartitionsISR 是动态的，这带来一个重要性质：acks=all 等待的是“当前 ISR”的确认，不是所有副本。如果 ISR 收缩到只剩 Leader，acks=all 实际退化为 acks=1。这就是必须配置 min.insync.replicas 的原因。11.4 min.insync.replicas 的保护作用假设 RF=3，min.insync.replicas=2：            场景      ISR 数量      acks=all 写入                  三台健康      3      成功，等全部 ISR              一台宕机      2      成功，至少两副本持久化              两台宕机      1      失败，抛 NotEnoughReplicasException      最后一种行为是故意的：宁可暂时不可写，也不允许只有一副本就确认“安全”。可用性与一致性的权衡，Kafka 把选择权交给你：  要一致性：RF=3 + min.insync.replicas=2 + acks=all；  要可用性：min.insync.replicas=1，宕机更多时仍可写，但有丢数据风险；  绝不推荐：RF=1 的“生产”主题。11.5 Leader 选举Leader 所在 Broker 宕机后，Controller 负责为受影响分区选新 Leader：  Controller 通过元数据日志感知 Broker 下线；  对该 Broker 上的所有分区，从 ISR 中按顺序选择第一个存活副本作为新 Leader；  写入元数据并广播给所有 Broker；  客户端元数据刷新后，把请求发往新 Leader。因为新 Leader 来自 ISR，它拥有 HW 之前的全部数据，已提交消息不会丢失。Unclean Leader Election如果 ISR 全部不可用，但某个非同步 Follower 还活着怎么办？  unclean.leader.election.enable=false（默认）：拒绝选举，分区不可用，保数据；  true：让落后副本上位，恢复可用性，但丢掉未同步的已提交消息。这是经典的 CAP 取舍。支付、订单类系统应保持 false；某些允许少量丢失的指标日志场景可以考虑 true，但要有清晰的业务确认。11.6 Leader Epoch：解决截断不一致老问题HW 更新有延迟。Follower 重启后需要截断日志到 HW 以保证与 Leader 一致，但在极端时序下（Follower 的 HW 尚未从 Leader 获取、而它曾短暂被当成 Leader），可能出现副本间数据分叉：场景：Follower A 先被选为 Leader，写入 2 条后宕机未同步；     B 上位成为新 Leader，A 恢复后按旧 HW 截断，可能多删或少删。Leader Epoch 机制每次 Leader 变更，epoch（纪元）+1。Follower 重启或追赶时，先向 Leader 发 OffsetsForLeaderEpoch 请求：Follower: "我这里有 epoch=5, endOffset=87，你们呢？"Leader:   "epoch=6 从 offset=87 开始，6 的 endOffset=90"-&gt; Follower 精确截断到 87，再从 87 开始同步副本不再依赖可能滞后的 HW 做截断，而是以 Leader 的权威 epoch 记录为准，消除了数据分叉。leader-epoch-checkpoint 文件保存的正是这组 (epoch, startOffset) 历史。11.7 副本均衡与机架感知  preferred replica：AR（Assigned Replicas）列表中的第一个副本。Kafka 倾向让分区的 preferred replica 成为 Leader，从而使 Leader 均匀分布；  auto.leader.rebalance.enable=true（默认）时，Broker 定期检查并触发平衡；也可手动执行：bin/kafka-leader-election.sh --bootstrap-server localhost:9092 \  --election-type preferred --all-topic-partitions  配置 broker.rack 后，Kafka 会尽量把副本分散到不同机架，防止机架交换机故障导致分区全灭。11.8 动手实验：亲历一次选举  伪集群创建 topic：test-ha，3 分区 RF=3；  describe 记录每个分区的 Leader/ISR；  kill -9 某 Leader 所在 Broker；  立刻 describe：Leader 变化、ISR 收缩；  期间持续发消息（注意 min.insync.replicas），观察写入是否正常；  重启被 kill 的 Broker，观察它作为 Follower 追赶、ISR 扩张；  打开该分区的 leader-epoch-checkpoint，看 epoch 历史。本章小结  Follower 通过 Fetch 拉取同步；LEO 是日志末端，HW 是消费者可见边界；  ISR 由 replica.lag.time.max.ms 动态判定，acks=all 只等当前 ISR；  RF=3 + min.insync.replicas=2 是可靠性的标准配置；  Controller 从 ISR 选新 Leader；unclean election 用可用性换丢失风险；  Leader epoch 解决了基于 HW 截断的分叉问题，是副本一致性的关键机制。思考题  为什么消费者只能读到 HW 之前的消息？如果允许读到 HW 之后会怎样？  replica.lag.time.max.ms 调大或调小，分别对可用性与数据安全有什么影响？  什么情况下 acks=all 仍然可能丢消息？给出至少两种场景与对应的防范配置。</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Kafka 的存储设计是它高性能的根基。本章拆开磁盘目录，看看一个分区到底由哪些文件组成、消息如何查找、数据如何过期删除。10.1 磁盘目录结构每个分区在每个 Broker 上对应一个目录：&lt;topic&gt;-&lt;partition&gt;。进入第 4 章实验的数据目录，你会看到：/tmp/kraft-1/order-events-0/    00000000000000000000.log    00000000000000000000.index    00000000000000000000.timeindex    leader-epoch-checkpoint    partition.metadata    .lock不同后缀的职责：            文件      作用                  .log      真正的消息数据，追加写入              .index      位移稀疏索引，offset -&gt; 物理位置              .timeindex      时间戳稀疏索引，timestamp -&gt; offset              leader-epoch-checkpoint      记录 leader 纪元变更，用于副本一致性（第 11 章）              partition.metadata      分区版本信息      文件名就是该 segment 的起始 offset，固定 20 位数字。例如 00000000000000005000.log 表示这个文件从 offset 5000 开始。10.2 Segment 分段分区日志不会无限追加在单个文件里，而是切成多个 segment：00000000000000000000.log   offset [0, 3762)0000000000000000003762.log offset [3762, 8911)0000000000000000008911.log offset [8911, ...)  &lt;- 活跃段滚动（roll）出新的活跃 segment 由以下条件触发，任一满足即可：            配置      默认值      含义                  log.segment.bytes      1GB      单段大小              log.roll.ms / log.roll.hours      7 天      段存活时长              log.index.size.max.bytes      10MB      索引文件大小上限              log.roll.jitter.ms      0      抖动，避免所有分区同时滚动造成 IO 尖峰      只对活跃段追加数据；非活跃段是只读的，可以被安全共享、索引、删除。分段是“按时间/大小删除数据”和“快速定位 offset”的基础。10.3 稀疏索引.index 不是每条消息一条记录，而是每隔 log.index.interval.bytes（默认 4KB）日志数据才记一条：index 文件内容（示意）：relative offset | physical position      0         |     0     37         |  4200     75         |  8410    112         | 12650查找 offset=90 的过程：  在 index 中二分找到“小于等于 90 的最大条目”：75；  从物理位置 8410 开始顺序扫描 .log；  最多扫描约 log.index.interval.bytes（4KB）就能命中目标。这就是稀疏索引 + 顺序扫描的组合：索引文件极小（可整个映射进内存），查找代价常数级可控。B 树那样的稠密索引在这里完全没有必要。.timeindex 同理，记录 timestamp -&gt; offset，支撑“按时间查找”（消费者的 offsetsForTimes 就靠它）。10.4 零拷贝与页缓存消费 fetch 请求的读路径：传统方式：磁盘 -&gt; 内核页缓存 -&gt; 用户空间(Kafka进程) -&gt; Socket 缓冲 -&gt; 网卡sendfile 零拷贝：磁盘 -&gt; 内核页缓存 -------------直接经 sendfile------------&gt; 网卡配合页缓存，热门分区被反复消费时，数据大概率已在内存，磁盘根本不参与。这也是 Kafka 建议 给页缓存留足内存、不要把大 JVM 堆开满 的原因（第 17 章）。注意两点零拷贝不生效的情况：  消息需要服务端转换格式（老版本 V0/V1 消息发给新客户端时会走普通读路径）；  启用了某些加密通道时，数据必须进入用户空间加解密。10.5 刷盘策略Kafka 依赖副本机制 + 页缓存保证可靠性，而不是 fsync 每条消息：            配置      默认      含义                  log.flush.interval.messages      Long.MaxValue      多少条消息后强制刷盘              log.flush.interval.ms      null      多久刷一次              log.flush.offset.checkpoint.interval.ms      60000      恢复检查点写入频率      官方建议：交给操作系统异步刷盘（默认即可），用多副本容错。每条 fsync 会把吞吐打骨折，而单机刷盘也无法对抗整机断电，真正的安全性来自“多副本分布在多台机器”。10.6 数据删除与日志压缩delete 模式后台线程定期检查非活跃 segment：  整段中所有消息都超过 retention.ms，或段整体超过大小预算，删除整个文件；  删除粒度是 segment 整体，不是单条消息。所以“7 天保留”意味着最老的一批段整体过期，而不是每条消息精确 7 天。compact 模式后台 Cleaner 选择“脏比例”超标的分区，重写日志：清理前：  k1:v1  k2:v1  k1:v2  k3:v1  k1:v3  k2:v2清理后：  k2:v1  k3:v1  k1:v3  k2:v2   (每 key 保留最新，保留原始顺序)关键点：  key 为空的消息直接丢；  保留每个 key 最后一条，且保留其在原日志中的相对顺序；  清理是异步的、批量的，min.cleanable.dirty.ratio（默认 0.5）控制触发时机。10.7 V2 消息格式0.11 起默认 V2 格式，核心思想是以 batch 为单位存储元数据：RecordBatch:  baseOffset, lastOffsetDelta, partitionLeaderEpoch  magic(2), CRC, attributes(压缩/时间戳类型/事务/控制批)  producerId, producerEpoch, baseSequence     &lt;- 幂等与事务  records[]:    length, attributes    timestampDelta, offsetDelta               &lt;- 增量编码    keyLen, key    valueLen, value    headers[]                                 &lt;- 变长头好处：  offset、timestamp 存增量（varint），体积大幅下降；  CRC 与压缩都作用在 batch 级，减少重复校验与解压开销；  producerId/epoch/sequence 让 broker 能识别并去重重试消息（第 14 章）。用 kafka-dump-log.sh 观察一个 zstd 压缩的日志文件，能直接看到这些字段的实际值。10.8 动手实验  建一个 topic，把 log.segment.bytes 调成 1024 字节（topic 级配置）；  发送几十条消息，观察目录里很快出现多个 segment；  用 kafka-dump-log.sh --files &lt;某个.index&gt; 看索引条目（不需要 --print-data-log）；  把 retention.ms 设成 60000，等一分钟后观察旧 segment 消失；  创建 compact topic，发送同 key 多版本消息，观察清理后只剩最新值。这些实验能把你刚学的概念全部落到文件层面。本章小结  分区 = 目录，segment = 滚动的日志文件，文件名是起始 offset；  .index/.timeindex 是稀疏索引，查找 = 二分索引 + 小范围顺序扫描；  活跃段可写，历史段只读；删除以 segment 为单位；  可靠性来自多副本而非每条 fsync，页缓存 + 零拷贝是读性能关键；  V2 格式以 batch 为中心，为增量编码、压缩与幂等打下基础。思考题  为什么索引不做成每条消息一个条目？这样查找不是更快吗？  retention.ms=86400000（1 天）时，一条消息实际最短存活多久？最长呢？  如果把 log.segment.bytes 调得非常小（如 1KB），会带来哪些问题？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Spring Kafka 是 Java 生态最常用的 Kafka 封装。本章从发送、消费、手动确认、错误处理讲到死信队列与批量监听，给出一套可以直接落地的工程模板。9.1 依赖与配置&lt;dependency&gt;    &lt;groupId&gt;org.springframework.kafka&lt;/groupId&gt;    &lt;artifactId&gt;spring-kafka&lt;/artifactId&gt;&lt;/dependency&gt;Spring Boot 3.x 的依赖管理会自动匹配合适的 spring-kafka 版本。application.yml：spring:  kafka:    bootstrap-servers: localhost:9092    producer:      key-serializer: org.apache.kafka.common.serialization.StringSerializer      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer      acks: all      properties:        enable.idempotence: true        linger.ms: 10        compression.type: zstd    consumer:      group-id: order-service      auto-offset-reset: earliest      enable-auto-commit: false      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer      properties:        spring.json.trusted.packages: "com.example.dto.*"        isolation.level: read_committed    listener:      ack-mode: manual      concurrency: 3要点：  concurrency 是每个监听器容器的线程数，最大有效值 = 分区数；  ack-mode: manual 配合代码里 Acknowledgment.acknowledge()；  JSON 反序列化必须配置可信包，否则会拒绝反序列化未知类型。9.2 发送消息：KafkaTemplate@Service@RequiredArgsConstructorpublic class OrderEventPublisher {    private final KafkaTemplate&lt;String, OrderCreated&gt; kafkaTemplate;    public void publish(OrderCreated event) {        kafkaTemplate.send("order-events", event.orderId(), event)                .whenComplete((result, ex) -&gt; {                    if (ex != null) {                        log.error("send failed, orderId={}", event.orderId(), ex);                        // 落补偿表或触发重试                    } else {                        var meta = result.getRecordMetadata();                        log.debug("sent partition={} offset={}",                                meta.partition(), meta.offset());                    }                });    }}同步发送（需要强确认时）：kafkaTemplate.send("order-events", key, event).get(3, TimeUnit.SECONDS);9.3 消费消息：@KafkaListener@Component@RequiredArgsConstructor@Slf4jpublic class OrderEventListener {    private final OrderService orderService;    @KafkaListener(        topics = "order-events",        groupId = "order-service",        concurrency = "3"    )    public void onMessage(ConsumerRecord&lt;String, OrderCreated&gt; record,                          Acknowledgment ack) {        try {            orderService.handle(record.value());            ack.acknowledge();   // 处理成功才提交        } catch (Exception e) {            log.error("handle failed, offset={}", record.offset(), e);            throw e;             // 交给错误处理器决定重试或进 DLT        }    }}9.4 错误处理与死信队列直接抛异常会触发容器重试，默认行为可能导致无限循环。生产推荐“有限重试 + 死信”：@Configurationpublic class KafkaErrorConfig {    @Bean    public DefaultErrorHandler errorHandler(KafkaTemplate&lt;Object, Object&gt; template) {        // 重试 3 次，间隔 1s，指数退避倍数 2        var recoverer = new DeadLetterPublishingRecoverer(                template,                (record, ex) -&gt; {                    // 死信目标：原 topic + ".DLT"，保持原分区                    return new TopicPartition(record.topic() + ".DLT", record.partition());                });        var backOff = new ExponentialBackOffWithMaxRetries(3);        backOff.setInitialInterval(1000L);        backOff.setMultiplier(2.0);        var handler = new DefaultErrorHandler(recoverer, backOff);        // 这些异常不重试，直接进死信        handler.addNotRetryableExceptions(                DeserializationException.class,                IllegalArgumentException.class);        return handler;    }}配套监听死信：@KafkaListener(topics = "order-events.DLT", groupId = "order-dlt-watcher")public void onDead(ConsumerRecord&lt;String, OrderCreated&gt; record) {    log.error("dead letter key={} offset={} value={}",            record.key(), record.offset(), record.value());    // 人工处理、告警、回补}死信设计建议：  死信消息带上原始 topic、partition、offset、异常堆栈头，方便追溯；  死信 topic 的保留期、副本策略要按业务重要性单独设置；  建死信看板与告警，否则问题只会在用户投诉时才暴露。9.5 批量消费下游是数据库或搜索引擎时，批量写入能带来数量级的性能提升：@KafkaListener(topics = "order-events", batch = "true")public void onBatch(List&lt;ConsumerRecord&lt;String, OrderCreated&gt;&gt; records,                    Acknowledgment ack) {    List&lt;OrderCreated&gt; events = records.stream()            .map(ConsumerRecord::value).toList();    orderService.batchHandle(events);    ack.acknowledge();}spring.kafka.consumer:  max-poll-records: 200  fetch-min-size: 1KB  fetch-max-wait: 500ms9.6 消费者多线程concurrency=3 相当于起 3 个 KafkaMessageListenerContainer，各自拥有独立 consumer，各自分到一部分分区。这是首选的并发模型，因为它保留了标准的位移与再平衡语义。只有当单条消息处理极重（比如调用多个慢接口）且分区数已到上限时，才考虑在监听器内部再套线程池。这时要自己解决：  位移提交粒度（一批任务全完成后再 ack）；  优雅停机（关闭线程池、等待任务完成）；  异常时的消息路由（重试/死信）。9.7 事务生产者需要“多条消息原子写入”或 consume-transform-produce 场景时：kafkaTemplate.executeInTransaction(ops -&gt; {    ops.send("order-events", key, orderCreated);    ops.send("audit-events", key, auditEvent);    return true;});配置：spring:  kafka:    producer:      transaction-id-prefix: order-tx-消费端记得 isolation.level: read_committed，否则会读到未提交数据（第 14 章展开）。9.8 一个完整的消费端模板@Component@Slf4jpublic class ReliableOrderListener {    private final OrderService orderService;    private final MeterRegistry metrics;    public ReliableOrderListener(OrderService orderService, MeterRegistry metrics) {        this.orderService = orderService;        this.metrics = metrics;    }    @KafkaListener(topics = "order-events", groupId = "order-service")    public void listen(ConsumerRecord&lt;String, OrderCreated&gt; record, Acknowledgment ack) {        long start = System.currentTimeMillis();        try {            orderService.handleIdempotent(record.value());            ack.acknowledge();            metrics.counter("kafka.consume.success").increment();        } catch (Exception e) {            metrics.counter("kafka.consume.failure").increment();            throw e;        } finally {            metrics.timer("kafka.consume.latency")                  .record(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);        }    }}这套结构包含：手动 ack、幂等处理、指标埋点、异常上抛交给统一错误处理。可以直接作为业务消费者模板复制。本章小结  concurrency 创建多个真实 consumer，是 Spring Kafka 并发消费的标准方式；  ack-mode: manual + 处理后确认，实现至少一次语义；  DefaultErrorHandler + DeadLetterPublishingRecoverer 构成“重试有限次、失败进 DLT”的闭环；  批量监听对数据库/搜索类下游收益巨大；  事务用 executeInTransaction，消费端配合 read_committed。思考题  concurrency=6 但 topic 只有 3 个分区，会发生什么？  监听器里 ack.acknowledge() 之后进程立即崩溃，这条消息会怎样？不 ack 又会怎样？  如何设计死信消息的元信息，才能让“重新投递”变得可操作？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Kafka 只负责搬运字节，不理解业务格式。格式选得好，系统演进就顺；选得差，下游会被各种兼容性问题反复折磨。本章讲主流方案与 Schema 治理。8.1 序列化的三个层面业务对象 -&gt; 编码格式（JSON/Avro/Protobuf） -&gt; 字节 -&gt; Kafka 传输评估一种格式，看四点：  体积：影响网络、磁盘、吞吐；  解析速度：影响生产者/消费者 CPU；  Schema 演进：加字段、改类型是否兼容；  生态与可读性：调试难度、团队熟悉度。8.2 JSON：简单但脆弱JSON 可读性好、语言无关，是原型阶段的首选。但直接裸用有几个坑：  字段类型弱："id": 1 和 "id": "1" 都合法，下游反序列化容易炸；  无 schema 约束：生产者加了字段，老消费者可能直接报错或静默忽略；  体积大、解析慢：字段名重复传输，海量数据下成本明显；  时间、金额等类型需要自行约定格式（统一 ISO-8601、分为单位等）。如果坚持用 JSON，建议：  定义明确的 DTO 类，禁止 Map&lt;String,Object&gt; 到处传；  统一时间格式与时区；  在消息头放版本号，如 schema-version: 2；  新旧消费者做兼容性测试。8.3 Avro：Kafka 生态的主力Avro 用 JSON 定义 schema，数据以紧凑二进制传输，且天然为 schema 演进设计。示例 OrderCreated.avsc：{  "type": "record",  "name": "OrderCreated",  "namespace": "com.example.order",  "fields": [    {"name": "orderId", "type": "string"},    {"name": "userId", "type": "string"},    {"name": "amount", "type": {"type": "bytes", "logicalType": "decimal", "precision": 12, "scale": 2}},    {"name": "createdAt", "type": {"type": "long", "logicalType": "timestamp-millis"}},    {"name": "channel", "type": ["null", "string"], "default": null}  ]}演进规则：            变更      是否兼容      说明                  新增字段且给 default      兼容      老数据读不出该字段时使用默认值              新增字段无 default      不兼容      老消费者解析新数据会失败              删除有 default 的字段      向后兼容      老消费者仍可读新数据              修改字段类型      通常不兼容      int-&gt;long 等少数放宽可兼容              重命名字段      不兼容      等价于删旧加新，需要别名迁移        口诀：加字段必给默认值，改类型先做双写过渡。8.4 Protobuf 简述Protobuf 生态更广（gRPC 原生），性能同样优秀，规则更严格：字段编号稳定后，增删字段相对安全。Kafka 生态对 Avro 的支持更“原生”（尤其 Confluent 生态），但 Protobuf + Schema Registry 也是完全可行的路线。选型一句话：团队已有 gRPC/Protobuf 基础就用 Protobuf，否则 Kafka 数据管道优先 Avro。8.5 Schema Registry 是什么Schema Registry 是独立部署的 schema 版本仓库，核心解决两个问题：  消息里只存 schema id，不必每条消息携带完整 schema，省带宽；  注册时校验兼容性，不兼容的 schema 直接拒绝，把问题挡在上游。工作流程Producer                              Consumer   │ 1.注册 schema                        │   │ 2.得到 schema id=42                  │   │ 3.消息 = [magic byte][id=42][payload] │   v                                     vSchema Registry  &lt;---- 4.按 id 拉取 schema ---- wire format 前 5 字节是 magic（0x0）+ 4 字节 schema id，之后才是 Avro/Protobuf 编码的业务数据。兼容模式            模式      含义      适用                  BACKWARD      新 schema 能读老数据      消费者先升级的场景              FORWARD      老 schema 能读新数据      生产者先升级的场景              FULL      双向兼容      最严格，推荐默认              NONE      不检查      仅测试环境      Maven 依赖与生产者示例&lt;dependency&gt;    &lt;groupId&gt;io.confluent&lt;/groupId&gt;    &lt;artifactId&gt;kafka-avro-serializer&lt;/artifactId&gt;    &lt;version&gt;7.6.0&lt;/version&gt;&lt;/dependency&gt;schema.registry.url=http://localhost:8081value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializerProducerRecord&lt;String, OrderCreated&gt; rec =        new ProducerRecord&lt;&gt;("order-events", orderId, orderCreated);producer.send(rec);消费者用 KafkaAvroDeserializer 即可还原成强类型对象。8.6 没有独立 schema 服务怎么办小团队可以先做“约定式治理”：  schema 文件进 Git，PR 评审变更；  CI 跑兼容性检查（Confluent 提供 schema-compatibility 工具）；  消息头带 version，消费者按版本分发处理；  破坏性变更发新 topic（...v2），旧 topic 保留到迁移完成。Schema Registry 的价值在于把这些流程自动化，规模上来后值得引入。本章小结  Kafka 传字节，格式与演进由业务负责；  JSON 原型友好但 schema 弱，海量与强演进场景选 Avro/Protobuf；  Avro 兼容性核心：新增字段给默认值，改类型要迁移；  Schema Registry 用“id 引用 + 兼容校验”把数据契约变成基础设施；  破坏性变更最稳妥的路径是“新版本 + 新 topic + 双写迁移”。思考题  为什么 Avro 消息里不用携带完整 schema？解析方如何知道结构？  生产者新增了一个无默认值字段，BACKWARD 模式下哪一方会出错？FULL 模式呢？  设计一个“订单金额从分改为厘”的字段类型变更方案，保证线上迁移不丢数据。</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。主题与分区是 Kafka 的“表结构”，一旦上线就很难随意修改。本章讲清楚分区数怎么定、副本因子怎么配、以及用 AdminClient 做自动化管理。7.1 分区数怎么规划分区数影响三个维度：  并行度：消费者实例上限 = 分区数；  吞吐：分区分布在多个 Broker 上，写入与读取并行；  资源与开销：每个分区都是一个目录、多组文件、多个副本、多个网络请求对象。分区不是越多越好。经验公式目标吞吐 / 单分区吞吐 = 所需分区数例：目标 100 MB/s，压测单分区 10 MB/s -&gt; 至少 10 个分区   考虑峰值与增长，规划为 20-30 个再检查消费端：如果下游每秒只能处理 1 万条、单消费者 2000 条/s，则至少需要 5 个分区（且对应 5 个消费者实例）。分区过多的代价  Broker 端文件句柄、索引内存、请求对象成倍增加；  Controller 故障转移时需要处理的分区状态变多，切换变慢；  生产端批次数增多，单批变小，压缩与吞吐效率下降；  未_leader 均衡或再分配时，迁移时间显著变长。建议：起步用 6/12/24 这类预留量，宁可中期扩一次，也不要一开始上千分区。7.2 副本因子与放置生产建议：  replication.factor = 3（多数场景）；  min.insync.replicas = 2；acks=all 的生产者配合，可容忍一台 Broker 故障且不丢已确认消息；  副本应分布在不同机架/可用区（broker.rack + replica.selector.class），防止机架级故障导致分区全丢。权衡：RF=2 时若一台宕机，ISR 剩 1，min.insync.replicas=2 会导致写入不可用（这是保护一致性的正确行为）；RF=3 则仍可写。7.3 命令行管理回顾与补充# 创建：显式指定分区与副本kafka-topics.sh --create --topic payments \  --partitions 12 --replication-factor 3 \  --config retention.ms=604800000 \  --config min.insync.replicas=2 \  --config cleanup.policy=delete# 查看某分区详细kafka-topics.sh --describe --topic payments --partitions 0# 修改 topic 级配置kafka-configs.sh --bootstrap-server localhost:9092 \  --alter --entity-type topics --entity-name payments \  --set-config retention.ms=259200000# 查看生效配置（含默认值）kafka-configs.sh --bootstrap-server localhost:9092 \  --describe --entity-type topics --entity-name payments --all7.4 用 AdminClient 管理主题import org.apache.kafka.clients.admin.*;import org.apache.kafka.common.config.ConfigResource;import java.util.*;import java.util.concurrent.ExecutionException;public class TopicAdmin {    public static void main(String[] args) throws Exception {        Properties props = new Properties();        props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");        try (AdminClient admin = AdminClient.create(props)) {            // 1. 创建主题            NewTopic newTopic = new NewTopic("payments", 12, (short) 3)                    .configs(Map.of(                            "retention.ms", "604800000",                            "min.insync.replicas", "2"                    ));            admin.createTopics(List.of(newTopic)).all().get();            // 2. 查看描述            var desc = admin.describeTopics(List.of("payments"))                             .allTopicNames().get().get("payments");            System.out.println("partitions=" + desc.partitions().size());            // 3. 增加分区            admin.createPartitions(Map.of("payments",                    NewPartitions.increaseTo(24))).all().get();            // 4. 修改配置            ConfigResource res = new ConfigResource(                    ConfigResource.Type.TOPIC, "payments");            admin.incrementalAlterConfigs(Map.of(res, List.of(                    new AlterConfigOp(                            new ConfigEntry("retention.ms", "259200000"),                            AlterConfigOp.OpType.SET)            ))).all().get();            // 5. 删除主题            // admin.deleteTopics(List.of("payments")).all().get();        }    }}AdminClient 是构建自服务管控台（申请 topic、查详情、改保留期）的基础。7.5 增加分区的副作用Kafka 只允许增加分区，不允许减少。增加时要注意：  新分区立即参与哈希计算，相同 key 的消息从此可能落进新分区，该 key 的新旧消息在全局上不再连续有序；  已有数据的分布不变，不会自动重分布（因此新分区是“空”的，短时间负载可能不均）；  消费组会触发再平衡，把新分区分配给成员。如果业务强依赖 key 顺序，两个选择：  一开始把分区规划到位，上线后不再扩；  升级时采用“新 topic + 双写/迁移”方案，消费端按新 key 顺序切换。7.6 保留策略与日志压缩            场景      推荐 cleanup.policy      说明                  业务事件、日志      delete      按 retention.ms/retention.bytes 滚动删除              状态快照、配置变更、CDC 最新值      compact      每个 key 保留最新一条              想先压缩再过期      compact,delete      同时生效      日志压缩要点：  key 为空的消息在 compact 主题中会被直接丢弃，必须带 key；  压缩是后台异步的，不是“写入即压缩”；  min.cleanable.dirty.ratio（默认 0.5）控制触发阈值：脏数据占比超过一半才清理；  适合“当前状态”而非“事件历史”。7.7 主题命名与治理推荐 &lt;域&gt;.&lt;数据集&gt;.&lt;事件类型&gt; 这类规范：order.domain.created.v1user.profile.changed.v1log.nginx.access.v2治理要点：  命名带版本号，schema 破坏性变更走新版本主题；  禁止 auto.create.topics.enable=true（生产环境），防止拼错 topic 名悄悄建出新主题；  用配额（quota）限制异常生产者，防止单个业务打挂集群；  建立 topic 清单与责任人登记，避免“僵尸主题”占用磁盘。本章小结  分区数由目标吞吐和消费并行度共同决定，预留余量但不盲目多建；  生产标准：RF=3、min.insync.replicas=2、跨机架放置；  增分区会改变 key 哈希落点，对顺序敏感的业务要提前规划或走新主题迁移；  compact 适合“每 key 最新状态”，delete 适合事件与日志；  AdminClient 可以把主题管理沉淀为自服务能力。思考题  一个 topic 从 6 分区扩到 12 分区后，为什么可能短时间出现新旧分区数据量严重不均？  为什么 Kafka 不支持减少分区？如果强行做，会遇到哪些一致性难题？  用户新增一个“查询用户当前等级”的需求，你会建 delete 还是 compact 主题？为什么？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。消费者是业务代码里最容易出问题的一环：丢消息、重复消费、堆积、频繁再平衡，几乎都源于对 poll 模型与位移提交理解不深。本章把消费者彻底讲透。6.1 消费者与消费组回顾消费者必须属于某个消费组（group.id）：  同一组内：一个分区只分配给一个消费者，实现负载均衡；  不同组之间：各自独立消费全量数据，实现广播。消费者数量与分区数的关系：分区 6 个：  消费者 3 个 -&gt; 每人 2 个分区（理想情况）  消费者 6 个 -&gt; 每人 1 个分区  消费者 8 个 -&gt; 6 个干活，2 个空闲想提高消费并行度，先加消费者实例；超过分区数后必须加分区（有代价，见第 7 章）。6.2 第一个消费者import org.apache.kafka.clients.consumer.*;import java.time.Duration;import java.util.Collections;import java.util.Properties;public class SimpleConsumer {    public static void main(String[] args) {        Properties props = new Properties();        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");        props.put(ConsumerConfig.GROUP_ID_CONFIG, "demo-group");        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,                  "org.apache.kafka.common.serialization.StringDeserializer");        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,                  "org.apache.kafka.common.serialization.StringDeserializer");        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");        try (KafkaConsumer&lt;String, String&gt; consumer = new KafkaConsumer&lt;&gt;(props)) {            consumer.subscribe(Collections.singletonList("order-events"));            while (true) {                ConsumerRecords&lt;String, String&gt; records =                        consumer.poll(Duration.ofMillis(500));                for (ConsumerRecord&lt;String, String&gt; r : records) {                    System.out.printf("partition=%d offset=%d key=%s value=%s%n",                            r.partition(), r.offset(), r.key(), r.value());                }            }        }    }}这个“单线程 poll 循环”是 Kafka 消费者的标准形态。所有框架（Spring Kafka、Flink 等）内部都是这个循环。6.3 poll 循环的心跳模型消费者靠两个后台线程维持“活着”的状态：  心跳线程：定期向协调器发 Heartbeat，证明进程还在。超过 session.timeout.ms 没心跳，被认为死亡，触发再平衡；  poll 线程（业务线程）：两次 poll() 的间隔如果超过 max.poll.interval.ms，即使心跳正常，也被认为“处理能力异常”，同样踢出组。session.timeout.ms  = 45000   进程级存活判定heartbeat.interval.ms = 3000  心跳频率，建议约 1/3 sessionmax.poll.interval.ms = 300000 业务处理耗时上限max.poll.records     = 500    单次 poll 最大条数处理慢导致被踢组的典型表现：日志里出现 MaxPollIntervalExceededException，随后再平衡，周而复始形成“再平衡风暴”。解法是调小 max.poll.records、调大 max.poll.interval.ms、优化业务逻辑或增加消费者。6.4 位移提交自动提交enable.auto.commit=true（默认）时，客户端每隔 auto.commit.interval.ms（默认 5 秒）在 poll 时自动提交当前位移。方便，但有两个问题：  先提交后处理的可能：自动提交发生在 poll 返回新数据时，若提交后还没处理完就宕机，重启后会跳过这些消息——丢消息；  处理完才提交的时序不可控：也可能处理完但还没到提交时间点就宕机，重启后重复消费。生产环境推荐关闭自动提交，改为手动。手动提交props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);while (true) {    var records = consumer.poll(Duration.ofMillis(500));    for (var r : records) {        process(r); // 业务处理    }    consumer.commitSync(); // 处理完成后提交}commitSync() 阻塞直到成功，失败会自动重试（无退休额限制内）；commitAsync() 异步且不重试，吞吐更高。常用的折中方案：try {    while (true) {        var records = consumer.poll(Duration.ofMillis(500));        process(records);        consumer.commitAsync();          // 平时异步，高吞吐    }} finally {    consumer.commitSync();               // 退出前同步兜底    consumer.close();}精确到分区的提交当一批 records 来自多个分区且处理时间差异大时，可以按分区粒度提交：for (var partition : records.partitions()) {    var partRecords = records.records(partition);    process(partRecords);    long lastOffset = partRecords.get(partRecords.size() - 1).offset();    consumer.commitSync(Map.of(partition, new OffsetAndMetadata(lastOffset + 1)));}提交的是“下一条要消费的 offset”，所以是 lastOffset + 1。至少一次的处理原则“先处理后提交”天然是至少一次：宕机时可能重复，但不会丢。重复交给业务幂等解决（数据库唯一键、Redis setnx、状态机判断等）。这是绝大多数系统的正确选择。6.5 再平衡监听器分区被收回或新分配时，可以在钩子里做清理与提交：consumer.subscribe(List.of("order-events"), new ConsumerRebalanceListener() {    @Override    public void onPartitionsRevoked(Collection&lt;TopicPartition&gt; partitions) {        // 分区即将被收回：同步提交位移、提交本地事务、释放资源        consumer.commitSync();    }    @Override    public void onPartitionsAssigned(Collection&lt;TopicPartition&gt; partitions) {        // 新分区到位：初始化缓存、拉取本地状态、打印日志    }});onPartitionsRevoked 里的同步提交非常重要：再平衡后新拥有者会从已提交位置开始，这里不提交就可能导致整批重复。6.6 优雅退出poll() 会阻塞，从外部线程唤醒它的标准方式是 wakeup()：final var main = Thread.currentThread();Runtime.getRuntime().addShutdownHook(new Thread(consumer::wakeup));try {    while (running) {        var records = consumer.poll(Duration.ofMillis(500));        process(records);        consumer.commitAsync();    }} catch (WakeupException e) {    // 收到退出信号，正常跳出循环} finally {    try {        consumer.commitSync();    } finally {        consumer.close();    }}K8s/容器环境里优雅退出尤其重要：直接 SIGKILL 会触发再平衡，增加消费中断时间。6.7 从任意位置开始消费auto.offset.reset没有已提交位移（新组或位移过期）时的策略：  earliest：从最早消息开始；  latest：只消费启动之后的新消息（默认）；  none：直接抛异常。seek 精确定位consumer.subscribe(List.of("order-events"));// 先 poll 一次加入组并获得分区consumer.poll(Duration.ofMillis(1000));for (var tp : consumer.assignment()) {    consumer.seek(tp, 100); // 从该分区 offset=100 开始}按时间定位Map&lt;TopicPartition, Long&gt; timestamps = new HashMap&lt;&gt;();for (var tp : consumer.assignment()) {    timestamps.put(tp, System.currentTimeMillis() - 3600_000L); // 1 小时前}var offsets = consumer.offsetsForTimes(timestamps);offsets.forEach((tp, om) -&gt; {    if (om != null) consumer.seek(tp, om.offset());});独立消费者不需要消费组语义、只想固定读某些分区时，用 assign 代替 subscribe：TopicPartition tp = new TopicPartition("order-events", 0);consumer.assign(List.of(tp));consumer.seekToBeginning(List.of(tp));注意：assign 的消费者不会参与再平衡，也没有组协调，但提交位移仍然需要 group.id。6.8 消费者核心参数            参数      默认值      说明                  group.id      无      消费组标识，同一组的消费者共同分摊分区              enable.auto.commit      true      自动提交；生产建议 false              auto.commit.interval.ms      5000      自动提交间隔              auto.offset.reset      latest      无位移时的起点策略              session.timeout.ms      45000      心跳超时，超时踢出组              heartbeat.interval.ms      3000      心跳频率              max.poll.interval.ms      300000      两次 poll 最大间隔              max.poll.records      500      单次 poll 最大记录数              fetch.min.bytes      1      Broker 返回的最小数据量，调大可降低请求频率              fetch.max.wait.ms      500      不够 fetch.min.bytes 时的最长等待              max.partition.fetch.bytes      1048576      每个分区单次 fetch 上限              isolation.level      read_uncommitted      事务场景用 read_committed（第 14 章）      6.9 提高消费吞吐的思路  增加消费者实例，直到等于分区数；  单实例内多线程处理，但位移管理与再平衡要自己做（提交粒度、线程池关闭）；  调大 max.poll.records 并批量处理（攒批写库比逐条快得多）；  消费逻辑尽量轻：重逻辑下沉到专门服务，消费者只做分发；  数据库/外部 IO 常是瓶颈，先优化下游再怪 Kafka。本章小结  消费者的标准形态是单线程 poll 循环，心跳与 poll 间隔共同决定存活判定；  手动提交 + 先处理后提交 = 至少一次，重复用业务幂等兜底；  再平衡监听器里做同步提交与资源清理，优雅退出用 wakeup()；  seek/offsetsForTimes 能从任意位置重放，这是 Kafka 作为“可回放日志”的核心能力；  堆积治理优先考虑下游性能、批处理与消费者扩容。思考题  自动提交在什么时序下会丢消息？什么时序下会重复消费？  为什么 commitSync 提交的是 lastOffset + 1 而不是 lastOffset？  消费者处理一条消息要 2 秒，max.poll.records=500，会发生什么？给出至少两种修复方案。</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章进入代码实战：用一个完整的 Java 项目理解生产者的发送流程、分区策略、拦截器、核心参数与顺序性保证。这些知识同样适用于 Python（kafka-python / confluent-kafka）、Go（franz-go / sarama）等客户端，因为参数语义是 Kafka 协议层统一的。5.1 Maven 依赖&lt;dependency&gt;    &lt;groupId&gt;org.apache.kafka&lt;/groupId&gt;    &lt;artifactId&gt;kafka-clients&lt;/artifactId&gt;    &lt;version&gt;3.9.0&lt;/version&gt;&lt;/dependency&gt;学习时选与集群相同或更高的 3.x/4.x 客户端版本即可。5.2 第一个生产者import org.apache.kafka.clients.producer.*;import java.util.Properties;public class SimpleProducer {    public static void main(String[] args) {        Properties props = new Properties();        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,                  "org.apache.kafka.common.serialization.StringSerializer");        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,                  "org.apache.kafka.common.serialization.StringSerializer");        try (KafkaProducer&lt;String, String&gt; producer = new KafkaProducer&lt;&gt;(props)) {            for (int i = 0; i &lt; 10; i++) {                ProducerRecord&lt;String, String&gt; record =                        new ProducerRecord&lt;&gt;("order-events", "user-" + i % 3, "value-" + i);                producer.send(record);            }            producer.flush();        }    }}try-with-resources 关闭时会隐式 flush()，确保缓冲区里的消息发出。注意：send() 是异步的，直接退出程序可能丢消息，这是新手最常踩的坑。5.3 发送流程全景                    main 线程                          sender IO 线程┌──────────┐   ┌──────────────┐   ┌────────────┐   ┌──────────────┐│ 拦截器    │ -&gt; │ 序列化 key/value │ -&gt; │ 分区器     │ -&gt; │ RecordAccumulator ││ (可选)    │   └──────────────┘   │ 选择分区    │   │ 按 (topic,分区)   │└──────────┘                      │            │   │ 组织批次 batch     │                                  └────────────┘   └───────┬──────┘                                                           │ 满足条件                                                           v                                                    ┌──────────────┐                                                    │ Sender 线程   │                                                    │ 按 Broker 分组 │                                                    │ 发送 Produce  │                                                    └──────────────┘几个要点：  消息先经过拦截器 onSend，再序列化，再选分区； 顺序是固定的，自定义拦截器不要在这里做重逻辑；  同一个 (topic, partition) 的消息在 RecordAccumulator 中攒成批次； 批次满了（batch.size）或等待超时（linger.ms）就发出；  Sender 线程按 Broker 维度合并请求，一个请求可以携带多个分区的批次；  返回结果通过回调交给业务线程。5.4 异步回调与异常处理生产环境必须写回调，否则发送失败你根本不知道：producer.send(record, (metadata, exception) -&gt; {    if (exception == null) {        System.out.printf("partition=%d offset=%d%n",                metadata.partition(), metadata.offset());    } else {        // 记录日志、落补偿表、触发告警，绝不能静默吞掉        exception.printStackTrace();    }});同步发送（性能差，仅特定场景）：try {    RecordMetadata md = producer.send(record).get();} catch (Exception e) {    // 处理执行异常}异常分两类：  可重试：RetriableException 的子类，如 TimeoutException、NotEnoughReplicasException。客户端按 retries/delivery.timeout.ms 自动重试；  不可重试：如 RecordTooLargeException（单条超过 max.request.size）、序列化失败、AuthorizationException。重试无意义，必须修代码或配置。5.5 分区策略发送一条消息时，目标分区的决策顺序：  ProducerRecord 指定了 partition，直接用；  否则若 key 不为空，对 key 做 murmur2 哈希 后对分区数取模：partition = Utils.toPositive(murmur2(keyBytes)) % numPartitions；  key 为空时使用粘性分区（sticky partitioning）：先随机选一个分区尽量填满批次，满了再换下一个，让无 key 消息也能均匀分布且保持较高批效率。自定义分区器示例——按用户等级路由：public class UserLevelPartitioner implements Partitioner {    @Override    public int partition(String topic, Object key, byte[] keyBytes,                         Object value, byte[] valueBytes, Cluster cluster) {        int numPartitions = cluster.partitionsForTopic(topic).size();        String k = key.toString();        if (k.startsWith("vip-")) return 0;          // VIP 固定走 0 号分区        return Math.abs(k.hashCode()) % (numPartitions - 1) + 1; // 其他走剩余分区    }    @Override    public void close() {}    @Override    public void configure(Map&lt;String, ?&gt; configs) {}}注册：props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, UserLevelPartitioner.class.getName());注意：把 VIP 单独塞进一个分区会破坏负载均衡，这里只是演示语法。实际业务更常见的做法是“按实体 ID 做 key，均匀哈希”。5.6 拦截器public class TimingInterceptor implements ProducerInterceptor&lt;String, String&gt; {    @Override    public ProducerRecord&lt;String, String&gt; onSend(ProducerRecord&lt;String, String&gt; record) {        record.headers().add("sent-at", String.valueOf(System.currentTimeMillis()).getBytes());        return record;    }    @Override    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {        // 统计成功率、耗时，注意这里在 IO 线程执行，必须快    }    @Override public void configure(Map&lt;String, ?&gt; configs) {}    @Override public void close() {}}典型用途：打链路追踪头、统一加时间戳、统计指标。不适合做业务校验或慢 IO。5.7 核心参数详解            参数      默认值      说明                  bootstrap.servers      无      初始连接地址，写 2-3 台即可，客户端会自动发现全量 Broker              acks      all      0 不等确认；1 Leader 写入即成功；all 等 ISR 确认，最可靠              retries      Integer.MAX_VALUE      重试次数（配合 delivery.timeout.ms 兜底）              delivery.timeout.ms      120000      消息从发送到成功/失败的最终期限，含重试与发送时间              linger.ms      0      攒批等待时间。调大到 5-20ms 常能显著提升吞吐              batch.size      16384      单批字节数。过小则请求碎片化，过大占内存              buffer.memory      33554432      生产者总缓冲。满时 send() 会阻塞（最多 max.block.ms）              compression.type      none      lz4/zstd 兼顾速度与压缩比，gzip CPU 换带宽              max.request.size      1048576      单请求上限，单条大消息需同时调大 Broker 端 message.max.bytes              enable.idempotence      true      幂等生产者，防止重试导致的 broker 端重复              max.in.flight.requests.per.connection      5      单连接未确认请求数；幂等开启且 &lt;=5 时重试仍保持分区内有序      acks 详解  acks=0：发出去就算成功。吞吐最高，Broker 宕机、网络丢包都会直接丢消息；  acks=1：Leader 本地写入成功即返回。Leader 刚确认就宕机且未同步到 Follower 时会丢；  acks=all：等待 min.insync.replicas 个 ISR 副本确认。配合 replication.factor&gt;=3、min.insync.replicas&gt;=2 是可靠性的黄金组合。注意：acks=all 在 ISR 只剩 Leader 自己时退化为 acks=1。min.insync.replicas=2 才能真正保证至少两个副本落盘。5.8 顺序性保证Kafka 只保证分区内有序。要保证业务顺序：  需要顺序的实体使用相同 key（如同一订单号）；  生产端开启幂等（默认已开），且 max.in.flight.requests.per.connection &lt;= 5；  不要在发送失败后自行乱序重试（例如把失败消息丢进另一个线程重新 send，而后续消息已发出）；  增加分区会改变 key 的哈希落点，历史顺序与新增分区间可能断开（第 7 章）。5.9 一个生产级配置模板Properties props = new Properties();props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "broker1:9092,broker2:9092");props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());props.put(ProducerConfig.ACKS_CONFIG, "all");props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120000);props.put(ProducerConfig.LINGER_MS_CONFIG, 10);props.put(ProducerConfig.BATCH_SIZE_CONFIG, 32 * 1024);props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "zstd");这套配置适合大多数“可靠性优先、吞吐要求高”的业务场景。日志类场景可以更激进地调大 linger.ms 和 batch.size。本章小结  send() 异步进入累加器，由 Sender 线程攒批发送；退出前必须 flush()/close()；  分区决策顺序：显式指定 &gt; key 哈希 &gt; 粘性分区；  生产环境必须实现回调，区分可重试与不可重试异常；  acks=all + min.insync.replicas&gt;=2 是不丢消息的底线组合；  顺序性的三要素：同 key 同分区、幂等开启、in-flight &lt;= 5。思考题  为什么 send() 返回成功不代表消息“安全”了？它与 acks 是什么关系？  linger.ms=0 和 linger.ms=20 在延迟、吞吐、CPU 上分别会带来什么变化？  如果业务要求“所有订单消息严格有序”，分区数必须是多少？这样做有什么代价？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。环境就绪后，本章用 Kafka 自带命令行完成“建 topic、发消息、收消息、看进度”，并把每个命令的输出读懂。命令行是最好的 Kafka “体检工具”，生产排查也离不开它们。约定：本章统一使用 --bootstrap-server localhost:9092（伪集群请换成 19092）。4.1 主题管理创建一个三分区、双副本的主题：bin/kafka-topics.sh --bootstrap-server localhost:9092 \  --create \  --topic order-events \  --partitions 3 \  --replication-factor 2查看主题列表与详情：bin/kafka-topics.sh --bootstrap-server localhost:9092 --listbin/kafka-topics.sh --bootstrap-server localhost:9092 \  --describe --topic order-eventsdescribe 输出解读：Topic: order-events  TopicId: xxxx  PartitionCount: 3  ReplicationFactor: 2    Topic: order-events  Partition: 0  Leader: 1  Replicas: 1,2  Isr: 1,2    Topic: order-events  Partition: 1  Leader: 2  Replicas: 2,3  Isr: 2,3    Topic: order-events  Partition: 2  Leader: 3  Replicas: 3,1  Isr: 3,1  Leader：当前处理读写的副本所在 Broker；  Replicas：副本分配方案（含不在同步状态的）；  Isr：同步副本集合。Isr 数量小于 Replicas 数量就是告警信号（第 19 章）。增加分区（只能增，不能减）：bin/kafka-topics.sh --bootstrap-server localhost:9092 \  --alter --topic order-events --partitions 6删除主题：bin/kafka-topics.sh --bootstrap-server localhost:9092 \  --delete --topic order-events若删除卡住，通常是 delete.topic.enable=false 或主题正被使用。4.2 控制台生产者最简单的生产者：bin/kafka-console-producer.sh \  --bootstrap-server localhost:9092 \  --topic order-events逐行输入内容，回车即发送一条消息。Ctrl+C 退出。带 key 的消息（key 与 value 用分隔符分开）：bin/kafka-console-producer.sh \  --bootstrap-server localhost:9092 \  --topic order-events \  --property parse.key=true \  --property key.separator=:输入示例：user-1001:{"type":"login"}user-1002:{"type":"order"}user-1001:{"type":"logout"}相同 key 会进同一分区，这是后面验证顺序性的基础。4.3 控制台消费者从头消费所有历史消息：bin/kafka-console-consumer.sh \  --bootstrap-server localhost:9092 \  --topic order-events \  --from-beginning显示 key、分区与 offset：bin/kafka-console-consumer.sh \  --bootstrap-server localhost:9092 \  --topic order-events \  --from-beginning \  --property print.key=true \  --property print.partition=true \  --property print.offset=true输出类似：Partition:0  Offset:5  key:user-1001  value:{"type":"login"}Partition:2  Offset:9  key:user-1002  value:{"type":"order"}指定消费组（生产推荐始终带 group）：bin/kafka-console-consumer.sh \  --bootstrap-server localhost:9092 \  --topic order-events \  --group demo-group再次执行同一命令时不会重复消费，因为位移已提交到 __consumer_offsets。4.4 消费组与位移管理列出所有消费组：bin/kafka-consumer-groups.sh \  --bootstrap-server localhost:9092 --list查看组详情与堆积：bin/kafka-consumer-groups.sh \  --bootstrap-server localhost:9092 \  --describe --group demo-group关键输出：GROUP   TOPIC          PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAGdemo    order-events   0          120             125             5demo    order-events   1          88              88              0demo    order-events   2          61              63              2  CURRENT-OFFSET：组已提交位移；  LOG-END-OFFSET：分区最新位移（可近似理解为“写入进度”）；  LAG：两者之差，即堆积量。这是日常监控最重要的数字。重置位移（必须先停止该组所有消费者）：bin/kafka-consumer-groups.sh \  --bootstrap-server localhost:9092 \  --group demo-group \  --topic order-events \  --reset-offsets --to-earliest --execute常用目标还有 --to-latest、--to-offset 100、--shift-by -10、--to-datetime 2026-08-25T00:00:00.000。查看某主题最新位移：bin/kafka-get-offsets.sh \  --bootstrap-server localhost:9092 --topic order-events4.5 动手实验：亲眼看见分区与副本按下面步骤做一遍，比读十遍文档更有效。  创建 demo 主题：6 分区、3 副本（伪集群）；  用带 key 的控制台生产者发送 20 条 user-1 到 user-5 的消息；  用 print.partition=true 的消费者观察：相同 key 是否总落在同一分区？  describe 该主题，记下每个分区的 Leader；  kill 掉某个 Leader 所在 Broker，再 describe：Leader 变了吗？ISR 少了谁？  重新启动被 kill 的 Broker，观察 ISR 恢复过程；  在数据目录中找到 demo-0、demo-1 等目录，看看 .log、.index、.timeindex 文件长什么样。这个实验覆盖了分区路由、副本、Leader 选举、ISR 与存储布局，是第 10、11 章的预习。4.6 查看日志文件内部kafka-dump-log.sh 能把二进制日志转成可读格式：bin/kafka-dump-log.sh \  --files /tmp/kraft-1/demo-0/00000000000000000000.log \  --print-data-log你会看到每个 batch 的 offset 范围、压缩算法、时间戳，以及（带 --print-data-log 时）消息内容。这个命令在生产排查“消息到底写没写进去”时非常常用。查看某个消费组在内部主题中的记录：bin/kafka-console-consumer.sh \  --bootstrap-server localhost:9092 \  --topic __consumer_offsets \  --from-beginning \  --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" \  --isolation-level read_committed能直观看到“提交位移”到底存了什么。4.7 命令速查表            目的      命令要点                  建主题      kafka-topics.sh --create --topic X --partitions N --replication-factor R              查主题      kafka-topics.sh --describe --topic X              加分区      kafka-topics.sh --alter --topic X --partitions N              发消息      kafka-console-producer.sh --topic X              收消息      kafka-console-consumer.sh --topic X --from-beginning              看组      kafka-consumer-groups.sh --describe --group G              重置位移      kafka-consumer-groups.sh --reset-offsets --to-earliest --execute              看最新位移      kafka-get-offsets.sh --topic X              转储日志      kafka-dump-log.sh --files &lt;log文件&gt;              选举 Leader      kafka-leader-election.sh --election-type preferred --all-topic-partitions      本章小结  主题创建后 describe 是最直观的健康检查：看 Leader、Replicas、ISR；  控制台生产者/消费者支持 key、分区、offset 打印，适合做实验和临时排查；  消费进度看 CURRENT-OFFSET / LOG-END-OFFSET / LAG 三件套；  位移重置前必须停组，--execute 才真正生效；  kafka-dump-log.sh 可以直接透视磁盘上的消息日志。思考题  为什么增加分区后，相同 key 的消息可能落到新分区，从而导致该 key 的历史顺序被“切断”？  --from-beginning 和 --group 同时使用时，行为是什么？为什么？  如果 LAG 一直是 0 但业务说“没收到消息”，你会按什么顺序排查？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。学 Kafka 最忌讳只看不练。本章从下载安装包开始，把单机 KRaft 集群跑起来，再扩展成一台机器上的三节点“伪分布式”集群，为后续所有实验打底。3.1 软件准备            软件      版本要求      说明                  JDK      17+      Kafka 4.x 要求 Java 17 及以上，推荐 17 或 21 LTS              Kafka      4.x      官网下载二进制包，选 Scala 2.13 构建              操作系统      Linux/macOS/Windows      学习推荐 WSL2 或 Linux；Windows 原生用 .bat 脚本              内存      4GB+ 空闲      三节点伪集群建议 8GB      验证 JDK：java -version下载并解压（以 4.0.0 为例，请按需替换版本号）：tar -xzf kafka_2.13-4.0.0.tgzcd kafka_2.13-4.0.0解压后的目录结构：bin/        可执行脚本（.sh）config/     配置文件libs/       依赖 jarlogs/       运行日志（server.log 等）site-docs/  文档3.2 Kafka 4.x 只有一种模式：KRaftKafka 3.x 之前依赖 ZooKeeper 管理元数据（Broker 注册、Controller 选举、topic 配置等）。这带来两个问题：  运维两套分布式系统，故障域更复杂；  元数据变更要先写 ZooKeeper 再通知 Broker，规模大时收敛慢。KRaft（Kafka Raft）把元数据放进 Kafka 自己的 Raft 日志（__cluster_metadata）里，由 Controller 仲裁，Broker 作为学习者同步元数据。Kafka 4.0 起彻底移除 ZooKeeper。所以本书从第一天就用 KRaft，这也是新集群的正确起点。3.3 单节点 KRaft 启动三步走：生成集群 ID -&gt; 格式化存储目录 -&gt; 启动服务。# 1. 生成集群 IDKAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"# 2. 用默认配置格式化存储目录bin/kafka-storage.sh format \  --standalone \  --config config/server.properties \  --cluster-id $KAFKA_CLUSTER_ID# 3. 启动bin/kafka-server-start.sh config/server.properties--standalone 会自动生成单节点控制器仲裁配置，非常适合本地学习。验证进程是否就绪：bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 | head -n 5能看到 Broker 返回 API 版本列表，说明启动成功。Windows 用户对应命令：bin\windows\kafka-storage.bat random-uuidbin\windows\kafka-storage.bat format --standalone --config config\server.properties --cluster-id &lt;上一步的ID&gt;bin\windows\kafka-server-start.bat config\server.properties3.4 server.properties 关键配置解读打开 config/server.properties，重点理解以下几行：# 节点 ID，集群内唯一node.id=1# 控制器仲裁者列表（多节点集群必配）# controller.quorum.voters=1@localhost:9093,2@localhost:9094,3@localhost:9095# 监听地址：名称://主机:端口listeners=PLAINTEXT://localhost:9092,CONTROLLER://localhost:9093# 对外公布的地址，客户端用它连接advertised.listeners=PLAINTEXT://localhost:9092# 哪些监听器用于控制器通信controller.listener.names=CONTROLLER# 监听器安全协议映射listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_SSL:SASL_SSL,SASL_PLAINTEXT:SASL_PLAINTEXT# 数据目录，多个目录用逗号分隔（可做多磁盘条带化）log.dirs=/tmp/kafka-logs# 自动创建 topic：学习期开，生产环境建议关auto.create.topics.enable=true三个易错点：  listeners 是进程实际绑定的地址；advertised.listeners 是返回给客户端的地址。生产环境必须填 Broker 可被客户端访问的主机名/IP，写成 localhost 会导致远程客户端连不上；  修改 log.dirs 后必须重新执行 format（或清空目录），否则元数据不匹配会启动失败；  端口冲突是新手最常见的启动报错，多个节点共机时端口必须全部错开。3.5 三节点伪分布式集群在同一台机器上模拟生产集群，理解节点交互非常有效。规划如下：            节点      node.id      broker 端口      controller 端口      数据目录                  broker-1      1      19092      19093      /tmp/kraft-1              broker-2      2      29092      29093      /tmp/kraft-2              broker-3      3      39092      39093      /tmp/kraft-3      创建三个配置文件 config/server-1.properties、server-2.properties、server-3.properties，内容以节点 1 为例：process.roles=broker,controllernode.id=1controller.quorum.voters=1@localhost:19093,2@localhost:29093,3@localhost:39093listeners=PLAINTEXT://:19092,CONTROLLER://:19093advertised.listeners=PLAINTEXT://localhost:19092controller.listener.names=CONTROLLERlistener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXTlog.dirs=/tmp/kraft-1num.partitions=3default.replication.factor=3min.insync.replicas=2offsets.topic.replication.factor=3transaction.state.log.replication.factor=3transaction.state.log.min.isr=2节点 2、3 只需替换 node.id、两个端口和 log.dirs。格式化并依次启动：KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"for i in 1 2 3; do  bin/kafka-storage.sh format \    --config config/server-$i.properties \    --cluster-id $KAFKA_CLUSTER_ID \    --ignore-formatteddonebin/kafka-server-start.sh -daemon config/server-1.propertiesbin/kafka-server-start.sh -daemon config/server-2.propertiesbin/kafka-server-start.sh -daemon config/server-3.properties验证三台都活着：bin/kafka-metadata-quorum.sh \  --bootstrap-server localhost:19092 describe --statusbin/kafka-broker-api-versions.sh --bootstrap-server localhost:19092 \  | grep -E '^localhost' describe --status 能看到 LeaderId 和三个 voter，说明 Raft 仲裁工作正常。3.6 用 Docker Compose 一键起环境如果不想手动管理目录和端口，可以用官方镜像快速起单节点：services:  kafka:    image: apache/kafka:4.0.0    container_name: kafka    ports:      - "9092:9092"    environment:      KAFKA_NODE_ID: 1      KAFKA_PROCESS_ROLES: broker,controller      KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1      KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"启动：docker compose up -d。进入容器执行命令：docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092。3.7 停止与日志优雅停止：bin/kafka-server-stop.sh排查启动问题时优先看：  logs/server.log：主日志，包含启动失败原因；  logs/controller.log：KRaft 控制器日志；  logs/kafkaServer.out：JVM 层输出，OOM、端口占用常在这里。3.8 常见启动故障速查            症状      常见原因      处理                  启动秒退，日志提示 InconsistentClusterIdException      目录已被其他集群格式化      清空 log.dirs 后重新 format              Address already in use      端口冲突      换端口或杀掉占用进程              客户端连不上，Broker 日志正常      advertised.listeners 返回了错误地址      改成客户端可达的主机名/IP              集群hang在选举      controller.quorum.voters 配错      检查每个节点 ID@host:port              JMX 端口冲突      多节点共机未设 JMX_PORT      每个节点指定不同 JMX_PORT      本章小结  Kafka 4.x 只有 KRaft 模式，元数据存在 __cluster_metadata Raft 日志中；  单机启动三步：random-uuid -&gt; format -&gt; server-start；  listeners 是绑定地址，advertised.listeners 是客户端连接地址，两者必须区分；  伪分布式集群能真实复现副本同步、Leader 选举与容错切换，是后续原理实验的基础；  遇到问题先看 server.log 与 controller.log。思考题  为什么 Kafka 一定要引入 advertised.listeners，只用 listeners 不行吗？  伪集群中 kill 掉某个 Broker，哪些 topic 分区会不可用？什么情况下仍然可用？  把 log.dirs 配成多个目录有什么好处？Kafka 如何在多个目录间分配分区？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章一次性建立 Kafka 的完整概念地图。后面所有章节都会反复引用这些名词，值得花时间彻底吃透。2.1 消息（Record）Kafka 中数据的基本单位叫消息，也叫记录（record/event）。一条消息由以下部分组成：┌──────────────────────────────────────┐│ Key        可选，用于分区与业务标识    ││ Value      消息体，业务数据本身        ││ Timestamp  时间戳（创建时间或追加时间） ││ Headers    可选键值对元数据            │└──────────────────────────────────────┘  Key 常用来标识“同一实体”，例如订单号、用户 ID。相同 key 的消息会进同一分区，从而保证该实体的消息在分区内有序；  Value 才是真正承载业务的字节串，格式完全由业务决定（JSON、Avro、Protobuf 都行，第 8 章细讲）；  Timestamp 有两种语义：CreateTime（生产者创建时间）和 LogAppendTime（Broker 追加时间），由 topic 级参数 message.timestamp.type 决定；  Headers 适合放链路追踪 ID、租户标识、消息版本等元信息，不参与分区计算。2.2 主题（Topic）与分区（Partition）Topic 是逻辑上的消息类别，比如 order-events、app-logs。生产者向 topic 发消息，消费者订阅 topic。每个 topic 被切分成若干个 partition：topic: order-events (3 个分区)partition-0: [m0] [m1] [m2] [m3] ... -&gt;partition-1: [m0] [m1] [m2] ... -&gt;partition-2: [m0] [m1] [m2] [m3] [m4] ... -&gt;分区是 Kafka 并行与扩展的基本单位：  每个分区是一个只追加的提交日志（append-only log），消息写入后不可修改；  分区可以分布在不同 Broker 上，写入与读取并行度随集群规模增长；  分区内有序，跨分区不保证全局有序——这是 Kafka 最重要的语义之一；  分区数一旦确定，只能增加，不能减少。什么消息会进哪个分区？由生产者的分区策略决定（第 5 章）：指定分区 &gt; 按 key 哈希 &gt; 粘性轮询。2.3 位移（Offset）与消费位置每个分区内的每条消息都有一个单调递增的编号，叫 offset。它由 Broker 在写入时分配，唯一标识“这条消息在该分区中的位置”。partition-0: [0] [1] [2] [3] [4] [5] ...                          ^              消费者当前位置：offset = 3              下一条要消费的是 offset = 3关于 offset 有三组容易混淆的概念：            概念      含义                  消息位移      消息在分区日志中的位置，Broker 分配，不可变              消费位移（committed offset）      消费组在某分区上“已经提交”的位置，存在 __consumer_offsets 里              当前位置（position）      消费者内存中下一次 poll 要读取的位置      “提交位移 100”的含义是：已经处理完 offset &lt; 100 的所有消息，下次从 100 开始消费。2.4 Broker 与集群一台 Kafka 服务器进程就是一个 Broker。多个 Broker 组成集群：  每个 Broker 保存若干分区副本，负责处理读写请求；  每个分区有多个副本，其中一个叫 Leader，其余是 Follower；  所有读写都由 Leader 处理，Follower 只是异步拉取同步（第 11 章）；  集群中有若干 Controller（KRaft 模式下是一个 Raft 组），负责元数据管理与 Leader 选举（第 12 章）。Broker 本身很“笨”：它不理解业务，只管按 topic/partition 存取字节流。这让它可以做到极高的通用性与吞吐。2.5 生产者与消费者Producer 向 topic 发布消息；Consumer 从 topic 读取消息。Kafka 是拉模式：消费者主动调用 poll() 拉取数据，按自己的节奏消费。好处是天然背压——消费者处理不过来时，只需要降低拉取频率，不会被打垮。消费组（Consumer Group）消费者必须属于某个消费组（由 group.id 标识）。消费组的规则：  同一个组内，一个分区最多分配给一个消费者（组内竞争，水平扩展）；  不同组之间，互相独立，各自都能拿到全量消息（组间广播）。topic 有 3 个分区，组 A 有 2 个消费者：  消费者1 &lt;- partition-0, partition-1  消费者2 &lt;- partition-2组 B 有 5 个消费者：  消费者1 &lt;- partition-0  消费者2 &lt;- partition-1  消费者3 &lt;- partition-2  消费者4 &lt;- 空闲  消费者5 &lt;- 空闲注意最后一行：消费者数量超过分区数时，多出来的消费者空闲。想让消费更快，要么加分区，要么在下游再做并行（第 6 章、第 17 章）。2.6 副本（Replica）与 ISR为了容错，每个分区可以有多个副本，分布在不同 Broker 上：  Leader Replica：处理所有读写请求；  Follower Replica：向 Leader 拉取数据保持同步，不对外服务；  ISR（In-Sync Replicas）：与 Leader 保持同步的副本集合（含 Leader）。当 Leader 所在 Broker 宕机，Controller 会从 ISR 中选一个 Follower 成为新 Leader，保证已提交的数据不丢失。细节（LEO、HW、leader epoch）见第 11 章。2.7 一张全景架构图                         ┌──────────────┐                         │   Producers  │                         └──────┬───────┘                                │ produce                                v   ┌──────────────── Kafka Cluster ────────────────┐   │  Broker1        Broker2        Broker3        │   │ ┌─────────┐   ┌─────────┐   ┌─────────┐      │   │ │ P0(L)   │   │ P0(F)   │   │ P0(F)   │      │   │ │ P1(F)   │   │ P1(L)   │   │ P1(F)   │      │   │ │ P2(F)   │   │ P2(F)   │   │ P2(L)   │      │   │ └─────────┘   └─────────┘   └─────────┘      │   │      Controller / KRaft Quorum               │   └───────────────┬───────────────────────────────┘                   │ fetch                   v        ┌─────────────────────┐        │   Consumer Groups   │        │  group-a   group-b  │        └─────────────────────┘一次完整的数据流：  Producer 按 key 计算目标分区，把批次发给分区 Leader；  Leader 追加到本地日志，Follower 拉取同步，满足 acks=all 与 min.insync.replicas 后返回成功；  Consumer Group 的成员向协调器加入组，分区被分配给组内消费者；  消费者向分区 Leader 发 fetch 请求，按 offset 拉取数据；  处理完成后提交位移到内部主题 __consumer_offsets；  数据按保留策略保留，与是否被消费无关。2.8 消息保留与日志压缩Kafka 不会在消息被消费后删除它，删除只由保留策略驱动：  retention.ms：按时间保留，默认 7 天；  retention.bytes：按大小保留，默认 -1（不限）；  cleanup.policy=compact：日志压缩，每个 key 只保留最新一条，适合状态类数据（配置表、用户资料、CDC 最新快照）；  两者可以组合为 compact,delete。“消费不删除”带来了两个重要能力：新消费者可以从头回放历史；同一份数据可被多个独立系统重复消费。2.9 内部主题Kafka 用普通 topic 机制实现自己的关键功能，值得认识三个内部主题：            主题      作用                  __consumer_offsets      50 个分区（默认），存储消费组提交的位移              __transaction_state      存储事务状态，事务协调器使用              __cluster_metadata      KRaft 模式下的元数据日志（Raft 日志）      看到这些主题不要惊讶，更不要删除它们。它们本身也是观察 Kafka 内部行为的窗口（第 12 章）。2.10 核心术语速查表            术语      英文      一句话解释                  消息      record / message / event      数据基本单位              主题      topic      逻辑消息类别              分区      partition      并行与顺序的单位，只追加日志              位移      offset      消息在分区内的编号              副本      replica      分区的拷贝，分布在不同 Broker              Leader      leader replica      处理读写请求的副本              ISR      in-sync replicas      与 Leader 同步的副本集合              Broker      broker      一台 Kafka 服务器进程              Controller      controller      管理元数据与 Leader 选举的“大脑”              生产者      producer      消息发布方              消费者      consumer      消息读取方              消费组      consumer group      组内竞争分区，组间独立广播              再平衡      rebalance      消费组内分区重新分配              位移提交      offset commit      记录消费进度的动作              消息堆积      consumer lag      消费进度落后于最新位移      本章小结  Topic 是逻辑分类，Partition 是物理并行单位，分区内有序、跨分区无序；  Offset 是分区内的位置编号，消费位移与消息位移是两回事；  读写都走分区 Leader，Follower 只做同步；Controller 负责元数据与选举；  消费组内一个分区只给一个消费者，组间互不影响；  数据按保留策略删除，消费本身不删除，因此可回放、可多订阅。思考题  要保证“同一个用户的所有行为按顺序处理”，应该怎么设计 key 和分区？  一个 topic 6 个分区、消费组 4 个消费者，分配结果可能是怎样的？如果消费者加到 8 个呢？  为什么 Kafka 的删除策略和消费进度解耦？这带来了哪些好处和代价？</li>
  <li>这是《Kafka 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。1.1 从一个真实痛点说起假设你在一个电商公司负责订单系统。用户下单后，系统要做很多事：扣库存、生成物流单、发积分、发短信、写风控日志、同步数据仓库。最直接的做法是订单服务在事务里依次调用这些下游接口：用户下单 -&gt; 订单库 -&gt; 扣库存 -&gt; 物流 -&gt; 积分 -&gt; 短信 -&gt; 数仓很快你会遇到一堆麻烦：  任何一个下游宕机，下单就失败，系统被最慢的依赖拖垮；  新增一个下游（比如优惠券核销），就要改订单系统代码，重新发版；  流量高峰一来，数据库和下游接口直接被打挂；  想回放“昨天 10 点到 11 点的订单事件”做故障复盘，做不到。这些问题的本质是：同步调用把“产生事件”和“处理事件”绑死了。Kafka 解决的正是这件事——订单系统只负责把“订单已创建”这个事实写下来，下游各自订阅、各自消费、各自重试，互不拖累。这个“写字的地方”，就是 Kafka。1.2 Kafka 是什么Apache Kafka 是一个分布式事件流平台（distributed event streaming platform）。这句话拆开看有三层含义：  发布/订阅消息流：像消息队列一样，生产者写消息，消费者读消息，实现系统解耦与异步通信；  存储消息流：消息以分布式、多副本、可容错的提交日志形式持久化在磁盘上，可以按需保留几分钟或 forever；  处理消息流：通过 Kafka Streams、ksqlDB 等组件，实时对流式数据做转换、聚合、关联。注意第三点：Kafka 不只是“队列”，它是以日志为核心的流式基础设施。这个定位贯穿全书。1.3 Kafka 的诞生与演化Kafka 诞生于 LinkedIn，2010 年左右开发，2011 年开源，2012 年成为 Apache 顶级项目。名字来源于捷克作家弗朗茨·卡夫卡（Franz Kafka），据说是因为开发者喜欢这位作家，而且这个名字读起来朗朗上口。LinkedIn 当时的痛点很典型：需要一套能处理海量活动数据（页面浏览、搜索、用户行为）的管道，当时的消息系统要么吞吐不够，要么不支持长时间保留数据。Kafka 的设计目标因此非常明确：高吞吐、可水平扩展、持久化、多订阅者。版本演进中的关键节点：            版本      时间      关键特性                  0.7      2011      初始开源版本              0.8      2013      引入副本机制，具备容错能力              0.9      2015      安全机制、Kafka Connect              0.10      2016      Kafka Streams，流处理框架              0.11      2017      幂等生产者、事务、消息头、V2 消息格式              1.x/2.x      2017-2021      性能与稳定性持续增强，2.4 引入协作式再平衡              2.8      2021      KRaft 早期访问，开始替代 ZooKeeper              3.3      2022      KRaft 生产可用              3.5      2023      ZooKeeper 模式标记为废弃              4.0      2025      彻底移除 ZooKeeper，KRaft 成为唯一模式      本书以 KRaft 模式为主线，第 16 章会专门讲这套新架构。老公司里仍然存在 ZooKeeper 模式集群，所以书中涉及差异时会单独提示。1.4 消息中间件要解决的四件事1. 解耦生产者不需要知道谁会消费消息。订单系统只管发布 order-created 事件，下游想加多少订阅者都行，订单系统代码零改动。2. 异步耗时的操作从主链路里挪出去。下单接口只需确保消息写入 Kafka（毫秒级），发短信、算积分这些慢操作由消费者异步完成，接口响应时间大幅下降。3. 削峰填谷秒杀开始瞬间每秒 10 万请求，数据库只能承受每秒 2 万写入。让请求先写入 Kafka，消费者按数据库能承受的速度平稳消费，系统在洪峰下依然稳定。4. 缓冲与重放消息被持久化保留，下游故障恢复后可以从上次位置继续消费，甚至把位移重置到过去，重新处理历史数据。这是 Kafka 与很多传统队列最大的区别：消费不删除数据，删除只由保留策略决定。1.5 Kafka 与传统消息队列的对比            维度      Kafka      RabbitMQ      ActiveMQ      Pulsar                  模型      分布式提交日志      经典 AMQP 代理      JMS/AMQP      分层（BookKeeper）              吞吐      极高（百万级/s 常见）      中等      中等      高              消息保留      按时间/大小长期保留      消费即删除（可配置）      可配置      分层存储              回放      天然支持，按 offset      较弱      较弱      支持              顺序      分区内严格有序      队列内有序      队列内有序      分区内有序              生态      流处理/大数据最强      传统企业集成      传统企业      云原生、多租户              延迟      毫秒级      微秒到毫秒级      毫秒级      毫秒级      选型上一句话概括：需要海量数据管道、流处理、日志与事件流，选 Kafka；需要复杂的路由规则、极低延迟的传统消息投递，RabbitMQ 也值得考虑。现实中 Kafka 的生态位更接近“数据中枢”，而不仅是 MQ。1.6 Kafka 为什么这么快这是面试与实战都绕不开的问题。Kafka 的高吞吐来自一组工程决策的叠加：  顺序写磁盘：消息只追加到日志文件末尾，顺序写磁盘的速度可以接近随机写内存的量级（几百 MB/s 甚至更高），彻底避开随机 IO；  页缓存（page cache）：Kafka 不自己管理缓存，而是充分利用操作系统页缓存。写入先进 page cache，由 OS 异步刷盘；热数据被消费时大概率直接命中缓存，读操作基本不打磁盘；  零拷贝（zero-copy）：消费者拉取消息时，Kafka 使用 sendfile 系统调用，数据从页缓存直接送到网卡，避免在内核态与用户态之间反复拷贝（第 13 章细讲）；  批量与压缩：生产者把多条消息攒成批次发送，一个批次用一种算法压缩（lz4、zstd、snappy 等），网络与存储开销显著降低；  分区水平扩展：一个主题可以切分成成百上千个分区，分布在不同机器上，读写并行度随机器数线性增长；  简洁的协议与数据结构：V2 消息格式做了大量字段压缩（变长 varint、增量编码），磁盘占用小，解析快；  拉模式（pull）：消费者按自身能力主动拉取，天然形成背压，不会把慢消费者压垮。记住这张速记图：顺序写 + 页缓存 + 零拷贝 + 批量压缩 + 分区并行 + pull 背压        =  高吞吐、低延迟、可水平扩展1.7 典型使用场景日志与埋点采集成千上万台机器上的应用日志、Nginx 访问日志、用户行为埋点统一汇聚到 Kafka，再流入 ES（检索）、ClickHouse（分析）、HDFS/数据湖（归档）。这是 Kafka 最经典的场景。消息总线与微服务解耦服务之间通过事件通信，配合 Outbox、Saga 等模式实现最终一致性（第 26 章展开）。流式计算实时大屏、实时风控、实时推荐：Kafka 作为源头，Flink/Spark Streams 做计算，结果再写回 Kafka 或数据库。CDC 数据同步用 Debezium 把 MySQL/PostgreSQL 的 binlog/WAL 变更捕获到 Kafka，实现缓存失效、搜索索引更新、数仓同步、异地多活。数据仓库与数据湖供给Kafka 作为 ODS 层入口，批量或流式落湖（Iceberg/Hudi/Delta），支撑离线与实时一体的湖仓架构。1.8 本书的学习地图入门            实战             原理             运维             架构01-04 章   -&gt;   05-09 章   -&gt;    10-16 章   -&gt;    17-22 章   -&gt;    23-26 章跑起来         写代码           懂内部           保稳定           做设计很多人卡在“会调用 API，但不懂原理”这一层，遇到消息丢失、堆积、再平衡风暴就束手无策。本书的原理篇会把这些“黑盒”一个个拆开，并且每章都给出可以动手验证的实验或命令。1.9 环境准备开始第 3 章之前，请准备：  JDK 17 或更高版本（java -version 可验证）；  Kafka 4.x 安装包（官网下载 tgz/zip）；  至少 4GB 空闲内存；  推荐使用 WSL2、Linux 虚拟机或 macOS；Windows 原生也可以跑（使用 bin\windows 下的 .bat 脚本），但生产级学习建议在 Linux 环境中完成。本章小结  Kafka 是分布式事件流平台：发布订阅、持久化存储、流处理三合一；  它解决解耦、异步、削峰、缓冲与重放四类核心问题；  高吞吐来自顺序写、页缓存、零拷贝、批量压缩、分区并行与拉模式的组合；  Kafka 4.x 起只有 KRaft 模式，ZooKeeper 已成为历史；  它的定位是数据中枢，适合日志、事件、CDC、流计算等海量数据场景。思考题  你的系统里哪些调用链适合改成事件驱动？哪些不适合？  “消费完消息就删除”和“按保留策略删除”这两种设计，分别适合什么业务？  如果面试官问“Kafka 为什么快”，你能不看书画出 1.6 节那张速记图吗？</li>
</ul>
