接手过一个大数据的业务集群,每天吞吐量在几十亿条消息级别,但经常出现消费端 Lag 持续上涨、业务方半夜来敲门的情况。一开始我以为是业务代码有瓶颈,追了一圈发现,问题出在 Kafka 集群本身——一堆节点用的几乎全是默认参数,Broker 端、Producer 端、Consumer 端都没有针对数据量和业务模型做过调整。后来花了两周时间把整个链路的配置摸了一遍,重新做了压测验证,才把集群稳下来。
这期间踩过的坑不少,也把 Kafka 高性能配置这件事彻底搞明白了。很多人一说 Kafka 调优,就拿官方文档参数抄一遍,其实方向就错了。Kafka 的配置不是孤立的几个参数,而是 Broker、Producer、Consumer、操作系统、存储介质这几个层面联动的一套体系。默认配置在大多数情况下是为了保证“正确性”,而不是为了跑出高性能。今天这篇就把我在大数据环境下调整 Kafka 配置的完整思路、核心参数、验证方法以及排障过程整理出来,希望对正在被 Kafka 性能问题折磨的人有帮助。
1. 性能瓶颈从哪来:吞吐、延迟与默认配置的底层逻辑
1.1 默认配置是为“正确性”服务的,不是为性能
Kafka 官方给出的默认参数,核心目标是保证消息不丢、不重复、不乱序,以及集群在异常情况下依然可用。比如默认的acks=all(在新版本客户端中),要等到所有 ISR 副本都写入成功才返回;比如unclean.leader.election.enable=false,宁可短暂不可用也不选举出一个落后太多的 Leader。这些设计都是业务安全的底线,但在高吞吐场景下,这些“安全垫”全都是性能损耗。
我遇到过最典型的情况:一个 Kafka 集群的 Broker 节点用的是 16 核 CPU、64GB 内存、万兆网卡,但生产端吞吐量死活上不去,单分区写入速率只有几 MB/s。查了一圈,发现客户端配置里acks=-1(也就是 all),同时linger.ms=0,每条消息都单独发出去,等于每次请求都在等全量副本确认,吞吐自然拉胯。
所以调优的第一步,是明确一件事:你需要的到底是“极高的吞吐”还是“极低的延迟”,还是两者平衡?这决定了后面所有参数的取值方向。大数据场景下,批量写入、批量消费是常态,所以绝大多数时候我们把吞吐放在第一优先级,延迟允许有几十毫秒到几百毫秒的弹性。
1.2 读写路径的耗时模型:磁盘顺序写和 Page Cache
想调好 Kafka 的配置,得先清楚一条消息从生产端到消费端,到底在哪些环节花了时间。生产端发送消息的路径大致是这样:应用构造 ProducerRecord,经过序列化、分区器、拦截器进入 Producer 的accumulator缓冲区,然后由发送线程按批次批量发送到 Broker。Broker 收到请求后,先由网络线程(network threads)解析请求,放入请求队列,再由 IO 线程(io threads)从请求队列取出并处理:追加日志到 Page Cache,同时写入对应分区的日志段文件。
消费端读取则依赖 Page Cache 的命中,加上 Kafka 的零拷贝机制(sendfile),直接从 Page Cache 把数据发送到网卡,中间不经过用户态拷贝。这也是 Kafka 能单机支撑大吞吐量的核心原因之一。
从时间构成上看,生产端的延迟包括网络 RTT、Broker 请求队列排队时间、IO 线程处理时间、副本同步时间、刷盘时间(如果开启同步刷盘)。消费端的延迟受 Consumer 拉取频率、处理逻辑复杂度、Broker 端 Page Cache 命中率影响。绝大多数性能问题,要么是队列排队过长,要么是磁盘 IO 瓶颈,要么是参数不匹配导致批处理没有形成,很少是 CPU 算力不够。
我曾经用一句话跟团队解释 Kafka 的高性能原理:它像一家餐厅,点菜单(消息)不要求厨师现炒一道端一道,而是把订单攒成一摞,一次性交给后厨批量做,做完再一次性端出去。谁要是让厨师每次只炒一个菜,哪怕灶台再多,翻台率也上不去。
1.3 动手调优之前:先把硬件基线确认清楚
很多人一上来就改参数,结果发现怎么调都没用,问题其实出在硬件层就已经吃亏了。比如磁盘本身是共享的云盘,IOPS 上限很低;比如网卡有丢包;比如内存不足导致 Page Cache 命中率差;比如 CPU 开启了节能模式,频率一直上不去。
我习惯的做法是先在集群上跑一轮基线检查:
- 用
iostat -x 1看磁盘的%util、await、w_await,确认顺序写性能是否正常; - 用
vmstat 1看si、so,确认是否存在内存换页; - 用
dmesg -T | tail检查有没有磁盘 IO 错误、网络丢包、软中断堆积; - 用
top看 CPU 是否大部分时间耗在si(软中断)上。
这一步很关键,如果底层硬件本身就拉胯,后面所有参数调整都是白费。我自己就遇到过磁盘%util长期 100%,但 IO 线程数怎么加都上不去的场景,最后换了块 SSD,同样的配置吞吐直接翻了几倍。硬件基线没问题,再进入配置调整阶段。
2. Broker 端配置:决定集群吞吐上限的几组核心参数
2.1 网络线程与 IO 线程的配合:不是越多越好
Broker 端有两个最容易被“拍脑袋”配置的参数:num.network.threads和num.io.threads。默认值分别是 3 和 8。
网络线程负责处理客户端连接、读取请求、写入响应。IO 线程负责真实的消息追加、副本拉取等逻辑。它们的理想工作状态是:网络线程能快速把请求放入队列,IO 线程能及时消费队列,不会出现某一侧积压。
官方建议num.network.threads可以设置为 CPU 核数的 2~3 倍,num.io.threads设置为 CPU 核数的 1~2 倍。但这里有个坑,实际生产环境中我见过太多人把num.io.threads直接拉到 32、64 然后发现吞吐没涨反降的情况。
原因是 Kafka 的 IO 线程并不像数据库连接池那样“线程越多并行度越高”。日志追加是磁盘顺序写,多个线程同时写同一批分区,最终也会在磁盘层排队。IO 线程过多反而增加上下文切换和锁竞争开销。
我的经验是:先按 CPU 核数的一半到一倍设置num.io.threads,再做压测观测请求队列深度(通过 JMX 指标RequestQueueAvgIdlePercent)。这个值如果长期低于 30%,说明 IO 线程不够;如果长期高于 70%,说明当前请求量远未达到瓶颈,加了也白加。num.network.threads同理,看NetworkProcessorAvgIdlePercent。
在现代高规格服务器上(32 核以上、万兆网卡),我通常给一组起始配置:
num.network.threads=8 num.io.threads=16 queued.max.requests=1000queued.max.requests是请求队列的最大积压数,默认 500。在网络抖动剧烈时,如果队列太浅,客户端很容易收到Connection refused或请求超时;调大到 1000~2000 可以给 Broker 更多缓冲空间,但要注意它也会增加请求排队等待时间,内存足够的前提下再调。
2.2 日志段策略、刷盘机制与 Page Cache 的取舍
Kafka 写入消息时,并不是每来一条就立刻刷到物理磁盘。它先写入 Page Cache(操作系统页缓存),由操作系统按脏页比例异步刷盘。这对性能是巨大的利好,因为 Page Cache 的写入速度是内存级的,远快于磁盘。
默认的log.flush.interval.messages是Long.MAX_VALUE,log.flush.interval.ms默认也没有设置强制刷盘间隔。这看起来像是不刷盘会丢数据,确实,如果机器突然断电或内核崩溃,Page Cache 里还没落盘的数据会丢失。但 Kafka 本身通过多副本机制来消除单点硬件故障带来的数据丢失风险,所以正常情况下,靠副本冗余而不是靠单机刷盘来保证数据安全,是所有生产集群的主流做法。
log.segment.bytes默认是 1GB,代表单个日志段文件的最大大小。日志段到了上限会触发滚动,滚动过程中旧文件可以被清理(如果保留策略是删除)。理解这一点对调优有帮助:如果log.segment.bytes设置太小,日志段的滚动频率会很高,无形中增加文件句柄切换和清理的开销;如果设置太大,又可能导致单个文件过大,清理不及时时磁盘占用膨胀。1GB 这个默认值在大多数场景下是合理的,我一般不动它。
真正值得关注的是log.retention.bytes和log.retention.hours的配合。大数据场景经常遇到“日志保留 7 天,但磁盘快满了”的情况。这时候要同时考虑总量:log.retention.bytes按分区维度统计,一个 topic 如果有 24 个分区,log.retention.bytes=1GB表示每个分区最多保留 1GB,总共最多 24GB。新手在这里经常算错总量,导致磁盘被写爆。
此外,log.index.interval.bytes和log.index.size.max.bytes控制索引文件的稀疏程度和最大大小。如果单分区消息很小但数量巨大,索引需要覆盖的条目就多,索引文件可能不够用。官方默认log.index.size.max.bytes是 10MB,在单分区日写入量超过百亿条的超大规模场景下,建议扩到 50MB 甚至 100MB,否则可能出现“索引文件已满导致消息无法追加”的异常。
2.3 分区数、副本数与吞吐的关系
分区数是 Kafka 性能设计中最关键的杠杆之一。分区越多,集群的并行度越高,生产端和消费端都能把压力分散到更多线程和节点上。但分区数不是越多越好。
每个分区在 Broker 上对应一组日志段文件、索引文件以及对应的文件句柄。分区太多,单 Broker 管理的文件数量剧增,内存开销、打开文件数、目录扫描耗时都会上升。极端情况下,Broker 启动加载元数据、关闭时刷盘都可能变慢。
分区数的合理取值,本质上取决于两个约束:单分区吞吐基线和消费组并发度。我做过一组参考性测试(机械盘+万兆网的集群):单分区单生产者,batch 攒到 16KB、acks=1的情况下,吞吐大约在 20~40 MB/s;SSD 上可以到 100 MB/s 以上。实际业务中因为消息大小不均匀、跨机房网络延迟等原因,通常要打个五折看。
假设业务要求 Topic 整体吞吐 500 MB/s,单分区基线按 30 MB/s 算,那至少需要 17 个分区。同时要看消费端:如果 Consumer Group 里只有 5 个消费者实例,那分区数再多,单个消费者也会同时处理多个分区。分区数最好设置成消费者数量的整数倍,方便负载均匀分配。
副本因子是另一个容易被忽略的点。副本数越多,数据冗余度越高,容灾能力越强,但每次写入需要同步的副本也越多,写放大越明显。大数据环境普遍采用replication.factor=3,配合min.insync.replicas=2,可以容忍一个副本故障而不丢数据。
这里有一个很多人搞不清楚的概念:min.insync.replicas不是“消息必须写入几个副本才会被确认”,它是 ISR 集合的最小值,配合acks=all使用。如果 ISR 数量低于这个值,Broker 会直接拒绝写入,返回NotEnoughReplicasException。把它设成 2,意味着三个副本里至少两个在 ISR 中,消息写入才算成功。这样既能保证一定的可用性,又不会因为要求三副本全部确认而放大写延迟。
2.4 副本同步参数:容易被忽视的跨机房性能隐患
跨机房部署 Kafka 集群时,副本同步参数特别关键。默认的replica.fetch.max.bytes是 1MB,表示 Follower 副本每次从 Leader 拉取数据的最大字节数。如果单条消息较大(比如超过 1MB),或者短时间内积压大量消息,这个默认值会成为副本同步的瓶颈,ISR 中的 Follower 可能长期追不上 Leader,进而触发 ISR 收缩,严重时会导致生产写入失败。
我踩过这个坑:一次业务方上线了一批平均大小约 800KB 的消息,结果集群的 ISR 频繁收缩,部分分区读写不可用。排查发现就是replica.fetch.max.bytes太小,Follower 要分好几次才能拉完一条大消息。调大到 10MB 后 ISR 恢复稳定。
类似需要一起调整的参数还有message.max.bytes和replica.fetch.response.max.bytes。如果业务消息体比较大,这三个参数要同步放大,否则生产端能写入,但副本同步会出问题,消费端也可能拉取失败。
3. Producer 与 Consumer 端的调优:吞吐与延迟的平衡艺术
3.1 Producer 端:批量、压缩与确认机制的配合
Producer 端对吞吐影响最大的三个参数是batch.size、linger.ms和compression.type,其次是acks和buffer.memory。
batch.size控制的是 Producer 端每个分区批次缓冲区的大小,默认 16KB。如果单条消息只有几百字节,16KB 可以攒下几十条消息才发一次;如果单条消息就是几 KB,那 16KB 装不下几条就发出去了。我的经验是,标准业务消息(几百字节到 1KB)用默认值问题不大;如果消息体在 5KB 以上,建议把batch.size调到 64KB 或 128KB。
linger.ms控制的是批次在缓冲区里等待更多消息的时间,默认 0,意味着缓冲区一有消息就立刻发送。这是“性能杀手”之一,因为linger.ms=0时批处理效果完全依靠瞬时并发,一旦消息到达不够密集,就会变成大量小请求。把linger.ms设为 5~20ms,可以让 Producer 攒一批再发,显著提升吞吐。
举个例子,我线上把一个主题的生产吞吐从 100 MB/s 提升到 300 MB/s,核心改动就是batch.size=64KB、linger.ms=10,数据压缩采用lz4。注意linger.ms不是越高越好,它直接增加了单条消息的可见延迟。延迟敏感型业务(比如秒级以内的实时风控)建议 2~5ms;离线数仓的上游数据管道,可以放宽到 20ms 甚至 50ms。
compression.type首选lz4或zstd。gzip压缩率高但 CPU 开销大,在 CPU 不富裕的情况下容易成为瓶颈。zstd的压缩率比lz4高,而且解压速度也不差,如果业务方需要长期存储较多数据,zstd是更划算的选择。压缩不只是省带宽,还能降低 Broker 端写入 IO 量,因为写的是压缩后的数据。
acks参数选择也很关键:
acks=0:发出去不确认,吞吐最高但会丢消息;acks=1:Leader 写入成功即返回,吞吐和可靠性相对平衡;acks=all:等待 ISR 全部确认,最安全但吞吐最低。
大数据实时链路一般至少用acks=1,很多对数据一致性要求高的业务直接用acks=all。用acks=all不代表性能一定差,配合min.insync.replicas=2和足够的批量大小,吞吐依然可以做到很高,只是延迟中位数会上升一些。
3.2 幂等与事务:它们对性能的真实影响
Kafka 从 0.11 开始支持幂等 Producer,通过给每条消息加序列号来去重,避免因网络重试导致的重复消息。enable.idempotence=true在默认情况下会自动把acks提升为all,同时把retries提升为Integer.MAX_VALUE。这个组合对性能的影响比很多人想象的要小,因为序列号校验只是个位数的 CPU 开销,换来的是消息不重复,这笔买卖很划算。
事务则完全是另一回事。transactional.id参与的跨分区原子写入,需要 Transaction Coordinator 协调,每个事务都有 Begin 和 Commit 的开销,吞吐会明显下降。我实测过一个场景:开启事务后,Producer 吞吐下降了约 30%,P99 延迟上涨了一倍多。
所以我的建议是:能用幂等就不要动事务,大多数实时链路其实用不到跨分区原子性,消息流转过程中的“最终一致”完全可以满足业务需求。如果你确实需要事务(比如从 Kafka 读取再写回 Kafka 的 Exactly-Once 流处理),那要做好性能让步的预期,并且把transaction.timeout.ms调整到一个合理值(默认 60 秒,过短会导致大事务频繁超时重试)。
3.3 Consumer 端:fetch 到 poll 之间的节奏控制
Consumer 端的吞吐瓶颈,往往不在 Kafka 本身,而在拉取参数与业务处理耗时之间的不匹配。
fetch.min.bytes控制 Consumer 每次拉取的最小字节数,默认 1 字节,意味着有数据就返回。fetch.max.wait.ms控制如果数据未达到fetch.min.bytes时的最长等待时间,默认 500ms。这两个参数适合配合使用:把fetch.min.bytes设为 64KB 或 1MB,fetch.max.wait.ms设为 500~1000,可以让 Consumer 攒够一批数据再处理,显著减少拉取次数。但副作用是单批数据的延迟上去了,对实时性要求高的场景要酌情调小。
max.poll.records决定一次poll()返回的最大消息条数,默认 500。如果每条消息的处理耗时为 10ms,500 条全部处理完需要 5 秒,而max.poll.interval.ms默认是 300 秒(5 分钟),理论上绰绰有余。但如果处理逻辑里有外部 RPC、DB 写入、锁等待等耗时操作,500 条可能处理不完就触发 Rebalance 了。
一个很常见的现象:Consumer 频繁 Rebalance,消费者组内成员不断变动,消费进度反复回退,Lag 居高不下。看起来是“性能问题”,本质上是max.poll.records太大或者max.poll.interval.ms太短。这时候应该做的不是加机器,而是把max.poll.records调小到 100~200,或者把max.poll.interval.ms调大到 600 秒以上。另一个更优雅的方案是在消费线程里做异步化:poll()完之后把消息丢到线程池,主线程立刻进行下一次poll(),此时需要把enable.auto.commit关掉,在异步处理完成后手动提交 offset。
3.4 一套可供参考的 Producer/Consumer 初始参数表
| 端 | 参数 | 推荐值 | 说明 |
|---|---|---|---|
| Producer | batch.size | 32768~131072 | 消息体大就调大 |
| Producer | linger.ms | 5~20 | 吞吐优先可到 50 |
| Producer | compression.type | lz4 / zstd | CPU 紧张用 lz4 |
| Producer | acks | all 或 1 | 按可靠性要求选 |
| Producer | buffer.memory | 64MB~256MB | 防止高并发下 Send 阻塞 |
| Producer | max.in.flight.requests.per.connection | 5 | 幂等开启时不用改 |
| Consumer | fetch.min.bytes | 65536 起 | 批量拉取 |
| Consumer | fetch.max.wait.ms | 500~1000 | 与上一项配合 |
| Consumer | max.poll.records | 100~500 | 按处理耗时调 |
| Consumer | max.poll.interval.ms | 300000~600000 | 处理慢就调大 |
| Consumer | enable.auto.commit | false | 推荐手动提交 |
4. 操作系统与部署层的隐形性能杠杆
4.1 存储选型:一块高速 SSD 比多块机械盘更省心
Kafka 对磁盘的核心诉求是顺序写性能和 IOPS。大数据场景下,如果条件允许,优先用 NVMe SSD。一条经验法则:Kafka 的吞吐上不去,先看磁盘是不是瓶颈。用机械盘做 RAID10 能获得比单盘更好的 IOPS,但机械盘在随机读写和并发刷盘场景下的延迟抖动依然无法和 SSD 相比。
单机多磁盘的部署方式,建议直接让 Kafka 把不同磁盘挂载到同一目录下的不同log.dirs路径,由 Kafka 自己管理分区在多块磁盘间的分配,不要自行做 RAID0 或 RAID5。Kafka 本身通过副本机制做冗余,底层用 RAID 做冗余属于画蛇添足,反而会引入 RAID 控制器的写缓冲和重建风险。我见过一个真实事故:底层的 RAID 卡电池老化后直接开启了写缓存降级,所有磁盘写入变成直写,Kafka 集群吞吐瞬间掉了一半。
如果确实只有机械盘,需要控制单 Broker 上的分区数,避免多个分区的日志段同时写入导致寻道开销放大。同时把log.flush.interval.messages维持默认(不主动刷盘),让操作系统自己做脏页聚合。
4.2 文件系统与挂载参数:被低估的吞吐收益
文件系统的选择上,生产环境推荐 XFS 或 ext4。XFS 对并发写入和大文件的扩展性更好,所以我主力集群用的都是 XFS。挂载参数对性能的影响很直接:
mount -o rw,noatime,nobarrier,allocsize=1M /dev/sdb /data/kafkanoatime表示读写文件不更新访问时间戳,减少元数据写入;nobarrier对 XFS 表示禁用写屏障,适用于有独立电池保护的 RAID 卡场景,可以提升写入吞吐,但如果 RAID 卡没有掉电保护,千万别用这个参数;allocsize预分配文件块大小,对大文件的顺序追加友好。
ulimit和文件句柄这块也要注意。Kafka 分区多时,文件句柄消耗很大。/etc/security/limits.conf里需要给 Kafka 进程放开nofile限制,建议设置655350或者直接unlimited。别等进程报Too many open files了才去改。
4.3 内核参数:回收策略、网络缓冲与 TCP 相关配置
vm.swappiness建议设为 1 甚至 0,避免系统把 Page Cache 中的内存换到 swap。Kafka 的性能高度依赖 Page Cache 命中,一旦发生 swap,延迟会陡增。修改方式:
sysctl -w vm.swappiness=1透明大页(THP)建议关闭。THP 在内存碎片化时可能触发较高的延迟,对 Kafka 这种对尾延迟敏感的系统不友好。检查方式:
cat /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/enabled网络参数方面,重点看 TCP 缓冲区。网络突发流量大时,默认缓冲区可能不够,导致丢包或重传:
net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.ipv4.tcp_rmem = 4096 87380 16777216 net.ipv4.tcp_wmem = 4096 65536 16777216此外,如果 Broker 所在主机的网卡支持多队列,建议开启 RPS(Receive Packet Steering),把网卡中断分散到多个 CPU 核心,减轻单核软中断压力。实际操作通过rps_cpus配置文件来设置,这在大流量场景下收益比较明显。
4.4 JVM 堆与堆外内存:不是堆越大越好
Kafka Broker 的 JVM 堆不是越大越好。Kafka 本身的数据读写走 Page Cache,堆内存只存 broker 内部元数据、请求队列、分区状态等对象。堆设得过大,反而会导致 Full GC 时间变长,或者让 JVM 花费大量时间扫描大堆中的对象。
我通常把 Kafka Broker 堆内存设置为 4GB 到 6GB。64GB 内存的机器,剩下的全部留给 Page Cache。KAFKA_HEAP_OPTS可以这样设:
export KAFKA_HEAP_OPTS="-Xms5g -Xmx5g -XX:MetaspaceSize=96m -XX:+UseG1GC -XX:MaxGCPauseMillis=20"G1 是默认收集器,重点关注MaxGCPauseMillis,如果观察到 Young GC 时间较长,可以适当调大-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent。但说实话,我很少在 Kafka 上花太多时间调 GC 参数,因为真正的数据不在堆上,GC 调优的边际收益远不如把 Page Cache 命中和磁盘 IO 优化好。
5. 压测、监控与一次真实排障复盘
5.1 用官方自带工具做一轮有效压测
配置调完后,不要直接上生产,先用 Kafka 自带的性能测试工具做个基线对比。生产端压测命令示例:
kafka-producer-perf-test \ --topic perf-test \ --num-records 10000000 \ --record-size 1024 \ --throughput -1 \ --producer-props bootstrap.servers=kafka1:9092,kafka2:9092 \ acks=1 linger.ms=10 batch.size=32768 compression.type=lz4--throughput -1表示不限制吞吐上限,压测结果会输出吞吐量(records/sec, MB/sec)以及平均延迟、P95/P99 延迟。消费端压测类似:
kafka-consumer-perf-test \ --bootstrap-server kafka1:9092 \ --topic perf-test \ --fetch-size 1048576 \ --messages 10000000 \ --threads 4压测的关键在于“可对比”。你在调整参数前跑一轮,调整后再跑一轮,对比中位数和 P99 变化。单独看某一次压测的绝对值意义不大,因为不同机器、网络、数据大小都会影响数值。我一般会在压测时同时观察 Broker 端指标,确认瓶颈到底在网络线程、IO 线程,还是磁盘。
5.2 监控指标清单:哪些数字需要经常盯
| 指标 | 来源 | 含义与分析 |
|---|---|---|
NetworkProcessorAvgIdlePercent | JMX | 网络线程空闲率,长期接近 0 说明网络线程过载 |
RequestQueueAvgIdlePercent | JMX | 请求队列空闲率,长期低说明 IO 线程处理不过来 |
UnderReplicatedPartitions | JMX | 非 0 说明有分区副本同步跟不上 |
IsrShrinksPerSec/IsrExpandsPerSec | JMX | ISR 频繁收缩/扩张是抖动信号 |
| Consumer Lag | 客户端/工具 | 消费堆积量,持续上涨必须追查 |
kafka.server:type=BrokerTopicMetrics下的BytesInPerSec/BytesOutPerSec | JMX | 集群整体吞吐基线 |
Lag 是大家最关注的指标,但它往往是个滞后信号,等 Lag 涨起来才发现问题,已经影响业务了。更提前的预警要看UnderReplicatedPartitions和IsrShrinksPerSec,这两个指标反映了 Broker 内部副本同步的健康度。副本同步异常往往先出现,随后才会表现为消费端 Lag 上涨。我在线上就养成了每天固定看这两个指标的习惯,能提前规避很多“半夜被叫醒”的事故。
5.3 真实案例复盘:一边 Lag 上涨一边磁盘 IO 超高的排查过程
有一次线上告警:某个核心 Topic 的消费 Lag 持续上涨,同时 Broker 磁盘%util打到 100%。第一反应是加 Consumer,加完发现 Lag 还在涨,立刻意识到问题不在消费端。
登录 Broker 用iostat -x 1观察,发现w_await已经飙到 200ms 以上。再查分区分布,发现这个 Topic 的 24 个分区全部落在 3 台 Broker 上,而其中一台机器的 8 个分区全是 Leader。这种分区倾斜本身不算异常,但如果写入模式是尾部热点(比如同一个用户 ID 的高频写入集中在少数分区),那单分区的写入压力会被放大很多倍。
用kafka-topics.sh --describe查了分区分布,再用生产端日志对照消息 key 的分布,确认了热点分区都集中在某几个分区上。解决思路有两条:一是扩大分区数并增加分区器对 key 的散列能力,把热点打散;二是针对单分区的写入瓶颈调大batch.size和linger.ms,让批次聚合更充分。
我同步做了两件事:
- 该 Topic 分区数从 24 扩到 48,让分区更均匀地分布在 3 台 Broker 上;
- 生产端
linger.ms从 5 调到 15,batch.size从 32KB 调到 64KB。
调整后大概过了 10 分钟,磁盘%util从 100% 降到 70%,Lag 开始回落。这说明磁盘 IO 确实是瓶颈,但导致 IO 过高的原因是小写入请求过多。如果只加消费者不加批处理,这个问题永远解不了。
5.4 按顺序调优:不要一上来就动参数
最后分享一个很多人容易犯的毛病:一遇到性能问题就改参数。我的习惯是先做分层排查:
第一层看资源:CPU、内存、磁盘 IO、网络。资源本身都有富余,才考虑是不是配置问题。 第二层看请求模型:生产端是否形成批量,消费端是否频繁 Rebalance,Broker 端请求队列是否积压。 第三层看数据特征:消息大小、分区 key 分布、业务峰值节奏。大消息和小消息的最优配置差别很大。 第四层才轮到具体的参数调优:优先调 Producer 的批量和压缩,再调 Consumer 的拉取和提交,最后动 Broker 的线程和副本相关参数。
这个顺序让我少走了很多弯路。举个例子,如果磁盘 IO 本身已经 100%,你调num.io.threads再大也没用,因为磁盘已经写满了,线程增加只会加大排队。反过来,如果网络线程处理不过来,你盲目加分区数只会让分区都堆积在请求队列里。
Kafka 的高性能配置本质上是平衡和取舍的艺术,它没有一套“万能最佳参数”,只有在一轮轮压测和监控中打磨出来的“适合你这套业务场景的参数”。我每次调完一版配置,都会在测试环境完整压一遍,记录下吞吐和延迟的前后对比,再决定是否推广到生产环境。这套方法虽然朴素,但比任何网上抄来的配置模板都可靠。
如果你现在正被 Kafka 性能问题困扰,建议先从本篇提到的磁盘基线和最基础的批量参数查起,大概率能解决 80% 的问题。剩下的 20%,就需要结合你自己的业务特征慢慢调了。