PIDL - 豆瓣的服务化实践
Shire 是豆瓣主站代码仓库,包含了早期的项目代码、各产品线公用的代码、遗留代码等。虽然大部分的产品线已经拆分出去使用 DAE 来服务,但是还是通过软链接的方式被包含进去。一开始这种代码组织方式还是可以接受的,但随着项目逐渐变大,以下问题越来越严重:
- 大部分产品线都依赖 Shire,改动产品线 A 的代码可能会影响到产品线 B,甚至造成全站出错。
- 任何改动都需要 Shire 的人工上线,上线很慢,故障修复不及时。
- 任何改动的持续集成时间都很长。
PIDL 是豆瓣解决这些问题的方案,严格的说它不是一种语言,只是一个 Python 模块/包的接口隔离层。它把服务全部隐藏起来,只通过 PIDL 文件暴露那些需要被外部使用的接口。它的应用实现了如下目的:
1.代码解耦。产品线的拆分更利于团队协作,大家再也不用往 Shire 上提 PR 了。
2.故障隔离。产品线的故障不再会影响其他产品线。
3.运维方便。产品线都使用 DAE 独立部署和监控,也方便扩容。
4.服务化方案灵活。无论是对产品线,还是对产品线的某个功能实现服务化都很方便,工程师稍加培训就可以胜任服务化工作。
5.对产品线开发方式的影响很小。虽然豆瓣在产品线某些功能上使用 Thrift,但是对整个产品线做类似的服务化意味着需要改动大量的逻辑,这是不可接受的。而使用 PIDL 对现有的业务逻辑几乎没有影响。
6.对服务化高度可控。自主研发可以使用最简单的解决方案,保证了高度的可控性以及与豆瓣现有架构的兼容性,PIDL 也和 DAE 等基础设施关系紧密。
7.优雅降级。当服务不可用或者失败,会返回预先定义的结果给客户端,不会让程序发生异常。可以容忍单点失败。
PIDL 和 Thrift 的区别在于:
- PIDL 文件直接使用,不需要对其编译。
- PIDL 文件不需要预先定义参数类型。
- PIDL 的结构体就是 Python 的类、对象、函数、常量等,更 Pythonic。
- PIDL 的接口可以有非常复杂的层级,写起来比 Thrift 的接口简洁得多。
- PIDL 基于 Pickle 的二进制协议,以牺牲语言无关性为代价,尽可能地减少对代码的修改。
PIDL 架构
假设现在有一个要服务化的产品 Story,它至少有如下 3 个文件。
1.PIDL 文件。对于 Story,通常叫作 story_pidl.py。它包含了 PIDL 设置、PIDL 接口声明和返回的默认值等:
import pidl # 文件中不能引用业务中的模块 __pidl_config__={ 'name':'story', 'implemented_by':'story_service', # 指定后端逻辑入口模块 'active_plugins':[ 'monitor', # PIDL 具有很高的扩展性,默认已经注册了一些插件,这里可 以列出来那些默认未激活而需要激活的插件 ] } def get_stories_by_author_id(author_id): # 定义的接口可以是函数、类、对象、 变量等 return [] # Fallback 模式,当服务不可用或者失败时返回给客户端的默认值 class Story(object): story_id=0 author_id=None category_id=100 creation_time=None def__init__(self, story_id, author_id, category_id, creation_time): pass def get(story_id): return '' @classmethod def get_stories_by_category_id(cls, category_id): return [] @pidl.retry(10) # retry 和 timeout 是默认已注册的插件,一般通过装饰器来激活插 件 @pidl.timeout(100) def refresh_stories_indices(): pass
2.PIDL 输出文件。一般叫作 story_export.py,客户端通过这个文件和服务端通信,它把 PIDL 封装好的对应接口都内嵌到这里面,比如客户端调用上面定义的 get_stories_by_author_id 函数就会是这样:
from story_export import get_stories_by_author_id
返回的是 PIDL 接口对象,这个对象带了那些需要的属性、方法等。
3.后端逻辑入口文件。通常叫作 story_service.py,存放对应在 story_pidl.py 文件中提供的接口的真实引用:
from story.models.bot import refresh_stories_indices from story.models.story import Story, get_stories_by_author_id
要保证 story_service.py 和 story_pidl.py 的接口一一对应。
要确保 Fallback 模式下返回值的类型和正确返回的值的类型一致。设想本来返回值是一个列表,逻辑上对返回结果做 for 循环,如果在 Fallback 模式返回 None,就会在执行 for 循环的时候抛出 TypeError 异常。
story_export.py 的输出模式有两种。
1.内嵌模式。常用于持续集成,减少服务化对测试的影响和复杂度。其实原始的模块/包都还在,只是通过 PIDL 把真实的模块/包和实际调用隔离开,也就是永远不能使用“from story_service import get_stories_by_author_id”的方式引用:
import pidl import story_pidl server=pidl.implement(story_pidl) server.embed(globals())
2.RPC 模式。在生产环境使用,通常是远程调用。通过 PSGI(Pidl Server Gateway Interface,基于 WSGI)协议,用独立模式启动 PIDL 服务:
pidl-server -b"127.0.0.1:8300"story_pidl
选择如下的方式调用:
import pidl import story_pidl client=pidl.make_client(story_pidl, server='pidl://127.0.0.1:8300') client.pidl_embed(globals())
RPC 模式的架构如图 10.1 所示。
图 10.1 RPC 模式的架构
二进制协议是高可用 RPC 框架必须支持的协议,PIDL 的二进制网络协议借鉴了 SPDY(http://bit.ly/2b5LByy )和 MessagePack-RPC(http://bit.ly/2aSvSDi )。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论