C++多進(jìn)程并發(fā)框架
三年來(lái)一直從事服務(wù)器程序開(kāi)發(fā),一直都是忙忙碌碌,不久前結(jié)束了職業(yè)生涯的***份工作,有了一個(gè)禮拜的休息時(shí)間,終于可以寫(xiě)寫(xiě)總結(jié)了。于是把以前的開(kāi)源代碼做了整理和優(yōu)化,這就是FFLIB。雖然這邊總結(jié)看起來(lái)像日記,有很多廢話(huà),但是此文仍然是有很大針對(duì)性的。針對(duì)服務(wù)器開(kāi)發(fā)中常見(jiàn)的問(wèn)題,如多線(xiàn)程并發(fā)、消息轉(zhuǎn)發(fā)、異步、性能優(yōu)化、單元測(cè)試,提出自己的見(jiàn)解。
面對(duì)的問(wèn)題
從事開(kāi)發(fā)工程中,遇到過(guò)不少問(wèn)題,很多時(shí)候由于時(shí)間緊迫,沒(méi)有使用優(yōu)雅的方案。在跟業(yè)內(nèi)的一些朋友交流過(guò)程中,我也意識(shí)到有些問(wèn)題是大家都存在的。簡(jiǎn)單列舉如下:
- 多線(xiàn)程與并發(fā)
- 異步消息/接口調(diào)用
- 消息的序列化與Reflection
- 性能優(yōu)化
- 單元測(cè)試
多線(xiàn)程與并發(fā)
現(xiàn)在是多核時(shí)代,并發(fā)才能實(shí)現(xiàn)更高的吞吐量、更快的響應(yīng),但也是把雙刃劍。總結(jié)如下幾個(gè)用法:
- 多線(xiàn)程+顯示鎖;接口是被多線(xiàn)程調(diào)用的,當(dāng)被調(diào)用時(shí),顯示加鎖,再操作實(shí)體數(shù)據(jù)。悲劇的是,工程師為了優(yōu)化會(huì)設(shè)計(jì)多個(gè)鎖,以減少鎖的粒度,甚至有些地方使用了原子操作。這些都為領(lǐng)域邏輯增加了額外的設(shè)計(jì)負(fù)擔(dān)。最壞的情況是會(huì)出現(xiàn)死鎖。
- 多線(xiàn)程+任務(wù)隊(duì)列;接口被多線(xiàn)程調(diào)用,但請(qǐng)求會(huì)被暫存到任務(wù)隊(duì)列,而任務(wù)隊(duì)列會(huì)被單線(xiàn)程不斷執(zhí)行,典型生產(chǎn)者消費(fèi)者模式。它的并發(fā)在于不同的接口可以使用不同的任務(wù)隊(duì)列。這也是我最常用的并發(fā)方式。
這是兩種最常見(jiàn)的多線(xiàn)程并發(fā),它們有個(gè)天生的缺陷——Scalability。一個(gè)機(jī)器的性能總是有瓶頸的。兩個(gè)場(chǎng)景的邏輯雖然由多個(gè)線(xiàn)程實(shí)現(xiàn)了并發(fā),但是運(yùn)算量十分有可能是一臺(tái)機(jī)器無(wú)法承載的。如果是多進(jìn)程并發(fā),那么可以分布式把其部署到其他機(jī)器(也可部署在一臺(tái)機(jī)器)。所以多進(jìn)程并發(fā)比多線(xiàn)程并發(fā)更加Scalability。另外采用多進(jìn)程后,每個(gè)進(jìn)程單線(xiàn)程設(shè)計(jì),這樣的程序更加Simplicity。多進(jìn)程的其他優(yōu)點(diǎn)如解耦、模塊化、方便調(diào)試、方便重用等就不贅言了。
異步消息/接口調(diào)用
提到分布式,就要說(shuō)一下分布式的通訊技術(shù)。常用的方式如下:
- 類(lèi)RPC;包括WebService、RPC、ICE等,特點(diǎn)是遠(yuǎn)程同步調(diào)用。遠(yuǎn)程的接口和本地的接口非常相似。但是游戲服務(wù)器程序一般非常在意延遲和吞吐量,所以這些阻塞線(xiàn)程的同步遠(yuǎn)程調(diào)用方式并不常用。但是我們必須意識(shí)到他的優(yōu)點(diǎn),就是非常利于調(diào)用和測(cè)試。
- 全異步消息;當(dāng)調(diào)用遠(yuǎn)程接口的時(shí)候,異步發(fā)送請(qǐng)求消息,接口響應(yīng)后返回一個(gè)結(jié)果消息,調(diào)用方的回調(diào)函數(shù)處理結(jié)果消息繼續(xù)邏輯操作。所以有些邏輯就會(huì)被切割成ServiceStart和ServiceCallback兩段。有時(shí)異步會(huì)講領(lǐng)域邏輯變得支離破碎。另外消息處理函數(shù)中一般會(huì)寫(xiě)一坨的 switch/case 處理不同的消息。***的問(wèn)題在于單元測(cè)試,這種情況傳統(tǒng)單元測(cè)試根本束手無(wú)策。
消息的序列化與Reflection
實(shí)現(xiàn)消息的序列化和反序列化的方式有很多,常見(jiàn)的有Struct、json、Protobuff等都有很成功的應(yīng)用。我個(gè)人傾向于使用輕量級(jí)的二進(jìn)制序列化,優(yōu)點(diǎn)是比較透明和高效,一切在掌握之中。在FFLIB 中實(shí)現(xiàn)了bin_encoder_t 和 bin_decoder_t 輕量級(jí)的消息序列化,幾十行代碼而已。
性能優(yōu)化
已經(jīng)寫(xiě)過(guò)關(guān)于性能方面的總結(jié),參見(jiàn)
http://www.cnblogs.com/zhiranok/archive/2012/06/06/cpp_perf.html
有的網(wǎng)友提到profiler、cpuprofiler、callgrind等工具。這些工具我都使用過(guò),說(shuō)實(shí)話(huà),對(duì)于我來(lái)說(shuō),我太認(rèn)同它有很高的價(jià)值。***他們只能用于開(kāi)發(fā)測(cè)試階段,可以初步得到一些性能上參考數(shù)據(jù)。第二它們?nèi)绾螌?shí)現(xiàn)跟蹤人們無(wú)從得知。運(yùn)行其會(huì)使程序變慢,不能反映真實(shí)數(shù)據(jù)。第三重要的是,開(kāi)發(fā)測(cè)試階段性能和上線(xiàn)后的能一樣嗎?Impossible !
關(guān)于性能,原則就是數(shù)據(jù)說(shuō)話(huà),詳見(jiàn)博文,不在贅述。
單元測(cè)試
關(guān)于單元測(cè)試,前邊已經(jīng)談?wù)摿艘恍?。游戲服?wù)器程序一般都比較龐大,但是不可思議的是,鄙人從來(lái)沒(méi)見(jiàn)有項(xiàng)目(c++ 后臺(tái)架構(gòu)的)有完整單元測(cè)試的。由于存在著異步和多線(xiàn)程,傳統(tǒng)的單元測(cè)試框架無(wú)法勝任,而開(kāi)發(fā)支持異步的測(cè)試框架又是不現(xiàn)實(shí)的。我們必須看到的是,傳統(tǒng)的單元測(cè)試框架已經(jīng)取得了非常大的成功。據(jù)我了解,使用web 架構(gòu)的游戲后臺(tái)已經(jīng)對(duì)于單元測(cè)試的使用已經(jīng)非常成熟,取得了極其好的效果。所以我的思路是利用現(xiàn)有的單元測(cè)試框架,將異步消息、多線(xiàn)程的架構(gòu)做出調(diào)整。
已經(jīng)多次談?wù)搯卧獪y(cè)試了。其實(shí)在開(kāi)發(fā)FFLIB的思路很大程度來(lái)源于此,否則可能只是一個(gè)c++ 網(wǎng)絡(luò)庫(kù)而已。我決定嘗試去解決這個(gè)問(wèn)題的時(shí)候,把FFLIB 定位于框架。
先來(lái)看一段非常簡(jiǎn)單的單元測(cè)試的代碼 :
- Assert(2 == Add(1, 1));
請(qǐng)?jiān)试S我對(duì)這行代碼做些解釋?zhuān)瑢?duì)Add函數(shù)輸入?yún)?shù),驗(yàn)證返回值是否是預(yù)期的結(jié)果。這不就是單元測(cè)試的本質(zhì)嗎?在想一下我們異步發(fā)送消息的過(guò)程,如果每個(gè)輸入消息約定一個(gè)結(jié)果消息包,每次發(fā)送請(qǐng)求時(shí)都綁定一個(gè)回調(diào)函數(shù)接收和驗(yàn)證結(jié)果消息包。這樣的話(huà)就恰恰滿(mǎn)足了傳統(tǒng)單元測(cè)試的步驟了。***還需解決一個(gè)問(wèn)題,Assert是不能處理異步的返回值的。幸運(yùn)的是,future機(jī)制可以化異步為同步。不了解future 模式的可以參考這里:
http://blog.chinaunix.net/uid-23093301-id-190969.html
http://msdn.microsoft.com/zh-cn/library/dd764564.aspx#Y300
來(lái)看一下在FFLIB框架下遠(yuǎn)程調(diào)用echo 服務(wù)的示例:
- struct lambda_t
- {
- static void callback(echo_t::out_t& msg_)
- {
- echo_t::in_t in;
- in.value = "XXX_echo_test_XXX";
- singleton_t<msg_bus_t>::instance()
- .get_service_group("echo")
- ->get_service(1)->async_call(in, &lambda_t::callback);
- }
- };
- echo_t::in_t in;
- in.value = "XXX_echo_test_XXX";
- singleton_t<msg_bus_t>::instance().get_service_group("echo")->get_service(1)->async_call(in, &lambda_t::callback);
當(dāng)需要調(diào)用遠(yuǎn)程接口時(shí),async_call(in, &lambda_t::callback); 異步調(diào)用必須綁定一個(gè)回調(diào)函數(shù),回調(diào)函數(shù)接收結(jié)果消息,可以觸發(fā)后續(xù)操作。這樣的話(huà),如果對(duì)echo 的遠(yuǎn)程接口做單元測(cè)試,可以這樣做:
- rpc_future_t< echo_t::out_t> rpc_future;
- echo_t::in_t in;
- in.value = "XXX_echo_test_XXX";
- const echo_t::out_t& out = rpc_future.call(
- singleton_t<msg_bus_t>::instance()
- .get_service_group("echo")->get_service(1), in);
- Assert(in.value == out.value);
這樣所有的遠(yuǎn)程接口都可以被單元測(cè)試覆蓋。
FFLIB 介紹
FFLIB 結(jié)構(gòu)圖
如圖所示,Client 不會(huì)直接和Service 相連接,而是通過(guò)Broker 中間層完成了消息傳遞。關(guān)于Broker 模式可以參見(jiàn):http://blog.chinaunix.net/uid-23093301-id-90459.html
進(jìn)程間通信采用TPC,而不是多線(xiàn)程使用的共享內(nèi)存方式。Service 一般是單線(xiàn)程架構(gòu)的,通過(guò)啟動(dòng)多進(jìn)程實(shí)現(xiàn)相對(duì)于多線(xiàn)程的并發(fā)。由于Broker模式天生石分布式的,所以有很好的Scalability。
消息時(shí)序圖
#p#
如何注冊(cè)服務(wù)和接口
來(lái)看一下Echo 服務(wù)的實(shí)現(xiàn):
- struct echo_service_t
- {
- public:
- void echo(echo_t::in_t& in_msg_, rpc_callcack_t<echo_t::out_t>& cb_)
- {
- logtrace((FF, "echo_service_t::echo done value<%s>", in_msg_.value.c_str()));
- echo_t::out_t out;
- out.value = in_msg_.value;
- cb_(out);
- }
- };
- int main(int argc, char* argv[])
- {
- int g_index = 1;
- if (argc > 1)
- {
- g_index = atoi(argv[1]);
- }
- char buff[128];
- snprintf(buff, sizeof(buff), "tcp://%s:%s", "127.0.0.1", "10241");
- msg_bus_t msg_bus;
- assert(0 == singleton_t<msg_bus_t>::instance().open("tcp://127.0.0.1:10241") && "can't connnect to broker");
- echo_service_t f;
- singleton_t<msg_bus_t>::instance().create_service_group("echo");
- singleton_t<msg_bus_t>::instance().create_service("echo", g_index)
- .bind_service(&f)
- .reg(&echo_service_t::echo);
- signal_helper_t::wait();
- singleton_t<msg_bus_t>::instance().close();
- //usleep(1000);
- cout <<"\noh end\n";
- return 0;
- }
- create_service_group 創(chuàng)建一個(gè)服務(wù)group,一個(gè)服務(wù)組可能有多個(gè)并行的實(shí)例
- create_service 以特定的id 創(chuàng)建一個(gè)服務(wù)實(shí)例
- reg 為該服務(wù)注冊(cè)接口
- 接口的定義規(guī)范為void echo(echo_t::in_t& in_msg_, rpc_callcack_t<echo_t::out_t>& cb_),***個(gè)參數(shù)為輸入的消息struct,第二個(gè)參數(shù)為回調(diào)函數(shù)的模板特例,模板參數(shù)為返回消息的struct 類(lèi)型。接口無(wú)需知道發(fā)送消息等細(xì)節(jié),只需將結(jié)果callback 即可。
- 注冊(cè)到Broker 后,所有Client都可獲取該服務(wù)
消息定義的規(guī)范
我們約定每個(gè)接口(遠(yuǎn)程或本地都應(yīng)滿(mǎn)足)都包含一個(gè)輸入消息和一個(gè)結(jié)果消息。來(lái)看一下echo 服務(wù)的消息定義:
- struct echo_t
- {
- struct in_t: public msg_i
- {
- in_t():
- msg_i("echo_t::in_t")
- {}
- virtual string encode()
- {
- return (init_encoder() << value).get_buff();
- }
- virtual void decode(const string& src_buff_)
- {
- init_decoder(src_buff_) >> value;
- }
- string value;
- };
- struct out_t: public msg_i
- {
- out_t():
- msg_i("echo_t::out_t")
- {}
- virtual string encode()
- {
- return (init_encoder() << value).get_buff();
- }
- virtual void decode(const string& src_buff_)
- {
- init_decoder(src_buff_) >> value;
- }
- string value;
- };
- };
- 每個(gè)接口必須包含in_t消息和out_t消息,并且他們定義在接口名(如echo _t)的內(nèi)部
- 所有消息都繼承于msg_i, 其封裝了二進(jìn)制的序列化、反序列化等。構(gòu)造時(shí)賦予類(lèi)型名作為消息的名稱(chēng)。
- 每個(gè)消息必須實(shí)現(xiàn)encode 和 decode 函數(shù)
這里需要指出的是,F(xiàn)FLIB 中不需要為每個(gè)消息定義對(duì)應(yīng)的CMD。當(dāng)接口如echo向Broker 注冊(cè)時(shí),reg接口通過(guò)C++ 模板的類(lèi)型推斷會(huì)自動(dòng)將該msg name 注冊(cè)給Broker, Broker為每個(gè)msg name 分配唯一的msg_id。Msg_bus 中自動(dòng)維護(hù)了msg_name 和msg_id 的映射。Msg_i 的定義如下:
- struct msg_i : public codec_i
- {
- msg_i(const char* msg_name_):
- cmd(0),
- uuid(0),
- service_group_id(0),
- service_id(0),
- msg_id(0),
- msg_name(msg_name_)
- {}
- void set(uint16_t group_id, uint16_t id_, uint32_t uuid_, uint16_t msg_id_)
- {
- service_group_id = group_id;
- service_id = id_;
- uuid = uuid_;
- msg_id = msg_id_;
- }
- uint16_t cmd;
- uint16_t get_group_id() const{ return service_group_id; }
- uint16_t get_service_id() const{ return service_id; }
- uint32_t get_uuid() const{ return uuid; }
- uint16_t get_msg_id() const{ return msg_id; }
- const string& get_name() const
- {
- if (msg_name.empty() == false)
- {
- return msg_name;
- }
- return singleton_t<msg_name_store_t>::instance().id_to_name(this->get_msg_id());
- }
- void set_uuid(uint32_t id_) { uuid = id_; }
- void set_msg_id(uint16_t id_) { msg_id = id_;}
- void set_sgid(uint16_t sgid_) { service_group_id = sgid_;}
- void set_sid(uint16_t sid_) { service_id = sid_; }
- uint32_t uuid;
- uint16_t service_group_id;
- uint16_t service_id;
- uint16_t msg_id;
- string msg_name;
- virtual string encode(uint16_t cmd_)
- {
- this->cmd = cmd_;
- return encode();
- }
- virtual string encode() = 0;
- bin_encoder_t& init_encoder()
- {
- return encoder.init(cmd) << uuid << service_group_id << service_id<< msg_id;
- }
- bin_encoder_t& init_encoder(uint16_t cmd_)
- {
- return encoder.init(cmd_) << uuid << service_group_id << service_id << msg_id;
- }
- bin_decoder_t& init_decoder(const string& buff_)
- {
- return decoder.init(buff_) >> uuid >> service_group_id >> service_id >> msg_id;
- }
- bin_decoder_t decoder;
- bin_encoder_t encoder;
- };
關(guān)于性能
由于遠(yuǎn)程接口的調(diào)用必須通過(guò)Broker, Broker會(huì)為每個(gè)接口自動(dòng)生成性能統(tǒng)計(jì)數(shù)據(jù),并每10分鐘輸出到perf.txt 文件中。文件格式為CSV,參見(jiàn):
http://www.cnblogs.com/zhiranok/archive/2012/06/06/cpp_perf.html
總結(jié)
FFLIB框架擁有如下的特點(diǎn):
- 使用多進(jìn)程并發(fā)。Broker 把Client 和Service 的位置透明化
- Service 的接口要注冊(cè)到Broker, 所有連接Broker的Client 都可以調(diào)用(publisher/ subscriber)
- 遠(yuǎn)程調(diào)用必須綁定回調(diào)函數(shù)
- 利用future 模式實(shí)現(xiàn)同步,從而支持單元測(cè)試
- 消息定義規(guī)范簡(jiǎn)單直接高效
- 所有service的接口性能監(jiān)控?cái)?shù)據(jù)自動(dòng)生成,免費(fèi)的午餐
- Service 單線(xiàn)程話(huà),更simplicity
源代碼:
Svn co http://ffown.googlecode.com/svn/trunk/
運(yùn)行示例:
- Cd example/broker && make && ./app_broker –l http://127.0.0.1:10241
- Cd example/echo_server && make && ./app_echo_server
- Cd example/echo_client && make && ./app_echo_client
原文鏈接:C++多進(jìn)程并發(fā)框架