RocketMQ vs Kafka04 - 高性能设计与调优

第四篇:高性能设计与调优

性能调优这事儿,七分靠设计,三分靠参数。Kafka 和 RocketMQ 都把"顺序写 + 批处理 + 零拷贝"写在了骨子里,但真正拉开差距的,是网络模型、内存管理、线程调度这些细节。本文会从源码级别剖析两者的性能设计,同时给出真实压测数据和调优经验,帮你绕过那些"看起来有用、实际没用"的优化手段。


4.1 高性能设计原则

先说结论:Kafka 和 RocketMQ 的性能都很强,单机 TPS 都能跑到百万级。差异主要在:

  • Kafka:更依赖 Page Cache,读写都追求"内存速度";批处理粒度更大(Producer 端 batch)
  • RocketMQ:更依赖堆外内存,写入链路短(CommitLog 一把梭);读取有"两跳"(ConsumeQueue → CommitLog)

但无论哪个,底层都遵循几条铁律:

  1. 顺序写 > 随机写:磁盘顺序写能跑到 500MB/s+,随机写只有几十 MB/s
  2. 批量 > 单条:网络/磁盘的开销都在"系统调用",批量能把单次开销摊薄
  3. 零拷贝 > 多次拷贝: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 的因素:

  1. 内存大小:Page Cache = 总内存 - JVM 堆 - OS 保留。一般留 70% 以上给 Page Cache。
  2. 读写模式:顺序读写的命中率远高于随机读写。
  3. 其他进程:同机器的其他进程(如 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。

压缩链路

  1. Producer 端压缩(压缩整个 batch)
  2. Broker 保持压缩状态存储(节省磁盘和网络)
  3. 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 框架,分为三层:

  1. Acceptor:接受新连接,单线程
  2. Processor :读写 socket,多线程(num.network.threads
  3. 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)
    }
  }
}

调优要点

  1. num.network.threads

    • 默认 3,一般设置为网卡队列数(4~8)
    • 如果 socket 读写慢,CPU 利用率低,可以加到 16
  2. num.io.threads

    • 默认 8,处理业务逻辑(Produce/Fetch)
    • 可以根据 CPU 核数调整(建议 CPU 核数的 0.5~1 倍)
  3. queued.max.requests

    • RequestChannel 的队列长度,默认 500
    • 如果队列经常满,说明 IO 线程处理不过来,需要加 num.io.threads
  4. 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:处理管理命令

调优要点

  1. serverSelectorThreads

    • Netty EventLoop 线程数,默认 3
    • 可以调到 CPU 核数的 1~2 倍
  2. sendMessageThreadPoolNums

    • 处理写入的线程数,默认 16
    • 根据 TPS 调整,保持队列长度 < 100
  3. 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 队列 依赖线程池队列 都要监控队列深度

网络层的通用优化

  1. 打开 TCP_NODELAY:禁用 Nagle 算法,降低延迟
  2. 调大 SO_SNDBUF / SO_RCVBUF:跨机房场景下,根据 BDP(带宽×延迟)计算合适的缓冲区大小
  3. 开启 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 的内存主要分两块:

  1. JVM 堆:6~8GB,存储元数据、请求对象、Log Cleaner 的缓冲等
  2. 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 的内存分三块:

  1. JVM 堆:8~16GB,存储消息对象、索引等
  2. 堆外内存:TransientStorePool、MappedFile
  3. 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 线程忙不过来

常见瓶颈

  1. IO 线程打满

    • 表现:RequestChannel 队列深度 > 100,CPU 打满
    • 解决:增加 num.io.threads
  2. Network 线程慢

    • 表现:socket buffer 满,clients 报 timeout
    • 解决:增加 num.network.threads,或优化网络硬件
  3. 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

调优策略

  1. 队列深度 > 100:线程池处理不过来,增加线程数
  2. 队列深度 = 0:线程池闲置,可以减少线程数
  3. 队列深度波动大:流量不均匀,考虑削峰或限流

源码(线程池创建):

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%:

  1. 查看线程池,发现 sendMessageExecutor 队列深度 5000+
  2. 线程数只有 16,处理不过来
  3. 调整 sendMessageThreadPoolNums=64
  4. 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=allmin.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 上不去,按以下顺序排查:

  1. CPU

    • top -H -p <pid>,看哪些线程占 CPU 高
    • 如果 CPU < 70%,说明不是 CPU 瓶颈
    • 如果 CPU = 100%,用 perf top 看热点函数
  2. 网络

    • iftopnload 看网络吞吐
    • 如果接近万兆上限(1.2GB/s),说明网络是瓶颈
  3. 磁盘

    • iostat -x 1 看磁盘利用率和吞吐
    • 如果 %util = 100%,说明磁盘是瓶颈
  4. 线程池

    • 看队列深度,如果 > 1000,说明线程池不够
  5. GC

    • 看 GC 日志,如果频繁 Full GC,说明堆太小或有泄漏

背景

某业务 Kafka 的 P99 延迟 2ms,但 P999 延迟 200ms+,影响用户体验。

排查

  1. 查看 Producer 日志,发现偶发的 timeout 异常
  2. 查看 Broker GC 日志,发现偶发 Full GC(500ms+)
  3. 查看网络监控,发现 Processor 线程偶尔卡住

定位

  1. Full GC 原因:Log Cleaner 占用大量内存,触发 Full GC
  2. Processor 卡住原因:某些慢 Consumer 的 Fetch 请求耗时长,阻塞了其他请求

优化

  1. 调整 Log Cleaner 配置:
properties 复制代码
log.cleaner.dedupe.buffer.size=256MB  # 从 128MB 调大
log.cleaner.threads=2                  # 增加线程
  1. 增加 num.network.threads(从 3 调到 8),避免慢请求阻塞

  2. Consumer 端优化:

    • 增加 Consumer 实例,减少单个 Consumer 的负载
    • 开启 max.poll.records=500,批量处理

效果

  • Full GC 频率降低 90%
  • P999 延迟从 200ms 降到 10ms

经验

P999 延迟的"长尾"往往是 GC、慢请求、网络抖动等偶发问题。需要结合日志和监控,逐个排查。


总结

性能调优是个"木桶效应":网络、磁盘、CPU、内存、线程、配置,任何一块短了都会拖后腿。

Kafka 调优重点

  1. Page Cache:保证有足够内存,避免被其他进程挤占
  2. 批量 + linger :Producer 端调整 linger.msbatch.size
  3. 线程池num.network.threadsnum.io.threads 要够
  4. 磁盘:用 SSD,XFS 文件系统,关闭 atime
  5. 监控:盯 Page Cache 命中率、GC、Under-Replicated Partitions

RocketMQ 调优重点

  1. 刷盘 + 复制:根据业务选择合适的组合(异步性能高,同步可靠性高)
  2. TransientStorePool:高吞吐场景下开启,减少 GC
  3. 线程池sendMessageThreadPoolNumspullMessageThreadPoolNums 根据 TPS 调整
  4. 文件预热 :开启 warmMappedFileEnable,减少首写延迟
  5. 监控:盯刷盘耗时、复制延迟、线程池队列深度

通用建议

  1. 压测要贴近真实:用真实的消息大小、Topic 数量、流量模式
  2. 监控要全面:CPU、内存、磁盘、网络、GC、线程池,缺一不可
  3. 优化要有数据:每次优化前后都要记录数据,对比效果
  4. 不要过度优化:达到业务目标就够了,不要追求"理论极限"

性能调优的本质,是在"可靠性、延迟、吞吐"之间找平衡。没有最优解,只有最适合你业务的解。

相关推荐
q***46522 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
ZZHHWW2 小时前
RocketMQ vs Kafka03 - 高可用机制深度剖析
后端
Lear2 小时前
如何解决MySQL唯一索引与逻辑删除冲突
后端
一 乐3 小时前
游戏助手|游戏攻略|基于SprinBoot+vue的游戏攻略系统小程序(源码+数据库+文档)
数据库·vue.js·spring boot·后端·游戏·小程序
方圆想当图灵3 小时前
Nacos 源码深度畅游:注册中心核心流程详解
分布式·后端·github
逸风尊者3 小时前
开发需掌握的知识:高精地图
人工智能·后端·算法
q***31893 小时前
Spring Boot--@PathVariable、@RequestParam、@RequestBody
java·spring boot·后端
CodeAmaz3 小时前
使用责任链模式设计电商下单流程(Java 实战)
java·后端·设计模式·责任链模式·下单
IT_陈寒4 小时前
Spring Boot 3.2震撼发布:5个必知的新特性让你开发效率提升50%
前端·人工智能·后端