从团队自研的百万并发中间件系统的内核设计看Java并发性能优化

这篇文章,给你们聊聊一个百万级并发的中间件系统的内核代码里的锁性能优化。不少同窗都对Java并发编程很感兴趣,学习了不少相关的技术和知识。好比volatile、Atomic、synchronized底层、读写锁、AQS、并发包下的集合类、线程池,等等。java

一、大部分人对Java并发仍停留在理论阶段
不少同窗对Java并发编程的知识,可能看了不少的书,也经过很多视频课程进行了学习。redis

可是,大部分人可能仍是停留在理论的底层,主要是了解理论,基本对并发相关的技术不多实践和使用,更不多作过复杂的中间件系统。sql

实际上,真正把这些技术落地到中间件系统开发中去实践的时候,是会遇到大量的问题,须要对并发相关技术的底层有深刻的理解和掌握。编程

而后,结合本身实际的业务场景来进行对应的技术优化、机制优化,才能实现最好的效果。安全

所以,本文将从笔者曾经带过的一个高并发中间件项目的内核机制出发,来看看一个实际的场景中遇到的并发相关的问题。性能优化

同时,咱们也将一步步经过对应的伪代码演进,来分析其背后涉及到的并发的性能优化思想和实践,最后来看看优化以后的效果。多线程

二、中间件系统的内核机制:双缓冲机制
这个中间件项目总体就不作阐述了,由于涉及核心项目问题。咱们仅仅拿其中涉及到的一个内核机制以及对应的场景来给你们作一下说明。架构

其实这个例子是大量的开源中间件系统、大数据系统中都有涉及到的一个场景,就是:核心数据写磁盘文件。并发

好比,大数据领域里的hadoop、hbase、elasitcsearch,Java中间件领域里的redis、mq,这些都会涉及到核心数据写磁盘文件的问题。分布式

而不少大型互联网公司自研的中年间系统,一样也会有这个场景。只不过不一样的中间件系统,他的做用和目标是不同的,因此在核心数据写磁盘文件的机制设计上,是有一些区别的。

那么咱们公司自研的中间件项目,简单来讲,须要实现的一个效果是:开辟两块内存空间,也就是经典的内存双缓冲机制。

而后核心数据进来所有写第一块缓冲区,写满了以后,由一个线程进行那块缓冲区的数据批量刷到磁盘文件的工做,其余线程同时能够继续写另一块缓冲区。

咱们想要实现的就是这样的一个效果。这样的话,一块缓冲区刷磁盘的同时,另一块缓冲区能够接受其余线程的写入,两不耽误。核心数据写入是不会断的,能够持续不断的写入这个中间件系统中。

咱们来看看下面的那张图,也来了解一下这个场景。

clipboard.png

如上图,首先是不少线程须要写缓冲区1,而后是缓冲区1写满以后,就会由写满的那个线程把缓冲区1的数据刷入磁盘文件,其余线程继续写缓冲区2。

这样,数据批量刷磁盘和持续写内存缓冲,两个事儿就不会耽误了,这是中间件系统设计中极为经常使用的一个机制,你们看下面的图。

clipboard.png

三、百万并发的技术挑战
先给你们说一下这个中间件系统的背景:这是一个服务某个特殊场景下的中间件系统,总体是集群部署。

而后每一个实例部署的都是高配置机器,定位是单机承载并发达到万级甚至十万级,总体集群足以支撑百万级并发,所以对单机的写入性能和吞吐要求极为高。

在超高并发的要求之下,上图中的那个内核机制的设计就显得尤其重要了。弄的很差,就容易致使写入并发性能过差,达不到上述的要求。

此外在这里多提一句,相似的这种机制在不少其余的系统里都有涉及。好比以前一篇文章:「高并发优化实践」10倍请求压力来袭,你的系统会被击垮吗?,那里面讲的一个系统也有相似机制。

只不过不一样的是,那篇文章是用这个机制来作MQ集群总体故障时的容灾降级机制,跟本文的高并发中间件系统还有点不太同样,因此在设计上考虑的一些细节也是不一样的。

并且,以前那篇文章的主题是讲这种内存双缓冲机制的一个线上问题:瞬时超高并发下的系统卡死问题。

四、内存数据写入的锁机制以及串行化问题
首先咱们先考虑第一个问题,你多个线程会并发写同一块内存缓冲,这个确定有问题啊!

由于内存共享数据并发写入的时候,必须是要加锁的,不然必然会有并发安全问题,致使内存数据错乱。

因此在这里,咱们写了下面的伪代码,先考虑一下线程如何写入内存缓冲。

clipboard.png

好了,这行代码弄好以后,对应着下面的这幅图,你们看一下。

clipboard.png

看到这里,就遇到了Java并发的第一个性能问题了,你要知道高并发场景下,大量线程会并发写内存的,你要是直接这样加一个锁,必然会致使全部线程都是串行化。

即一个线程加锁,写数据,而后释放锁。接着下一个线程干一样的事情。这种串行化必然致使系统总体的并发性能和吞吐量会大幅度下降的。

五、内存缓冲分片机制+分段枷锁机制
所以在这里必需要对内存双缓冲机制引入分段加锁机制,也就是将内存缓冲切分为多个分片,每一个内存缓冲分片就对应一个锁。

这样的话,你彻底能够根据本身的系统压测结果,调整内存分片数量,提高锁的数量,进而容许大量线程高并发写入内存。

咱们看下面的伪代码,对这块就实现了内存缓冲分片机制:

clipboard.png

好!咱们再来看看,目前为止的图是什么样子的:

clipboard.png

这里由于每一个线程仅仅就是加锁,写内存,而后释放锁。

因此,每一个线程持有锁的时间是很短很短的,单个内存分片的并发写入通过压测,达到每秒几百甚至上千是没问题的,所以线上系统咱们是单机开辟几十个到上百个内存缓冲分片的。

通过压测,这足以支撑每秒数万的并发写入,若是将机器资源使用的极限,每秒十万并发也是能够支持的。

六、缓冲区写满时的双缓冲交换
那么当一块缓冲区写满的时候,是否是就必需要交换两块缓冲区?接着须要有一个线程来将写满的缓冲区数据刷写到磁盘文件中?

此时的伪代码,你们考虑一下,是否是以下所示:

clipboard.png

一样,咱们经过下面的图来看看这个机制的实现:

clipboard.png

七、且慢!刷写磁盘不是会致使锁持有时间过长吗?
且慢,各位同窗,若是按照上面的伪代码思路,必定会有一个问题:要是一个线程,他获取了锁,开始写内存数据。

而后,发现内存满了,接着直接在持有锁的过程当中,还去执行数据刷磁盘的操做,这样是有问题的。

要知道,数据刷磁盘是很慢的,根据数据的多少,搞很差要几十毫秒,甚至几百毫秒。

这样的话,岂不是一个线程会持有锁长达几十毫秒,甚至几百毫秒?

这固然不行了,后面的线程此时都在等待获取锁而后写缓冲区2,你怎么能一直占有锁呢?

一旦你按照这个思路来写代码,必然致使高并发场景下,一个线程持有锁上百毫秒。刷数据到磁盘的时候,后续上百个工做线程所有卡在等待锁的那个环节,啥都干不了,严重的状况下,甚至又会致使系统总体呈现卡死的状态。

八、内存 + 磁盘并行写机制
因此此时正确的并发优化代码,应该是发现内存缓冲区1满了,而后就交换两个缓冲区。

接着直接就释放锁,释放锁了以后再由这个线程将数据刷入磁盘中,刷磁盘的过程是不会占用锁的,而后后续的线程均可以继续获取锁,快速写入内存,接着释放锁。

你们先看看下面的伪代码的优化:

clipboard.png

按照上面的伪代码的优化,此时磁盘的刷写和内存的写入,彻底能够并行同时进行。

由于这里核心的要点就在于大幅度下降了锁占用的时间,这是java并发锁优化的一个很是核心的思路。

你们看下面的图,一块儿来感觉一下:

clipboard.png

九、为何必需要用双缓冲机制?
其实看到这里,你们可能或多或少都体会到了一些双缓冲机制的设计思想了,若是只用单块内存缓冲的话,那么从里面读数据刷入磁盘的过程,也须要占用锁,而此时想要获取锁写入内存缓冲的线程是获取不到锁的。

因此假如只用单块缓冲,必然致使读内存数据,刷入磁盘的过程,长时间占用锁。进而致使大量线程卡在锁的获取上,没法获取到锁,而后没法将数据写入内存。这就是必需要在这里使用双缓冲机制的核心缘由。

十、总结
最后作一下总结,本文从笔者团队自研的百万并发量级中间件系统的内核机制出发,给你们展现了Java并发中加锁的时候:

如何利用双缓冲机制 内存缓冲分片机制 分段加锁机制 磁盘 + 内存并行写入机制 高并发场景下大幅度优化多线程对锁的串行化争用问题 长时间占用锁的问题

其实在不少开源的优秀中间件系统中,都有不少相似的Java并发优化的机制,主要就是应对高并发的场景下大幅度的提高系统的并发性能以及吞吐量。你们若是感兴趣,也能够去了解阅读一下相关的底层源码。

欢迎工做一到五年的Java工程师朋友们加入Java高级架构:617912068群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用本身每一分每一秒的时间来学习提高本身,不要再用"没有时间“来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!

相关文章
相关标签/搜索