隨著項(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)用)。
因此眼光落在了SimpleXMLRPCServer和zerorpc上蟹地。
關(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è)問題:
- RPC服務(wù)很有可能在另一臺(tái)主機(jī)上块仆,甚至是線上服務(wù)器,不能停機(jī)調(diào)試
- 涉及到的函數(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è)問題:
- 每次更新代碼都要重啟一遍服務(wù)筐付,能不能autoreload?
- 重啟服務(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ò)誤米辐,歡迎指正。