最近hen ci hen ci用C++寫完了一整套證券行情系統,但是不是服務滬深交易所的,是給文交所用的。整個系統涵蓋了從DBF文件解析開始到客戶端展現這一整條邏輯。想來一年多沒有更新博客了,所以趁這個機會,把整個系統的架構和開發中遇到的問題寫下來,權當總結和分享。
首先要說明的是,整個系統的架構都是以當前業務為出發點的,所以和目前網上看到的,比方說廣發自研的系統是肯定有差別的,我們就沒有合規一說。另外,從用戶規模和市場活躍程度來看,我們也無法和國內證券市場比較,所以和目前公開出來的系統結構相比也還是有差異的。我們根據自身人力資源限制和當前業務角度考慮,首要目標是希望整體架構要簡單,易於橫向擴展。因為一,人太少,平均下來,我就倆人;二,不懂業務,其中接手我服務端開發的還是應屆畢業生。
這里先把系統結構圖羅列一下:
你會看到里面有很多讓人意想不到的東西,比方說SQLite!
容我后續慢慢來說!
DBF文件解析
目前滬深L1數據更新的頻率是3秒。文交所這邊是1秒。總得來說解析DBF文件沒有太大的難度,就是要理解文件結構。DBF文件結構其實也是開放的,隨便查。這個程序沒有任何難度,不需要多線程,只有一個要求,就是解析文件越快越好。
關於DBF么,我就畫一個結構圖在這里好了。方便大家查閱。
行情數據庫程序
首先來張圖展示下行情數據庫程序的結構。
從圖上看,我們的行情程序分三大模塊:
- 業務驅動對象
- Logger
- Config Agent
Logger
也就是日志。我們這里是直接用了Linux自帶的Syslog。當然了回歸到代碼的話,是一個logger接口,然后在Linux上基於Syslog實現了這個logger接口。
為什么選用Syslog?一是我們沒有那么多資源搞一個異步logger;二是以我們目前的壓力來看,Syslog從各方面都滿足我們的要求。而且是獨立的進程,萬一發生不測,不影響我們行情正常運行,無非就是沒有日志了。
Config Agent
配置文件讀取對象。好像沒有神馬好說的。
業務驅動對象
我們重點來說下這個業務驅動對象。
首先來說一說這個業務驅動對象到底是用來干啥的。
服務端程序在設計的時候,你肯定會將業務進行層次划分,這樣做的好處就是結構清晰,易於維護。每一層就是一個模塊,模塊之間規定好訪問接口,形象地說就是高內聚低耦合。
在我們這里,模塊之間的接口都是以boost:: signals2::signal來做的。
網絡層
網絡層只開放了5個接口:
其中shield是指DBF文件解析器。當連上了DBF解析器的時候,網絡層會向上層發送一個shield_connected_signal_t類型的信號,一般這個信號上一層都沒有訂閱。當DBF解析器推送數據過來的時候,網絡層解析成功后,就會發送一個shield_data_ready_signal_t信號。這個信號上次肯定會訂閱的,不然程序就不用跑了。
另外,cache打頭的信號指的是從緩存程序發送過來的請求。和DBF的shield類似,基本上一目了然,顧名思義。
數據轉換層
這層其實可有可無。這層的作用就是將網絡過來的二進制數據解碼成protobuf message對象,然后將protobuf message對象解碼成無第三方庫依賴的本地數據包,並根據數據包的類型,發送相應的信號給數據層。之所以有一個protobuf到本地包的轉換,主要是感謝Google,畢竟Google是出了名的喜歡棄坑。或者說,等哪天有了更好的數據包二進制序列號反序列化庫,只需要考慮將這一場替換掉,就萬事大吉了。當然,目前來看,這一層肯定是我想多了!
數據層
數據層也就是我們真正處理業務的模塊。這個模塊的特點是,宏觀上看簡單,微觀上看復雜。
宏觀上看無非就三件事情:
- 從DBF數據計算出各種周期行情數據
- 將最新的行情數據存盤
- 將最新的行情數據推送到前端緩存
微觀上復雜怎么說呢?復雜就復雜在計算周期行情數據。
行情數據計算
我們給每一個行情數據都指定了一個數據項ID。比方說開盤價我們可以用0xFFFFFFFF這個ID來表示,收盤價可以用0xFFFFFFFE來表示。
除此以外,我們還具體定義了周期ID。實時周期,一分鍾周期,五分鍾周期,十五分鍾周期,三十分鍾周期,六十分鍾周期,日周期,周月季年周期等。
DBF里的數據就相當與實時周期數據。
所以我們有多少數據要計算?顯而易見,數據項ID個數x周期個數!
數據項ID說實話,並不少!所以計算周期數據真的是相當的重體力!
那么計算行情數據到底應該是:
- 遍歷每一個數據項,計算出這個數據項所有周期的數據
- 還是先根據周期來,計算出每一個周期下所有數據項的值
這個話題說到這里,感覺說不下去了。因為我最后付諸的行動不是這么搞的。我不確定我的算法是不是最優算法,但是我想應該八九不離十?
要把這個想法說清楚,估計還是要真正寫一把,你才知道到前面提到的說法到底對不對。
我前前后后寫了大概至少兩遍。我最終的想法容我下來嗎慢慢說來。
先說一說我們數據的特點。行情數據其實都是以時間為順序的離散數據點!這個能理解吧?所以,我們的行情數據在根據DBF文件計算的時候對於任意一個周期來說,無非遇到兩種情況:
- 更新最新的這個時間點數據
- 生成一個最新時間點的數據
另外,在這兩個大類的情下還要考慮一個因素:
- 更新當前最新數據點的時候是否和該周期前一個數據點有依賴關系?
- 生成一個最新數據點的時候是否和該周期前一個數據點有依賴關系?
所以我在計算行情數據時,先區分是否生成一個新的數據點。然后根據周期來計算的。
這個代碼注釋可能不對,心領神會就好。
關於行情計算這里再說最后一點,前段時間發現一個八哥。如果DBF文件里的某只代碼/某個證券的昨收為0時,這個時候,這只代碼當天的昨收是0還是其他值?正確答案是,不是零,是上一個交易日的昨收。
數據存盤
終於說到數據存盤,說到SQLite了。
先不從業務角度考慮這個問題。當有大量數據要存儲的時候,一般的解決方案是什么?沒有特殊要求的情況下,多數會使用數據庫來解決這個問題。畢竟自研一套數據存儲工具,而且要做得好,並不是一件簡單的事情。接着我說一說同花順和恆生,據我了解同花順和恆生都是自己設計的二進制文件格式來保存行情數據的(不要問我怎么知道的,畢竟當初我是同花順的)。恆生的不知道,同花順我是有切身體會的。這個二進制文件經常會莫名其妙寫掛了。所以現在經典的統一版客戶端出現數據錯誤的時候,多半是文件已經寫壞了。你需要“重新初始化”!
基於上面的考慮,我果斷選擇了SQLite。好處就是事務性,不容易寫壞,並且易於通過第三方工具快速查閱。當然相比較自定義的二進制結構文件,劣勢是查詢數據的方式會慢一點,你得構造出一個SQL語句,然后解析返回的數據才可以。不過這其實並不是問題,大量頻繁的查詢操作我們都定義在緩存里,全部交由Redis來解決了。除了查詢歷史數據,我們一般都不需要訪問數據庫。
數據庫另外一個優勢就是歷史庫。我們可以把很久遠的數據導到歷史庫里,這比自己搞一個二進制文件,然后要支持分割歷史到歷史庫里要方便,這種復雜的操作,我不是不相信自己寫代碼的能力,而是我覺得何必呢?
既然經驗證明存文件都沒有問題,那么數據庫我選用SQLite應該不會出大問題,如果出問題了,我改用非嵌入式數據庫就行了。等性能問題即將出現的時候,profile一下,問題若出在SQLite,再改不遲。
線程分配/管理
接下來說一說線程分配管理模塊。這個模塊的設想是怎么出來的呢?顯然,並不是瞎想出來的。
我們先來回顧一下我們經常用到的幾個網絡庫,比方說Libevent,ASIO。這些類庫沒有在業務邏輯里面自己搞線程池,說神馬自己創建一個線程然后跑。最多就是支持一下線程安全。該有鎖的地方配好一把鎖。所以,這個基本特性也告訴我,我的網絡層我的數據層這些,他們都不應該自己去分配線程。這些層級要做的就是要確保線程安全,這樣才能方便做橫向拓展。
要實現這個目的,就需要網絡層對象、數據層對象有一個dependence injection的構造函數,傳入一個libevent event base wrapper對象。一個對象只能跑在一個線程上。那么一個客戶端session到時候就只在指定的一個(我希望是這樣,避免不必要的線程切換)或兩個(看網絡層和數據層這些是在同一條線程上跑還是分開跑)線程上跑。在這樣的設計下,加之不同客戶端之間其實業務行為都是獨立的,包括數據都可以是獨立的。那么不同線程間的客戶session都是相互不影響的。有了這個分析結果,再配合thread_local,你在業務層面就不需要鎖了。所以接下來通過增加線程的方式可以很輕松做到橫向的多線程擴展。是不是?
未完,待續,to be continued。。。