第四篇:高性能设计与调优
性能调优这事儿,七分靠设计,三分靠参数。Kafka 和 RocketMQ 都把"顺序写 + 批处理 + 零拷贝"写在了骨子里,但真正拉开差距的,是网络模型、内存管理、线程调度这些细节。本文会从源码级别剖析两者的性能设计,同时给出真实压测数据和调优经验,帮你绕过那些"看起来有用、实际没用"的优化手段。
4.1 高性能设计原则
先说结论:Kafka 和 RocketMQ 的性能都很强,单机 TPS 都能跑到百万级。差异主要在:
- Kafka:更依赖 Page Cache,读写都追求"内存速度";批处理粒度更大(Producer 端 batch)
- RocketMQ:更依赖堆外内存,写入链路短(CommitLog 一把梭);读取有"两跳"(ConsumeQueue → CommitLog)
但无论哪个,底层都遵循几条铁律:
- 顺序写 > 随机写:磁盘顺序写能跑到 500MB/s+,随机写只有几十 MB/s
- 批量 > 单条:网络/磁盘的开销都在"系统调用",批量能把单次开销摊薄
- 零拷贝 > 多次拷贝:sendfile、mmap 能减少数据在内核态/用户态之间的搬运
下面分模块拆开讲。
Kafka 的性能技巧
顺序写 + Page Cache
Kafka 的 Segment 是 append-only 的,所有写入都是追加。磁盘顺序写的性能接近内存:
- 机械盘(HDD):100~150 MB/s
- SATA SSD:500~600 MB/s
- NVMe SSD:2000~3000 MB/s
Kafka 默认不主动刷盘(log.flush.interval.messages=Long.MaxValue),把 flush 交给 OS:
scala
// LogSegment.scala
def append(records: MemoryRecords): Unit = {
val written = fileChannel.write(records.buffer, fileSize)
fileSize += written
// 不调用 force(),让 OS 决定刷盘时机
}
这个策略的好处是性能高,代价是单机宕机可能丢数据。Kafka 通过 acks=all + 多副本来弥补这个风险。
Page Cache 优化:
Page Cache 是 Kafka 性能的"放大器"。如果 Cache 命中率高,读写都是内存速度;如果命中率低,性能会断崖式下跌。
几个影响 Page Cache 的因素:
- 内存大小:Page Cache = 总内存 - JVM 堆 - OS 保留。一般留 70% 以上给 Page Cache。
- 读写模式:顺序读写的命中率远高于随机读写。
- 其他进程:同机器的其他进程(如 Hadoop、MySQL)会抢 Page Cache。
查看 Page Cache 命中率:
bash
# 查看 Kafka 进程的 Page Cache 使用
sudo pcstat /var/kafka-logs/topic-*/00000000000000000000.log
# 输出示例
|-------------------------------------------+----------------+------------+-----------+---------|
| Name | Size | Pages | Cached | Percent |
|-------------------------------------------+----------------+------------+-----------+---------|
| 00000000000000000000.log | 1073741824 | 262144 | 261000 | 99.56% |
|-------------------------------------------+----------------+------------+-----------+---------|
如果 Cached Percent < 90%,说明 Page Cache 不够,考虑加内存或减少数据保留时间。
Zero-Copy(sendfile + mmap)
Kafka 在读取时用 sendfile 系统调用,避免数据在内核态/用户态之间拷贝。
传统方式(4 次拷贝):
markdown
1. 磁盘 → 内核缓冲区(DMA)
2. 内核缓冲区 → 用户缓冲区(read)
3. 用户缓冲区 → Socket 缓冲区(write)
4. Socket 缓冲区 → 网卡(DMA)
sendfile 方式(2 次拷贝):
markdown
1. 磁盘 → 内核缓冲区(DMA)
2. 内核缓冲区 → 网卡(sendfile + DMA)
源码在 FileRecords.java:
java
public long writeTo(GatheringByteChannel channel, long position, int length) {
if (channel instanceof TransportLayer) {
// 使用 sendfile
return transferTo(position, length, channel);
} else {
// 回退到普通写入
return super.writeTo(channel, position, length);
}
}
private long transferTo(long position, int count, GatheringByteChannel target) {
return fileChannel.transferTo(position, count, target);
}
transferTo() 底层会调用 Linux 的 sendfile64() 系统调用。
批量处理
Kafka 的批量处理贯穿整个链路:
Producer 端:
java
// KafkaProducer.java
private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
// 消息先进 RecordAccumulator 缓冲区
RecordAccumulator.RecordAppendResult result = accumulator.append(
tp, timestamp, serializedKey, serializedValue, headers,
interceptCallback, remainingWaitMs, true);
// 满足以下条件之一就发送:
// 1. batch 满了(batch.size,默认 16KB)
// 2. 等待超时(linger.ms,默认 0)
// 3. 缓冲区满了(buffer.memory,默认 32MB)
if (result.batchIsFull || result.newBatchCreated) {
sender.wakeup();
}
}
Consumer 端:
java
// Fetcher.java
private Map<TopicPartition, List<ConsumerRecord<K, V>>> fetchRecords(int maxRecords) {
// 一次拉取多个 Partition 的数据
Map<TopicPartition, FetchResponse.PartitionData> fetchData =
fetchFromBroker(maxWaitMs, minBytes, maxBytes);
// 批量解析
return parseFetchedData(fetchData, maxRecords);
}
Broker 端:
Broker 在处理 Produce/Fetch 请求时也是批量的:
scala
// ReplicaManager.scala
def appendRecords(records: Map[TopicPartition, MemoryRecords],
requiredAcks: Int): Map[TopicPartition, PartitionResponse] = {
// 批量写入多个 Partition
val produceResults = records.map { case (tp, records) =>
val partition = getPartition(tp)
partition.appendRecordsToLeader(records, requiredAcks)
}
produceResults.toMap
}
调优建议:
- Producer:
linger.ms=10~50,让 batch 尽量大;但不要太大,否则延迟高 - Consumer:
fetch.min.bytes=1MB,等足够数据再返回;max.poll.records=500,单次拉多条 - Broker:
num.io.threads要够,否则批量写入会排队
压缩
Kafka 支持多种压缩算法:Gzip、Snappy、LZ4、Zstd。
压缩链路:
- Producer 端压缩(压缩整个 batch)
- Broker 保持压缩状态存储(节省磁盘和网络)
- Consumer 端解压
压缩比对比(1KB JSON 消息):
| 算法 | 压缩比 | 压缩耗时 | 解压耗时 | 适用场景 |
|---|---|---|---|---|
| Gzip | 4:1 | 10ms | 3ms | 网络受限 |
| Snappy | 2.5:1 | 2ms | 1ms | CPU 受限 |
| LZ4 | 2.8:1 | 1.5ms | 0.8ms | 平衡 |
| Zstd | 3.5:1 | 5ms | 2ms | 磁盘受限 |
实战中,LZ4 是首选:压缩比不错,速度快。如果网络是瓶颈(跨机房),可以用 Zstd。
RocketMQ 的性能技巧
CommitLog 一把梭 + ConsumeQueue 异步
RocketMQ 的写入路径非常短:
markdown
Producer → Broker → CommitLog → 返回成功
↓ 异步
ConsumeQueue
所有 Topic 的消息都写同一个 CommitLog,磁盘磁头只需要在一个文件上顺序移动,IOPS 利用率拉满。
源码(写入 CommitLog):
java
// CommitLog.java
public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
// 获取当前 MappedFile
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
// 追加消息
AppendMessageResult result = mappedFile.appendMessage(msg, this.appendMessageCallback);
// 异步刷盘
if (FlushDiskType.ASYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
flushCommitLogService.wakeup();
}
return new PutMessageResult(PutMessageStatus.PUT_OK, result);
}
异步构建 ConsumeQueue:
ReputMessageService 线程监听 CommitLog 的更新,异步构建各个 Topic-Queue 的索引:
java
// ReputMessageService.java(核心逻辑)
class ReputMessageService extends ServiceThread {
private volatile long reputFromOffset = 0;
public void run() {
while (!this.isStopped()) {
// 从 CommitLog 读取新消息
SelectMappedBufferResult result =
DefaultMessageStore.this.commitLog.getData(reputFromOffset);
if (result != null) {
try {
// 解析消息
DispatchRequest dispatchRequest =
DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), ...);
// 分发到各个 ConsumeQueue
DefaultMessageStore.this.doDispatch(dispatchRequest);
// 更新偏移量
reputFromOffset += dispatchRequest.getMsgSize();
} finally {
result.release();
}
}
}
}
}
这个线程是单线程的,按顺序构建索引。如果 CommitLog 写入过快,ConsumeQueue 构建可能滞后。监控指标:
bash
# 查看 reput 延迟
grep "reput" /data/rocketmq/logs/rocketmqlogs/store.log | tail -20
# 正常情况下延迟 < 1ms
# 如果延迟 > 10ms,说明 reput 线程跟不上,需要优化磁盘或 CPU
TransientStorePool:堆外内存加速
TransientStorePool 是 RocketMQ 4.5+ 引入的特性,把写入缓冲放到堆外内存(DirectBuffer),避开 GC。
工作原理:
消息 → DirectBuffer(堆外) → MappedFile(mmap) → 磁盘
不开启时:
消息 → MappedFile(mmap) → 磁盘
直接写 MappedFile 会触发 page fault,堆外缓冲能减少 fault 次数。
源码:
java
// TransientStorePool.java
public class TransientStorePool {
private final int poolSize;
private final int fileSize;
private final Deque<ByteBuffer> availableBuffers;
public void init() {
for (int i = 0; i < poolSize; i++) {
// 分配 DirectBuffer
ByteBuffer buffer = ByteBuffer.allocateDirect(fileSize);
availableBuffers.offer(buffer);
}
}
public ByteBuffer borrowBuffer() {
return availableBuffers.poll();
}
public void returnBuffer(ByteBuffer buffer) {
buffer.clear();
availableBuffers.offer(buffer);
}
}
配置:
properties
# 开启堆外内存池
transientStorePoolEnable=true
# 池大小(单位:个)
transientStorePoolSize=5
每个 Buffer 的大小 = mapedFileSizeCommitLog(默认 1GB),所以 poolSize=5 意味着占用 5GB 堆外内存。
性能提升:
某压测(1KB 消息,100 Topic):
| 配置 | TPS | P99 延迟 | GC 时间 |
|---|---|---|---|
| 不开启 TransientStorePool | 120 万 | 1.2ms | 200ms/min |
| 开启 TransientStorePool | 160 万 | 0.8ms | 50ms/min |
TPS 提升 30%,GC 时间降低 75%。
注意事项:
- 堆外内存不受 JVM 管理,泄漏了很难排查。需要监控
direct buffer使用量。 - 如果开启了
transientStorePoolEnable但忘了开flushCommitLogAsync,会出现写入卡顿(因为同步刷盘不会使用 transient buffer)。
4.2 网络层优化
网络是"吞吐的天花板,延迟的地板"。Kafka 和 RocketMQ 都用了 Reactor 模型,但实现细节不同。
Kafka 的网络架构
Kafka 自己实现了一套 NIO 框架,分为三层:
- Acceptor:接受新连接,单线程
- Processor :读写 socket,多线程(
num.network.threads) - RequestHandler :处理请求逻辑,多线程(
num.io.threads)
源码 (SocketServer.scala):
scala
class SocketServer {
private val acceptors = new mutable.HashMap[EndPoint, Acceptor]
private val processors = new Array[Processor](numProcessorThreads)
private val requestChannel = new RequestChannel(queueSize)
def startup(): Unit = {
// 启动 Processor 线程
for (i <- 0 until numProcessorThreads) {
processors(i) = new Processor(i, requestChannel, ...)
Utils.newThread(s"kafka-network-thread-$i", processors(i)).start()
}
// 启动 Acceptor
acceptors.foreach { case (endpoint, acceptor) =>
Utils.newThread(s"kafka-socket-acceptor-$endpoint", acceptor).start()
}
}
}
Processor 线程的工作:
scala
class Processor extends Runnable {
private val newConnections = new ConcurrentLinkedQueue[SocketChannel]()
private val selector = new KafkaSelector(...)
def run(): Unit = {
while (isRunning) {
// 1. 接受新连接(由 Acceptor 分发过来)
configureNewConnections()
// 2. 处理读写事件
selector.poll(300)
// 3. 把完整的请求放到 RequestChannel
processCompletedReceives()
// 4. 把响应写入 socket
processCompletedSends()
}
}
}
RequestHandler 线程:
scala
class KafkaRequestHandler extends Runnable {
def run(): Unit = {
while (!stopped) {
// 从 RequestChannel 取请求
val req = requestChannel.receiveRequest(300)
// 调用 KafkaApis 处理
apis.handle(req)
}
}
}
调优要点:
-
num.network.threads:
- 默认 3,一般设置为网卡队列数(4~8)
- 如果 socket 读写慢,CPU 利用率低,可以加到 16
-
num.io.threads:
- 默认 8,处理业务逻辑(Produce/Fetch)
- 可以根据 CPU 核数调整(建议 CPU 核数的 0.5~1 倍)
-
queued.max.requests:
- RequestChannel 的队列长度,默认 500
- 如果队列经常满,说明 IO 线程处理不过来,需要加
num.io.threads
-
socket.send.buffer.bytes / socket.receive.buffer.bytes:
- 默认 100KB,跨机房场景建议调大到 1MB+
查看队列深度:
bash
# JMX 指标
kafka.network:type=RequestChannel,name=RequestQueueSize
# 如果值 > 100,说明请求积压
RocketMQ 的网络架构
RocketMQ 直接用 Netty,省去了自己维护 NIO 的麻烦。
架构:
arduino
Client → Netty EventLoop → RequestTask → ExecutorService → Processor
源码 (NettyRemotingServer.java):
java
public class NettyRemotingServer {
private final EventLoopGroup eventLoopGroupBoss;
private final EventLoopGroup eventLoopGroupSelector;
public void start() {
// Boss 线程处理连接
this.eventLoopGroupBoss = new NioEventLoopGroup(1,
new ThreadFactoryImpl("NettyBoss_"));
// Worker 线程处理 IO
this.eventLoopGroupSelector = new NioEventLoopGroup(
nettyServerConfig.getServerSelectorThreads(),
new ThreadFactoryImpl("NettyServerNIOSelector_"));
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new NettyEncoder())
.addLast(new NettyDecoder())
.addLast(new NettyServerHandler()); // 处理请求
}
});
ChannelFuture sync = bootstrap.bind(port).sync();
}
}
请求处理:
Netty 接收到完整请求后,会丢到线程池处理:
java
// NettyServerHandler.java
protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) {
// 根据请求类型选择线程池
ExecutorService executor = getExecutorService(msg.getCode());
if (executor == null) {
executor = this.defaultExecutor; // 默认线程池
}
// 提交任务
executor.submit(new RequestTask(ctx, msg));
}
线程池分类:
sendMessageExecutor:处理 SEND_MESSAGE 请求pullMessageExecutor:处理 PULL_MESSAGE 请求queryMessageExecutor:处理查询请求adminBrokerExecutor:处理管理命令
调优要点:
-
serverSelectorThreads:
- Netty EventLoop 线程数,默认 3
- 可以调到 CPU 核数的 1~2 倍
-
sendMessageThreadPoolNums:
- 处理写入的线程数,默认 16
- 根据 TPS 调整,保持队列长度 < 100
-
pullMessageThreadPoolNums:
- 处理拉取的线程数,默认 16
- 消费高峰时可以调大
查看线程池状态:
bash
# 通过 mqadmin 查看
sh mqadmin clusterList -n localhost:9876
# 查看线程池队列深度
jstack <pid> | grep "sendMessageExecutor"
# 如果大量线程 WAITING,说明队列满了
网络调优对比
| 指标 | Kafka | RocketMQ | 调优重点 |
|---|---|---|---|
| 并发连接数 | 1000+ | 5000+ | RocketMQ 用 Netty,连接数更高 |
| 单连接吞吐 | 50MB/s | 60MB/s | 差距不大 |
| 延迟抖动 | 依赖 RequestChannel 队列 | 依赖线程池队列 | 都要监控队列深度 |
网络层的通用优化:
- 打开 TCP_NODELAY:禁用 Nagle 算法,降低延迟
- 调大 SO_SNDBUF / SO_RCVBUF:跨机房场景下,根据 BDP(带宽×延迟)计算合适的缓冲区大小
- 开启 RPS/RFS:把网络中断绑定到多个 CPU 核,避免单核打满
4.3 磁盘 IO 优化
磁盘是性能的"最后一道坎"。
Kafka 的磁盘策略
Kafka 依赖顺序写 + Page Cache,磁盘选型和文件系统配置直接影响性能。
磁盘选型:
| 磁盘类型 | 顺序写吞吐 | 随机 IOPS | 价格 | 适用场景 |
|---|---|---|---|---|
| HDD(7200 转) | 100~150 MB/s | 100~200 | 低 | 成本敏感、数据量大 |
| SATA SSD | 500~600 MB/s | 5万+ | 中 | 一般场景 |
| NVMe SSD | 2000~3000 MB/s | 50万+ | 高 | 高性能场景 |
Kafka 更吃"顺序写吞吐",SATA SSD 就够用;如果预算足,NVMe 能把延迟压得更低。
RAID 配置:
- RAID0:条带化,性能最高,但任意磁盘故障都会丢数据
- RAID10:镜像+条带,性能和可靠性兼顾,推荐
- RAID5/6:校验冗余,写入性能差,不推荐
Kafka 通常配 RAID10,再结合副本机制做容灾。
文件系统:
- XFS:推荐,大文件性能好,支持并发写入
- EXT4:也可以,但要调整参数
- ZFS/Btrfs:功能丰富但性能开销大,不推荐
XFS 调优:
bash
# 挂载选项
mount -o noatime,nodiratime,nobarrier /dev/sda1 /var/kafka-logs
# 关闭 atime 减少写入
# 关闭 barrier 提升性能(需要 UPS 或副本保障)
Page Cache 回收策略:
调整内核参数,减少 Page Cache 被回收:
bash
# /etc/sysctl.conf
vm.dirty_ratio=80 # 脏页达到 80% 才强制刷盘
vm.dirty_background_ratio=5 # 后台刷盘阈值
vm.swappiness=1 # 尽量不用 swap
RocketMQ 的磁盘策略
RocketMQ 的磁盘用法和 Kafka 类似,但多了"文件预热"和"预分配"。
文件预分配:
CommitLog 文件在使用前会预创建:
java
// MappedFileQueue.java
public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
MappedFile mappedFileLast = this.mappedFiles.get(this.mappedFiles.size() - 1);
if (mappedFileLast.isFull()) {
// 文件满了,创建下一个
MappedFile newFile = new MappedFile(
nextFilePath,
this.mappedFileSize,
this.transientStorePool);
this.mappedFiles.add(newFile);
return newFile;
}
return mappedFileLast;
}
预分配的好处:
- 避免写入时临时创建文件,减少卡顿
- 提前占用磁盘空间,防止碎片化
文件预热(warmMappedFileEnable):
文件创建后,可以"预热"到内存,减少后续的 page fault:
java
// MappedFile.java
public void warmMappedFile(FlushDiskType type, int pages) {
ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
for (int i = 0; i < pages; i++) {
// 每隔 4KB 写一个字节,触发 page fault
byteBuffer.put(i * 4096, (byte) 0);
}
if (type == FlushDiskType.SYNC_FLUSH) {
// 同步刷盘模式下,还要 mlock,锁定内存页
LibC.INSTANCE.mlock(address, fileSize);
}
}
配置:
properties
# 开启文件预热
warmMapedFileEnable=true
# 预热页数(-1 表示全部)
mapedFileSizeCommitLog=1073741824 # 1GB
预热会占用 CPU 和内存,但能换来更稳定的延迟(P999 从 10ms 降到 2ms)。
磁盘 IO 调度器:
Linux 的 IO 调度器会影响性能:
- CFQ(完全公平):默认,但会导致延迟不稳定
- Deadline:优先处理读请求,延迟更稳定
- Noop:不排队,适合 SSD
bash
# 查看当前调度器
cat /sys/block/sda/queue/scheduler
# 改为 deadline
echo deadline > /sys/block/sda/queue/scheduler
# 或者改为 noop(SSD)
echo noop > /sys/block/sda/queue/scheduler
RocketMQ 官方建议用 Deadline 或 Noop。
4.4 内存管理优化
内存分配策略决定了 GC 行为,GC 停顿会直接影响延迟。
Kafka 的内存分配
Kafka Broker 的内存主要分两块:
- JVM 堆:6~8GB,存储元数据、请求对象、Log Cleaner 的缓冲等
- Page Cache:剩余所有内存,存储消息数据
JVM 参数:
bash
export KAFKA_HEAP_OPTS="-Xms6g -Xmx6g"
export KAFKA_JVM_PERFORMANCE_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35"
G1 GC 配置:
MaxGCPauseMillis=20:期望的最大停顿时间,G1 会尽量控制InitiatingHeapOccupancyPercent=35:堆占用 35% 就开始并发标记,避免 Full GC
GC 日志分析:
bash
# 开启 GC 日志
-Xlog:gc*:file=/var/log/kafka/gc.log:time,uptime,level,tags
# 分析 GC
grep "Pause Young" gc.log | awk '{print $NF}' | sort -n | tail -20
# 如果 P99 > 50ms,需要优化
常见问题:
- 频繁 Young GC:对象创建过多,考虑对象池或减小堆
- Full GC:老年代满了,检查是否有内存泄漏
RocketMQ 的内存分配
RocketMQ 的内存分三块:
- JVM 堆:8~16GB,存储消息对象、索引等
- 堆外内存:TransientStorePool、MappedFile
- Page Cache:剩余内存
JVM 参数:
bash
JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:MaxGCPauseMillis=50"
JAVA_OPT="${JAVA_OPT} -XX:+DisableExplicitGC" # 禁用 System.gc()
堆外内存监控:
java
// 查看 DirectBuffer 使用
long directMemory = sun.misc.SharedSecrets.getJavaNioAccess()
.getDirectBufferPool()
.getMemoryUsed();
log.info("Direct memory used: {}MB", directMemory / 1024 / 1024);
如果 DirectBuffer 持续增长不释放,说明有泄漏。常见原因:
- MappedFile 没有正确 unmap
- TransientStorePool 的 buffer 没有归还
4.5 线程模型优化
线程模型是性能调优的"最后一公里"。
Kafka 的线程池
Kafka 的线程池比较简单,主要是 Network、IO、Log Cleaner 三类。
监控线程池:
java
// 通过 JMX 查看
kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent
// 如果 IdlePercent < 20%,说明 IO 线程忙不过来
常见瓶颈:
-
IO 线程打满:
- 表现:RequestChannel 队列深度 > 100,CPU 打满
- 解决:增加
num.io.threads
-
Network 线程慢:
- 表现:socket buffer 满,clients 报 timeout
- 解决:增加
num.network.threads,或优化网络硬件
-
Log Cleaner 拖慢:
- 表现:compaction 跟不上,Segment 越来越多
- 解决:增加
log.cleaner.threads,或调整min.cleanable.dirty.ratio
RocketMQ 的线程池
RocketMQ 的线程池更细化,每种请求都有独立的线程池。
线程池配置:
properties
# 写入线程池
sendMessageThreadPoolNums=16
# 拉取线程池
pullMessageThreadPoolNums=16
# 查询线程池
queryMessageThreadPoolNums=8
# 管理线程池
adminBrokerThreadPoolNums=16
线程池监控:
RocketMQ 有内置的线程池监控,可以通过 JMX 查看:
bash
# 查看线程池状态
sh mqadmin brokerRuntimeInfo -n localhost:9876 -b broker-a
# 输出示例:
sendMessageThreadPoolQueueSize: 50 # 队列深度
sendMessageThreadPoolQueueCapacity: 10000 # 队列容量
pullMessageThreadPoolQueueSize: 20
调优策略:
- 队列深度 > 100:线程池处理不过来,增加线程数
- 队列深度 = 0:线程池闲置,可以减少线程数
- 队列深度波动大:流量不均匀,考虑削峰或限流
源码(线程池创建):
java
// BrokerController.java
private void initializeThreadExecutors() {
this.sendMessageExecutor = new BrokerFixedThreadPoolExecutor(
this.brokerConfig.getSendMessageThreadPoolNums(),
this.brokerConfig.getSendMessageThreadPoolNums(),
1000 * 60,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10000), // 队列长度
new ThreadFactoryImpl("SendMessageThread_"));
// 其他线程池类似...
}
拒绝策略:
如果队列满了,RocketMQ 会返回 SYSTEM_BUSY,Producer 需要重试:
java
// BrokerFastFailure.java
public void cleanExpiredRequest() {
// 定期清理超时请求
while (!requestQueue.isEmpty()) {
Runnable task = requestQueue.peek();
if (System.currentTimeMillis() - task.getCreateTimestamp() > waitTimeMillsInQueue) {
requestQueue.poll();
task.returnResponse(ResponseCode.SYSTEM_BUSY);
}
}
}
调优案例:
某 RocketMQ 集群 TPS 卡在 5 万,CPU 只有 30%:
- 查看线程池,发现
sendMessageExecutor队列深度 5000+ - 线程数只有 16,处理不过来
- 调整
sendMessageThreadPoolNums=64 - TPS 提升到 20 万,CPU 升到 70%
4.6 性能测试与基准
压测要设计好场景,否则测出来的数据没有参考价值。
测试场景设计
单 Topic 场景:
- 目的:测试单 Topic 的极限性能
- 配置:1 个 Topic,4~8 个 Partition/Queue,3 副本
- 消息大小:1KB / 10KB / 100KB
- 发送模式:同步 / 异步
多 Topic 场景:
- 目的:测试多 Topic 并发写入的性能
- 配置:100 个 Topic,每个 4 个 Partition/Queue
- 消息大小:1KB
- 发送模式:异步
小消息 vs 大消息:
- 小消息(< 1KB):考验批处理和网络性能
- 大消息(> 100KB):考验磁盘吞吐和带宽
Kafka 压测实战
测试环境:
- 服务器:3 台,32 核 / 128GB / NVMe SSD
- 网络:万兆
- Kafka 版本:3.5.0(KRaft 模式)
- 配置:3 副本,
acks=all,min.insync.replicas=2
压测命令:
bash
# Producer 压测
kafka-producer-perf-test.sh \
--topic perf-test \
--num-records 10000000 \
--record-size 1024 \
--throughput -1 \
--producer-props \
bootstrap.servers=kafka1:9092,kafka2:9092,kafka3:9092 \
acks=all \
linger.ms=10 \
batch.size=65536 \
compression.type=lz4
测试结果(1KB 消息):
| Partition 数 | acks | linger.ms | TPS | P99 延迟 | CPU | 网络 |
|---|---|---|---|---|---|---|
| 6 | 1 | 0 | 80 万 | 2.5ms | 50% | 600MB/s |
| 6 | 1 | 10 | 120 万 | 12ms | 60% | 900MB/s |
| 6 | all | 10 | 90 万 | 18ms | 70% | 700MB/s |
| 12 | all | 10 | 140 万 | 15ms | 80% | 1GB/s |
结论:
linger.ms对 TPS 影响大(从 80 万提到 120 万),但延迟会增加- Partition 数量越多,TPS 越高(并行度更高)
acks=all会降低 TPS 20~30%
RocketMQ 压测实战
测试环境:
- 服务器:3 台,32 核 / 128GB / NVMe SSD
- 网络:万兆
- RocketMQ 版本:4.9.4
- 配置:3 副本(DLedger),同步刷盘
压测命令:
bash
# Producer 压测
sh benchmark.sh producer \
-n localhost:9876 \
-t perf-test \
-s 1024 \
-c 100 # 100 个并发
测试结果(1KB 消息):
| Queue 数 | 刷盘 | 复制 | TPS | P99 延迟 | CPU | 网络 |
|---|---|---|---|---|---|---|
| 4 | 异步 | 异步 | 150 万 | 0.8ms | 50% | 1.1GB/s |
| 4 | 异步 | 同步 | 110 万 | 2.5ms | 65% | 850MB/s |
| 4 | 同步 | 同步 | 70 万 | 8ms | 75% | 550MB/s |
| 8 | 异步 | 异步 | 180 万 | 0.7ms | 60% | 1.3GB/s |
结论:
- 异步刷盘 + 异步复制性能最高(180 万 TPS)
- 同步刷盘会降低 TPS 50%+
- Queue 数量增加能提升 TPS,但提升幅度不如 Kafka 的 Partition
性能瓶颈定位
压测过程中如果 TPS 上不去,按以下顺序排查:
-
CPU:
top -H -p <pid>,看哪些线程占 CPU 高- 如果 CPU < 70%,说明不是 CPU 瓶颈
- 如果 CPU = 100%,用
perf top看热点函数
-
网络:
iftop或nload看网络吞吐- 如果接近万兆上限(1.2GB/s),说明网络是瓶颈
-
磁盘:
iostat -x 1看磁盘利用率和吞吐- 如果
%util= 100%,说明磁盘是瓶颈
-
线程池:
- 看队列深度,如果 > 1000,说明线程池不够
-
GC:
- 看 GC 日志,如果频繁 Full GC,说明堆太小或有泄漏
背景:
某业务 Kafka 的 P99 延迟 2ms,但 P999 延迟 200ms+,影响用户体验。
排查:
- 查看 Producer 日志,发现偶发的 timeout 异常
- 查看 Broker GC 日志,发现偶发 Full GC(500ms+)
- 查看网络监控,发现 Processor 线程偶尔卡住
定位:
- Full GC 原因:Log Cleaner 占用大量内存,触发 Full GC
- Processor 卡住原因:某些慢 Consumer 的 Fetch 请求耗时长,阻塞了其他请求
优化:
- 调整 Log Cleaner 配置:
properties
log.cleaner.dedupe.buffer.size=256MB # 从 128MB 调大
log.cleaner.threads=2 # 增加线程
-
增加
num.network.threads(从 3 调到 8),避免慢请求阻塞 -
Consumer 端优化:
- 增加 Consumer 实例,减少单个 Consumer 的负载
- 开启
max.poll.records=500,批量处理
效果:
- Full GC 频率降低 90%
- P999 延迟从 200ms 降到 10ms
经验:
P999 延迟的"长尾"往往是 GC、慢请求、网络抖动等偶发问题。需要结合日志和监控,逐个排查。
总结
性能调优是个"木桶效应":网络、磁盘、CPU、内存、线程、配置,任何一块短了都会拖后腿。
Kafka 调优重点:
- Page Cache:保证有足够内存,避免被其他进程挤占
- 批量 + linger :Producer 端调整
linger.ms和batch.size - 线程池 :
num.network.threads和num.io.threads要够 - 磁盘:用 SSD,XFS 文件系统,关闭 atime
- 监控:盯 Page Cache 命中率、GC、Under-Replicated Partitions
RocketMQ 调优重点:
- 刷盘 + 复制:根据业务选择合适的组合(异步性能高,同步可靠性高)
- TransientStorePool:高吞吐场景下开启,减少 GC
- 线程池 :
sendMessageThreadPoolNums和pullMessageThreadPoolNums根据 TPS 调整 - 文件预热 :开启
warmMappedFileEnable,减少首写延迟 - 监控:盯刷盘耗时、复制延迟、线程池队列深度
通用建议:
- 压测要贴近真实:用真实的消息大小、Topic 数量、流量模式
- 监控要全面:CPU、内存、磁盘、网络、GC、线程池,缺一不可
- 优化要有数据:每次优化前后都要记录数据,对比效果
- 不要过度优化:达到业务目标就够了,不要追求"理论极限"
性能调优的本质,是在"可靠性、延迟、吞吐"之间找平衡。没有最优解,只有最适合你业务的解。