開(kāi)發(fā)筆記(25) : 改進(jìn)的 RPC自從動(dòng)了重新實(shí)現(xiàn) skynet 的念頭,最近忙的跟狗一樣。每天 10 點(diǎn)醒來(lái)就忙著寫代碼,一句廢話都不想說(shuō),一直到晚上 11 點(diǎn)回家睡覺(jué)。連續(xù)干了快一個(gè)月了。 到昨天,終于把全部代碼基本移植到了新框架下,正常啟動(dòng)了起來(lái)。這項(xiàng)工作算是搞一段落。慶幸的是,我這個(gè)月的工作,并沒(méi)有影響到其他人對(duì)游戲邏輯的開(kāi)發(fā)。只是我單方面的同步不斷新增的邏輯邏輯代碼。 Skynet 的重寫,實(shí)際上在半個(gè)月前就已經(jīng)完成。那時(shí),已經(jīng)可以用新的服務(wù)器承載原有的獨(dú)立的用戶認(rèn)證系統(tǒng)了。那么后半個(gè)月的這些瑣碎工作,其實(shí)都是在移植那些游戲邏輯代碼。 在 Skynet 原始設(shè)計(jì)的時(shí)候,api 是比較簡(jiǎn)潔的,原則上講,是可以透明替換。但實(shí)際上,在使用中,增加了許多陰暗角落。一些接口層的小變動(dòng),增加的隱式特性,使得并不能百分百兼容。另外,原來(lái)的一些通訊協(xié)議和約定不算太合理,在重新制作時(shí),我換掉了部分的方案,但需要編寫一個(gè)兼容的鏈路層。 比如:以前,我們把通過(guò) tcp 接入的 client 和 server 內(nèi)部同進(jìn)程內(nèi)的服務(wù)等同處理。認(rèn)為它們都是通過(guò)相同的二進(jìn)制數(shù)據(jù)包協(xié)議通訊。但是,同進(jìn)程內(nèi)的服務(wù)間通訊明顯是可以被優(yōu)化的,他們可以通過(guò) C 結(jié)構(gòu)而不是被編碼過(guò)的數(shù)據(jù)包交換信息,并可以做到由發(fā)起請(qǐng)求方分配內(nèi)存,接受方釋放內(nèi)存,減少無(wú)謂的數(shù)據(jù)復(fù)制。在老的版本中,強(qiáng)行把兩者統(tǒng)一了起來(lái),失去了許多優(yōu)化空間。在新版本里,我增加了較少的約定,修改了一點(diǎn)接口,就大幅度提升了進(jìn)程內(nèi)服務(wù)間信息交換的效率。 另一方面,一旦固定采用單進(jìn)程多線程方案,之前的多進(jìn)程共享數(shù)據(jù)的模塊就顯得過(guò)于厚重了。新的方案更為輕量,也更適合 lua 使用。這項(xiàng)工作在上一篇 blog 中提到過(guò)。這和 skynet 的重寫原本是兩件事情,但我強(qiáng)行放在一起做遷移,增加了許多難度。但考慮到,原本我就需要梳理一次我們的全部服務(wù)器端代碼(包括大量我沒(méi)有 review 過(guò)的),就把這兩件事情同時(shí)做了。 在這個(gè)過(guò)程中,可以剔除許多冗余代碼,去掉一些我們?cè)?jīng)以為會(huì)用到,到實(shí)際廢棄的模塊。徹底解決一些歷史變更引起的問(wèn)題。過(guò)程很痛苦,但很值得。新寫的代碼各種類型檢查更嚴(yán)格,就此發(fā)現(xiàn)了老的邏輯層代碼中許多隱藏的 bug 。一些原有用 erlang 實(shí)現(xiàn)的模塊,重新用 lua 實(shí)現(xiàn)了一遍,混合太多語(yǔ)言做開(kāi)發(fā),一些很疼的地方,經(jīng)歷過(guò)的人自然清楚。以后如非必要,盡量不用 lua 之外的語(yǔ)言往這個(gè)系統(tǒng)里增加組件了。 btw, 新系統(tǒng)還沒(méi)有經(jīng)過(guò)壓力測(cè)試。一些優(yōu)化工作也沒(méi)有展開(kāi)。但初步看起來(lái),還是卓有成效的。至少,改進(jìn)了數(shù)據(jù)共享模塊,以及提出許多冗余后,整個(gè)系統(tǒng)的內(nèi)存占用量下降到原來(lái)的 1/5 不到。CPU 占用率也有大幅度的下降。當(dāng)然,這幾乎不關(guān) C 還是 Erlang 做開(kāi)發(fā)的事,重點(diǎn)得益于經(jīng)過(guò)半年的需求總結(jié),以及我梳理了大部分模塊后做的整體改進(jìn)。 今天想重點(diǎn)談?wù)勏旅嬉欢螘r(shí)間我希望做的改進(jìn)。是關(guān)于服務(wù)間 RPC 的。 目前能找到年初的一篇記錄 ,經(jīng)過(guò)大半年的演化,已經(jīng)不完全是記錄的那個(gè)樣子了。但大體上的思路一直沿用著。 這個(gè)方案的優(yōu)點(diǎn)在于,使用通用的 google proto buffer 協(xié)議做嚴(yán)格的 RPC 協(xié)議定義。但缺點(diǎn)也是很明顯的,最麻煩的地方在于,在一個(gè)需要大量服務(wù)間交互的應(yīng)用環(huán)境內(nèi),新實(shí)現(xiàn)一組 RPC 需要做大量的工作,這對(duì)程序員是一個(gè)負(fù)擔(dān)。 程序員需要:一,在一個(gè)協(xié)議描述文件中,定義出協(xié)議名以及對(duì)應(yīng)的協(xié)議號(hào);二,在對(duì)應(yīng)名字的 proto buffer 文件中,定義出協(xié)議對(duì)應(yīng)的輸入?yún)?shù)和輸出參數(shù)列表;三,在特定的位置,創(chuàng)建特定名字的 lua 源文件,在里面實(shí)現(xiàn)特定名字的協(xié)議過(guò)程。 有時(shí),這個(gè)流程是好事,它能夠在 lua 這種弱類型系統(tǒng)中,輔助檢查出潛在的錯(cuò)誤;但對(duì)開(kāi)發(fā)效率的影響也是顯著的。同時(shí),這個(gè)長(zhǎng)長(zhǎng)的流程,以及對(duì)應(yīng)的各種編解碼工作,也引起了部分性能損失。 我希望在以后的系統(tǒng)中,引入更為方便簡(jiǎn)潔的 RPC 機(jī)制。 簡(jiǎn)單的說(shuō),我的最終需求是:程序員不需要為 RPC 調(diào)用和本地調(diào)用寫不同的代碼。程序員需要心里了解一次調(diào)用是遠(yuǎn)程調(diào)用,但他不必為實(shí)現(xiàn)這些方法做額外的事情。在他不需要把一些方法定義為遠(yuǎn)程方法時(shí),只需要把源文件換個(gè)位置,或是重新組織一下代碼加載的過(guò)程,就可以輕松的完成。遠(yuǎn)程對(duì)象和本地對(duì)象對(duì)調(diào)用者來(lái)說(shuō),也應(yīng)該盡可能的透明。 我今天把這個(gè)想法基本實(shí)現(xiàn)了。 經(jīng)過(guò)重寫 skynet ,我對(duì)這類事務(wù)的處理稍微建立了一點(diǎn)模式。首先,應(yīng)該把同進(jìn)程內(nèi)的服務(wù)間 RPC 調(diào)用同跨進(jìn)程的調(diào)用區(qū)分開(kāi)。 跨進(jìn)程(跨機(jī))調(diào)用,可以通過(guò)增加一個(gè)特定的服務(wù),在鏈路層把它們接起來(lái)即可。應(yīng)該專心實(shí)現(xiàn)同進(jìn)程內(nèi),不同服務(wù)間的高性能 RPC 才是重點(diǎn)。等這一步完成,只需要為異地對(duì)象建立一個(gè)本地副本做消息中轉(zhuǎn)就夠了。 我們應(yīng)該專心考慮 Lua State 實(shí)現(xiàn)的服務(wù),而不必過(guò)于考慮不同語(yǔ)言間的 RPC 調(diào)用。一起以 Lua 為主語(yǔ)言來(lái)考慮功能。 首先我引入了之前實(shí)現(xiàn)好的 Lua 數(shù)據(jù)序列化模塊 。并對(duì)它做了一些改動(dòng) ,合并到 skynet 項(xiàng)目中。 這個(gè)改動(dòng)是,讓序列化模塊底層理解遠(yuǎn)程對(duì)象。增加了一個(gè)遠(yuǎn)程對(duì)象類型。因?yàn)樗械?Lua State 其實(shí)是在一個(gè)系統(tǒng)進(jìn)程內(nèi)的,數(shù)據(jù)交換工作通過(guò) Lua 的 C 擴(kuò)展庫(kù)就可以完成。在同個(gè)進(jìn)程內(nèi),每個(gè)遠(yuǎn)程對(duì)象都有唯一的數(shù)字 id 。傳遞遠(yuǎn)程對(duì)象,只需要傳遞這個(gè) id 即可。 每個(gè) Lua State 中,維護(hù)一張表,記錄所有在這個(gè) State 中創(chuàng)建出來(lái)的遠(yuǎn)程對(duì)象,以及對(duì)應(yīng)的 id 。在序列化模塊中,一旦發(fā)現(xiàn)提到的遠(yuǎn)程對(duì)象是自己進(jìn)程內(nèi)的,就自動(dòng)翻譯成本地對(duì)象。 序列化模塊本身不處理任何遠(yuǎn)程對(duì)象的特殊行為。它把這個(gè)工作交給外部注入。skynet 模塊把遠(yuǎn)程調(diào)用的方法注入到遠(yuǎn)程對(duì)象中。 所謂 RPC 調(diào)用,就是一個(gè)遠(yuǎn)程對(duì)象加一個(gè)方法名,加上若干參數(shù)。通過(guò)序列化方法,打包成一個(gè)數(shù)據(jù)包,查詢到遠(yuǎn)程對(duì)象所在的服務(wù)地址,發(fā)送過(guò)去即可。 而所謂遠(yuǎn)程對(duì)象,其實(shí)只是在 lua table 里設(shè)置一個(gè)叫 這個(gè)模塊并不復(fù)雜,我實(shí)現(xiàn)在了 skynet.lua 中,不到 100 行代碼。 作為一個(gè)范例,我實(shí)現(xiàn)了一個(gè)叫 root 的遠(yuǎn)程對(duì)象,讓它在一個(gè)獨(dú)立服務(wù) 中啟動(dòng)。它可以提供一個(gè)基本的名字服務(wù)。在一個(gè)簡(jiǎn)單的 test 程序 中,我們可以看到一個(gè)遠(yuǎn)程對(duì)象把自己注冊(cè)到 root 里,別的服務(wù)從 root 拿到這個(gè)對(duì)象,就可以向本地對(duì)象一樣調(diào)用上面的方法了。 云風(fēng) 提交于 August 29, 2012 03:44 PM | 固定鏈接 Comments靈活與不安全共存,不知道外掛檢測(cè)的代價(jià)有多大? Posted by: Anonymous | (9) September 4, 2012 02:12 PM RPC本來(lái)就是remote,沒(méi)有必要再把inner-proc的概念歸并到RPC,remote的數(shù)據(jù)串化肯定要值拷貝。邏輯多線程并發(fā)服務(wù)器,并發(fā)的是IO以及針對(duì)所有邏輯實(shí)體的IO觸發(fā),而針對(duì)單個(gè)邏輯實(shí)體可以采取順序執(zhí)行(當(dāng)前只能被一根線程執(zhí)行)。這也就是erlang里的概念吧,Actor Model。 Posted by: tinyzhang | (8) September 1, 2012 02:48 PM 一定要用RPC么,見(jiàn)識(shí)過(guò)的項(xiàng)目代碼都木有用到,去開(kāi)發(fā)這個(gè)是不是有點(diǎn)兒奢侈? Posted by: pass86 | (7) August 31, 2012 11:50 PM 越來(lái)月看不動(dòng) Posted by: ctx | (6) August 30, 2012 02:10 PM 年初的時(shí)候看到描述的rpc,當(dāng)時(shí)覺(jué)得為每個(gè)rpc調(diào)用都定義一個(gè)proto buffer的協(xié)議,是為支持跨語(yǔ)言的rpc調(diào)用。 我是覺(jué)得如果rpc只在lua里面進(jìn)行使用,那只需要定義一個(gè)對(duì)lua數(shù)據(jù)類型描述的協(xié)議就行了,這樣對(duì)rpc的編寫者/調(diào)用者來(lái)說(shuō)都是透明的。 Posted by: 呂子熏 | (5) August 30, 2012 01:38 PM 我也感覺(jué)profbuf實(shí)現(xiàn)起來(lái)有些麻煩,以及效率問(wèn)題,所以我的RPC方案就是序列化+類型反射 Posted by: qele | (4) August 30, 2012 01:38 PM 一直排斥將rpc和本地調(diào)用透明化,說(shuō)不上理由,只是覺(jué)得味道不對(duì)吧。 Posted by: zcpro | (3) August 29, 2012 06:17 PM 個(gè)人覺(jué)得rpc主要的設(shè)計(jì)要點(diǎn)很簡(jiǎn)單,第一個(gè)是網(wǎng)絡(luò)io與應(yīng)用層分離,這樣多個(gè)應(yīng)用邏輯可以復(fù)用io(socket),第二個(gè)是協(xié)議分層設(shè)計(jì),底層協(xié)議需要嚴(yán)格的變動(dòng)很小的邏輯,上層應(yīng)用層就可以靈活多變了。 Posted by: 劉聰 | (2) August 29, 2012 06:00 PM 什么時(shí)候,終結(jié)者病毒會(huì)自主產(chǎn)生呢 |
|