怎么用这篇附录
这不是一篇从头读到尾的文章。它是查表用的:你在写一份配置、review 一份别人的配置、或者排查一个「配置都看着对但就是不生效」的问题时,翻到对应那一节。
每一行只回答两个问题:
- 保什么:这项配置打开(或调成某个值)之后,防住了哪一种失败。
- 代价是什么:为这个保护付出了什么——延迟、吞吐、磁盘、内存、或者运维复杂度。
只写「建议开启」而不写代价的配置文档,是可靠性事故的常见来源:有人照着全开,然后发现吞吐掉了 80%,又把它们全关回去。
三个提醒:
- 本表的默认值按 Kafka 4.3.0 与 kafka-clients 4.3.0 列出。默认值在版本之间变过(
acks和enable.idempotence都是 3.0 才改的),升级后请以ConfigDef为准。 - 「建议值」是起点不是终点。每个系统对延迟与吞吐的容忍度不同,先按建议值跑通,再根据自己的观测调。
- 配置之间会互相抵消。单独看每一项都正确、合起来保护归零的组合,集中列在最后两节。
读表的前提:三端各自的视野
同一个「不丢」,三端看到的不是同一件事:
| 端 | 能保证的 | 保证不了的 |
|---|---|---|
| 生产端 | 消息到达 leader 并被足够的副本确认 | 消费端是否处理成功 |
| broker | 已确认的消息在副本中存活、不因重启丢失 | 生产端是否重发、消费端读到几次 |
| 消费端 | 位移在什么时候推进 | 业务处理与位移之间的一致性 |
所以配置矩阵必须三端一起看。生产端配了 acks=all、broker 配了 min.insync.replicas=1,前者等于白配——这就是下面「危险组合」要讲的事。
生产端
确认与重试
| 配置 | 建议值 | 保什么 | 代价是什么 |
|---|---|---|---|
acks | all | leader 等所有 ISR 副本写入后才确认,leader 立刻挂也不丢 | 延迟增加一个副本复制往返;ISR 收缩到 1 时写入会失败而非降级 |
enable.idempotence | true | 生产端重试不会产生重复(broker 侧按 PID + 序列号去重) | 需要 acks=all、retries>0、max.in.flight≤5,否则客户端启动时直接报错 |
retries | 保持默认(极大值) | 由 delivery.timeout.ms 统一控制重试窗口 | 调小会让瞬时故障直接变成失败,通常不该动 |
delivery.timeout.ms | 120000 | 一条消息从 send 到最终失败的总时限,含重试与排队 | 设得太小会在网络抖动时误判失败;必须 ≥ linger.ms + request.timeout.ms |
request.timeout.ms | 30000 | 单次请求的等待上限 | 太小会误伤慢请求;太大则故障发现变慢 |
max.in.flight.requests.per.connection | 5 | 在开启幂等时,5 是保证顺序的上限 | 调大超过 5 会破坏幂等前提,客户端会拒绝启动 |
acks 与 enable.idempotence 的关系要说清:enable.idempotence=true 时客户端强制 acks=all,你显式写 acks=1 会直接报配置冲突。这是好事——它把「想开幂等但忘了改 acks」这个常见错误变成了启动失败。
第一章讨论过 acks=all 的三个前提,这里补一个本机实测过的细节:acks=all 的「all」是「当前 ISR 的所有成员」,不是「所有副本」。ISR 收缩到 1 时,acks=all 和 acks=1 行为一致,唯一的区别是 broker 会检查 min.insync.replicas,不满足就拒写。
批量与压缩
| 配置 | 建议值 | 保什么 | 代价是什么 |
|---|---|---|---|
linger.ms | 5 到 20 | 攒批,降低请求数、提升压缩率 | 每条消息最多多等这么久;对延迟敏感的场景要调小 |
batch.size | 16384 到 65536 | 单个分区的批次上限 | 与 linger.ms 联动;只调大 batch.size 而 linger.ms=0 基本无效 |
compression.type | lz4 或 zstd | 降低磁盘与网络、缩短副本重建量 | 生产端与 broker 端 CPU 上升;已压缩载荷收益接近零 |
buffer.memory | 33554432 | 客户端攒批的内存上限 | 设小了 send() 会阻塞;设大了占用堆外内存 |
第二卷 15.2 节算过压缩对成本的乘数效应:3:1 的压缩比让存储、复制、重建三项同时降三分之二。这是全书性价比最高的单项优化,代价只在 CPU。
但 lz4/snappy/zstd 的编解码器不在 kafka-clients 里。 本机核实:kafka-clients-4.3.0.jar 内不含 net.jpountz.lz4、org.xerial.snappy、com.github.luben.zstd 任何一个包。只配 compression.type=lz4 而不加依赖,第一次发送就会抛:
java.lang.NoClassDefFoundError: net/jpountz/lz4/LZ4Factory
错误码是 -1,信息里不提压缩,很容易被误判成网络问题。11.2 节实测时用的依赖版本:lz4-java 1.8.0、snappy-java 1.1.10.7、zstd-jni 1.5.7-24。broker 端只做校验与转发,压缩在客户端完成,所以依赖要加在生产者和消费者的 classpath 上;broker 若要对其他压缩格式重压缩,镜像里已自带这些库。
消费端
位移与提交
| 配置 | 建议值 | 保什么 | 代价是什么 |
|---|---|---|---|
enable.auto.commit | false | 位移由业务在处理完成后显式提交,避免「拉到了但没处理完就提交」 | 需要自己管理提交时机与失败重试 |
auto.commit.interval.ms | 仅在自动提交时有意义 | —— | 自动提交下,这个间隔内的崩溃会导致重复消费 |
isolation.level | read_committed | 读不到未提交事务的消息,配合事务生产者才有意义 | 消费端要多等 LSO 推进,可能看到短暂的空洞 |
auto.offset.reset | earliest 或 latest,按业务定 | 无位移时的起点 | 选错会导致「上线就重放全量」或「上线就漏掉历史」 |
enable.auto.commit=true 不是「一定会丢」,而是丢与重复都不可控。它在 poll 返回时后台提交上一次 poll 的位移,你既不知道它什么时候提交,也无法在业务处理失败时阻止它提交。1.3 节拆过这个时序,结论是:只要业务处理可能失败,就关掉它。
拉取与心跳
| 配置 | 建议值 | 保什么 | 代价是什么 |
|---|---|---|---|
max.poll.records | 500(默认),按处理耗时调 | 单次 poll 返回的最大条数,直接决定一次处理循环的时长 | 调大能提升吞吐,但会把 max.poll.interval.ms 撑爆 |
max.poll.interval.ms | 300000 | 两次 poll 之间的最大间隔,超时即被踢出组 | 设大了故障发现变慢;设小了处理慢就进「踢出—重平衡—再踢出」的循环 |
session.timeout.ms | 45000 | 心跳超时,超时后消费者被判定死亡 | 与 heartbeat.interval.ms 联动(通常为 1/3) |
heartbeat.interval.ms | 3000 | 心跳频率 | 太小增加协调器负担,太大则对网络抖动不敏感 |
fetch.min.bytes | 1 到 1048576 | 攒够这么多字节才返回,减少空转 | 调大增加延迟 |
fetch.max.wait.ms | 500 | 配合 fetch.min.bytes 的最长等待 | 与上一条联合调参,单独调任何一条都可能适得其反 |
max.partition.fetch.bytes | 1048576 | 单分区单次拉取上限 | 必须 ≥ 单条最大消息,否则大消息会让消费者卡死 |
session.timeout.ms 与 max.poll.interval.ms 是两套独立机制,这是最容易混淆的一点:
session.timeout.ms由后台心跳线程维持,它检测的是「进程还活着吗」。max.poll.interval.ms由 poll 循环维持,它检测的是「处理线程还转吗」。
进程活着但处理卡住,心跳照发,session.timeout.ms 不会触发——只有 max.poll.interval.ms 能发现。这也是为什么「消费者明明在跑,却被踢出组」这类问题,要去看的是 max.poll.interval.ms 而不是心跳配置。
broker 端
副本与确认
| 配置 | 建议值 | 保什么 | 代价是什么 |
|---|---|---|---|
min.insync.replicas | 2(默认 1) | 与 acks=all 配合,ISR 不足时拒写,避免「确认了但只有一份」 | ISR 收缩时写入会失败,可用性下降;必须与 RF 一起看 |
default.replication.factor | 3 | 新建 topic 的副本数 | 存储与复制流量 ×3 |
unclean.leader.election.enable | false(默认) | 禁止非 ISR 副本当 leader,避免丢数据 | leader 选举变慢,极端情况下分区不可用 |
replica.lag.time.max.ms | 30000 | 判定 follower 掉出 ISR 的时间 | 调小会让 ISR 频繁收缩;调大则故障副本留在 ISR 里更久 |
offsets.topic.replication.factor | 与集群规模匹配 | __consumer_offsets 的副本数 | 单节点集群设 3 会导致位移主题建不出来 |
min.insync.replicas 的默认值是 1,这是 Kafka 里最危险的默认值。它意味着:只要 leader 活着就允许写,acks=all 退化成 acks=1。很多「我们开了 acks=all 但还是丢数据」的事故,根因在这里。
4.2 节实测过 ISR 收缩时的行为:min.insync.replicas=2 而 ISR 只剩 1 时,生产端收到 NotEnoughReplicasException,26 毫秒内失败。这个异常是保护在起作用,不是 bug。把它调低来「消除写入失败」,等于把保护关掉。
offsets.topic.replication.factor 有一条本机实战踩过的坑:单节点集群如果沿用三节点的配置写 3,__consumer_offsets 因为凑不齐副本数而创建失败,表现是消费组永远无法提交位移。多节点集群反过来的坑也存在——设成 1 时这个主题成为单点。
主题与保留
| 配置 | 建议值 | 保什么 | 代价是什么 |
|---|---|---|---|
auto.create.topics.enable | false | 防止拼错主题名时静默创建出一个没人消费的空主题 | 新主题必须显式创建,流程上多一步 |
log.retention.ms | 按故障发现周期定 | 出事之后还能重放多久 | 存储线性增长 |
log.segment.bytes | 1073741824 | 段大小,影响保留期的删除粒度 | 段太大导致删除不及时、段尾空段多 |
message.max.bytes | 1048576 | broker 接受的最大消息/批次 | 调大意味着单条消息最多可占这么多内存与网络;必须与生产端、消费端上限联动 |
num.partitions | 按并行度定,不按默认 | 新建主题的分区数 | 分区过多增加段尾空占与元数据开销 |
关于消息大小的「三个上限」,11.1 节做过完整的联动实验。简单说:message.max.bytes(broker)、max.request.size(生产端)、max.partition.fetch.bytes(消费端)三者必须协调,任一个偏小都会在某个方向上卡住,而且报错位置往往不在你调的那一端。
主题级 vs broker 级
上面 broker 端的配置多数可以在主题级覆盖:
/opt/kafka/bin/kafka-configs.sh --bootstrap-server 127.0.0.1:9092 \
--alter --entity-type topics --entity-name libraryd.borrow \
--add-config 'min.insync.replicas=2,retention.ms=604800000'
两个要点:
- 多个
--add-config不接受逗号分隔的多项。要像上面这样写成一个--add-config,值内部用逗号拼。写成两个--add-config参数时,后一个会覆盖前一个。 - 动态配置写入后有生效窗口。改完立刻去验证可能看到旧值,等几秒再查。
按主题覆盖的意义在于「按价值分级」:审计类主题用 retention.ms=2592000000(30 天)、min.insync.replicas=2;缓存失效通知用 retention.ms=3600000、cleanup.policy=delete。全站一个等级既贵又不合适。
六组危险组合
每一组单独看都「配置正确」,合起来保护归零或者互相打架。
| 组合 | 表面上 | 实际上 |
|---|---|---|
acks=all + min.insync.replicas=1 | 等所有 ISR 确认 | ISR 为 1 时退化成 acks=1,丢数据无告警 |
enable.idempotence=true + max.in.flight>5 | 幂等去重 | 客户端拒绝启动,或(旧版本)顺序被破坏导致去重失效 |
| 自动提交 + 多线程处理 | 位移自动推进 | 位移动了但处理没完成,崩溃即丢 |
max.poll.interval.ms < 单批处理耗时 | 消费者在跑 | 反复被踢出组,lag 反而越积越多 |
| RF=3 + 单 broker 集群 | 高可用 | 副本分配不下去,主题创建失败或退化为 1 |
delivery.timeout.ms < linger.ms + request.timeout.ms | 有总超时保护 | 客户端启动即报配置非法 |
第五组值得多说一句:副本数的「声明」和「实际」是两回事。单 broker 上写 replication.factor=3,Kafka 要么创建失败,要么在允许的情况下创建出实际副本数为 1 的主题。查证方式是 kafka-topics.sh --describe,看 Replicas 那一列的真实数量,不要相信自己写进配置里的数字。
症状对照:从现象反查配置
排查时通常是从一个现象开始,而不是从配置名开始。这张表把常见现象映射到该先看哪几项:
| 现象 | 先查 | 常见根因 |
|---|---|---|
send() 长时间阻塞 | buffer.memory、linger.ms | 攒批内存耗尽,客户端在等 broker 确认释放缓冲 |
偶发 NotEnoughReplicasException | min.insync.replicas、ISR 状态 | ISR 收缩到低于 min,写入被拒(这是保护生效) |
| 消费者反复被踢出组、lag 越积越多 | max.poll.interval.ms、单批处理耗时 | 处理时间超过 poll 间隔,进了「踢出—重平衡—再踢出」循环 |
| 位移推进正常但业务数据有缺失 | enable.auto.commit、提交时机 | 自动提交在业务处理完成前推进了位移 |
| 流里有重复记录 | enable.idempotence、消费端去重 | 生产端重试,或消费端崩溃后重放 |
| lag 长期不降 | 分区数、消费者数 | 消费者多于分区时多出来的实例空转 |
| 某条大消息发不出去 | max.request.size、message.max.bytes | 三个上限里有一端偏小,报错位置常不在你调的那端 |
| 消费组无法提交位移 | offsets.topic.replication.factor | 副本数超过 broker 数,位移主题建不出来 |
| 新主题莫名出现 | auto.create.topics.enable | 主题名拼错,被自动创建成一个空主题 |
| 消息在预期保留期之前消失 | log.retention.ms、log.segment.bytes | 段已过期才整段删除,但段太大导致删除滞后;反之亦然 |
最后一行值得解释:Kafka 的保留删除是按段进行的,一个段里只要还有未过期的消息,整段都不删。所以「保留 7 天」实际可能是 7 到 14 天之间,取决于段什么时候攒满。要精确控制保留期,得同时调 log.segment.bytes 和 log.roll.ms。
怎么确认配置真的生效了
配置文件写了不等于生效。三个入口,从外到内:
broker 端实际值。 主题级和 broker 级要分别查,主题级会覆盖 broker 级:
# 主题级
/opt/kafka/bin/kafka-configs.sh --bootstrap-server 127.0.0.1:9092 \
--describe --entity-type topics --entity-name libraryd.borrow
# broker 级(关键:4.x 下必须带 --all 才能看到全部默认值)
/opt/kafka/bin/kafka-configs.sh --bootstrap-server 127.0.0.1:9092 \
--describe --entity-type brokers --entity-name 0 --all
不带 --all 时,输出只列出被显式改过的配置,默认值不显示。这一点经常导致误判:「查了,没有 min.insync.replicas 这一行,所以没配」——实际是它取了默认值 1。
副本数是不是真的。 不要相信自己写进创建命令的数字:
/opt/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 \
--describe --topic libraryd.borrow
看 Replicas 与 Isr 两列的实际数量。单 broker 集群上写 replication.factor=3,这里会暴露真相。
客户端实际值。 最直接的办法是在构造后把配置打出来:
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
props.put(ProducerConfig.ACKS_CONFIG, "all");
ProducerConfig cfg = new ProducerConfig(props);
Map<String, Object> effective = cfg.originals();
System.out.println(effective.get(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG));
ProducerConfig / ConsumerConfig 继承了 AbstractConfig,originals() 返回合并了默认值之后的完整配置。以它打印出来的值为准,不要以你写进 properties 文件的为准——客户端在构造时会做合法性检查并可能调整值,比如 enable.idempotence=true 时会强制 acks=all。
默认值在版本之间变过
这一节专门防「从五年前的文章里抄配置」。有三个默认值在 Kafka 3.0 变过,抄错会导致保护缺失:
| 配置 | 3.0 之前 | 3.0 及以后 |
|---|---|---|
acks(producer) | 1 | all |
enable.idempotence(producer) | false | true |
session.timeout.ms(consumer) | 10000 | 45000 |
第一、二条是一起来的:enable.idempotence 默认打开之后,客户端自动要求 acks=all、max.in.flight≤5、retries>0。所以升级到 3.0 以后,延迟会上升、吞吐可能略降,但重复与丢失都减少了。有人升级后发现延迟变高就去显式写 acks=1,那等于把新版本的保护退回去。
第三条 session.timeout.ms 从 10 秒变成 45 秒,意味着故障发现变慢:消费者进程真的挂了,要 45 秒才被判定死亡、才触发重平衡。如果业务对恢复时间敏感,需要显式调小,但同时要把 heartbeat.interval.ms 按 1/3 比例一起调。
还有几个默认值没变但容易被误记的:auto.offset.reset 默认 latest(不是 earliest)、isolation.level 默认 read_uncommitted(不是 read_committed)、enable.auto.commit 默认 true。这三个「默认开着不安全」或「默认读未提交」的项,都要在基线里显式改掉。
一份可以直接抄走的基线
三端合起来,一个「标准档」的起点:
# producer
acks=all
enable.idempotence=true
max.in.flight.requests.per.connection=5
delivery.timeout.ms=120000
linger.ms=10
compression.type=lz4 # 需另加 lz4-java 依赖,见本节上方说明
# consumer
enable.auto.commit=false
isolation.level=read_committed
max.poll.records=500
max.poll.interval.ms=300000
session.timeout.ms=45000
heartbeat.interval.ms=3000
# broker / topic
min.insync.replicas=2
default.replication.factor=3
unclean.leader.election.enable=false
auto.create.topics.enable=false
这份基线还有三件事没解决,它故意不解决:
- 位移提交与业务处理的一致性:
enable.auto.commit=false只把控制权交给你,怎么用是第二章的事。 - 重复的幂等:生产端幂等只管「生产端不重复」,消费端重放产生的重复要靠业务去重,2.2 节给了三种实现。
- 跨集群:这份基线是单集群的,灾备要额外一套,13.3 节讨论。
配置是可靠性的下限,不是上限。第 15.1 节把这份基线对应的三十条验收检查项列全了。
附录 B 给出可以直接复制的幂等与事务模板。这两个附录都不含新概念,只在需要查参数或抄代码时翻。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。