從團隊自研的百萬併發中間件系統的內核設計看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等多個知識點的架構資料)合理利用本身每一分每一秒的時間來學習提高本身,不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!

相關文章
相關標籤/搜索