最近线上的RocketMQ集群遇到了性能瓶颈,消息发送延迟高,消费堆积严重,花了一周时间做了一轮性能优化,把吞吐量从每秒几千条提升到了十几万条,延迟也降了下来。

今天这篇文章就来分享一下这次RocketMQ性能优化的实战经验,从问题排查、瓶颈定位,到具体的优化措施,一步步带你把RocketMQ从慢调到快。

内容都是实际生产环境中验证过的经验,希望能给正在使用RocketMQ的朋友一些参考。

一、先说说遇到的问题

先说说我们遇到的问题。

我们的业务是一个电商系统,RocketMQ主要用来处理订单消息、库存消息、日志消息等。随着业务量的增长,消息量越来越大,最近开始出现一些问题:

  1. 消息发送延迟高:Producer发送消息的RT从原来的几毫秒涨到了几十毫秒,高峰期甚至能到几百毫秒,影响了业务接口的响应时间。
  2. 消费堆积严重:Consumer消费不过来,消息堆积越来越多,高峰期堆积量能到几百万条,导致业务处理延迟。
  3. Broker CPU高:Broker节点的CPU使用率经常在80%以上,高峰期甚至能到100%,系统负载很高。
  4. 磁盘IO高:Broker的磁盘IO使用率很高,经常出现IO等待,影响消息的写入和读取。

这些问题在业务高峰期尤其明显,已经影响到了正常的业务,所以必须做性能优化。

二、性能优化的思路

在讲具体的优化措施之前,先说说性能优化的思路。

性能优化不是盲目地调参数,而是要有一套系统的方法:

  1. 先监控,后优化:先把监控做好,搞清楚系统的各个指标,比如吞吐量、延迟、CPU、内存、磁盘IO、网络等,找到瓶颈在哪里。
  2. 先定位,后优化:根据监控数据,定位性能瓶颈,是Broker的问题,还是Producer的问题,还是Consumer的问题,是CPU瓶颈,还是IO瓶颈,还是网络瓶颈。
  3. 先架构,后参数:先从架构层面优化,比如集群扩容、读写分离、分区等,架构层面的优化效果往往比调参数大得多。架构优化完了,再调参数。
  4. 逐步优化,逐步验证:不要一次性改很多东西,要一个一个地改,改完之后验证效果,确认没问题了再改下一个。这样如果出了问题,也能快速定位是哪个改动引起的。

按照这个思路,我们开始了这次性能优化。

三、Broker端优化

Broker是RocketMQ的核心,消息的存储和转发都在Broker上,所以Broker的性能很重要。

1. 磁盘优化

RocketMQ的消息是存在磁盘上的,所以磁盘IO的性能对RocketMQ影响很大。

我们最开始用的是普通的SATA机械硬盘,IOPS只有100多,根本扛不住高并发的消息写入。后来我们做了以下优化:

  • 换SSD硬盘:把机械硬盘换成SSD硬盘,IOPS从100多提升到了几万,磁盘IO性能有了质的飞跃。这是效果最明显的一个优化。
  • CommitLog和ConsumeQueue分离:RocketMQ的消息存在CommitLog里,消费队列存在ConsumeQueue里。如果这两个在同一块磁盘上,会有IO竞争。我们把CommitLog和ConsumeQueue放到不同的磁盘上,减少IO竞争,提升性能。
  • 设置合适的磁盘刷新策略:RocketMQ有同步刷盘和异步刷盘两种方式。同步刷盘可靠性高,但是性能差;异步刷盘性能好,但是可能会丢消息。我们的业务对可靠性要求不是特别高,所以用了异步刷盘,提升了写入性能。如果你的业务对可靠性要求很高,建议用同步刷盘,或者用主从复制保证可靠性。
  • 调整磁盘预热:RocketMQ启动的时候会做磁盘预热,提前分配好磁盘空间,避免运行时动态分配影响性能。我们确保磁盘预热开启,并且预热的文件大小合适。

2. 内存优化

RocketMQ大量使用内存来提升性能,比如PageCache、消息缓存等。

  • 设置合适的JVM堆内存:Broker的JVM堆内存不要设置得太大,也不要太小。太大的话会导致GC时间长,太小的话会频繁GC。我们设置的是4G堆内存,其中新生代1.5G,老年代2.5G,用G1垃圾回收器,GC停顿时间控制在100ms以内。
  • 利用PageCache:RocketMQ大量利用操作系统的PageCache来提升读写性能。要确保系统有足够的空闲内存给PageCache用,不要把内存都分配给JVM。一般来说,JVM堆内存占系统内存的一半左右比较合适,剩下的留给PageCache和系统使用。
  • 调整文件句柄数:RocketMQ会打开很多文件,要确保系统的文件句柄数足够大,不然会出现"Too many open files"的错误。我们设置的是655350。

3. 线程和参数优化

  • 调整Broker的线程数:RocketMQ的Broker有很多线程池,比如发送消息线程池、拉取消息线程池、查询消息线程池等。要根据业务情况调整线程池的大小,线程太少会导致请求排队,线程太多会导致上下文切换开销大。我们根据压测结果,把发送线程池调到了32,拉取线程池调到了32。
  • 调整消息大小限制:RocketMQ默认的消息最大是4M,如果你的消息比较大,可以调整这个参数。但是消息太大也会影响性能,建议消息不要太大,最好控制在1M以内。
  • 开启堆外内存:RocketMQ可以用堆外内存来存储消息,减少JVM堆的压力,减少GC。我们开启了堆外内存,大小设置为2G。

4. 集群架构优化

  • 多Master多Slave架构:我们最开始是单Master架构,性能和可靠性都不好。后来改成了多Master多Slave架构,Master负责读写,Slave负责备份和读,既提升了性能,又提升了可靠性。
  • Broker分组:把不同的Topic放到不同的Broker组上,避免热点Topic影响其他Topic。比如订单消息放到一个Broker组,日志消息放到另一个Broker组,互不影响。
  • 读写分离:让Master负责写,Slave负责读,提升读的性能。RocketMQ支持配置Consumer从Slave读取消息,减轻Master的压力。

四、Producer端优化

Producer是消息的发送方,Producer端的优化也很重要。

1. 批量发送

如果你的业务场景允许,尽量用批量发送,把多条消息打包成一批发送,减少网络IO的次数,提升发送性能。

RocketMQ的批量发送很简单,把多个消息放到一个List里,然后调用send方法就行。但是要注意,一批消息的总大小不能超过4M,而且最好是同一个Topic的消息。

我们把日志消息改成了批量发送,每100条发一批,发送性能提升了好几倍。

2. 异步发送

如果你的业务不需要同步等待发送结果,可以用异步发送,发送之后不等结果,继续处理后面的逻辑,通过回调来处理发送结果。

异步发送可以减少业务接口的等待时间,提升接口的响应速度。我们把一些非核心的消息,比如日志消息、统计消息,都改成了异步发送,接口响应时间明显下降。

但是要注意,异步发送要处理好发送失败的情况,比如重试、记录日志、告警等,避免消息丢失。

3. 合理设置重试次数

RocketMQ的Producer发送失败会自动重试,默认重试2次。要根据业务情况设置合理的重试次数,重试次数太少可能会导致消息发送失败,重试次数太多会导致发送延迟高。

我们设置的是重试3次,而且只对同步发送重试,异步发送不重试,通过回调来处理失败。

4. 合理设置超时时间

发送超时时间不要设置得太长,也不要太短。太长的话,如果Broker有问题,会导致业务线程长时间等待;太短的话,可能会因为网络抖动导致发送失败。

我们设置的是3秒超时,根据业务情况可以调整。

5. 连接复用

Producer和Broker之间的连接要复用,不要每次发送都新建连接。RocketMQ的Producer默认就是长连接复用的,要确保不要频繁创建和销毁Producer实例,最好用单例或者连接池。

我们最开始有个业务,每次发送消息都new一个Producer,导致连接数很多,性能很差。后来改成单例,性能就好了。

五、Consumer端优化

Consumer是消息的消费方,消费慢了就会导致消息堆积。

1. 提高消费并行度

消费堆积最常见的原因就是消费并行度不够。可以通过以下方式提高消费并行度:

  • 增加Consumer实例数:如果是集群消费,增加Consumer的实例数,每个实例消费一部分队列,提升消费能力。但是要注意,Consumer实例数不要超过Topic的队列数,超过的话多余的实例不会分配到队列。
  • 增加消费线程数:每个Consumer实例内部有消费线程池,可以调整线程池的大小,增加消费的并行度。默认是20个线程,我们根据业务情况调到了64个线程。
  • 增加队列数:如果Topic的队列数太少,即使Consumer实例和线程再多,也提升不了消费能力,因为队列数决定了最大的并行度。可以给Topic增加队列数,提升消费的并行度。我们把一些热点Topic的队列数从8个增加到了32个,消费能力提升了好几倍。

2. 批量消费

如果你的业务场景允许,可以用批量消费,一次拉取多条消息,批量处理,减少消费的次数,提升消费性能。

RocketMQ的批量消费需要自己实现,通过pull的方式拉取一批消息,然后批量处理。我们把一些可以批量处理的消息,比如日志消息、统计消息,改成了批量消费,消费性能提升了很多。

3. 优化消费逻辑

消费慢,很多时候不是RocketMQ的问题,而是消费逻辑本身慢。要优化消费逻辑,减少消费时间。

  • 避免在消费逻辑里做耗时操作:比如调用外部接口、查询数据库、写文件等,如果必须做,要做好超时控制,或者异步处理。
  • 异步化处理:如果消费逻辑里有一些非核心的耗时操作,可以异步处理,先把核心逻辑处理完,返回消费成功,非核心逻辑异步处理。
  • 批量处理数据库操作:如果消费逻辑里要写数据库,尽量用批量插入、批量更新,减少数据库的IO次数。
  • 加缓存:如果消费逻辑里要查询一些不常变的数据,可以加缓存,减少数据库查询。

我们有个消费逻辑,每次消费都要查三次数据库,后来加了缓存,又改成了批量写数据库,消费时间从原来的100多毫秒降到了10毫秒以内,消费能力提升了10倍。

4. 合理设置拉取参数

  • 调整拉取大小:Consumer每次从Broker拉取的消息数,可以调整。默认是32条,如果消息比较小,消费能力强,可以调大一些,比如100条,减少拉取的次数。
  • 调整拉取间隔:Consumer拉取消息的间隔,如果消费能力强,可以调小一些,比如10ms,让Consumer更频繁地拉取消息,避免消息堆积。
  • 调整消费超时时间:消费超时时间要根据消费逻辑的处理时间来设置,不要太短,不然会因为消费超时导致消息重复消费;也不要太长,不然消费卡住了不能及时重试。

5. 避免重复消费的影响

RocketMQ至少会投递一次,所以可能会有重复消费。要确保消费逻辑是幂等的,重复消费不会影响业务。如果消费逻辑不是幂等的,重复消费可能会导致数据错误,而且重试也会影响消费性能。

我们通过数据库唯一索引、Redis去重等方式,保证了消费逻辑的幂等性。

六、JVM和操作系统优化

除了RocketMQ本身的参数,JVM和操作系统的优化也很重要。

1. JVM优化

  • 选择合适的垃圾回收器:JDK8推荐用G1垃圾回收器,停顿时间短,适合RocketMQ这种需要低延迟的应用。JDK11及以上可以用ZGC,停顿时间更短。
  • 设置合适的堆内存:堆内存不要太大,也不要太小。太大的话GC时间长,太小的话频繁GC。Broker的堆内存一般4-8G就够了,剩下的内存留给PageCache。
  • 设置合适的新生代大小:新生代太小会频繁Minor GC,太大的话Major GC时间长。一般新生代占堆内存的1/3到1/2比较合适。
  • 开启GC日志:开启GC日志,方便排查GC相关的问题。可以通过GC日志分析GC的频率、停顿时间等,然后针对性地优化。

2. 操作系统优化

  • 调整文件句柄数:前面说了,RocketMQ会打开很多文件,要把文件句柄数调大,比如655350。
  • 调整进程数限制:调整用户的最大进程数,避免因为进程数不够导致问题。
  • 关闭swap:RocketMQ对延迟敏感,swap会导致性能下降,建议关闭swap,或者设置swappiness=0,尽量不用swap。
  • 调整磁盘调度算法:SSD硬盘推荐用noop或者deadline调度算法,机械硬盘推荐用cfq。
  • 调整TCP参数:调整TCP的缓冲区、连接数等参数,提升网络性能。比如net.core.somaxconn、net.ipv4.tcptwreuse等。
  • 关闭防火墙或者优化防火墙规则:防火墙会影响网络性能,如果是内网环境,可以关闭防火墙,或者优化防火墙规则,减少对性能的影响。

七、优化效果

经过这一轮优化,我们的RocketMQ集群性能有了很大的提升:

  • 吞吐量:从原来的每秒几千条提升到了每秒十几万条,提升了20多倍。
  • 发送延迟:从原来的几十毫秒降到了几毫秒,高峰期也能控制在10毫秒以内。
  • 消费堆积:高峰期不再堆积,消费能跟上生产的速度。
  • Broker CPU:从原来的80%以上降到了30%左右,系统负载大大降低。
  • 磁盘IO:从原来的IO等待严重,降到了IO使用率20%以下,磁盘不再是瓶颈。

当然,优化的效果和业务场景、硬件配置都有关系,我们的优化经验不一定完全适用于你的环境,但是优化的思路和方法是通用的。

八、性能优化的注意事项

最后说几个性能优化的注意事项:

第一,不要盲目调参。性能优化要基于数据,基于监控,先找到瓶颈,再针对性地优化,不要看到参数就乱调,那样可能会适得其反。

第二,压测验证。每一个优化措施,都要在测试环境压测验证,确认没问题了再上生产环境。不要直接在生产环境改参数,那样很容易出问题。

第三,逐步优化。不要一次性改很多东西,要一个一个地改,改完之后观察效果,确认没问题了再改下一个。这样如果出了问题,也能快速定位。

第四,关注业务场景。不同的业务场景,优化的重点不一样。比如消息量大但是消息小的场景,重点在吞吐量;消息量小但是消息大的场景,重点在消息大小的处理;对延迟要求高的场景,重点在低延迟优化。要根据自己的业务场景,选择合适的优化措施。

第五,可靠性和性能的平衡。性能优化不能以牺牲可靠性为代价。比如异步刷盘性能好,但是可能会丢消息;异步发送性能好,但是要处理好发送失败。要在可靠性和性能之间找到平衡,根据业务的要求来选择。

九、写在最后

RocketMQ是一个优秀的消息中间件,性能很强,但是要发挥出它的性能,需要做好配置和优化。

这篇文章分享了我们在生产环境中做RocketMQ性能优化的实战经验,包括Broker端、Producer端、Consumer端、JVM、操作系统等各个方面的优化措施,希望能给大家一些参考。

性能优化是一个持续的过程,不是一次性的。业务在不断发展,数据量在不断增长,性能瓶颈也会不断变化,所以要持续监控,持续优化。

如果大家在使用RocketMQ的过程中遇到性能问题,欢迎在评论区留言讨论,一起交流学习。