news 2026/10/15 12:46:35

RocketMQ性能调优实战:从全链路瓶颈到参数优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RocketMQ性能调优实战:从全链路瓶颈到参数优化

RocketMQ 用得好好的,突然某个大促或者业务高峰一来,消息发送超时、消费积压、Broker 的 CPU 飙到 90% 以上,这时候再去翻日志找参数,往往已经晚了。我这些年排查过不少类似的问题,一个很深的体会是:RocketMQ 的性能调优,从来不是某一个神奇参数一改就起飞的事,而是从整体链路去判断瓶颈在哪,再针对性调整。这篇文章就把我对 RocketMQ 调优的经验整理出来,从 Broker 到 Producer、Consumer,再到 JVM 和系统层,每一步说清楚为什么这样改,改完预期是什么效果,适合正在被消息堆积或延迟抖动困扰的团队参考。

调优之前,你得先接受一个事实:RocketMQ 的性能瓶颈大多不在 MQ 本身,而在你的使用姿势。我会尽量把参数背后的原理讲透,也把那些我踩过的坑一并写出来,希望你能少走一点弯路。

1. RocketMQ性能调优的整体思路:先定位瓶颈,再动参数

RocketMQ 跑得慢,很多人第一反应就是去翻 Broker 的配置,把线程数调大、刷盘改成异步,以为参数一改性能就上去了。我在实际排查中见过太多次这样的场景:参数确实改了,效果却不明显,甚至更糟。原因很简单,没搞清楚瓶颈到底在哪个环节就动手,属于瞎调优。

1.1 一条消息要经过哪些环节,哪里可能变慢

RocketMQ 一条消息从产生到被消费,大致要经过这样一条链路:Producer 应用线程把消息交给 Producer 客户端,客户端通过 Netty 把数据发到 Broker;Broker 收到后,经过 Remoting 层解码,进入业务线程池,然后写入 CommitLog,触发刷盘;如果配置了主从同步,还要等从节点确认;之后 Consumer 通过长轮询拉取消息,把消息拉到自己本地,再交给消费线程去处理业务逻辑。

这条链路上的每一个环节都可能成为瓶颈。比如 Producer 发送慢,可能是客户端所在的机器网络有问题,也可能是 Broker 的接收线程池打满了;消费慢,可能是消费线程数不够,也可能是业务处理逻辑本身太慢,跟 MQ 一点关系都没有。我见过一个团队把消费线程从 20 调到 64,积压不但没缓解,反而更严重——原因是他们的消费逻辑里有慢 SQL,线程越多数据库连接池越不够用,最终互相等待,整体吞吐更差。

所以在调优之前,第一步永远是做定位,而不是直接改参数。你需要先回答三个问题:消息是在发送端变慢了,还是在 Broker 积压了,还是消费端跟不上?这三个环节的表征完全不同,定位手段也不一样。

1.2 调优的基本流程:监控、定位、小步快跑

我自己的调优流程基本是固定的四步。第一步,先把监控补齐。RocketMQ 的 Broker 端有丰富的指标,比如 put TPS、get TPS、消息积压量、线程池活跃线程数、队列容量等,这些通过后台管理系统或者暴露出来的指标接口都能看到。没有监控数据就调优,等于闭着眼睛开车。

第二步,做一次链路压测或者观察高峰期的数据,确定瓶颈环节。你可以从 Broker 后台看写入 TPS 和消费 TPS 的差值,差值大说明消费端跟不上;看发送端的平均耗时和 TP99,耗时高说明网络或者 Broker 接收有问题;还可以通过jstack看线程栈,线程都卡在哪个方法里,一眼就能看出是IO等待、锁等待还是业务代码的问题。

第三步,针对定位到的问题,选最可疑的一两个参数做调整,一次只改一个,改完观察至少 15 分钟到半小时,看指标的走势。不要一次改很多参数,否则出了效果你也不知道是谁的功劳,出了副作用你也不知道是谁闯的祸。

第四步,每一步改动都要有记录,调优前后做对比。无论是写文档还是记在笔记里,都要把改动原因、改动前后的参数、结果记下来。这东西看着繁琐,但对长期维护非常重要,尤其当集群要扩容或者有人问你为什么当时要把某个参数改成这个值时,能省去大量扯皮时间。

2. Broker端核心调优:存储策略与线程模型决定上限

Broker 是 RocketMQ 的发动机,也是绝大多数性能问题的集中地。Broker 端调优,核心就两块:一是怎么把消息快速可靠地存下来,二是怎么让各个线程池高效地把消息流转起来。

2.1 刷盘与复制策略:性能和数据安全之间的取舍

Broker 端存储相关的最关键参数有三个:刷盘方式、主从复制方式、以及是否开启堆外内存池。先说刷盘。

RocketMQ 默认的刷盘方式是异步刷盘,也就是消息写入内存的 Page Cache 之后,由后台线程定期把脏页刷到磁盘。对应的参数是flushDiskType = ASYNC_FLUSH。这种模式下,单机写入 TPS 可以做到很高,几十万都不是问题,代价是如果机器突然断电或者进程崩溃,Page Cache 里还没落盘的消息会丢失。而SYNC_FLUSH是每条消息写入后,必须等数据真正刷到磁盘才返回成功,可靠性高,但性能会大幅下降,一般只有金融、交易这类场景才用。

主从复制也是同样的道理。SYNC_MASTER模式下,主节点要等从节点返回确认才给 Producer 回执,消息在主从两个节点都落了地,安全但写入耗时会明显增加;ASYNC_MASTER则是主节点写完就返回,从节点异步去同步。大部分互联网业务,异步刷盘加异步复制就足够了,毕竟极端掉电场景本身是极小概率事件。

我在给某电商的订单系统调优时,他们一开始用的是同步刷盘加同步双写,写入 TPS 只有不到 3000,Broker 的 CPU 和磁盘负载都很高。后来经过评估,他们决定把刷盘改成异步、复制保持同步,结果写入 TPS 直接到了 12000 以上。这里要强调的是:改刷盘方式意味着对可靠性要求的重新评估,必须让业务方确认能接受极端场景下的少量消息丢失,而不是我们自己拍脑袋决定。

2.2 发送线程池:sendMessageThreadPoolNums 为什么默认只有 1

很多人会忽略一个事实:RocketMQ Broker 处理消息写入的线程池sendMessageThreadPoolNums,默认值只有 1。这意味着不管你的机器有多少核,默认情况下写入请求都要排队进同一个线程处理,这在高并发下很容易成为瓶颈。

我建议把这个值调到 CPU 核数的 2 到 4 倍,比如一个 32 核的机器,可以设为 64。调大之后,Broker 接收写入请求的能力会立刻提升。但这里有一个容易踩的坑:线程数并不是越大越好。线程太多会导致线程上下文切换的 CPU 开销急剧上升,甚至可能把内存占满,所以建议用压测来找合适的值,从 16、32、48、64 这样逐渐往上加,观察写入 TPS 和 CPU 使用率的变化,找到拐点。

除了sendMessageThreadPoolNums,Broker 还有pullMessageThreadPoolNums、queryMessageThreadPoolNums、ackMessageThreadPoolNums等线程池参数。其中拉取线程池pullMessageThreadPoolNums默认值是32 + CPU核数 x 2,一般来说已经够用,除非你是大规模读取密集型的场景。查询线程池queryMessageThreadPoolNums默认是8 + CPU核数,在消息轨迹查询、消息回溯比较多的场景下需要适当调大。

我见过一个真实案例:某公司的 Broker 上,写入高峰期sendThreadPoolQueue的排队消息数经常冲到几万,发送 RT 从几毫秒飙到几百毫秒。通过jstack抓线程栈发现,写入线程池只有一个线程在干活,而且它的大量时间花在 Page Cache 写入上。把sendMessageThreadPoolNums从 1 调到 16 之后,发送 RT 立刻恢复到个位数毫秒,CPU 也只上升了 10% 左右。

2.3 transientStorePoolEnable:写入路径上的堆外加速

transientStorePoolEnable是一个被低估的参数。默认情况下它是false,此时消息写入走的是直接映射 Page Cache 的路径,也就是mmap写内存。这个路径本身很快,但在高并发写入时,写入线程和刷盘线程会争抢同一片 Page Cache 的锁,导致性能有所损耗。

开启transientStorePoolEnable = true之后,Broker 会预先分配一块堆外内存池,消息写入时先写到这块堆外内存的 ByteBuffer 里,再由专门的线程把数据批量刷到 FileChannel 对应的 Page Cache。这样做的好处是写入线程和刷盘动作解耦,写入路径更短,性能可以进一步提升。我实际测试下来,开启后写入 TPS 大约能提升 15% 到 30%,具体看消息大小和并发数。

但这个参数有一个要注意的地方:堆外内存池的大小由transientStorePoolSize控制,默认是 5 个 buffer,每个 buffer 的大小跟 CommitLog 的页大小一致。如果消息量特别大,池子被用完了,新来的消息会阻塞等待,反而可能引发性能下降。所以开启这个参数时,建议把池子大小适当调大,比如 10 到 20,并且要同步调大 JVM 的MaxDirectMemorySize,否则堆外内存可能不够用。

还有一点,开启堆外池后,Broker 进程崩溃时,堆外池里还未刷入 Page Cache 的消息会全部丢失,这个可靠性风险要提前评估清楚。如果你们的业务对可靠性要求极高,这个参数就不要开。

2.4 其他容易忽略的 Broker 参数

还有几个参数值得注意。osPageCacheBusyTimeOutMills默认是 1000,表示 Broker 在尝试获取 Page Cache 锁的时候,如果超过这个毫秒数还没拿到,就会返回系统繁忙。在高并发写入时,如果发送端经常看到SYSTEM_BUSY这个错误码,除了调大发送线程数,还可以把这个值适当调大到 3000 到 5000,让写入线程多等一会儿,减少返回繁忙的概率。

maxTransferBytesOnMessageInDisk和maxTransferCountOnMessageInDisk控制的是 Consumer 从磁盘拉取消息时,单次传输的最大字节数和条数,默认分别是 64KB 和 8 条。如果消费场景经常要读历史消息、磁盘 IO 压力大,这两个值可以适当调大,减少读盘次数。反过来,如果 Broker 内存充足、消息基本都是热数据在 Page Cache 里,这两个参数不会起太大作用,真正的瓶颈在消费端拉取频次。

还有一个是sendMessageThreadPoolNums的配套参数waitTimeMillsInSendQueue,默认 200ms。发送线程池队列满的时候,新请求会在队列里等待多久才返回系统繁忙。这个值调大了,发送端会多一些等待容忍度,但也会让发送 RT 变长。一般建议保持默认,除非你已经确认真的是线程池队列满导致的问题。

3. Producer端优化:发送延迟高,先看客户端怎么发

很多时候 Broker 端性能没问题,消息却发不出去,问题出在 Producer 的用法上。Producer 端的优化空间往往被忽视,但这里面的收益其实很大,尤其是批量发送和异步发送这两个手段,能把发送吞吐和延迟拉开数量级的差距。

3.1 发送方式:同步、异步还是批量

RocketMQ 的 Producer 有三种基本发送方式。同步发送,调用后要等服务端返回结果才继续,RT 一般在一到几个毫秒,适合对可靠性要求高、对延迟不太敏感的场景。异步发送,发送请求后立刻返回,通过回调或者 Future 获取结果,发送吞吐可以高一个量级,适合高吞吐场景。批量发送,则是一次发送请求携带多条消息,能充分利用网络带宽和 Broker 的写入吞吐。

我见过很多团队,明明消息量已经很大了,还在用同步发送一条一条地发,导致发送线程大量时间耗在等待响应上。其实 RocketMQ 客户端在处理同步发送时也是有内部队列的,但每个发送动作都占用一个发送线程等待网络响应,线程池一旦打满,发送队列就会堆积,延迟自然上去。

对于绝大多数高吞吐场景,我的建议是优先考虑异步发送。比如某个告警平台,每天要上报几百万条日志消息,之前用同步发送,TPS 只能跑到几千,改成异步发送后直接到几万,而且 Producer 端的内存和线程开销反而降下来了。异步发送唯一要注意的是回调里的逻辑不能太重,否则回调线程也会被拖死。

批量发送则需要业务侧配合,把多条消息攒到一批再发。一个很典型的做法是:用生产者本地做一个大小可控的缓冲队列,积攒几十条或者几百条消息后一次性发送。RocketMQ 的批量发送有单次最大 4MB 的限制,需要注意不要超过maxMessageSize。批量发送对吞吐的提升非常明显,我这边测试过,单条发送和 100 条一批发送,TPS 差值可以到 5 到 10 倍。

3.2 消息体大小与压缩阈值

RocketMQ 的消息体大小默认上限是 4MB,大消息会直接影响发送性能。大消息不仅占用更多的网络带宽和 Broker 内存,还会影响 Page Cache 的命中率。所以使用上有一条很强的建议:消息体控制在几十 KB 以内,如果超过 64KB,优先考虑是否可以把数据存到外部存储,只把引用或者 ID 发到消息里。

压缩是另一个容易忽视的手段。RocketMQ 有一个参数compressMsgBodyOverHowmuch,默认是 4096 字节,当消息体超过 4KB 时,Producer 会自动对消息体做压缩。这个机制对文本类消息非常有效,比如 JSON 数据,压缩率通常能达到 80% 以上,能显著降低网络传输和存储压力。

但要注意,压缩不是免费的,它要消耗 CPU。如果你的业务消息本来就很小,比如几百字节,打开压缩反而会浪费时间。另外,如果消息体是图片、视频这类本身压缩率很低的数据,开压缩基本没有收益。我一般会观察消息的平均大小和压缩前后对比,如果压缩后大小减少不到 30%,就不值得开。

3.3 超时、重试与故障规避

Producer 的sendMsgTimeout默认是 3000ms。如果业务高峰期发送响应变慢,这个值很容易触发超时,然后客户端会走重试逻辑。重试本身是好事,但在高并发下,过多的重试会反过来压垮 Broker。

我建议把超时时间根据业务情况适当调大到 5000 到 8000ms,同时降低重试次数。retryTimesWhenSendFailed默认是 2 次,一般不建议超过这个值。重试过多反而会导致消息顺序错乱、重复投递和 Broker 负载飙升。

还有sendLatencyFaultEnable这个参数,默认是 false。开启后,Producer 会记录各个 Broker 的发送延迟,当一个 Broker 出现故障或者高延迟时,会自动把它隔离一段时间,把消息路由到其他 Broker。这个机制在 Broker 故障场景下非常有用,能显著降低发送端的超时率。我在一个多 Broker 集群上开启后,单 Broker 掉线时的发送成功率从 70% 提升到了 99% 以上。

3.4 Topic 和队列数的合理规划

Topic 的读写队列数是创建 Topic 时指定的,默认是 4 个写队列、4 个读队列。写队列数决定了这个 Topic 在单个 Broker 上的并发写入通道数,读队列数决定了消费端并发拉取的分片数。

如果你们的业务有较高的吞吐需求,建议创建 Topic 时把读写队列数都设成 8 到 16。队列数增加,意味着 Consumer 可以有更多线程并行去各队列拉取,也能在一个 Broker 上摊开写入压力。但队列数也并不是越大越好,因为每个队列在 Consumer 端都是独立的线程去拉取,队列太多个个消费线程都在空转,反而浪费资源。

另外要注意,Topic 的队列数在创建后可以调整,可以变大。如果刚开始队列数不够,后来涨了,可以扩容队列。但是缩容就要小心,消息分布在不同队列上,如果写队列减少,可能会影响消息顺序或造成部分队列堆积。

4. Consumer端调优:把消费能力真正顶上去

消费端的问题比较特殊,因为它一半是 RocketMQ 参数问题,一半是你的业务代码问题。RocketMQ 本身的消费性能其实非常可观,绝大多数消费慢的案例,根因都在业务处理逻辑上,或者消费线程配置和实际资源不匹配。

4.1 消费线程模型与参数配置

RocketMQ 的 PushConsumer 本质上是一个拉模式加长轮询的实现,消费线程从队列里拉消息然后执行监听器。客户端有几个关键参数:consumeThreadMin默认 20,consumeThreadMax默认 64。在消费集群模式下,这些线程会分到各个消息队列上拉取消息。

调线程数之前,要想清楚你的消费任务是 CPU 密集型还是 IO 密集型。如果是 CPU 密集型,线程数最多也就比 CPU 核数多一点,多了反而因为上下文切换而变慢。如果是 IO 密集型,比如消费时要写数据库、调外部接口,那么线程数可以大一些。我有一个经验值:IO 密集型场景,线程数可以设为 CPU 核数的 4 到 8 倍;CPU 密集型场景,线程数设为 CPU 核数的 1 到 2 倍。

consumeMessageBatchMaxSize默认是 1,表示每次调用监听器只传一条消息。有同学喜欢把它调大,比如调到 50,希望通过批量处理来提升吞吐。想法没错,但要注意:如果监听器里是一条一条处理的,批量参数根本不会提升性能;只有你在监听器里真的按照 List 批量处理,比如批量写数据库,才有收益。而且批量消费时,如果处理的中间一条失败,可能导致整批消息都进入重试,这个在业务上要做容错。

4.2 拉取参数与流量控制

RocketMQ 的 Consumer 拉消息时,有几个内部的流量控制参数容易踩坑。pullBatchSize默认 32,表示一次拉取的最大消息条数。如果你的消费处理速度较快,可以适当调大这个值,比如调到 64 或 128,减少拉取请求次数,降低网络和 Broker 的负载。

还有一个容易被忽略的机制:当某个队列的消息积压太多时,Consumer 会触发本地流量控制,拉取会暂停,直到消费完部分积压消息。这个阈值由pullThresholdForQueue控制,默认是 1000 条,以及pullThresholdSizeForQueue默认 100MB。如果你们有瞬时大量消息涌入的场景,比如秒杀、活动,这个阈值可能要调大,否则 Consumer 会频繁地拉一下停一下,消费速度会很难看。

调大阈值同样有代价。阈值越大,Consumer 本地内存缓冲的消息就越多,占用的堆内存也越多,而且这些消息在客户端本地,还没有提交位点,如果 Consumer 宕机,这部分消息会重复投递。所以阈值要根据消费者的堆内存大小来权衡。

还有一个consumeConcurrentlyMaxSpan参数,默认 2000。它表示当前队列中最多允许被拉取但未消费的消息条数,同一条队列的消费窗口超过这个值则会暂停拉取。如果你发现消费端拉取效率低,可以适当调大这个值,让单队列在本地多缓冲一些消息,减少拉取等待。但要配合内存一起看,不要无限调大。

4.3 消费业务逻辑才是真正的瓶颈

这一点我想重点强调。无数次的排查经验让我确认:消息积压问题中,80% 以上是消费业务逻辑的问题,只有不到 20% 是 MQ 本身参数的问题。

举个例子,某个订单状态同步服务,消费一条消息要去查询一次订单数据库,然后调一次外部接口更新缓存。单条消息的处理链路平均要 200ms,那么这个服务单线程最多每秒处理 5 条,20 个线程就是每秒 100 条,这基本就是天花板了。如果这时候进来的消息速率是每秒 1000 条,积压是必然的。你怎么调consumeThreadMax都是没用的,因为瓶颈是那个外部接口的延迟。

遇到这种情况应该怎么做?首先,消费逻辑里能不做的操作就别做。能批量查询的,就不要一条一条查;能异步化的,不要让消费线程同步等待;能用本地缓存的,就不要每次打远程。其次,看外部依赖的耗时分布,如果某个接口的 TP99 很高,建议给消费线程池单独设置合理的并发上限,避免因为消费过慢导致线程全部阻塞。

另外,消息的幂等处理一定要做好。消费端超时或者失败后会重试,重试意味着重复消费。如果你的消费逻辑不是幂等的,重试只会让下游系统更慢,问题更严重。幂等可以用业务主键、消费记录表、Redis 分布式锁等方式去保证,这块不能偷懒。

5. 系统层与JVM调优:容易被忽视的地基

你可能会发现一个现象:同样的 RocketMQ 版本、同样的参数配置,在不同机器上跑出来的性能差距很大。这种差距往往来自系统层和 JVM 层。如果这些底子没打好,Broker 端怎么调都调不出理想效果。

5.1 Linux 内核参数与 IO 调度

RocketMQ 是磁盘 IO 密集型的应用,所以文件系统和 IO 调度策略影响非常大。首先,磁盘必须用 SSD 级别以上的存储,如果是机械盘,写入 TPS 会被死死压着。在云上,建议选择 ESSD 或者吞吐型 SSD 的规格;裸机的话,建议用 NVMe 盘。

然后是文件系统。推荐使用 ext4 或者 xfs,挂载时加上noatime选项,避免读取时更新文件访问时间带来的额外写盘开销。分区对齐也要检查,不对齐会导致文件系统读写效率下降明显。

Linux 内核的vm.swappiness参数也值得一调。它默认是 60,表示系统在内存压力较大时倾向于使用 swap。RocketMQ 的 Page Cache 对性能至关重要,如果操作系统把 Page Cache 里的页交换到 swap,消息写入就会变慢。建议把vm.swappiness调低一点,比如 10 或者更低,让内核尽量回收文件缓存,而不是把匿名页换出。这个参数对整体 IO 稳定性帮助很明显。

vm.overcommit_memory也要注意。当开启transientStorePoolEnable时,RocketMQ 会申请大量堆外内存,如果内核的内存过度分配策略过于保守,可能导致内存申请失败。一般建议设置为 1,允许内存超额分配,让 JVM 自己有更大的控制空间。

线程调度器也有讲究。部分发行版默认的调度器在 NVMe 盘上表现不佳,建议改成none或noop。因为 NVMe 盘本身有比较聪明的队列调度,内核层面的 IO 调度器再做一层反而增加了开销。机械盘时代常用的deadline或者cfq,在 NVMe 场景下不要用。

5.2 磁盘空间与容量规划

性能调优不光是速度,还有容量问题。RocketMQ 的消息默认保存 3 天,到期会自动清理。但如果你的峰值写入量很大,磁盘空间不够,Broker 会触发保护机制,暂停写入,生产环境出现这种问题是灾难性的。

我习惯在规划磁盘容量时,按峰值 TPS 乘消息平均大小乘保留小时数再加 20% 的余量来计算。比如峰值每秒 5000 条,每条平均 2KB,保留 72 小时,那么需要的空间大约是5000 x 2KB x 72 x 3600 = 2.6TB,加上余量就需要 3TB 以上。这个估算不复杂,但很多团队根本没算过,结果大促时磁盘被写满。

还要做好磁盘空间监控,RocketMQ 的 Broker 后台能看到存储水位,建议在磁盘使用率达到 60% 到 70% 时就开始告警,千万不要等到磁盘满了再处理。另外,元数据的 ConsumeQueue 和 IndexFile 也是要占空间的,虽然小,但是索引文件在消息量大的时候也会膨胀,需要留出空间。

5.3 JVM 堆、GC 与堆外内存

RocketMQ Broker 的 JVM 参数,官方脚本提供了一套默认配置,但不同版本、不同机器差异很大,建议还是根据具体场景定制。核心原则是:堆内存不要设置得过大,但也不能太小。

为什么不能过大?因为 RocketMQ 最关键的存储路径是通过 mmap 映射文件到 Page Cache,由操作系统管理,而不是在 JVM 堆内。如果堆内存设置得太大,留给 Page Cache 的空间就少了,消息写入都要先经过磁盘,性能会大幅下降。给一个经验值,默认的-Xms8g -Xmx8g在 32G 内存的机器上比较合理,如果机器内存是 64G,堆可以放宽到 16G 左右,但不能无脑堆。

堆内存太小也不行。RocketMQ 的 Broker 在解码消息、构建响应、管理消费者信息、消息轨迹等环节都要用堆内存。如果堆太小,会频繁触发 Full GC,导致长尾延迟。我一般建议堆内存至少 4G 起步。

GC 选型上,JDK 8 时代官方用 CMS,JDK 11 以上建议直接上 G1。G1 在大堆场景下停顿更可控,而且对上百 GB 的大堆也友好。如果你的 Broker 是 8G 堆左右,CMS 和 G1 差别不大;堆到 16G 以上,G1 的优势就非常明显了。

堆外内存要重点关注。Broker 使用 Netty 做网络通信,默认的堆外内存池是开启的。如果MaxDirectMemorySize设置得太小,高并发读写时堆外内存会不够用,导致 Full GC 频繁甚至 OOM。我建议MaxDirectMemorySize至少设置为堆大小的一半到相同大小。在开启transientStorePoolEnable时,这部分堆外内存还要额外加上内存池的占用,要整体计算。

还有一个小建议是,-XX:+UseNUMA参数在老版本上有提升局部性的效果,但在 G1 下需要小心。G1 对 NUMA 的支持有些版本有 bug,开启后反而可能出现性能波动。我在生产环境基本不开这个参数,除非做过充分测试。

6. 常见问题与排查实录:从现象到根因

最后这块,我整理几个我们在生产环境遇到过的典型问题,把现象、排查路径和最终解法完整写出来。这些场景在你们自己的环境中大概率也会碰上。

6.1 现象:Broker 写入 TPS 上不去,CPU 烧满

某个业务集群的 Broker,峰值时 CPU 利用率到了 95% 以上,但写入 TPS 始终在几千,上不了一个量级。一开始以为是磁盘问题,但检查 IO 吞吐后发现磁盘负载并不高,CPU 却很高。

用top -H -p <pid>看了线程,发现大量线程在跑org.apache.rocketmq.store.CommitLog#putMessage,再配合jstack看,写入线程池只有一个线程在持续运行。这个现象的根因就是sendMessageThreadPoolNums默认值为 1。将这个参数调整到 32,再观察 CPU 分布,发现多核被利用起来,写入 TPS 很快从 5000 提升到了 2 万以上。

6.2 现象:消息延迟忽高忽低,发送 RT 长尾明显

另一个案例是发送端观察到消息平均延迟很好,但 TP99 和 TP999 经常出现几百毫秒甚至一秒多的尖刺。排查时先看 Broker 的 GC 日志,发现频繁发生 Full GC,每次停顿都在几百毫秒以上。

进一步分析,堆内存只有 4G,但 Broker 上 Topic 数量很多,消费者组很多,元数据对象占满了堆。我们做了两件事:一是把堆内存从 4G 扩到 8G,二是把MaxDirectMemorySize从默认值调整到 4G。调整之后 Full GC 明显减少,TP99 从几百毫秒降到了几十毫秒内。这个案例说明,RocketMQ 调优不能光看 Broker 参数,JVM 参数和集群规模要一起考虑。

6.3 现象:消费积压持续增长,线程数怎么加都没用

一个消息处理服务出现了严重的消息积压,运营同学在后台看到积压量从几百涨到了几十万。开发团队把consumeThreadMax从 64 调到 128,积压不降反升。我介入后先看消费线程在干什么,用jstack发现几乎所有的消费线程都阻塞在数据库查询上。

原来是消费逻辑每条消息都会执行一次订单详情查询,这个查询在数据量大之后走了全表扫描,单次耗时超过 500ms。线程加多之后,数据库连接池被打满,更多线程在等连接。解决思路不是调线程数,而是优化查询:给关键字段加索引、把单条查询改成批量查询、引入本地缓存。优化之后,单条消息处理耗时降到了 50ms 以内,1000 条积压很快被消费完。

6.4 一个完整调优过程:从 2000 TPS 到 12000 TPS

最后分享一个完整的调优过程。某业务系统,消息量从平时每秒 500 条涨到高峰期每秒 5000 条,出现了发送超时和消费积压。我们按照前面说的流程来操作。

第一步,看监控。发现发送端 RT 在峰值时从 5ms 涨到 300ms,Broker 写入 TPS 只有 2000 上下,消费端积压达到 5 万条。

第二步,排查 Broker。先看线程池队列,发现sendThreadPoolQueue有大量堆积,于是确认 Broker 写入线程数是瓶颈。同时检查磁盘状态,磁盘 IO 利用率不高,说明不是刷盘慢。

第三步,做改动。先把sendMessageThreadPoolNums从默认 1 调到 32,开启transientStorePoolEnable,把 JVM 堆从 4G 调到 8G。改完后单测 15 分钟,写入 TPS 到了 8000 左右,发送 RT 降到了 20ms 以内。

第四步,处理消费端。消费端用的是同步发送式的业务逻辑,每条消息调用一次远程服务。我们把消费线程从 20 调到 40,同时在业务侧做了批量调用优化,消费 TPS 从 1000 提升到了 4000。最后再看积压曲线,开始稳步下降,整个调优结束。

调优的过程中,没有一步是玄学,每一步都是基于监控数据和线程栈的定位。这也是我想反复强调的:把链路摸清楚,把参数背后的原理搞明白,性能问题最终都能定位到根因。

最后说一个我自己的体会。RocketMQ 调优没有银弹,也没有一套放之四海皆准的参数模板。每一套配置都要根据你的消息大小、吞吐量、可靠性要求、机器规格去压测和调整。不要照抄别人的一堆参数就上生产,那很容易出更大的问题。从默认参数开始,加监控、做压测、看数据、改参数,一步一步稳扎稳打,这才是最可靠的调优路径。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 12:45:31

PostGIS管理DEM栅格数据的实战指南与性能优化

1. 为什么非要把DEM数据塞进PostGIS&#xff1f;——一个被低估的空间数据管理真相“postgis/postsql 导入dem栅格数据到数据库”这个标题看起来像一句技术指令&#xff0c;但背后藏着一个空间数据工程师每天都在面对的现实困境&#xff1a;你手头有几十GB的SRTM、ASTER或LiDAR…

作者头像 李华
网站建设 2026/10/15 12:45:07

acctres.dll丢失怎么办?官方修复流程与单文件下载风险详解

在使用电脑系统时经常会出现丢失找不到某些文件的情况&#xff0c;由于很多常用软件都是采用 Microsoft Visual Studio 编写的&#xff0c;所以这类软件的运行需要依赖微软Visual C运行库&#xff0c;比如像 QQ、迅雷、Adobe 软件等等&#xff0c;如果没有安装VC运行库或者安装…

作者头像 李华
网站建设 2026/10/15 12:44:58

Moark Skills:AI代理能力复用的轻量级接口契约

1. Moark Skills 不是新框架&#xff0c;而是AI代理能力复用的“插座标准”你第一次看到“Moark Skills”这个词&#xff0c;大概率是在某篇技术分享里被顺带提起&#xff0c;或者在OpenClaw项目的文档角落里瞥见一行小字&#xff1a;“支持 Moark Skills 协议”。它不像LangCh…

作者头像 李华
网站建设 2026/10/15 12:44:56

机器学习硬核资料骨架:可执行、可验证、可落地的知识集

1. 这份资料集不是“资源搬运工”&#xff0c;而是我筛掉97%冗余内容后留下的硬核骨架你点开过多少个标着“机器学习全套资料”的网盘链接&#xff1f;下载解压&#xff0c;发现里面塞着2016年的TensorFlow 1.x教程、早已失效的Coursera课程邀请码、PDF扫描版模糊到连公式都认不…

作者头像 李华
网站建设 2026/10/15 12:44:21

Python开发者社区生态与8大核心平台解析

1. Python开发者社区生态现状 Python作为当前最流行的编程语言之一&#xff0c;其开发者社区呈现出多元化、专业化的发展趋势。根据2023年Stack Overflow开发者调查报告显示&#xff0c;Python已连续六年成为最受欢迎的编程语言之一&#xff0c;全球活跃开发者数量超过1500万。…

作者头像 李华
网站建设 2026/10/15 12:44:20

ImageMagick图像处理:从基础操作到企业级应用

1. 项目概述&#xff1a;为什么需要掌握ImageMagick&#xff1f;在数字内容爆炸式增长的今天&#xff0c;图像处理已成为开发者和设计师的日常刚需。我曾参与过一个电商项目&#xff0c;每天需要处理上万张商品图——从简单的尺寸调整到复杂的合成水印&#xff0c;传统手动操作…

作者头像 李华