ZeroRPC應(yīng)用

隨著項(xiàng)目的發(fā)展陨倡,除了業(yè)務(wù)所在的WebService之外敛滋,有了內(nèi)部系統(tǒng)的業(yè)務(wù)需求,涵蓋客服財(cái)務(wù)統(tǒng)計(jì)報(bào)表等玫膀,在項(xiàng)目子系統(tǒng)篇中能看到詳細(xì)的介紹矛缨。今天在這里要說的是系統(tǒng)間的橋梁:RPC(Remote Procedure Call)

其實(shí)這也不是什么新鮮的概念,上世紀(jì)70年代就提出過理論帖旨,80年代就實(shí)際應(yīng)用過箕昭。RPC多是用于對(duì)部署于其他主機(jī)或者網(wǎng)絡(luò)空間的服務(wù)的請(qǐng)求。所以比作系統(tǒng)間的橋梁也是比較合適的解阅。

我們的業(yè)務(wù)系統(tǒng)和內(nèi)部系統(tǒng)都是由Django搭建的落竹,所以rpc服務(wù)顯然也是要找支持python語(yǔ)言的。其他語(yǔ)言下的優(yōu)秀框架很多货抄,也不是說不能用述召,但是不要隨意增加項(xiàng)目的技術(shù)復(fù)雜度(Python和其他語(yǔ)言的協(xié)同應(yīng)用)。

因此眼光落在了SimpleXMLRPCServerzerorpc上蟹地。
關(guān)于各自的介紹在鏈接里都能看到积暖,就簡(jiǎn)單比較一下優(yōu)缺點(diǎn)把。

優(yōu)點(diǎn) 缺點(diǎn)
SimpleXMLRPCServer python自帶庫(kù)怪与,不用額外安裝 數(shù)據(jù)包大夺刑,速度慢
zerorpc 底層使用ZeroMQ和MessagePack,速度快分别,響應(yīng)時(shí)間短遍愿,并發(fā)高 額外安裝,文檔不多

調(diào)研時(shí)自己寫過一個(gè)workbench耘斩,把代碼貼上來沼填。

[server端代碼]
import zerorpc
from SimpleXMLRPCServer import SimpleXMLRPCServer

class RPCServer(object):
    """docstring for RPCServer"""

    def __init__(self):
        super(RPCServer, self).__init__()
        self.data = {str(i): i for i in range(100)}
        self.data2 = None

    def getObj(self):
        print('get data')
        return self.data

    def sendObj(self, data):
        print('send data')
        self.data2 = data
# zerorpc
s = zerorpc.Server(RPCServer())
s.bind('tcp://0.0.0.0:4243')
s.run()
# SimpleXMLRPCServer
server = SimpleXMLRPCServer(('localhost',4242), allow_none=True)
server.register_introspection_functions()
server.register_instance(RPCServer())
server.serve_forever()
[client端代碼]
import zerorpc
import time
import xmlrpclib

# zerorpc
def zerorpc_client():
    print('zerorpc client')
    c = zerorpc.Client()
    c.connect('tcp://127.0.0.1:4243')
    data = {str(i): i for i in range(100)}
    start = time.clock()
    for i in range(5000):
        c.getObj()
    for i in range(5000):
        c.sendObj(data)
    print('total time %s' % (time.clock() - start))

# SimpleXMLRPCServer
def xmlrpc_client():
    print('xmlrpc client')
    c = xmlrpclib.ServerProxy('http://localhost:4242')
    data = {str(i): i for i in range(100)}
    start = time.clock()
    for i in range(5000):
        c.getObj()
    for i in range(5000):
        c.sendObj(data)
    print('xmlrpc total time %s' % (time.clock() - start))

都是本機(jī)測(cè)試,結(jié)果zerorpc性能要高10%-20%括授,加上ZeroMQ和MessagePack帶來的優(yōu)勢(shì)坞笙,所以選擇了zerorpc。

后來業(yè)務(wù)上又新增了NodeJS的服務(wù)荚虚,同樣需要請(qǐng)求業(yè)務(wù)服務(wù)器的數(shù)據(jù)薛夜,相比HTTP請(qǐng)求,RPC消耗的資源更少曲管,這時(shí)就非常慶幸最初選擇了zerorpc却邓,因?yàn)樗€能夠無縫兼容javascript。

從zerorpc的使用方式可以看出我們只需要提供一個(gè)包含所有函數(shù)的instance院水,因此這里尤其適合將函數(shù)按模塊劃分腊徙,并由一個(gè)主類(MainClass)多重繼承而來。

就像這樣:

class RPCModuleA(object):
    ...

class RPCModuleB(object):
    ...

class RPCServer(RPCModuleA,
                RPCModuleB):
    ...

最初在項(xiàng)目中使用的RPC服務(wù)就僅僅只有幾個(gè)小類檬某,一個(gè)主類撬腾,然后命令行一跑,往后臺(tái)一放恢恼,OK了民傻。
在簡(jiǎn)單的需求下,RPC服務(wù)的壓力不高,調(diào)用不頻繁漓踢,這樣的結(jié)構(gòu)已經(jīng)足夠了牵署。
但是隨著依賴RPC的業(yè)務(wù)越來越多,問題也就一點(diǎn)一點(diǎn)暴露出來喧半,首先就是調(diào)試的問題奴迅。所有rpc服務(wù)都會(huì)將server端拋出的異常返回給client端,但是如果是一個(gè)不會(huì)拋異常的BUG呢挺据?
我就遇到這樣一個(gè)問題取具,只是更新數(shù)據(jù)庫(kù)里的一條記錄,但是卻傳錯(cuò)了參數(shù)扁耐,導(dǎo)致錯(cuò)誤的記錄被更新了暇检。通常的代碼里,我們打個(gè)斷點(diǎn)或者在函數(shù)里打個(gè)日志輸出一下參數(shù)和返回值就行婉称,但是在這里我們會(huì)面臨2個(gè)問題:

  1. RPC服務(wù)很有可能在另一臺(tái)主機(jī)上块仆,甚至是線上服務(wù)器,不能停機(jī)調(diào)試
  2. 涉及到的函數(shù)可能不止一個(gè)酿矢,考慮到以后會(huì)有越來越多的函數(shù)榨乎,不可能在每個(gè)函數(shù)里都重復(fù)一遍打日志的代碼,那會(huì)很愚蠢

所以需要一個(gè)簡(jiǎn)單的方法能夠打出所有的調(diào)用請(qǐng)求瘫筐、參數(shù)和返回值蜜暑。
好在我們用的是python。 so let's do it in python way.
我們都知道python instance在初始化時(shí)調(diào)用的是 __init__ 函數(shù)策肝,因此我們可以在所有父類的__init__ 函數(shù)執(zhí)行完后對(duì)這個(gè)instance做些手腳肛捍。

class func_wrapper(object):

    def __init__(self, func):
        self.func = func

class RPCServer(RPCModuleA,
                RPCModuleB):

    def __init__(self):
        super(RPCServer, self).__init__()
        for func_name in dir(self):
            if not func_name.startswith('_'):
                func = getattr(self, func_name)
                if callable(func):
                    setattr(self, func_name, func_wrapper(func))

代碼里我們能看到我對(duì)RPCServer的所有以非下劃線開頭的函數(shù)(包括繼承而來的)都封裝了一遍并替換掉了原函數(shù)。但是func_wrapper(func)是一個(gè)instance之众,不是函數(shù)拙毫,也就不能以函數(shù)形式調(diào)用。了解python的同學(xué)應(yīng)該知道棺禾,python的built-in函數(shù)callable可以檢查一個(gè)object是否可以以函數(shù)形式調(diào)用缀蹄,看一下文檔(python2 callable)就能知道class instance如果有__call__函數(shù)就能被調(diào)用。那這就很簡(jiǎn)單了膘婶,我們加上這個(gè)函數(shù)并在其中調(diào)用原先的函數(shù)對(duì)象缺前,然后把我們關(guān)心的函數(shù)名,參數(shù)悬襟,返回值都打出來衅码,就像這樣:

class func_wrapper(object):

    def __init__(self, func):
        self.func = func
        
    def __call__(self, *args, **kwargs):
        result = self.func(*args, **kwargs)
        print(self.func.__name__, args, kwargs, result)
        return result

非常好,這樣目的達(dá)到了脊岳,運(yùn)行一下吧逝段。垛玻。。呵呵奶躯!報(bào)錯(cuò)了帚桩。

AttributeError: 'func_wrapper' object has no attribute '__name__'

沒錯(cuò),RPC server需要把所有函數(shù)名都暴露給client巫糙,把原來的函數(shù)替換了但是名字沒留下朗儒,自然是要出錯(cuò)颊乘。修改一下参淹,很簡(jiǎn)單:

class func_wrapper(object):

    def __init__(self, func):
        self.func = func
        
    def __call__(self, *args, **kwargs):
        result = self.func(*args, **kwargs)
        print(self.func.__name__, args, kwargs, result)
        return result
        
    @property    
    def __name__(self):
        return self.func.__name__
  
class RPCServer(RPCModuleA,
                RPCModuleB):

    def __init__(self):
        super(RPCServer, self).__init__()
        for func_name in dir(self):
            if not func_name.startswith('_'):
                func = getattr(self, func_name)
                if callable(func):
                    setattr(self, func_name, func_wrapper(func))

好了,這樣就完成了我們對(duì)每一次調(diào)用都把函數(shù)名乏悄,參數(shù)浙值,返回值打出日志的目的了。
但是這還沒完檩小。項(xiàng)目是越做越大的开呐,新問題是越來越多的。隨著rpc服務(wù)的內(nèi)容變多规求,新出現(xiàn)了2個(gè)問題:

  1. 每次更新代碼都要重啟一遍服務(wù)筐付,能不能autoreload?
  2. 重啟服務(wù)是簡(jiǎn)單阻肿,但是經(jīng)常遇到client把端口占住了瓦戚,RPCServer關(guān)了就開不了了。

第二個(gè)問題需要解釋一下丛塌,測(cè)試時(shí)client和server都在同一臺(tái)主機(jī)上较解,client端為了節(jié)省資源,把rpcclient對(duì)象一直保持住赴邻,避免多次建立連接印衔。發(fā)布到生產(chǎn)環(huán)境時(shí)基本不會(huì)這么處理,但是我們畢竟還是創(chuàng)業(yè)初期姥敛,服務(wù)器資源也很緊張奸焙,難免遇到多個(gè)服務(wù)部署在同一臺(tái)機(jī)器上的時(shí)候,所以這個(gè)問題還是需要解決的彤敛。

解決辦法是使用了django.utils.autoreload模塊与帆,它把兩個(gè)問題都解決了。

def startRPCServer():
    s = zerorpc.Server(RPCServer())
    s.bind('tcp://0.0.0.0:' + RPC_PORT)
    s.run()

if __name__ == '__main__':
    from django.utils.autoreload import main
    main(startRPCServer)

django用manager.py runserver來啟動(dòng)也是使用的autoreload來監(jiān)測(cè)文件變化臊泌。這樣既能占住端口鲤桥,又能無縫更新代碼。
不過實(shí)際使用時(shí)還是有點(diǎn)小問題渠概。如果你使用Sublime Text的REPL包來運(yùn)行python腳本的話茶凳,當(dāng)你把REPL tab關(guān)掉后不會(huì)如你所想的一樣把占用端口的進(jìn)程也殺掉嫂拴。其中的原因我想是因?yàn)閍utoreload起了2個(gè)進(jìn)程,一個(gè)進(jìn)程監(jiān)測(cè)文件贮喧,一個(gè)進(jìn)程是我們實(shí)際的RPCServer筒狠,而關(guān)閉tab只是關(guān)閉了監(jiān)測(cè)進(jìn)程而已。關(guān)于這個(gè)還沒有什么解決辦法箱沦,日后有辦法了再來更新吧辩恼。

大體上關(guān)于zerorpc的應(yīng)用就到這里了,項(xiàng)目的體量還不至于大到需要分布式RPC服務(wù)谓形。雖然我很想嘗試灶伊,但是可能得以后才有機(jī)會(huì)了。除了上述說到的內(nèi)容以外還做了一些輸出重定向的工作寒跳,用于其它的日志輸出聘萨,里面有些打印調(diào)用棧的知識(shí)點(diǎn),就在以后關(guān)于python技巧的文章里再說吧童太。

如有錯(cuò)誤米辐,歡迎指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末书释,一起剝皮案震驚了整個(gè)濱河市翘贮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爆惧,老刑警劉巖狸页,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異检激,居然都是意外死亡肴捉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門叔收,熙熙樓的掌柜王于貴愁眉苦臉地迎上來齿穗,“玉大人,你說我怎么就攤上這事饺律∏砸常” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵复濒,是天一觀的道長(zhǎng)脖卖。 經(jīng)常有香客問我,道長(zhǎng)巧颈,這世上最難降的妖魔是什么畦木? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮砸泛,結(jié)果婚禮上十籍,老公的妹妹穿的比我還像新娘蛆封。我一直安慰自己,他們只是感情好勾栗,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布惨篱。 她就那樣靜靜地躺著,像睡著了一般围俘。 火紅的嫁衣襯著肌膚如雪砸讳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天界牡,我揣著相機(jī)與錄音簿寂,去河邊找鬼。 笑死欢揖,一個(gè)胖子當(dāng)著我的面吹牛陶耍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播她混,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼喘先,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼贫堰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起盲厌,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤馒过,失蹤者是張志新(化名)和其女友劉穎臭脓,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腹忽,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡来累,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窘奏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘹锁。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖着裹,靈堂內(nèi)的尸體忽然破棺而出领猾,到底是詐尸還是另有隱情,我是刑警寧澤骇扇,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布摔竿,位于F島的核電站,受9級(jí)特大地震影響少孝,放射性物質(zhì)發(fā)生泄漏继低。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一稍走、第九天 我趴在偏房一處隱蔽的房頂上張望袁翁。 院中可真熱鬧冷溃,春花似錦、人聲如沸梦裂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)年柠。三九已至凿歼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冗恨,已是汗流浹背答憔。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掀抹,地道東北人虐拓。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像傲武,于是被迫代替她去往敵國(guó)和親蓉驹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容