從Redis+Lua到Goroutine,日均10億次的股票行情計算實踐

http://www.10tiao.com/html/730/201609/2652941778/1.html

2016-09-14 10:28 陶瑞甫

股票行情數據是一種典型的時序數據(Time-series Data),在一般的IT系統中,日誌數據其實也是一種時序數據,在大數據的世界裏,也有大量應用是基於時序數據處理的,可以說時間序列的數據無處不在。所以,哪怕是不炒股、不熟悉金融世界的工程師,從本文也可以瞭解一些具有普遍性的技術思考,例如在大規模、高密度的數據處理中,是把數據快照搬到計算節點作運算還是把計算能力放到數據節點中“就地計算”?協程(co-routine)在這類計算密集型系統中又有何作用?

實時行情服務是券商的基礎服務,給普通投資者描繪出風雲變幻的動態市場畫面,也給量化投資者提供最重要的建模基礎數據和下單信號,時間就是金錢,行情服務必須要快。證券交易系統行情指標很豐富,最基礎常見的包括:行情報價、分筆數據、分時數據、分鐘K線、日周月年K線、各類財務技術指標、多維度排序、多維度統計等等,這些指標需要由交易所的行情數據流結合時間流進行統計計算。本文分享廣發證券行情服務並行化計算演進的過程,包括五個部分:

  • 股票行情是怎麼回事

  • 日均10億次的行情指標計算

  • 基於Redis+Lua的方案:“就地計算”

  • 引入Goroutine的方案:“海量算子”

  • 孰優孰劣

一、不炒股沒關係,股票行情科普在這裏

任何交易在達成之前,通常都有一個討價還價的僵持過程,或者買方讓價,或者賣方讓價,兩不相讓的時候需要第三方介入撮合取個“平均價”,這個過程就是定價。證券股票交易也不例外,不同的是買賣雙方互不可見,雙方通過券商渠道把自己的價量報給交易所,交易所通常按照達成最大成交量的原則撮合定價。撮合涉及order book和tick data兩個概念。交易所維護兩個“賬本”分別記錄買方和賣方申報的價量,實時或按照一定頻率進行撮合定價成交,申報、成交時對“賬本”進行增、改、刪操作。

這兩個“賬本”就稱作order book,對order book增改刪操作引起數據變化,每個變化的快照就稱作tick data。國內股民數超 1.2 億戶,A股上市公司近3000家,可以想見的到order book是一個大“賬本”,tick data瞬息萬變,一個交易日產生的tick data量更是驚人。國內交易所目前採用抓取快照的方式,抓取order book的前五/十檔價量,剔除”無用的”其他檔價量數據,統計交易當日開市時點到目前時點的最高、最低、成交量、成交額數據,我們將這種數據稱爲原始行情數據(如圖1)。

交易所將原始行情數據近實時的發送給券商,券商行情繫統對這份原始數據進一步處理,比如按時間區間統計出5分鐘、10分鐘、15分鐘、30分鐘、1小時、1天、1周、1月、1季度、1年這十個週期時間段K線蠟燭圖的高、開、低、收價格及成交量、成交額數據。

券商將處理好的行情數據揭示給投資者做再報價參考。可見這類數據量大、變化頻繁、中間統計計算耗時,一旦延遲,以後就再也跟不上“實時”的節奏了。又快又準,是股票行情服務的基本要求,任何誤差、延遲,都可以引起交易者的經濟損失,導致投訴甚至社會事件。系統該如何設計才能高速的處理和展示這類實時行情數據,是個很大的技術挑戰。

圖1

二、你們股炒的爽,我們指標算的酸爽

券商行情繫統可以分爲行情原始數據接收解析、中間指標統計計算處理、行情數據請求響應三個模塊。原始數據接收解析模塊只需要做好與不同交易所的行情消息格式適配即可,行情數據請求響應模塊要達到原始行情報價及各類行情統計指標的快速展現需要減少中間處理步驟,在行情原始數據接收解析、統計指標計算完成後立即推送給終端用戶,同時存一份到內存數據庫中以便終端用戶再次查詢讀取,推送與查詢兩路相結合,達到在數據落地之前即已展示給用戶的效果。

剩下要做的就是要儘量減少中間指標統計計算的處理時延。A股上市公司3000多家,基金加債券數更是上萬,但它們之間是無關聯的,在統計計算時完全可以分片、並行處理,存儲上採用內存數據庫,redis內置多種數據結構的支持滿足多統計指標存儲的需求、容易分片部署便於並行計算,如圖2。

圖 2

以最簡單的十個週期時間段的K線統計指標,證券數保守算1w只,10*1w=10w,也就是說分分秒秒就有近10w的計算量,國內滬深交易所一個交易日開市4個小時,按照交易所行情數據每3秒更新一次,可得出日計算量就有4.8億,加上其他指標計算如市盈率、漲跌幅、換手率、委比委差、多板塊多指標排序,日計算量已突破10億。

三、“就地計算”的方案 – 把計算能力放在數據裏

好在不同的證券可以分片並行、同樣各週期段的K線指標也可以並行統計計算,實時處理這麼大的計算量,我們顯然需要高度並行處理。

我們最早的方案基於Redis,因爲Redis是一個非常好的承載時間序列數據的高性能內存存儲技術。Redis除了內置多種數據結構,還內置了Lua語言解釋器,這意味着Redis除了具備數據存儲能力也擁有了數據計算的能力。一個Redis存儲集羣中,每個節點加載Lua腳本,這基本上就是一個分佈式並行計算集羣了。

我們剩下要做的是事情是實現一個行情收集器,接收來自不同交易所的行情原始數據(node.js善於處理io型事務,很適合用來實現收集器,細節不是本文焦點,不在此詳述),發送eval lua指令到Redis集羣。 Redis收到指令後執行我們用Lua實現的指標計算邏輯完成指標計算,之後通過Redis pub將行情數據推送出去。如圖3通過將計算挪到Redis存儲節點,我們避免了複雜易出錯的多進程管理問題,也大大簡化了開發的工作量。

圖 3

這個方案的一個優點,是技術架構比較簡單,從數據存儲到運算處理,都在Redis上,我們僅專注於Redis集羣的性能優化、高可用方案實現、容災備份、數據複製。對於運維來說,運維一套相對單一的技術系統,“零部件”(moving parts)越少越好,出現故障、單點失敗的環節也少了很多。

四、“海量算子”- 把數據快照挪到技術節點

上述Redis+Lua的方案,早期也服務了我們的市場與客戶,但是當指標計算量不斷增大後,其不足也日益顯著,主要體現在高度密集的計算導致影響存儲集羣的性能而產生延遲,並且在交易期間對存儲集羣作動態擴容是非常困難的。

有鑑於此,我們很自然的只能把指標計算任務從數據存儲中回收,放棄一個數據與計算一體化的相對簡單的架構,把計算的職責交給存儲之外的專用運算節點。此時Redis變成單純的內存存儲。這種實現將計算與存儲分離,計算所需的數據不再能從”本地”獲取到了,對於Redis而言,運算服務算是“out of process”(進程外),所以計算指標所需的數據,必須以一份數據快照的方式從Redis傳遞到運算節點,用以計算和更新,計算結果最終回寫到Redis,如圖4。

這個對於Redis而言“out of process”的計算能力,我們稱之爲運算節點(相對於Redis的數據節點),是一系列非常容易水平擴容的、高度併發的程序,我們採用了Golang來實現。Golang這個語言,天生支持協程(Goroutine) – 在一個運行的Golang程序中,可輕易啓動上萬的協程,所消耗的資源遠小於線程,天生適合並行計算。

當接收到交易所tick數據時, 在一個運算節點中對每隻證券解析及每類指標計算啓動單獨的goroutine,每個goroutine在它的生命週期中只做一件事就結束(存活時間毫秒級),這很像數學上的函數執行過程完全沒有副作用,goroutine就是一個閉包算子,相互之間毫無影響。 

Golang的內存回收是並行的,數萬個goroutine啓動到銷燬對性能的影響很小,另外tick data本身是有間隔的從交易所發過來的,在goroutine銷燬那一刻往往是tick data空閒的間隔時間,這個空閒時機點用來做goroutine回收是合適的。當然也有一些常駐goroutine用來將指標結果數據落地到Redis。程序語言本身是有各自不同的設計哲學的,Golang正是這樣一種語言,其協程機制讓我們能夠更細粒度的處理可分而治之的系統,同時免去了複雜易出錯的多進程、多線程問題。

圖 4

五、方案比較 - 孰優孰劣

兩種方案的對比:

1、Redis既負責存儲也負責計算

  • 我們通過搭建Redis集羣來進行分佈式並行計算,Redis集羣本身有主備節點,這就涉及到主備同步的問題,雖然Redis自己解決了同步問題並且也支持增量同步,但通過eval lua指令在存儲節點上計算時,同步的不是計算結果而是計算本身,也就是說同一個指標計算在主節點和備節點都需要各自計算一次。Redis又是單線程處理,對於複雜的指標計算通過查看SLOWLOG會有上百毫秒的延遲,這會造成阻塞,對於要求快速的行情服務是不可接受的,爲了減少阻塞,就需要儘可能多的進行分片,這意味着需要起更多的redis節點。

  • Redis集羣一旦搭建,節點縮擴容需要人工干預。對於證券行情服務這種目前9:30-15:00業務高峯,其他時段空閒的系統來說,高峯時無法彈性擴容,低峯時Redis節點進程無法回收造成資源浪費。

2、Redis負責存儲,海量Goroutine做算子

  • 將指標計算從Redis拿出來交給goroutine,Redis僅作存儲節點,主備節點同步的是指標計算結果,這樣降低了Redis進程的cpu使用率,也不再有SLOWLOG記錄,同時僅需要很少的分片,大大減少了Redis節點數。

  • Redis集羣規模的大小僅需要根據業務數據量確定。指標計算量的變化可以輕易通過啓動更多的goroutine來進行方便的彈性擴容,goroutine數也隨投資者活躍程度變化,對交易頻繁的股票只需要啓動更多的goroutine,不會像方案1那樣造成部分Redis節點成爲熱點。在休市時段goroutine完全回收釋放。

圖 5

從Redis+Lua的數據存儲與運算一體化,過度到數據存儲與並行運算分離,也大大減少了系統對硬件資源的要求,Redis集羣規模減少十倍,如圖表1。採用Redis+Lua方案實現上比較簡單,開發工作量較小,收集器與Redis數據交換量小,這種方案更適合簡單的邏輯計算比如計數器;goroutine的實現方案,雖然開發上覆雜,與Redis數據交換量大了一些,但更適合像行情指標計算這類複雜的應用場景。

方案 機器數(1個部署單元) Redis集羣 總CPU核數 總內存 計算最大延遲
In-process:數據“就地計算”(利用Lua) 5臺高配 90個節點 128核 128G 幾十毫秒
Out of  process:數據快照挪到運算節點(基於goroutine) 4臺中配 9個節點 32核 64G 幾毫秒
總結

計算機科學發展到今日,基本的邏輯計算單元從進程到線程,再演進到比線程輕量的協程(co-routine),給開發人員更多的“工具”和更簡單的方法去“榨取”硬件資源的利用率,同時又帶來業務系統性能的提升。在面對諸多工具時,需要我們結合實際業務的場景特點對不同工具進行對比做出合適的設計選擇。

作者介紹 

陶瑞甫,中山大學信科院碩士畢業至今六年多一直從事軟件研發工作,曾在華爲參與底層進程管理、上層雲管理系統研發,在騰訊參與多個互聯網社交增值服務產品研發,2013年初加入廣發證券負責證券行情雲服務建設與研發工作,目前參與證券交易系統相關研發工作。關注軟件層面高性能並行計算技術,並致力於將這些理論技術應用於證券業的實際場景,給投資者帶來更優質的服務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章