天天看點

C++多程序并發架構FFLIB

         三年來一直從事伺服器程式開發,一直都是忙忙碌碌,不久前結束了職業生涯的第一份工作,有了一個禮拜的休息時間,終于可以寫寫總結了。于是把以前的開源代碼做了整理和優化,這就是FFLIB。雖然這邊總結看起來像日記,有很多廢話,但是此文仍然是有很大針對性的。針對伺服器開發中常見的問題,如多線程并發、消息轉發、異步、性能優化、單元測試,提出自己的見解。

         從事開發工程中,遇到過不少問題,很多時候由于時間緊迫,沒有使用優雅的方案。在跟業内的一些朋友交流過程中,我也意識到有些問題是大家都存在的。簡單列舉如下:

多線程與并發

異步消息/接口調用

消息的序列化與Reflection

性能優化

單元測試

         現在是多核時代,并發才能實作更高的吞吐量、更快的響應,但也是把雙刃劍。總結如下幾個用法:

多線程+顯示鎖;接口是被多線程調用的,當被調用時,顯示加鎖,再操作實體資料。悲劇的是,工程師為了優化會設計多個鎖,以減少鎖的粒度,甚至有些地方使用了原子操作。這些都為領域邏輯增加了額外的設計負擔。最壞的情況是會出現死鎖。

多線程+任務隊列;接口被多線程調用,但請求會被暫存到任務隊列,而任務隊列會被單線程不斷執行,典型生産者消費者模式。它的并發在于不同的接口可以使用不同的任務隊列。這也是我最常用的并發方式。

  這是兩種最常見的多線程并發,它們有個天生的缺陷——Scalability。一個機器的性能總是有瓶頸的。兩個場景的邏輯雖然由多個線程實作了并發,但是運算量十分有可能是一台機器無法承載的。如果是多程序并發,那麼可以分布式把其部署到其他機器(也可部署在一台機器)。是以多程序并發比多線程并發更加Scalability。另外采用多程序後,每個程序單線程設計,這樣的程式更加Simplicity。多程序的其他優點如解耦、子產品化、友善調試、友善重用等就不贅言了。

         提到分布式,就要說一下分布式的通訊技術。常用的方式如下:

類RPC;包括WebService、RPC、ICE等,特點是遠端同步調用。遠端的接口和本地的接口非常相似。但是遊戲伺服器程式一般非常在意延遲和吞吐量,是以這些阻塞線程的同步遠端調用方式并不常用。但是我們必須意識到他的優點,就是非常利于調用和測試。

全異步消息;當調用遠端接口的時候,異步發送請求消息,接口響應後傳回一個結果消息,調用方的回調函數處理結果消息繼續邏輯操作。是以有些邏輯就會被切割成ServiceStart和ServiceCallback兩段。有時異步會講領域邏輯變得支離破碎。另外消息處理函數中一般會寫一坨的switch/case 處理不同的消息。最大的問題在于單元測試,這種情況傳統單元測試根本束手無策。

         實作消息的序列化和反序列化的方式有很多,常見的有Struct、json、Protobuff等都有很成功的應用。我個人傾向于使用輕量級的二進制序列化,優點是比較透明和高效,一切在掌握之中。在FFLIB 中實作了bin_encoder_t 和 bin_decoder_t 輕量級的消息序列化,幾十行代碼而已。

         已經寫過關于性能方面的總結,參見

     有的網友提到profiler、cpuprofiler、callgrind等工具。這些工具我都使用過,說實話,對于我來說,我太認同它有很高的價值。第一他們隻能用于開發測試階段,可以初步得到一些性能上參考資料。第二它們如何實作跟蹤人們無從得知。運作其會使程式變慢,不能反映真實資料。第三重要的是,開發測試階段性能和上線後的能一樣嗎?Impossible !

     關于性能,原則就是資料說話,詳見博文,不在贅述。

         關于單元測試,前邊已經談論了一些。遊戲伺服器程式一般都比較龐大,但是不可思議的是,鄙人從來沒見有項目(c++ 背景架構的)有完整單元測試的。由于存在着異步和多線程,傳統的單元測試架構無法勝任,而開發支援異步的測試架構又是不現實的。我們必須看到的是,傳統的單元測試架構已經取得了非常大的成功。據我了解,使用web 架構的遊戲背景已經對于單元測試的使用已經非常成熟,取得了極其好的效果。是以我的思路是利用現有的單元測試架構,将異步消息、多線程的架構做出調整。

         已經多次談論單元測試了。其實在開發FFLIB的思路很大程度來源于此,否則可能隻是一個c++ 網絡庫而已。我決定嘗試去解決這個問題的時候,把FFLIB 定位于架構。

         先來看一段非常簡單的單元測試的代碼 :

         Assert(2 == Add(1, 1));

         請允許我對這行代碼做些解釋,對Add函數輸入參數,驗證傳回值是否是預期的結果。這不就是單元測試的本質嗎?在想一下我們異步發送消息的過程,如果每個輸入消息約定一個結果消息包,每次發送請求時都綁定一個回調函數接收和驗證結果消息包。這樣的話就恰恰滿足了傳統單元測試的步驟了。最後還需解決一個問題,Assert是不能處理異步的傳回值的。幸運的是,future機制可以化異步為同步。不了解future 模式的可以參考這裡:

         來看一下在FFLIB架構下遠端調用echo 服務的示例:

  當需要調用遠端接口時,async_call(in, &lambda_t::callback); 異步調用必須綁定一個回調函數,回調函數接收結果消息,可以觸發後續操作。這樣的話,如果對echo 的遠端接口做單元測試,可以這樣做:

 FFLIB 結構圖

C++多程式并發架構FFLIB

   程序間通信采用TPC,而不是多線程使用的共享記憶體方式。Service 一般是單線程架構的,通過啟動多程序實作相對于多線程的并發。由于Broker模式天生石分布式的,是以有很好的Scalability。

C++多程式并發架構FFLIB

  來看一下Echo 服務的實作:

create_service_group 建立一個服務group,一個服務組可能有多個并行的執行個體

create_service 以特定的id 建立一個服務執行個體

reg 為該服務注冊接口

接口的定義規範為void echo(echo_t::in_t& in_msg_, rpc_callcack_t<echo_t::out_t>& cb_),第一個參數為輸入的消息struct,第二個參數為回調函數的模闆特例,模闆參數為傳回消息的struct 類型。接口無需知道發送消息等細節,隻需将結果callback 即可。

注冊到Broker 後,所有Client都可擷取該服務

    我們約定每個接口(遠端或本地都應滿足)都包含一個輸入消息和一個結果消息。來看一下echo 服務的消息定義:

 每個接口必須包含in_t消息和out_t消息,并且他們定義在接口名(如echo _t)的内部

所有消息都繼承于msg_i, 其封裝了二進制的序列化、反序列化等。構造時賦予類型名作為消息的名稱。

每個消息必須實作encode 和 decode 函數

  這裡需要指出的是,FFLIB 中不需要為每個消息定義對應的CMD。當接口如echo向Broker 注冊時,reg接口通過C++ 模闆的類型推斷會自動将該msg name 注冊給Broker, Broker為每個msg name 配置設定唯一的msg_id。Msg_bus 中自動維護了msg_name 和msg_id 的映射。Msg_i 的定義如下:

         由于遠端接口的調用必須通過Broker, Broker會為每個接口自動生成性能統計資料,并每10分鐘輸出到perf.txt 檔案中。檔案格式為CSV,參見:

<a href="http://www.cnblogs.com/zhiranok/archive/2012/06/06/cpp_perf.html">http://www.cnblogs.com/zhiranok/archive/2012/06/06/cpp_perf.html</a>

FFLIB架構擁有如下的特點:

使用多程序并發。Broker 把Client 和Service 的位置透明化

Service 的接口要注冊到Broker, 所有連接配接Broker的Client 都可以調用(publisher/ subscriber)

遠端調用必須綁定回調函數

利用future 模式實作同步,進而支援單元測試

消息定義規範簡單直接高效

所有service的接口性能監控資料自動生成,免費的午餐

Service 單線程話,更simplicity

源代碼:

運作示例:

Cd example/echo_server &amp;&amp; make &amp;&amp; ./app_echo_server

Cd example/echo_client &amp;&amp; make &amp;&amp; ./app_echo_client

繼續閱讀