在Django中使用zerorpc

前言

随着系统架构从集中式单点服务器到分布式微服务方向的迁移,RPC是一个不可回避的话题.如何在系统中引入对开发者友好,性能可靠的RPC服务是一个值得深思的问题.

在调研了Thrift,gRPC,zerorpc等方案后,基于以下2点最后选择了zerorpc:

  • Thrift,gRPC学习成本高,开发者需要重新定义返回结构增加了工作量
  • zerorpc完美契合Python,能快速开发,并且支持Node.js,适用于当前技术栈

问题

虽然zerorpc可以直接嵌入当前系统框架中,但是还是有一些问题需要去考虑解决

  • rpc 接口如何定义

  • rpc 服务如何启动

  • 高并发情况下客户端的可靠性

服务端

在当前的系统中大量使用Celery,djang-celery定义Task的方式是在每个install app中定义tasks.py文件,然后通过@task装饰器来生成Task.所以这里为了方便定义rpc interface设计一套类似于Celery的规范.需要输出rpc interface的app下面创建rpcs.py文件

# rpcs.py
# coding: utf-8 from eebo.core.utils.zrpc import rpc
from .models import Ticket
from .serializers import TicketSerializer @rpc.register()
def get_ticket():
t = Ticket.objects.first()
s = TicketSerializer(t)
return s.data @rpc.register(name='ticket_list', stream=True)
def get_tickets(n):
qs = Ticket.objects.all()[:n]
s = TicketSerializer(qs, many=True)
return iter(s.data)

  

rpc.register装饰器用来注册函数到rpc服务上,可选参数:

  • name: 客户调用方法名称, 没有写的情况下就是func name如get_ticket
  • stream: 默认False, 如果为True, 则使用zerorpc的流式响应传输, 数据量比较大的情况时使用, 返回可迭代对象

我们来看看eebo.core.utils.zrpc如何来实现这个注册过程:

# coding: utf-8

import zerorpc

class RPC(object):
@classmethod
def register(cls, name=None, stream=False):
def _wrapper(func):
setattr(cls, name or func.__name__, zerorpc.stream(
lambda self, *args, **kwargs: func(*args, **kwargs)) if stream
else staticmethod(func))
return func return _wrapper rpc = RPC()

  

通过一个类方法来往类上面绑定方法,需要注意的是name的定义必须是全局唯一的.

现在我们有了定义rpc interface的方法,下面来看看如何启动rpc server.

# runrpc.py
# coding: utf-8 import re
import sys
import imp as _imp
import importlib
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from eebo.core.utils.zrpc import rpc, ServerExecMiddleware naiveip_re = re.compile(r"""^(?:
(?P<addr>
(?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) | # IPv4 address
(?P<ipv6>\[[a-fA-F0-9:]+\]) | # IPv6 address
(?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN
):)?(?P<port>\d+)$""", re.X) class Command(BaseCommand):
help = "Starts a lightweight RPC server for development." default_addr = '127.0.0.1'
default_port = '4242' def add_arguments(self, parser):
parser.add_argument('addrport',
nargs='?',
help='Optional port number, or ipaddr:port') def handle(self, *args, **options): self.use_ipv6 = False
if not options['addrport']:
self.addr = ''
self.port = self.default_port
else:
m = re.match(naiveip_re, options['addrport'])
if m is None:
raise CommandError('"%s" is not a valid port number '
'or address:port pair.' %
options['addrport'])
self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
if not self.port.isdigit():
raise CommandError("%r is not a valid port number." %
self.port)
if self.addr:
if _ipv6:
self.addr = self.addr[1:-1]
self.use_ipv6 = True
self._raw_ipv6 = True
elif self.use_ipv6 and not _fqdn:
raise CommandError('"%s" is not a valid IPv6 address.' %
self.addr)
if not self.addr:
self.addr = self.default_addr_ipv6 if self.use_ipv6 else self.default_addr
self._raw_ipv6 = self.use_ipv6
self.run(**options) def run(self, **options):
"""Run the server, using the autoreloader if needed."""
self.autodiscover_rpc() server = self.get_server() try:
server.run()
except KeyboardInterrupt:
server.close()
sys.exit(0) def autodiscover_rpc(self, related_name='rpcs'):
for pkg in settings.INSTALLED_APPS:
try:
pkg_path = importlib.import_module(pkg).__path__
except AttributeError:
continue try:
_imp.find_module(related_name, pkg_path)
except ImportError:
continue try:
importlib.import_module('{0}.{1}'.format(pkg, related_name))
except ImportError:
pass def get_server(self, *args, **options):
"""Return the default zerorpc server for the runner."""
import zerorpc
server = zerorpc.Server(rpc, heartbeat=30)
server.bind("tcp://{0}:{1}".format(self.addr, self.port))
# close django old connections
zerorpc.Context.get_instance().register_middleware(ServerExecMiddleware()) # for sentry
try:
from raven.contrib.zerorpc import SentryMiddleware
if hasattr(settings, 'RAVEN_CONFIG'):
sentry = SentryMiddleware(hide_zerorpc_frames=False,
dsn=settings.RAVEN_CONFIG['dsn'])
zerorpc.Context.get_instance().register_middleware(sentry)
except ImportError:
pass return server

  

runrpc.py是一个Django management commands 文件需要放到某个install app目录的management/commands下面,启动服务器:

python manage.py runrpc 0.0.0.0:4242
  • autodiscover_rpc 自动发现rpc interface注册函数
  • get_server 生成zerorpc server对象

get_server中对zerorpc注册了2个中间件,SentryMiddleware用于捕获rpc interface抛出的异常发送到sentry,ServerExecMiddleware用于处理Django db connection,看看代码:

# zrpc.py
# coding: utf-8 from django.db import close_old_connections class ServerExecMiddleware(object): def server_before_exec(self, request_event):
close_old_connections() def server_after_exec(self, request_event, reply_event):
close_old_connections()

  

在每个rpc interface被调用前与调用后都调用close_old_connections关闭db connection,这里是为了实现django.db中对请求处理前与处理后注册信号:

django.db.__init__.py

signals.request_started.connect(close_old_connections)
signals.request_finished.connect(close_old_connections)

  

目的是保证在rpc interface中使用ORM时,connection没有超时断开.

客户端

由于rpc的调用是阻塞的,不能全局只创建一个client.但是也不能每个请求都创建client,所以这里参考redis-py的client实现,定义一个支持连接池的zerorpc client.

# zrpc.py
# coding: utf-8 import os
import zerorpc from redis.connection import BlockingConnectionPool
from gevent.queue import LifoQueue class Connection(object):
def __init__(self, connect_to, heartbeat=30):
self.client = zerorpc.Client(heartbeat=heartbeat)
self.client.connect(connect_to)
self.pid = os.getpid() def disconnect(self):
self.client.close() class RPCClient(object):
def __init__(self, connect_to, heartbeat=30):
self.connection_pool = BlockingConnectionPool(connection_class=Connection,
queue_class=LifoQueue, timeout=heartbeat, connect_to=connect_to, heartbeat=heartbeat) def close(self):
self.connection_pool.disconnect() def __getattr__(self, name):
return lambda *args, **kwargs: self(name, *args, **kwargs) def __call__(self, name, *args, **kwargs):
connection = self.connection_pool.get_connection('')
try:
return getattr(connection.client, name)(*args, **kwargs)
finally:
self.connection_pool.release(connection)

  

这里直接复用了redis-py定义的连接池,当前系统使用gunicorn + gevent的方式启动Django服务,所以queue_class使用了gevent的LifoQueue.

在使用过程中还发现了这个问题:

https://github.com/0rpc/zerorpc-python/issues/123

需要打个补丁解决:

import zmq.green as zmq

# patch zmq garbage-collection Thread to use green Context:
from zmq.utils.garbage import gc
gc.context = zmq.Context()

  

总结

技术的选型需要契合项目实际情况,不要盲目上新技术引入不必要的成本.为了推广方案,必须全局的考虑方案是否易使用,是否易部署.

完整代码:

https://gist.github.com/zhu327/5b6c06eccc5758d4e642ee899a518687

在Django中使用zerorpc的更多相关文章

  1. 异步任务队列Celery在Django中的使用

    前段时间在Django Web平台开发中,碰到一些请求执行的任务时间较长(几分钟),为了加快用户的响应时间,因此决定采用异步任务的方式在后台执行这些任务.在同事的指引下接触了Celery这个异步任务队 ...

  2. Mysql事务探索及其在Django中的实践(二)

    继上一篇<Mysql事务探索及其在Django中的实践(一)>交代完问题的背景和Mysql事务基础后,这一篇主要想介绍一下事务在Django中的使用以及实际应用给我们带来的效率提升. 首先 ...

  3. Mysql事务探索及其在Django中的实践(一)

    前言 很早就有想开始写博客的想法,一方面是对自己近期所学知识的一些总结.沉淀,方便以后对过去的知识进行梳理.追溯,一方面也希望能通过博客来认识更多相同技术圈的朋友.所幸近期通过了博客园的申请,那么今天 ...

  4. Django 中url补充以及模板继承

    Django中的URL补充 默认值 在url写路由关系的时候可以传递默认参数,如下: url(r'^index/', views.index,{"name":"root& ...

  5. django中css问题

    django中加载的css,js,图片其中js和图片可以加载出来,而css没有效果.原因如下: 这是因为你安装的某些IDE 或者其他更改了注册表导致的系统的注册表\HKEY_CLASSES_ROOT\ ...

  6. 在Django中进行注册用户的邮件确认

    之前利用Flask写博客时(http://hbnnlove.sinaapp.com),我对注册模块的逻辑设计很简单,就是用户填写注册表单,然后提交,数据库会更新User表中的数据,字段主要有用户名,哈 ...

  7. django中tinymce添加图片上传功能

    主要参考以下: https://pixabay.com/en/blog/posts/direct-image-uploads-in-tinymce-4-42/ http://blog.csdn.net ...

  8. django中migration文件是干啥的

    昨天很蠢的问leader git push的时候会不会把本地的数据库文件上传上去,意思是django中那些migration文件修改之后会不会上传. 然后得知不会,因为所有的数据库都存在本机的mysq ...

  9. Django中Celery的实现介绍(一)

    Django中Celery的实现 Celery官网http://www.celeryproject.org/ 学习资料:http://docs.jinkan.org/docs/celery/ Cele ...

随机推荐

  1. misc之压缩文件处理

    misc之压缩文件处理 一.预备知识 1.头文件名代码   二.实验环境 1.winhex   三.题目来源 攻防世界杂项:János-the-Ripper.再见李华   实验一 打开题目文件是一个压 ...

  2. HDU 6470:Count(矩阵快速幂)

    Count Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total Submi ...

  3. JSP中的九大内置对象

    JSP九大内置对象 pageContext 存东西 Request 存东西 Response Session 存东西 Application(servletContext) 存东西 config(se ...

  4. 基于Spring MVC + Spring + MyBatis的【人事管理系统】

    资源下载:https://download.csdn.net/download/weixin_44893902/33163160 一.语言和环境 实现语言:JAVA语言 环境要求:IDEA/Eclip ...

  5. 制作四个选项卡页 Tab,用户可以通过切换不同的 Tab 页查看不同类别的新闻信息,每个 Tab 有对应的内容版块,点击某个选项卡时,显示对应的内容版块,隐藏其他内容版块,并且为了突出当前的选项卡,还

    查看本章节 查看作业目录 需求说明: 制作四个选项卡页 Tab,用户可以通过切换不同的 Tab 页查看不同类别的新闻信息,每个 Tab 有对应的内容版块,点击某个选项卡时,显示对应的内容版块,隐藏其他 ...

  6. 编写Java程序,模拟五子棋博弈过程中的异常声明和异常抛出

    返回本章节 返回作业目录 需求说明: 模拟五子棋博弈过程中的异常声明和异常抛出,判断用户所下棋子的位置,是否超越了棋盘的边界. 棋盘的横坐标的范围为0-9,纵坐标范围为0-14,如果用户所放棋子的坐标 ...

  7. 认识MongoDB(二)

    MongoDB是什么 MongoDB 是一个开源的.可扩展的.跨平台的.面向文档的非关系型数据库,它由 C++ 语言编写,旨在为 WEB 应用提供可扩展的高性能数据存储解决方案. 在 MongoDB ...

  8. 初识python:斐波拉契数(列表获取)

    使用 列表 获取斐波拉契数,代码如下: n = int(input('您想获取前几个斐波拉契数?\n')) li = [] for i in range(n): if i <= 1: li.ap ...

  9. Python_多任务:进程、线程、协程

    进程 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体.进程是一种抽象的概念,从来没有统一的标准定义.进程一般由程序 ...

  10. unittest+ddt_实现数据驱动测试(7)

    我们设计测试用例时,会出现测试步骤一样,只是其中的测试数据有变化的情况,比如测试登录时的账号密码.这个时候,如果我们依然使用一条case一个方法的话,会出现大量的代码冗余,而且效率也会大大降低.此时, ...