☰
3台廉价服务器跑出200万TPS:Kafka写入压测全链路调优复盘
2026/10/10 4:18:54 网站建设 项目流程

Kafka压测这事儿,听起来不算稀奇,但如果有人告诉你,三台总价不到三千块的二手服务器,能组成一个稳定扛住200万TPS写入的Kafka集群,你信吗?我第一次听到这个目标时也不信。直到那次压测在某测试机房跑完,结果摆在面前:200万TPS,纯生产端写入吞吐,Broker平均CPU大约60%,全程没有副本掉队,稳定跑了30分钟。这篇文章讲的是那次压测的完整复盘,从硬件、系统、Broker参数、Producer参数到压测执行方式,以及中途踩进去的坑,希望对做消息中间件性能验证的同学有点帮助。

1. 项目概述:为什么用3台旧服务器去挑战200万TPS

1.1 200万TPS背后的数据量:先算一笔账

很多同学听到“200万TPS”的第一反应是“不可能”,第二反应是“这有什么意义”。其实这两种反应,都是因为没把账算明白。压测时每条消息我们设定为200字节,200万条每秒,裸数据流量就是2000000 × 200 = 400000000字节,约等于381MB/s。注意,这还只是“净数据”,没算Kafka协议头、批次封装、CRC校验、压缩包装和ACK响应带来的额外开销。

实际跑起来,200字节的小消息恰恰是协议开销占比很高的场景。按我们压测中的实测,整体物理网卡流量大约是净数据的1.5到1.8倍,也就是说Broker侧单节点网卡流量已经逼近几百MB/s的量级。你可以拿这组数字去反推:一张千兆网卡的理论极限约125MB/s,满双工也只有250MB/s的合计能力,谁要是拿千兆网卡跑Kafka还说能到200万TPS,那基本是在编故事。

所以“廉价服务器”这个表述,准确说应该是“机箱可以便宜,CPU可以便宜,内存可以便宜,但网卡必须给足”。这也是我整篇压测复盘里最想强调的第一件事:性能瓶颈往往是系统性的,不是某一个参数的事。如果不先把物理带宽缺口算清楚,后面所有调优都是空中楼阁。

1.2 廉价服务器的配置与真实成本

再说说这三台“廉价”Broker的真实配置。它们是我们从二手渠道买来的退役机,单台含运费不到九百块,配置大概是这样:

部件配置
CPU8核16线程,基础频率2.2GHz左右
内存32GB DDR4,未超频
系统盘256GB SATA SSD,仅装系统和Kafka程序
数据盘1TB SATA SSD,用于Kafka日志目录
原机网卡双千兆,测试时已更换为万兆网卡

为什么强调32GB内存而不是更大的?Kafka的性能模型里,消息写入其实是先进操作系统页缓存,再由后台线程异步刷到磁盘。也就是说,短时间内大量写入不会立刻落盘,而是在内存里“转一圈”。32GB对于压测场景已经足够,真正挡在200万TPS前面的不是内存,而是网络带宽和CPU处理中断的能力。

这里还得解释一下,标题里的“3台廉价服务器”,指的是3台Broker节点。压测机是另外准备的两台普通PC服务器,负责运行生产者压测脚本。我见过不少人把压测机和Broker混在一起,结果Broker还没到瓶颈,压测机自己先把CPU跑满了,那样测出来的数据完全没有参考意义。压测环境里,施压端和数据端必须分开。

2. 压测环境搭建:网络、系统和集群部署

2.1 网络是第一瓶颈:万兆网卡和交换机不能省

在调任何Kafka参数之前,我先做了两件事:给3台Broker都换上双口万兆网卡,把压测机和Broker全部接入同一台万兆交换机。当时买这些二手万兆网卡的成本其实很低,但效果立竿见影。为什么必须万兆?回到前面的计算——如果单节点入站流量接近300MB/s,加上Broker之间的副本同步流量和响应流量,一张万兆网卡才勉强不成为瓶颈。

网卡装好之后,别忘了检查网卡的队列设置。Linux网卡默认的RX/TX ring buffer可能只有256或512,在高压场景下会产生大量丢包和软中断堆积。我们这次压测前用ethtool做了调整:

ethtool -G eth0 rx 4096 tx 4096 ethtool -L eth0 combined 8

第二个命令是把多队列网卡的队列数设为8,让CPU能分散处理网络中断。如果不做这步,你可能会发现一个诡异的现象:Broker CPU还有大量空闲,但吞吐就是上不去,因为所有网络中断全打在一个CPU核心上,那个核先被软中断塞满了。

交换机这边也要留意。压测时如果交换机背板带宽不足,或者开启了流控,可能会出现莫名其妙的TCP重传率升高。我的建议是压测前先用ping -f或者iperf测一下各节点间的实际带宽,确认为满速,再进入Kafka层面的调优。

2.2 操作系统参数调整清单

系统层参数我调整得不多,但每一条都有明确理由。你在网上搜Kafka调优,能看到一大串sysctl配置,其中一半是给高并发TCP服务用的,另一些则是古老的推荐,放到新内核上可能有副作用。下面这张表是我这次实际使用的,也是我认为性价比最高的一组:

参数取值作用
vm.swappiness1尽量不换出页缓存,避免消息数据被swap到磁盘
vm.dirty_ratio20控制脏页占内存比例,防止刷盘时抖动
vm.dirty_background_ratio5后台开始刷脏页的阈值,配合上一项使用
net.core.rmem_max16777216增大接收缓冲区上限,配合socket buffer调优
net.core.wmem_max16777216增大发送缓冲区上限
net.ipv4.tcp_rmem4096 87380 16777216TCP接收窗口,压测时推荐加大
net.ipv4.tcp_wmem4096 65536 16777216TCP发送窗口
net.core.netdev_max_backlog250000提高网卡队列积压能力

这些参数配置写入/etc/sysctl.conf后执行sysctl -p生效。注意,不要为了追求大而把buffer调得过猛,socket buffer越大,内存占用越高,如果消息本身只有几百字节,过大的buffer反而会造成内存浪费。压测场景下16MB足够,生产环境可能还需要再评估。

2.3 用KRaft模式部署3节点Kafka集群

这次部署我选了Kafka 3.x的KRaft模式,也就是不依赖ZooKeeper的那套架构。KRaft模式最大的好处是3个节点既是Broker,又是Controller候选节点,省掉了额外维护一套ZooKeeper集群的体力活。对于只有3台机器的压测环境,这种模式非常合适。

每个节点的基础配置长这样,关键的几项我先列出来:

process.roles=broker,controller node.id=1 controller.quorum.voters=1@broker1:9093,2@broker2:9093,3@broker3:9093 listeners=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 advertised.listeners=PLAINTEXT://broker1:9092 log.dirs=/data/kraft-combined-logs num.network.threads=8 num.io.threads=16 socket.send.buffer.bytes=102400 socket.receive.buffer.bytes=102400 socket.request.max.bytes=104857600 log.segment.bytes=1073741824 log.flush.interval.messages=9223372036854775807 log.flush.interval.ms=1000 num.partitions=24 default.replication.factor=2 min.insync.replicas=1

创建topic时没有靠默认配置,而是显式指定分区和副本:

bin/kafka-topics.sh --bootstrap-server broker1:9092 \ --create --topic perf-topic \ --partitions 24 --replication-factor 2 \ --config min.insync.replicas=1

这里的分区和副本选择不是拍脑袋,我会在下一节详细展开。部署完成后,先用kafka-producer-perf-test.sh小流量跑一遍,确认3个节点都能正常收发,再进行参数调整和正式压测。

3. Kafka压测参数调优:Broker端与Producer端的配合

3.1 Broker端:线程、Socket、日志落盘怎么调

Broker端的调优逻辑其实可以总结成一句话:让网络线程和IO线程足够并行,同时别让磁盘刷盘打断节奏。我们这次改动最大的是num.io.threads,从默认的8调到了16。为什么要加大?Kafka的IO线程负责实际读写日志、处理请求,3台机器各有8个分区,如果IO线程太少,分区数据在写入时容易排队,尤其压测时大量请求同时到达,IO线程不够会直接表现为吞吐抖动。

socket.send.buffer.bytes和socket.receive.buffer.bytes分别表示Broker向客户端发送数据、接收客户端数据时使用的socket缓冲区,压测环境下我都设成了100KB左右。这个值不是越大越好,对于批量发送的小消息,适中的buffer反而能提高单个TCP连接上的利用率。

日志段大小log.segment.bytes我设成了1GB。默认值通常是1GB,但网上有人为了“增强灵活性”调小到256MB甚至128MB,这在压测场景下是个坑。段文件越小,Kafka需要创建的日志文件越密集,磁盘IOPS飙升,同时旧段文件清理也频繁,对吞吐没有任何好处。压测时保持大段文件,让消息尽量连续写入,是SATA固态盘也能支撑高吞吐的关键之一。

另外两个和刷盘相关的参数,我说说背后的坑。log.flush.interval.messages设为Long.MaxValue,log.flush.interval.ms设为1000,含义是让消息先留在页缓存里,最多1秒触发一次刷盘。很多教程喜欢设成“每10000条刷一次”或者更小值,这会直接破坏Kafka的性能模型。Kafka设计上就依赖操作系统页缓存来换吞吐,你要做的不是强制它高频落盘,而是保证网络带宽足够、CPU不被打爆、页缓存脏页比例正常。真正关心数据安全,应该通过副本机制去解决,而不是靠拉高刷盘频率。

3.2 生产者端:batch、压缩、ack确认机制的取舍

Broker端调完之后,另一大半性能来自生产者客户端。我先说一个最常见的误区:压测脚本里只设了bootstrap.servers和topic就不管了,结果Producer默认参数下拼命发小消息,每个请求就几条消息,网络往返次数爆炸,吞吐自然上不去。

生产者端的核心调优就是“攒批”。batch.size控制一个批次最多能攒多少字节,我设成了65536,也就是64KB。linger.ms控制等多久再发送,我设成了10毫秒。两者配合,意思是生产者在这10毫秒内尽量把消息攒到64KB的批次里,满了就发,没满到点也发。这样单次请求能携带大量消息,网络往返数大幅减少,Broker的请求压力也骤降。

为什么是10毫秒而不是50毫秒?linger.ms越大,消息在客户端堆积的时间越久,吞吐确实更高,但延迟也会线性上升。压测目标是高吞吐,但也不想完全牺牲实时性。10毫秒在大多数场景下是个甜点值。如果你测试的是纯离线写入场景,比如日志归档,调到50毫秒、100毫秒也完全可以。

压缩选型上我们最终用了lz4,而不是snappy或zstd。三者的差别很简单:zstd压缩率最高但CPU开销相对大,snappy居中,lz4强在压缩速度和CPU占用低。200字节的小消息,压缩率本来就不高,省下来的带宽有限,反而应该更在意CPU开销。lz4在压测机和Broker CPU都比较紧张的环境下,是性价比最高的选择。如果你愿意接受更多CPU开销,可以换zstd,网络流量能再降一截,但那是另一个调优方向了。

ack确认机制的取舍,我用一个生活类比来解释。acks=all相当于发完消息必须等对方明确回复“我收到了”,还要确保所有副本都同步了,最安全但最慢。acks=1相当于发完消息等主节点确认,在Kafka里就是leader写入日志后立刻返回,速度快很多,数据安全性上有一小段窗口。acks=0则是扔出去就不管,最快但可能丢消息。这次200万TPS压测用的是acks=1,追求高吞吐下的平衡。如果你一定要用acks=all跑200万TPS,那需要的硬件和参数配置又是另一套标准,后面的所有数字也需要重新评估。

下面这张表是我最终落地到压测脚本里的Producer核心参数:

参数值说明
acks1等待leader确认,不等待所有副本
batch.size6553664KB,控制批量发送大小
linger.ms10攒批等待时间
buffer.memory6710886464MB,生产端缓冲区
compression.typelz4压缩消息体,降低网络流量
max.in.flight.requests.per.connection5允许单个连接内多个未确认请求
enable.idempotencetrue开启幂等,实测性能损耗很小

3.3 分区与副本设计:为什么是24分区+副本因子2

分区数直接决定并行度的上限。在Kafka里,一个分区同一时刻只能由一个IO线程负责写入,所以分区太少,Broker的并行能力会被锁死。选24分区,是因为3台Broker每台8个分区,对应我们设定的16个IO线程,既不会因为分区太多导致元数据膨胀、文件句柄占用过高,也足够让每个IO线程有活干。

副本因子选2而不是3,主要原因是网络流量。如果副本因子是3,每个Broker既要接收生产者的写入流量,还要向另外两个副本推送数据,网络流量会被放大接近两倍。3台机器之间来回传数据,在200万TPS的场景下,网卡压力会急剧上升。副本因子2则是在可用性和吞吐之间取平衡——任意一台机器挂了,集群仍然有完整数据,只是部分分区会缺少冗余,但压测场景下完全够用。

min.insync.replicas设为1,配合acks=1,意味着写入时只要有leader副本在线就算成功。这里有一个值得注意的地方:acks=1和min.insync.replicas其实关系不大,min.insync.replicas只有在acks=all时才会发挥强制约束作用。很多人把这两个概念搞混,要么以为设了min.insync=2就安全,要么以为delta了复制因子就万事大吉。压测环境中抓吞吐没错,但要清楚自己在一致性上做了多少让步,别回头把压测数据等同于生产环境可用性。

4. 压测执行过程与结果解读

4.1 压测命令:从5万到200万TPS的阶梯式加压

压测工具用的是Kafka自带的kafka-producer-perf-test.sh,不用额外装一堆第三方组件,减少环境变量干扰。正式压测时,我们在两台压测机上各启动6个生产者进程,一共12个进程分担压力。单个进程的发送命令长这样:

bin/kafka-producer-perf-test.sh \ --topic perf-topic \ --num-records 360000000 \ --record-size 200 \ --throughput 200000 \ --producer-props \ bootstrap.servers=broker1:9092,broker2:9092,broker3:9092 \ batch.size=65536 \ linger.ms=10 \ buffer.memory=67108864 \ compression.type=lz4 \ acks=1

这里有几个细节值得解释。--throughput 200000表示每个进程的目标速率是每秒20万条,12个进程加起来就是240万TPS上限,实际上会低于这个值一点点,因为还有网络抖动和broker调度,最终稳定在200万附近。--num-records设成360000000,对应200万TPS跑30分钟的总量,实际跑的时候不需要一次性跑完,可以配合外部计时器控制时长。

为什么要多进程而不是单进程拉高吞吐?因为单进程在客户端侧会先遇到CPU瓶颈,JVM里的发送线程、序列化、压缩都集中在少数核心上,根本压不满网络。多进程除了分摊CPU压力,还能让分区分配更均匀,每个进程尽量分散往不同的分区发送,避免局部热点。压测时如果你发现客户端CPU已经100%但Broker还有余量,多半就是单进程压制了。

4.2 每一步怎么判断是“能上”还是“该停”

阶梯式加压是我比较推荐的做法,不要一上来就冲200万。我们是从5万、10万、20万、50万、80万、120万、150万、200万这样一路加上去的,每个阶段稳定跑5到10分钟,记录以下几项指标:

  • Broker的CPU使用率,特别是软中断占比
  • 网卡吞吐量,入站和出站分别记录
  • 磁盘iostat里的await和%util
  • ISR状态,有没有副本掉队
  • 生产者侧的99分位延迟

每加一档,先看副本是否稳定,再看延迟有没有拐头。如果ISR出现收缩,或者延迟突然从几毫秒跳到几十毫秒,说明当前节点接近某个瓶颈,需要停下来排查,而不是继续往上加。这里记录一下我们当时的阶梯数据,给你一个量级参考:

加压档位Broker平均CPU网络入站流量/节点生产者平均延迟
50万TPS约20%约70MB/s约2ms
100万TPS约38%约140MB/s约3ms
150万TPS约50%约210MB/s约4ms
200万TPS约60%约280MB/s约6ms

这组数据最直观的结论是:网络和CPU没有出现单点打满,磁盘也一直平稳,延迟虽然上升了,但还处在可接受范围,所以继续加压是安全的。如果某一步看到软中断已经超过20%,或者iostat里的await超过了10ms,就应该停下来检查那个环节。

4.3 200万TPS时集群长什么样

最终稳定在200万TPS时,3台Broker的CPU大概在60%上下,系统态的软中断比例不低,但没有哪个核心被完全打满。内存方面,free命令看到buff/cache占了全部内存的绝大部分,这是Kafka正常工作的标志——消息大量在页缓存中流动,而不是直接写盘。磁盘这边,SATA SSD在200万TPS下已经出现持续写入,但%util保持在70%以下,说明盘还能撑,瓶颈不在盘。

GC数据是另一个容易被忽略的点。我们当时给JVM配了4GB堆,压测过程中用jstat观察,Minor GC频率大约每分钟几次,全程没有Full GC。如果堆设得太小,会有大量对象频繁触发GC;设得太大比如超过8GB,则可能导致Full GC时停顿时间过长,反而吞吐骤降。Kafka并不需要很大的JVM堆,消息数据都在页缓存里,堆里主要是各类元数据、批次引用和网络缓冲。

这里还是要强调一下压测口径:我们测的是“纯生产端写入”,也就是Producer往Broker写消息的吞吐,不包含消费端读取和端到端链路。如果你看到的压测报告是Kafka全链路200万TPS,那通常是指生产加消费都能扛住200万,那背后的硬件规格、参数配置和我们今天聊的完全是两码事。比较压测数据之前,先确认大家的统计口径一致。

5. 压测常见问题与排查技巧实录

5.1 ISR频繁收缩,问题出在哪

第一次跑到120万TPS时,我们发现某个分区的ISR只剩1个副本,持续几分钟后又恢复了,然后又收缩。这种情况在压测中最常见的原因不是磁盘或CPU,而是副本同步跟不上。

ISR收缩的本质是follower拉取leader数据的间隔超时了。Kafka的副本同步是follower主动向leader发Fetch请求,如果leader侧发送缓冲不够,或者follower处理不过来,就会导致同步进度落后,最终被踢出ISR。当时我们的解决办法有三步:第一,把Broker端的socket.send.buffer.bytes从默认值调到100KB;第二,把replica.fetch.max.bytes调到10MB,保证一次拉取能带回更多数据;第三,检查OS的TCP发送缓冲区是否够大。改完这三个地方,ISR就稳定了。

排查ISR问题时要学会用命令:

bin/kafka-topics.sh --describe --bootstrap-server broker1:9092 --topic perf-topic

重点看每个分区的Leader和ISR列,如果ISR数量长期少于Replicas数量,说明有副本同步不上。压测时出现瞬时收缩可能是网络抖动,但要持续超过1分钟,就必须停下来查。

5.2 120万TPS卡住上不去:先查客户端和网卡队列

有段时间吞吐一直卡在120万左右上不去,Broker CPU还有空闲,磁盘也不忙,看起来哪哪儿都没满,可就是上不去。排查了一圈,最后发现是压测机的网卡队列太小,软中断都堆积在个别CPU核上。

这在压测中属于“非典型瓶颈”,CPU总分看起来很低,但某个核心已经被中断打满了。判断方法是看top输出里的软中断占比,或者用mpstat -P ALL观察各核使用率。如果发现最高的核心跑到了100%,而平均只有30%,基本就是中断分配不均。

解决办法很简单,把压测机和Broker的网卡多队列都打开,并用ethtool -L把combined队列数调上去。调整之后,120万的坎很快就迈过去了。这个坑的启示是:吞吐上不去时宁愿多花点时间看CPU逐核状态、看软中断分布,也不要盲目猜测是不是Broker配置不够,很多时候问题根本不在Broker。

5.3 分区热点、JVM大小、页缓存误判:三个易踩的坑

第一个坑是分区热点。创建topic时如果分区一开始没有分配均匀,某个Broker上可能会集中较多分区,导致这台机器压力远超另外两台。查kafka-topics --describe的输出,能看到分区的leader分布。如果发现leader集中在某台机器,用kafka-reassign-partitions.sh做一次均衡即可。压测前确认这一点,比压测中发现瓶颈再调整省事得多。

第二个坑是JVM堆设太大。我们一开始为了“性能”把堆设成了12GB,结果压测时每分钟出现一次Full GC,期间Broker请求完全卡住,ISR也开始收缩。后来阅读Kafka对堆的建议,调到4GB,Full GC消失,吞吐反而上去了。Kafka的高吞吐靠的是页缓存和顺序写,堆只是辅助,不适合走大数据内存那套思路。

第三个坑是页缓存误判。200万TPS压测时,free命令看到buff/cache占了几乎全部内存,很多刚开始接触Kafka的同学会以为是内存泄漏,其实这是正常状态。Kafka消息进入Broker后会先写页缓存,再由后台线程异步刷盘,所以buff/cache高是健康的标志。真正需要关注的是dirty回写比例,以及是否频繁出现swap。swap只要不为0,就可能影响吞吐,这也是开头把vm.swappiness调成1的原因。

最后再分享一点个人体会:三台廉价服务器能扛住200万TPS,不是因为服务器不廉价了,而是因为Kafka的设计本身就允许你通过调优把性能吃透。但你别急着拿这套配置直接套到生产上——生产环境的数据安全要求、消息大小模型、消费端压力都不同,同样的参数可能背道而驰。一次压测解决的是“当前场景下系统能不能扛住”,而调优的真正价值,是让你理解每个参数背后到底在交换什么。摸透了这些交换关系,你才算真正上手了Kafka。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询