轉自 知乎 https://zhuanlan.zhihu.com/p/21355046html
order從client端傳入,decode後進行matching,一旦存在可成交的價格,就要publish到time series,而且把trade存到local的database裏。如何handle這麼大數量的數據?java
這並非一個新生的問題。一個常常想到的模型是producer consumer model。git
當系統的處理速度比不上導入數據的速度時,能夠增長一個queue(buffer)暫存數據,等待consumer處理。數據在queue中被執行的順序和交易策略有關。github
除了handle大量數據,電子商務對數據延遲的要求也很高。首先定義一下什麼是數據的延遲。多線程
數據的延遲包括數據的處理時間和數據的移動時間。其中第二部分在編寫代碼時常被忽略,但在實際生產過程當中常佔有很大比重。app
Blocking queue有兩種,array-based和linked list based。array-based相對更優,這裏咱們先對它進行一些分析。性能
Producer和consumer處理速度每每是不一樣的,這樣容易造成兩種狀況:一種是producer速度快,queue易全滿,另外一種是consumer速度快,queue易全空。google
Blocking queue的缺點主要有兩個:一個是producer只能從head放數據,producer之間會競爭head指針,存在寫競爭。consumer之間會競爭tail指針,它們之間也存在寫競爭。而且不少狀況下,queue是處於全空狀態,head/tail指針指向同一個entry,producer和consumer之間也存在寫競爭。所以須要lock來實現synchronization。另外一個缺點是heal/tail指針的false sharing。atom
在進行下面講解前先下兩個結論,理由後續會涉及。spa
如今的計算機構架每每是有CPU,memory,它們之間有多層的cache。這種構架產生的緣由在於CPU速度遠高於memory速度。
爲了解決速度間不一樣步,可使用cache。Cache是對歷史數據的保存。若是數據以前沒有讀取,不存在於本層cache,則須要從上層cache裏獲取,存到本層cache裏;若以前有讀取,本層cache存有數據歷史信息。有了cache,數據的移動路徑變短。同時,寫操做相對費時,CPU會在較優的時間進行寫操做。
具體cache如何工做,見下圖:
Memory的最小單位叫block,cache的最小單位叫cache line。Memory到cache存在多對一的mapping。圖中用相同顏色表示它們之間的mapping。舉例說明,若是有一個integer array,裏面存有10個數據。它們一一映射到cache裏。
但每每cache的尺寸小於memory,19,20沒法寫入。對於多對一的這種mapping,存在對原有數據的eviction,19,20寫在原來11,12處。
Memory的block每每大於一個integer。在讀取A[0]時,實際上也把A[1]讀取進去。因此在實際讀取A[1]時,它已經存在於cache裏。這是一種spatial cache locality。
若是A[9]變成35,只須要對cache裏的數據進行更改。
若是使用多個核,這種方法會出現問題。好比第二個核裏的數據仍是原來的20。
多核中對數據修改時,若是數據存在於多個核的cache裏,要將其餘核裏數據設爲invalid。
下面介紹false sharing。假設有兩個integer a = 13和b = 14。若是第一個核在訪問a,第二個核在訪問b。
Core 1在訪問a時讓core 2中對應數據invalid,core 2修改時發現invalid,從新讀取數據。可是core 2在讀寫時又把core 1對應數據invalid。Blocking queue裏由於head/tail指針常是同一個,而producer和consumer在不一樣的core上運行,常會發生上述的false sharing,加大了數據移動的時間。
爲何使用lock會形成不少數據的移動?
如下圖爲例:
Core 1裏有thread 1在運行,當遇到lock後,thread 1 sleep,core 1裏運行thread 2。對於thread 2,core 1裏cache的數據都是無用的。
Thread 2從新加載數據運行。當thread 1醒來時,只能在core 2上運行,從新加載數據。因此當有lock的時候,出現了不少的cache miss,增長了數據移動的時間。
總結一下,blocking queue很慢的緣由在於:寫競爭形成的thread arbitrage以及false sharing致使的不少memory access。
Design of Disruptor
在設計Disruptor時要避免寫競爭,讓數據更久的留在cache裏。
設計原則有:只有一個consumer,避免使用lock等等。
Disruptor的核心是一個circular array,有個cursor,裏面有sequence number,數據類型是long。若是不考慮consumer,只有一個producer在寫,就是不停的往entry裏寫東西,而後增長cursor上的sequence number。爲了不cursor裏的sequence number和其餘variable造做false sharing,disruptor定義了7個long型,並無給它們賦值,而後再定義cursor。這樣cursor就不會和其餘variable同時出如今一個cache line裏。
若是producer在寫的過程當中,超出了原來array的長度,就不停地overwrite原來的entry,增長cursor裏的sequence number。bucket裏的entry都是pre-allocated,避免每次都new一個object。由於disruptor是用java寫的,這樣能夠避免garbage collection。producer寫的過程是two phase commit。
若是加入了consumer,以下圖:
若是consumer當前訪問的sequence number爲5,producer當前訪問到18。那麼consumer能夠一路訪問到18,producer往前寫不能超過5。
若是有多個consumer:
在disruptor裏不一樣consumer之間沒有contention。如上圖中consumer 1能夠從5讀到18,consumer 2能夠不用管consumer 1的存在,也一路讀到18,consumer之間能夠忽略對方的存在。
Consumer每次在訪問時須要先檢查sequence number是否available,若是不available,會有多種策略。latency最高的一種是盲等。producer在寫的時候,須要檢查最低的sequence number在哪兒。這裏不須要lock的緣由是sequence number是遞增的。producer不須要趕在最低sequence number前面,於是沒有write contention。此外,disruptor使用memory barrier通知數據的更新。
Memory barrier
CPU認爲邏輯上沒有衝突的instruction能夠reorder。寫操做須要花不少時間,能夠在schedule pipeline比較方便的時候把instruction插進去。好比core 1須要寫a,b,c,d。由於這四個variable之間沒有關係,它們的順序也是能夠打亂的。在disruptor中並不直接把它們寫入cache中,而是寫入core和cache直接的一個store buffer裏,在store buffer裏四個variable是reorder的。
單線程下沒有任何問題,可是多線程時,core 2角度來看,c先被寫,而後是d,a,b。在disruptor裏producer最後update cursor裏的sequence number,告訴你們這個entry已經ready,全部的consumer能夠讀它。可是若是寫entry的順序和寫sequence number的順序不一致,會形成一種現象:sequence number的寫已經完成,consumber能夠去讀對應數據,可是對應的entry的寫尚未ready。
在java裏用volatile字段修飾。CPU在執行時,遇到這個字段把store barrier裏的數據清空。
在大部分狀況下,consumer是跟在producer後面的。disruptor比較理想的狀況就是一個producer,多個consumer。
若是一個consumer處理的很慢,producer會被block,這是一個瓶頸。解決方法能夠是把buffer變大。寫較慢的一種操做是寫往database中,這時能夠寫多個數據後再統一commit,這也是一種方法。還有不少其餘方面的技巧,這裏再也不一一介紹。
若是涉及到多個producer,也不須要lock。每一個時刻只有一個thread能夠increment這個數,保證只有一個producer能更新sequence number,實現atomicity。這裏面使用了一個producer barrier類,裏面有不少method作具體的實現。
前面所講都是比較簡單的狀況,現實中依據dependence graph,disruptor能夠構成很複雜的情形。
好比producer寫入數據後被consumer 1和2處理,1,2處理完後consumer 3才能接着處理。這些能夠經過設置不一樣的waiting strategy來實現。
經過圖表能夠看出,disruptor的性能確實比blocking queue好不少。
最後回答一下常見的問題:
1. 若是buffer經常是滿的怎麼辦?
一種是把buffer變大,另外一種是從源頭解決producer和consumer速度差別太大問題,好比試着把producer分流,或者用多個disruptor,使每一個disruptor的load變小。
2. 何時使用disruptor?
若是對latency的需求很高,能夠考慮使用。
Reference:
Source code
https://lmax-exchange.github.io/disruptor/
Technical paper
http://disruptor.googlecode.com/files/Disruptor-1/pdf
Blogs
http://bad-concurrency.blogspot.com/
http://mechanitis.blogspot.com/
Latency Numbers Every Programmer Should Know
http://www.eecs.berkeley.edu/~rcs/research/interactive_latency.html