面對的問題
從事開發(fā)工程中季春,遇到過不少問題馒胆,很多時(shí)候由于時(shí)間緊迫徙菠,沒有使用優(yōu)雅的方案讯沈。在跟業(yè)內(nèi)的一些朋友交流過程中,我也意識到有些問題是大家都存在的。簡單列舉如下:
多線程與并發(fā)
異步消息/接口調(diào)用
消息的序列化與Reflection
性能優(yōu)化
單元測試
多線程與并發(fā)
現(xiàn)在是多核時(shí)代缺狠,并發(fā)才能實(shí)現(xiàn)更高的吞吐量问慎、更快的響應(yīng),但也是把雙刃劍挤茄∪绲穑總結(jié)如下幾個(gè)用法:
多線程+顯示鎖;接口是被多線程調(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)死鎖奕短。
多線程+任務(wù)隊(duì)列宜肉;接口被多線程調(diào)用,但請求會(huì)被暫存到任務(wù)隊(duì)列翎碑,而任務(wù)隊(duì)列會(huì)被單線程不斷執(zhí)行谬返,典型生產(chǎn)者消費(fèi)者模式。它的并發(fā)在于不同的接口可以使用不同的任務(wù)隊(duì)列日杈。這也是我最常用的并發(fā)方式朱浴。
這是兩種最常見的多線程并發(fā),它們有個(gè)天生的缺陷——Scalability达椰。一個(gè)機(jī)器的性能總是有瓶頸的翰蠢。兩個(gè)場景的邏輯雖然由多個(gè)線程實(shí)現(xiàn)了并發(fā),但是運(yùn)算量十分有可能是一臺(tái)機(jī)器無法承載的啰劲。如果是多進(jìn)程并發(fā)梁沧,那么可以分布式把其部署到其他機(jī)器(也可部署在一臺(tái)機(jī)器)。所以多進(jìn)程并發(fā)比多線程并發(fā)更加Scalability蝇裤。另外采用多進(jìn)程后廷支,每個(gè)進(jìn)程單線程設(shè)計(jì),這樣的程序更加Simplicity栓辜。多進(jìn)程的其他優(yōu)點(diǎn)如解耦恋拍、模塊化、方便調(diào)試藕甩、方便重用等就不贅言了施敢。
異步消息/接口調(diào)用
提到分布式,就要說一下分布式的通訊技術(shù)。常用的方式如下:
類RPC僵娃;包括WebService概作、RPC、ICE等默怨,特點(diǎn)是遠(yuǎn)程同步調(diào)用讯榕。遠(yuǎn)程的接口和本地的接口非常相似。但是游戲服務(wù)器程序一般非常在意延遲和吞吐量匙睹,所以這些阻塞線程的同步遠(yuǎn)程調(diào)用方式并不常用愚屁。但是我們必須意識到他的優(yōu)點(diǎn),就是非常利于調(diào)用和測試痕檬。
全異步消息霎槐;當(dāng)調(diào)用遠(yuǎn)程接口的時(shí)候,異步發(fā)送請求消息谆棺,接口響應(yīng)后返回一個(gè)結(jié)果消息栽燕,調(diào)用方的回調(diào)函數(shù)處理結(jié)果消息繼續(xù)邏輯操作罕袋。所以有些邏輯就會(huì)被切割成ServiceStart和ServiceCallback兩段改淑。有時(shí)異步會(huì)講領(lǐng)域邏輯變得支離破碎。另外消息處理函數(shù)中一般會(huì)寫一坨的switch/case 處理不同的消息浴讯。最大的問題在于單元測試朵夏,這種情況傳統(tǒng)單元測試根本束手無策。
消息的序列化與Reflection
實(shí)現(xiàn)消息的序列化和反序列化的方式有很多榆纽,常見的有Struct仰猖、json、Protobuff等都有很成功的應(yīng)用奈籽。我個(gè)人傾向于使用輕量級的二進(jìn)制序列化饥侵,優(yōu)點(diǎn)是比較透明和高效,一切在掌握之中衣屏。在FFLIB 中實(shí)現(xiàn)了bin_encoder_t 和 bin_decoder_t 輕量級的消息序列化躏升,幾十行代碼而已。
性能優(yōu)化
有的網(wǎng)友提到profiler狼忱、cpuprofiler膨疏、callgrind等工具。這些工具我都使用過钻弄,說實(shí)話佃却,對于我來說,我太認(rèn)同它有很高的價(jià)值窘俺。第一他們只能用于開發(fā)測試階段饲帅,可以初步得到一些性能上參考數(shù)據(jù)。第二它們?nèi)绾螌?shí)現(xiàn)跟蹤人們無從得知。運(yùn)行其會(huì)使程序變慢洒闸,不能反映真實(shí)數(shù)據(jù)染坯。第三重要的是,開發(fā)測試階段性能和上線后的能一樣嗎丘逸?Impossible 单鹿!
關(guān)于性能,原則就是數(shù)據(jù)說話深纲,詳見博文仲锄,不在贅述。
單元測試
關(guān)于單元測試湃鹊,前邊已經(jīng)談?wù)摿艘恍┤搴啊S螒蚍?wù)器程序一般都比較龐大,但是不可思議的是币呵,鄙人從來沒見有項(xiàng)目(c++ 后臺(tái)架構(gòu)的)有完整單元測試的怀愧。由于存在著異步和多線程,傳統(tǒng)的單元測試框架無法勝任余赢,而開發(fā)支持異步的測試框架又是不現(xiàn)實(shí)的芯义。我們必須看到的是,傳統(tǒng)的單元測試框架已經(jīng)取得了非常大的成功妻柒。據(jù)我了解扛拨,使用web 架構(gòu)的游戲后臺(tái)已經(jīng)對于單元測試的使用已經(jīng)非常成熟,取得了極其好的效果举塔。所以我的思路是利用現(xiàn)有的單元測試框架绑警,將異步消息、多線程的架構(gòu)做出調(diào)整央渣。
已經(jīng)多次談?wù)搯卧獪y試了计盒。其實(shí)在開發(fā)FFLIB的思路很大程度來源于此,否則可能只是一個(gè)c++ 網(wǎng)絡(luò)庫而已芽丹。我決定嘗試去解決這個(gè)問題的時(shí)候北启,把FFLIB 定位于框架。
先來看一段非常簡單的單元測試的代碼 :
請?jiān)试S我對這行代碼做些解釋志衍,對Add函數(shù)輸入?yún)?shù)暖庄,驗(yàn)證返回值是否是預(yù)期的結(jié)果。這不就是單元測試的本質(zhì)嗎楼肪?在想一下我們異步發(fā)送消息的過程培廓,如果每個(gè)輸入消息約定一個(gè)結(jié)果消息包,每次發(fā)送請求時(shí)都綁定一個(gè)回調(diào)函數(shù)接收和驗(yàn)證結(jié)果消息包春叫。這樣的話就恰恰滿足了傳統(tǒng)單元測試的步驟了肩钠。最后還需解決一個(gè)問題泣港,Assert是不能處理異步的返回值的。幸運(yùn)的是价匠,future機(jī)制可以化異步為同步当纱。不了解future 模式的可以參考這里:
來看一下在FFLIB框架下遠(yuǎn)程調(diào)用echo 服務(wù)的示例:
當(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ù)操作。這樣的話洋腮,如果對echo 的遠(yuǎn)程接口做單元測試箫柳,可以這樣做: