返回介绍

PIDL - 豆瓣的服务化实践

发布于 2025-04-20 18:52:17 字数 3523 浏览 0 评论 0 收藏

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。