构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue--转载
原文地址:http://maoyidao.iteye.com/blog/1663193
一個僅僅部署在4臺服務器上的服務,每秒向Database寫入數據超過100萬行數據,每分鐘產生超過1G的數據。而每臺服務器(8核12G)上CPU占用不到100%,load不超過5。這是怎么做到呢?下面將給你描述這個架構,它的核心是一個高效緩沖區設計,我們對它的要求是:
1,該緩存區要盡量簡單
2,盡量避免生產者線程和消費者線程鎖
3,盡量避免大量GC
緩沖 vs 性能瓶頸
提高硬盤寫入IO的銀彈無疑是批量順序寫,無論是在業界流行的分布式文件系統或數據,HBase,GFS和HDFS,還是以磁盤文件為持久化方式的消息隊列Kafka都采用了在內存緩存數據然后再批量寫入的策略。這一個策略的性能核心就是內存中緩沖區設計。這是一個經典的數據產生者和消費者場景,緩沖區的要求是當同步寫入和讀出時:(1)寫滿則不寫(2)讀空則不讀(3)不丟失數據(4)不讀重復數據。最直接也是常用的方式就是JDK自帶的LinkedBlockingQueue。LinkedBlockingQueue是一個帶鎖的消息隊列,寫入和讀出時加鎖,完全滿緩沖區上面的四個要求。但是當你的程序跑起來之后,看看那個線程CPU消耗最高?往往就是在線程讀LinkedBlockingQueue鎖的時候,這也成為很多對吞吐要求很高的程序的性能瓶頸。
Disruptor
解決加鎖隊列產生的性能問題?Disruptor是一個選擇。Disruptor是什么?看看開源它的公司LMAX自己是怎么介紹的:
?
我們花費了大量的精力去實現更高性能的隊列,但是,事實證明隊列作為一種基礎的數據結構帶有它的局限性——在生產者、消費者、以及它們的數據存儲之間的合并設計問題。Disruptor就是我們在構建這樣一種能夠清晰地分割這些關注問題的數據結構過程中所誕生的成果。
?
OK,Disruptor是用來解決我們這個場景的問題的,而且它不是隊列。那么它是什么并且如何實現高效呢?我這里不做過多介紹,網上類似資料很多,簡單的總結:
1,Disruptor使用了一個RingBuffer替代隊列,用生產者消費者指針替代鎖。
2,生產者消費者指針使用CPU支持的整數自增,無需加鎖并且速度很快。Java的實現在Unsafe package中。
?
使用Disruptor,首先需要構建一個RingBuffer,并指定一個大小,注意如果RingBuffer里面數據超過了這個大小則會覆蓋舊數據。這可能是一個風險,但Disruptor提供了檢查RingBuffer是否寫滿的機制用于規避這個問題。而且根據maoyidao測試結果,寫滿的可能性不大,因為Disrutpor確實高效,除非你的消費線程太慢。
?
并且使用一個單獨的線程去處理RingBuffer中的數據:
?
Java代碼???
ValueEvent通常是個自定義的類,用于封裝你自己的數據:
?
Java代碼???
?
生產者通過RingBuffer.publish方法向buffer中添加數據,同時發出一個事件通知消費者有新數據達到,并且,,,注意我們是怎么規避數據覆蓋問題的:
?
Java代碼???
數據消費者代碼在EventHandler中實現:
?
Java代碼???
很好,完成!用以上代碼跑個壓測,結果果然比加鎖隊列快很多(Disruptor官網上有benchmark數據,我這里就不提供對比數據)。好,用到線上環境。。。。結果是。。。CPU反而飆升了!??
Disruptor的坑
?
書接上文,Disruptor壓測良好,但上線之后CPU使用達到650%,LOAD接近300!分析diruptor源碼可知,造成cpu過高的原因是 RingBuffer 的waiting策略,Disruptor官網例子使用的策略是 SleepingWaitStrategy ,這個類的策略是當沒有新數據寫入RingBuffer時,每1ns檢查一次RingBuffer cursor。1ns!跟死循環沒什么區別,因此CPU暴高。改成每100ms檢查一次,CPU立刻降為7.8%。
?
為什么Disruptor官網例子使用這種有如此風險的SleepingWaitStrategy呢?原因是此策略完全不使用鎖,當吞吐極高時,RingBuffer中始終有數據存在,通過輪詢策略就能最大程度的把它的性能優勢發揮出來。但這顯然是理想狀態,互聯網應用有明顯的高峰低谷,不可能總處于滿負荷狀態。因此還是BlockingWaitStrategy 這種鎖通知機制更好:
?
Java代碼???這樣寫入不加鎖,讀出加鎖。相對加鎖隊列少了一半,性能還是有顯著提高。
?
還有沒有更好的方法?
Disruptor是實現緩沖區的很好選擇。但它本質的目的是提供線程間交換數據的高效實現,這是一個很好的通用選擇。那么真對我們數據異步批量落地的場景,還有沒有更好的選擇呢?答案是:Yes,we have!我最終設計了一個非常簡單的buffer,原因是:
1,Disruptor很好,但畢竟多引入了一個依賴,對于新同學也有學習成本。
2,Disruptor不能很好的解決GC過多的問題。
那么更好的緩存是什么呢?這首先要從場景說起。
首先的問題是:我需要一個buffer,但為啥要一個跨線程buffer呢?如果我用同一個線程讀,再用這個線程去寫,這個buffer完全是線程本地buffer,鎖本身就無意義。同時異步Database落地沒有嚴格的順序要求,因此我是多線程同步讀寫,也不需要集中時的buffer來維護順序,因此一個內置于線程中的二維byte[][]數組就可以解決全部問題!
?
Java代碼??實際測試和上線效果良好(效果見本文第一節)!
總結
能夠使用最簡化的代碼完成性能和業務要求,是最完美的方法。根據使用場景,你可以有很多假設,但不要被眼花繚亂的新技術迷惑而拿你自己的服務做小白鼠,最適合的,最簡單的,就是最好的。
?
本文系maoyidao原創,轉載請引用原鏈接:
http://maoyidao.iteye.com/blog/1663193
同時推薦本系列前2篇
?
構建高性能服務(一)ConcurrentSkipListMap和鏈表構建高性能Java Memcached
http://maoyidao.iteye.com/blog/1559420
構建高性能服務(二)java高并發鎖的3種實現
http://maoyidao.iteye.com/blog/1563523
轉載于:https://www.cnblogs.com/davidwang456/p/4559180.html
總結
以上是生活随笔為你收集整理的构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue--转载的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LMAX Disruptor – Hig
- 下一篇: spring security源码分析之