RocketMQ作为消息中间件,经常会被用来和其他消息中间件做比较,比对rabbitmq, kafka... 但个人觉得它一直对标的,都是kafka。因为它们面对的场景往往都是超高并发,超高性能要求的场景。
所以,有必要深挖下其实现高性能,高并发的原因。实际上,这是非常大的话题,我这里也不打算一口吃个大胖子。我会给出个大概答案,然后我们再深入挖掘其中部分实现。如题所述。
1:高性能高并发系统的底层技能概述
我不打算单讲rocketmq到底是如何实现高性能高并发的,因为实际上的底层原则都是差不多的。rocketmq不过是其中的一个实现者而已。
那么,要想实现高性能高并发,大概需要怎么做的呢?本质上讲,我们的系统服务能利用的东西并不多,CPU、内存、磁盘、网络、硬件特性。。。当然了,还有一个非常重要的东西,就是我们基本都是在做应用层服务,所以我们的能力往往必须依托于操作系统提供的服务,由这些服务去更好地利用底层硬件的东西。好吧,显得逼格好像有点高了,实际上就是一个系统API调用。
接下来,我们从每个小点出发,来看看我们如何做到高性能高并发:
第一个:CPU。可以说,CPU代表了单机的极限。如果能够做有效利用CPU, 使其随时可保证在80%以上的使用率,那么你这个服务绝对够牛逼了(注意不是导致疯狂GC的那种利用率哦)。那么,我们如何做到高效利用CPU呢?有些应用天然就是CPU型的,比如提供一些做大数的运算服务,天生就需要大量CPU运算。而其他的很多的IO型的应用,则CPU往往不会太高,或者说我们应该往高了的方向优化。
第二个:内存。内存是一个非常宝贵的资源,因为内存的调度速度非常快。如果一个应用的业务处理都是基于内存的,那么这个应用基本上就会超级强悍。大部分情况下,越大的内存往往也能提供越高的性能,比如ES的搜索服务,要想性能好必需有足够内存。当然了,内存除了使用起来非常方便之外,它还有一个重要的工作,就是内存的回收。当然,这部分工作一般都会被编程语言屏蔽掉,因为它实在太难了。我们一般只需按照语言特性,合理的处理对象即可。另外,我们可以使一些可能需要从外部设备读入的数据,加载到内存中长期使用,这将是一件非常重要的优化动作。如何维护好数据一致性与安全性和准确性,是这类工作的重点。
第三个:磁盘。内存虽好,但却不常有。内存往往意味着大小受限。而与之对应的就是磁盘,磁盘则往往意味空间非常大,数据永久存储安全。磁盘基本上就代表了单机的存储极限,但也同时限制了并发能力。
第四个:网络。也许这个名词不太合适,但却是因为网络才发生了变化。如果说前面讲的都是单机的极限性能,那么,网络就会带来分布式系统的极限性能。一个庞大的系统,一定是分布式的,因此必然会使用到网络这个设备。但我们一般不会在这上面节省多少东西,我们能做的,也许就是简单的压缩下文件数据而已。更多的,也许我们只是申请更大的带宽,或者开辟新的布线,以满足系统的需要。在网络这一环境,如何更好地组织网络设备,是重中之重,而这往往又回到了上面三个话题之一了。
最后,排除上面几个硬技能,还有一个也是非常重要的技能:那就是算法,没有好的算法,再多的优化可能也只是杯水车薪。(当然了我们大部分情况下是无需高级算法的,因为大部分时间,我们只是大自然的搬运工)
2. 高性能高并发操作系统api列举
前面说的,更多是理论上讲如何形成牛逼的应用服务。但我们又没那能力去搞操作系统的东西,所以也只能想想而已。那么说到底,我们能做什么呢?所谓工欲善其事,必先利其器。所谓利器,也就是看看操作系统提供什么样的底层API 。
我这里就稍微列几个吧(我也就知道这么些了):
epoll系列: IO多路复用技术,高并发高性能网络应用必备。大致作用就是使用极少数的线程,高效地管理大量io事件,通知应用等。大概的接口有: epoll_create(), epoll_ctl(), epoll_wait();
pagecache系列: 操作系统页缓存,高效读写文件必备。大致作用就是保留部分磁盘数据在内存中,以便应用想读取或者磁盘数据数据时能够非常快速的响应。相关接口如: read(), write(), readpage(), writepage(), sync(), fsync().
mmap系列: 内存映射。可以将文件映射到内存中,用户写数据时直接向该内存缓冲区写数据,即可达到写磁盘的作用了,从而提高写入性能。接口如: mmap(), munmap();
directio系列: 直接io操作,避免用户态数据到内核态数据的相互copy, 节省cpu和内存占用。
cas系列: 高效安全锁实现。相关接口: cmpxchg() 。
多线程系列: 大部分网络应用,都io型的,那么如何同时处理多个请求就非常重要了。多线程提供非常便捷的并发编程基础,使得我们可以更简单的处理业务而且提供超高的处理能力。这自然是编程语言直接提供的。
3. rocketmq中的高性能法宝
rocketmq想要实现高并发高性能处理能力,自然要从操作系统层面去寻求方法,自然也就逃不过前面的几点说法了。
首先,它基于netty实现了高性能的网络通信,netty基于事件的io模型,零拷贝的技术,已经提供了非常好的技术前提,rocketmq只需利用一下,就可以将自己变得很厉害了。当然,这只是其厉害的一个点,因为单有了高效网络通信处理能力还不够的。至少,rocketmq得提供高效的数据序列化方式。
其次,有了netty作为通信框架,高效接入请求后,rocketmq自身处理业务的方式非常重要。如果能够直接基于内存保存数据,那必然是最高性能的。但是它不能那样做,因为内存太小,无法容纳下应有的消息。所以,只能基于文件做存储。而文件本身的操作又是代价非常高的,所以,必须要有些稍微的措施,避免重量级的操作文件。所以,文件的多级存储又是非常重要的了,即如索引文件在db中的应用就知道了。
再其次,java提供了非常好的多线程编程环境,不加以利用就对不起观众了。良好的线程模型,为其高性能呐喊助威。
最后,基于pagecache和mmap的高效文件读写,才是其制胜法宝。这也是我们接下来想要重点说明的。
4. rocketmq中对mmap和pagecache的应用
上一点中提到的每个点,都是rocketmq出众的原因,但我们今天只会来说一点:rocketmq的高效文件存储。
实际上,根据我之前的几篇文章,我们很容易找到rocketmq是如何对文件进行读写的。我们就以producer写消息数据为例,来回顾看看rmq是如何进行高效文件存储的。
以上过程看似复杂,实则只有最后一个bytebuffer.putXxx() 是真正的和mappedFile 是相关的。当然了,还有MappedFile的初始过程,它会先尝试从现有打开的mappFiles中获取最后一个实例,如果mappedFile满了之后,就会尝试创建一个新的mappedFile, 这个过程一般伴随着新的commitLog文件的创建。
mappedFile 的刷盘动作,主要分为同步刷盘和异步刷,底层都是一样的,即调用 flush(),MappedFileChannel.force(), 将pagecache强制刷入到磁盘上。一般地,将数据写入pagecache,基本就能保证不丢失了。但还是有例外情况,比如机器掉电,或者系统bug这种极端情况,还是会导致丢数据哟。
下面大致来看看 mappedFile 同步刷盘过程:
最后,来看看mappedFile 的创建和预热过程如何:
mappedFile 预热:
如此,整个rocketmq对mappedfile的使用过程就厘清了。
5. mappedFile压测性能几何
到底使用mappedFile 之后,性能提升了多少呢?以便衡量收益如何。使用jmh 压测下。
测试结果如下:
很明显,mmap厉害些!结论粗糙,仅供参考!
领取专属 10元无门槛券
私享最新 技术干货