RocketMQ引发的磁盘预警复盘
- 前言
- 发现问题
- 排查过程
- Step 1
- Step 2
- Step 3
- Step 4
- Step 5
- Step 6
- Step 7
- Step 8
- Step 9
- 解决方式
- 写在最后
前言
一款优秀的中间件,参数的缺省值必然是经过反复验证得出的最优解,勿动!
发现问题
某台SaaS服务器磁盘不足发出告警。但是另外台业务量更大、配置相同的SaaS服务器一切正常,于是开始着手排查问题。
排查过程
Step 1
怀疑是日志文件导致的磁盘不足。排查后发现并没有大量的日志文件生成,并且归档的脚本运行正常。
Step 2
对比两台服务器的磁盘使用情况。使用df -h
指令后发现,是RocketMQ下的store文件夹竟然有80多G,而另外一台业务量更大的服务器只有50多G。基本就定位到了本次磁盘告警的罪魁祸首了。
Step 3
进入目录store/commitlog/,确认是否真的产生了这么多的消息。找到最新的commitlog文件,把文件名换算成G发现一共也就只有20多G的消息,那么剩下的60多G在哪里呢?
Step 4
使用du -sh *
指令后发现,consumequeue文件夹大的离谱。consumequeue下只是存储了commitlog的消息索引不应该有这么大啊。排查了几个topic后发现,所有consumequeue文件夹下的索引文件,大小都为477MB。
Step 5
翻阅源码,找到consumequeue创建文件大小时的配置参数。
org.apache.rocketmq.store.queue.ConsumeQueueStore#createConsumeQueueByType
所以mappedFileSizeConsumeQueue
这个配置决定了consumeQueue的文件大小,查了下生产的配置,发现了问题。
mappedFileSizeConsumeQueue这里的单位是字节,换算成MB后问题一目了然。
为了验证上述结论,通过MQ Dashborad手动创建了新的Topic,然后手动发送了一条新的消息。确认了新创建的ConsumeQueue文件就是477MB。
Step 6
到此,满怀欣喜的以为完成了闭环。然而,这是个开始。
再测试环境用相同的配置文件部署了一套新MQ,重复上述的测试流程发现
文件大小是4K。用stat
指令对比了下两个文件
Blocks字段指的是512字节构成的块的个数,IO Block是指文件系统的块的大小一般为4096字节。
Step 7
分析下源码,看下RocketMQ在创建文件时,是否会预占空间。
Broker启动时会创建默认的消费存储处理类,在DefaultMessageStore的构造方法中会创建AllocateMappedFileService
org.apache.rocketmq.broker.BrokerController#initialize
AllocateMappedFileService可以理解为一个异步线程,主要工作是初始化MappedFile和预热MappedFile,这里有一段比较关键的逻辑。
org.apache.rocketmq.store.AllocateMappedFileService#mmapOperation
当开启文件预热的配置时,才会去通过mlock方式预占空间。但是,warmMapedFileEnable默认值为false,并且没有做修改。所以按理来说,不应该会有预占空间这回事才对,477MB还是不合理。
Step 8
到此为止,代码层面已经无法解释了。通过github联系上了RocketMQ作者,请教了这个问题。RocketMQ作者给出的排查方向是硬件和文件系统。于是,对比了一下两台SaaS服务器的硬盘,果真还不一样。
一台高效云盘 200GiB (3400 IOPS)
一台是ESSD云盘 PL0 200GiB (4200 IOPS)
以下为RocketMQ作者给出的解释
每个文件初始化的时候,会设置一个filename,不同的文件系统对这个filename的实现是不一样的,如果我猜测没错的话,因为那个ESSD,它为了提高性能,所以在set filename的时候,它底层把那些block全部给预生成出来,这样可以提高写入的性能。但如果是高效云盘,它可能是懒加载的方式,set filename时,它是虚拟的,等到边写的时候边生成,但是这样的话,效率就会比较低。
总结一下就是两种硬件的底层实现方式一个是饿加载,一个是懒加载。这个分析很合理,令人信服。但是出于技术的严谨性,还是需要再验证一下的。
Step 9
刚好强哥有ESSD云盘的阿里云服务器,就麻烦强哥帮忙测试了一下上述的场景,结果翻车了。
只能把最后的希望寄托到文件系统上了,通过mount
查看后发现,两台服务器的文件系统都是ext4类型。
解决方式
问题的排查到此为止,先打上一个TODO吧,后续有想法了会继续排查,如果解决了也会更新博客。最后说一下怎么解决这个问题吧。
- 生产的RocketMQ是双主模式,所以先使用指令
wipeWritePerm
停止其中一台broker的写入,静默后找出消息量较小的topic,进行队列的缩容,减少consumeQueue文件的数量。 - 重启RocketMQ,释放文件句柄。这里切记不可修改
mappedFileSizeConsumeQueue
,该配置生效后若发生修改,可能会造成文件的截断,导致消费的异常。 - 最后大概释放了20多G的空间出来,算是解决了磁盘空间的问题。但这个神奇的477MB问题,还会继续研究下去。
写在最后
排查问题期间也怀疑过是不是RocketMQ版本的问题,因为部署的是3x版本,GitHub上也找不到该版本的源码了。从介入问题到解决问题大概花了一周的时间吧,真是睁眼MQ闭眼也MQ的。我甚至怀疑,很多人都遇到了这个问题,不过没有修改mappedFileSizeConsumeQueue
的默认值,所以文件大小默认也就不到1MB,就算是预占了磁盘空间,也不会有多大的影响。所以跟这个问题也算是蛮有缘分的吧。最后的最后,我觉得mappedFileSizeConsumeQueue
的缺省值其实已经过大了, 实际的空载率已经非常高了,consumequeue文件的头部可能都已经是过期的索引了,所以对于消息量不大的应用来说,单个topic的消息量不超过10万条的,我甚至觉得都可以减少mappedFileSizeConsumeQueue
的配置值。