ZeroRPC应用

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

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

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

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

优点缺点
SimpleXMLRPCServerpython自带库,不用额外安装数据包大,速度慢
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技巧的文章里再说吧。

如有错误,欢迎指正。

    原文作者:AlfredX
    原文地址: https://www.jianshu.com/p/698b7406dc09
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞