《Java 消息中间件·可靠消息与消费治理》附录 A 可靠性配置矩阵

生产端、消费端、broker 端三张配置表,每一项只回答两个问题:它保住了什么,代价是什么。附六组看起来没问题、实际互相抵消的危险组合,以及一份可以直接抄走的基线配置。默认值按 Kafka 4.3.0 与 kafka-clients 4.3.0 列出。

怎么用这篇附录

这不是一篇从头读到尾的文章。它是查表用的:你在写一份配置、review 一份别人的配置、或者排查一个「配置都看着对但就是不生效」的问题时,翻到对应那一节。

每一行只回答两个问题:

  • 保什么:这项配置打开(或调成某个值)之后,防住了哪一种失败。
  • 代价是什么:为这个保护付出了什么——延迟、吞吐、磁盘、内存、或者运维复杂度。

只写「建议开启」而不写代价的配置文档,是可靠性事故的常见来源:有人照着全开,然后发现吞吐掉了 80%,又把它们全关回去。

三个提醒:

  1. 本表的默认值按 Kafka 4.3.0 与 kafka-clients 4.3.0 列出。默认值在版本之间变过(acks 和 enable.idempotence 都是 3.0 才改的),升级后请以 ConfigDef 为准。
  2. 「建议值」是起点不是终点。每个系统对延迟与吞吐的容忍度不同,先按建议值跑通,再根据自己的观测调。
  3. 配置之间会互相抵消。单独看每一项都正确、合起来保护归零的组合,集中列在最后两节。

读表的前提:三端各自的视野

同一个「不丢」,三端看到的不是同一件事:

端能保证的保证不了的
生产端消息到达 leader 并被足够的副本确认消费端是否处理成功
broker已确认的消息在副本中存活、不因重启丢失生产端是否重发、消费端读到几次
消费端位移在什么时候推进业务处理与位移之间的一致性

所以配置矩阵必须三端一起看。生产端配了 acks=all、broker 配了 min.insync.replicas=1,前者等于白配——这就是下面「危险组合」要讲的事。

生产端

确认与重试

配置建议值保什么代价是什么
acksallleader 等所有 ISR 副本写入后才确认,leader 立刻挂也不丢延迟增加一个副本复制往返;ISR 收缩到 1 时写入会失败而非降级
enable.idempotencetrue生产端重试不会产生重复(broker 侧按 PID + 序列号去重)需要 acks=all、retries>0、max.in.flight≤5,否则客户端启动时直接报错
retries保持默认(极大值)由 delivery.timeout.ms 统一控制重试窗口调小会让瞬时故障直接变成失败,通常不该动
delivery.timeout.ms120000一条消息从 send 到最终失败的总时限,含重试与排队设得太小会在网络抖动时误判失败;必须 ≥ linger.ms + request.timeout.ms
request.timeout.ms30000单次请求的等待上限太小会误伤慢请求;太大则故障发现变慢
max.in.flight.requests.per.connection5在开启幂等时,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.ms5 到 20攒批,降低请求数、提升压缩率每条消息最多多等这么久;对延迟敏感的场景要调小
batch.size16384 到 65536单个分区的批次上限与 linger.ms 联动;只调大 batch.size 而 linger.ms=0 基本无效
compression.typelz4 或 zstd降低磁盘与网络、缩短副本重建量生产端与 broker 端 CPU 上升;已压缩载荷收益接近零
buffer.memory33554432客户端攒批的内存上限设小了 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.commitfalse位移由业务在处理完成后显式提交,避免「拉到了但没处理完就提交」需要自己管理提交时机与失败重试
auto.commit.interval.ms仅在自动提交时有意义——自动提交下,这个间隔内的崩溃会导致重复消费
isolation.levelread_committed读不到未提交事务的消息,配合事务生产者才有意义消费端要多等 LSO 推进,可能看到短暂的空洞
auto.offset.resetearliest 或 latest,按业务定无位移时的起点选错会导致「上线就重放全量」或「上线就漏掉历史」

enable.auto.commit=true 不是「一定会丢」,而是丢与重复都不可控。它在 poll 返回时后台提交上一次 poll 的位移,你既不知道它什么时候提交,也无法在业务处理失败时阻止它提交。1.3 节拆过这个时序,结论是:只要业务处理可能失败,就关掉它。

拉取与心跳

配置建议值保什么代价是什么
max.poll.records500(默认),按处理耗时调单次 poll 返回的最大条数,直接决定一次处理循环的时长调大能提升吞吐,但会把 max.poll.interval.ms 撑爆
max.poll.interval.ms300000两次 poll 之间的最大间隔,超时即被踢出组设大了故障发现变慢;设小了处理慢就进「踢出—重平衡—再踢出」的循环
session.timeout.ms45000心跳超时,超时后消费者被判定死亡与 heartbeat.interval.ms 联动(通常为 1/3)
heartbeat.interval.ms3000心跳频率太小增加协调器负担,太大则对网络抖动不敏感
fetch.min.bytes1 到 1048576攒够这么多字节才返回,减少空转调大增加延迟
fetch.max.wait.ms500配合 fetch.min.bytes 的最长等待与上一条联合调参,单独调任何一条都可能适得其反
max.partition.fetch.bytes1048576单分区单次拉取上限必须 ≥ 单条最大消息,否则大消息会让消费者卡死

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.replicas2(默认 1)与 acks=all 配合,ISR 不足时拒写,避免「确认了但只有一份」ISR 收缩时写入会失败,可用性下降;必须与 RF 一起看
default.replication.factor3新建 topic 的副本数存储与复制流量 ×3
unclean.leader.election.enablefalse(默认)禁止非 ISR 副本当 leader,避免丢数据leader 选举变慢,极端情况下分区不可用
replica.lag.time.max.ms30000判定 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.enablefalse防止拼错主题名时静默创建出一个没人消费的空主题新主题必须显式创建,流程上多一步
log.retention.ms按故障发现周期定出事之后还能重放多久存储线性增长
log.segment.bytes1073741824段大小,影响保留期的删除粒度段太大导致删除不及时、段尾空段多
message.max.bytes1048576broker 接受的最大消息/批次调大意味着单条消息最多可占这么多内存与网络;必须与生产端、消费端上限联动
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'

两个要点:

  1. 多个 --add-config 不接受逗号分隔的多项。要像上面这样写成一个 --add-config,值内部用逗号拼。写成两个 --add-config 参数时,后一个会覆盖前一个。
  2. 动态配置写入后有生效窗口。改完立刻去验证可能看到旧值,等几秒再查。

按主题覆盖的意义在于「按价值分级」:审计类主题用 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 确认释放缓冲
偶发 NotEnoughReplicasExceptionmin.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)1all
enable.idempotence(producer)falsetrue
session.timeout.ms(consumer)1000045000

第一、二条是一起来的: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 给出可以直接复制的幂等与事务模板。这两个附录都不含新概念,只在需要查参数或抄代码时翻。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收