ZeroRPC应用

随着项目的发展,除了业务所在的WebService之外,有了内部系统的业务需求,涵盖客服财务统计报表等,在项目子系统篇中能看到详细的介绍。今天在这里要说的是系统间的桥梁:RPC(Remote Procedure Call)

其实这也不是什么新鲜的概念,上世纪70年代就提出过理论,80年代就实际应用过。RPC多是用于对部署于其他主机或者网络空间的服务的请求。所以比作系统间的桥梁也是比较合适的。

我们的业务系统和内部系统都是由Django搭建的,所以rpc服务显然也是要找支持python语言的。其他语言下的优秀框架很多,也不是说不能用,但是不要随意增加项目的技术复杂度(Python和其他语言的协同应用)。

因此眼光落在了SimpleXMLRPCServerzerorpc上。
关于各自的介绍在链接里都能看到,就简单比较一下优缺点把。

优点 缺点
SimpleXMLRPCServer python自带库,不用额外安装 数据包大,速度慢
zerorpc 底层使用ZeroMQ和MessagePack,速度快,响应时间短,并发高 额外安装,文档不多

调研时自己写过一个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))

都是本机测试,结果zerorpc性能要高10%-20%,加上ZeroMQ和MessagePack带来的优势,所以选择了zerorpc。

后来业务上又新增了NodeJS的服务,同样需要请求业务服务器的数据,相比HTTP请求,RPC消耗的资源更少,这时就非常庆幸最初选择了zerorpc,因为它还能够无缝兼容javascript。

从zerorpc的使用方式可以看出我们只需要提供一个包含所有函数的instance,因此这里尤其适合将函数按模块划分,并由一个主类(MainClass)多重继承而来。

就像这样:

class RPCModuleA(object):
    ...

class RPCModuleB(object):
    ...

class RPCServer(RPCModuleA,
                RPCModuleB):
    ...

最初在项目中使用的RPC服务就仅仅只有几个小类,一个主类,然后命令行一跑,往后台一放,OK了。
在简单的需求下,RPC服务的压力不高,调用不频繁,这样的结构已经足够了。
但是随着依赖RPC的业务越来越多,问题也就一点一点暴露出来,首先就是调试的问题。所有rpc服务都会将server端抛出的异常返回给client端,但是如果是一个不会抛异常的BUG呢?
我就遇到这样一个问题,只是更新数据库里的一条记录,但是却传错了参数,导致错误的记录被更新了。通常的代码里,我们打个断点或者在函数里打个日志输出一下参数和返回值就行,但是在这里我们会面临2个问题:

  1. RPC服务很有可能在另一台主机上,甚至是线上服务器,不能停机调试
  2. 涉及到的函数可能不止一个,考虑到以后会有越来越多的函数,不可能在每个函数里都重复一遍打日志的代码,那会很愚蠢

所以需要一个简单的方法能够打出所有的调用请求、参数和返回值。
好在我们用的是python。 so let's do it in python way.
我们都知道python instance在初始化时调用的是 __init__ 函数,因此我们可以在所有父类的__init__ 函数执行完后对这个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))

代码里我们能看到我对RPCServer的所有以非下划线开头的函数(包括继承而来的)都封装了一遍并替换掉了原函数。但是func_wrapper(func)是一个instance,不是函数,也就不能以函数形式调用。了解python的同学应该知道,python的built-in函数callable可以检查一个object是否可以以函数形式调用,看一下文档(python2 callable)就能知道class instance如果有__call__函数就能被调用。那这就很简单了,我们加上这个函数并在其中调用原先的函数对象,然后把我们关心的函数名,参数,返回值都打出来,就像这样:

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

非常好,这样目的达到了,运行一下吧。。。呵呵!报错了。

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

没错,RPC server需要把所有函数名都暴露给client,把原来的函数替换了但是名字没留下,自然是要出错。修改一下,很简单:

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))

好了,这样就完成了我们对每一次调用都把函数名,参数,返回值打出日志的目的了。
但是这还没完。项目是越做越大的,新问题是越来越多的。随着rpc服务的内容变多,新出现了2个问题:

  1. 每次更新代码都要重启一遍服务,能不能autoreload?
  2. 重启服务是简单,但是经常遇到client把端口占住了,RPCServer关了就开不了了。

第二个问题需要解释一下,测试时client和server都在同一台主机上,client端为了节省资源,把rpcclient对象一直保持住,避免多次建立连接。发布到生产环境时基本不会这么处理,但是我们毕竟还是创业初期,服务器资源也很紧张,难免遇到多个服务部署在同一台机器上的时候,所以这个问题还是需要解决的。

解决办法是使用了django.utils.autoreload模块,它把两个问题都解决了。

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来启动也是使用的autoreload来监测文件变化。这样既能占住端口,又能无缝更新代码。
不过实际使用时还是有点小问题。如果你使用Sublime Text的REPL包来运行python脚本的话,当你把REPL tab关掉后不会如你所想的一样把占用端口的进程也杀掉。其中的原因我想是因为autoreload起了2个进程,一个进程监测文件,一个进程是我们实际的RPCServer,而关闭tab只是关闭了监测进程而已。关于这个还没有什么解决办法,日后有办法了再来更新吧。

大体上关于zerorpc的应用就到这里了,项目的体量还不至于大到需要分布式RPC服务。虽然我很想尝试,但是可能得以后才有机会了。除了上述说到的内容以外还做了一些输出重定向的工作,用于其它的日志输出,里面有些打印调用栈的知识点,就在以后关于python技巧的文章里再说吧。

如有错误,欢迎指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容