標籤:
原文地址:http://maoyidao.iteye.com/blog/1663193
一個僅僅部署在4台伺服器上的服務,每秒向Database寫入資料超過100萬行資料,每分鐘產生超過1G的資料。而每台伺服器(8核12G)上CPU佔用不到100%,load不超過5。這是怎麼做到呢?下面將給你描述這個架構,它的核心是一個高效緩衝區設計,我們對它的要求是:
1,該緩衝區要盡量簡單
2,盡量避免生產者線程和消費者線程鎖
3,盡量避免大量GC
緩衝 vs 效能瓶頸
提高硬碟寫入IO的銀彈無疑是批量順序寫,無論是在業界流行的Distributed File System或資料,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代碼
- RingBuffer ringBuffer = new RingBuffer<ValueEvent>(ValueEvent.EVENT_FACTORY,
- new SingleThreadedClaimStrategy(RING_SIZE),
- new SleepingWaitStrategy());
-
- SequenceBarrier barrier = ringBuffer.newBarrier();
-
- BatchEventProcessor<ValueEvent> eventProcessor = new BatchEventProcessor<ValueEvent>(ringBuffer, barrier, handler);
- ringBuffer.setGatingSequences(eventProcessor.getSequence());
- // only support single thread
- new Thread(eventProcessor).start();
ValueEvent通常是個自訂的類,用於封裝你自己的資料:
Java代碼
- public class ValueEvent {
- private byte[] packet;
-
- public byte[] getValue()
- {
- return packet;
- }
-
- public void setValue(final byte[] packet)
- {
- this.packet = packet;
- }
-
- public final static EventFactory<ValueEvent> EVENT_FACTORY = new EventFactory<ValueEvent>()
- {
- public ValueEvent newInstance()
- {
- return new ValueEvent();
- }
- };
- }
生產者通過RingBuffer.publish方法向buffer中添加資料,同時發出一個事件通知消費者有新資料達到,並且,,,注意我們是怎麼規避資料覆蓋問題的:
Java代碼
- // Publishers claim events in sequence
- long sequence = ringBuffer.next();
-
- // if capacity less than 10%, don‘t use ringbuffer anymore
- if(ringBuffer.remainingCapacity() < RING_SIZE * 0.1) {
- log.warn("disruptor:ringbuffer avaliable capacity is less than 10 %");
- // do something
- }
- else {
- ValueEvent event = ringBuffer.get(sequence);
- event.setValue(packet); // this could be more complex with multiple fields
- // make the event available to EventProcessors
- ringBuffer.publish(sequence);
- }
資料消費者代碼在EventHandler中實現:
Java代碼
- final EventHandler<ValueEvent> handler = new EventHandler<ValueEvent>()
- {
- public void onEvent(final ValueEvent event, final long sequence, final boolean endOfBatch) throws Exception
- {
- byte[] packet = event.getValue();
- // do something
- }
- };
很好,完成!用以上代碼跑個壓測,結果果然比加鎖隊列快很多(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代碼
- RingBuffer ringBuffer = new RingBuffer<ValueEvent>(ValueEvent.EVENT_FACTORY,
- new SingleThreadedClaimStrategy(RING_SIZE),
- new BlockingWaitStrategy());
這樣寫入不加鎖,讀出加鎖。相對加鎖隊列少了一半,效能還是有顯著提高。
還有沒有更好的方法?
Disruptor是實現緩衝區的很好選擇。但它本質的目的是提供線程間交換資料的高效實現,這是一個很好的通用選擇。那麼真對我們資料非同步批量落地的情境,還有沒有更好的選擇呢?答案是:Yes,we have!我最終設計了一個非常簡單的buffer,原因是:
1,Disruptor很好,但畢竟多引入了一個依賴,對於新同學也有學習成本。
2,Disruptor不能很好的解決GC過多的問題。
那麼更好的緩衝是什麼呢?這首先要從情境說起。
首先的問題是:我需要一個buffer,但為啥要一個跨線程buffer呢?如果我用同一個線程讀,再用這個線程去寫,這個buffer完全是執行緒區域buffer,鎖本身就無意義。同時非同步Database落地沒有嚴格的順序要求,因此我是多線程同步讀寫,也不需要集中時的buffer來維護順序,因此一個內建於線程中的二維byte[][]數組就可以解決全部問題!
Java代碼
- public class ThreadLocalBoundedMQ {
- private long lastFlushTime=0L;
-
- private byte[][] msgs=new byte[Constants.BATCH_INS_COUNT][];
-
- private int offset=0;
-
- public byte[][] getMsgs(){
- return msgs;
- }
-
- public void addMsg(byte[] msg)
- {
- msgs[offset++]=msg;
- }
-
- public int size() {
- return offset;
- }
-
- public void clear() {
- offset=0;
- lastFlushTime=System.currentTimeMillis();
- }
-
- public boolean needFlush(){
- return (System.currentTimeMillis()-lastFlushTime > Constants.MAX_BUFFER_TIME)
- && offset>0;
- }
- }
實際測試和上線效果良好(效果見本文第一節)!
總結
能夠使用最簡化的程式碼完成效能和業務要求,是最完美的方法。根據使用情境,你可以有很多假設,但不要被眼花繚亂的新技術迷惑而拿你自己的服務做小白鼠,最適合的,最簡單的,就是最好的。
本文系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
構建高效能服務(三)Java高效能緩衝設計 vs Disruptor vs LinkedBlockingQueue--轉載