服务化是知乎几年来技术演进故事里的一个主角,公司规模从几十人到几百人,在监控、tracing、框架、容器等基础设施从无到有的同时,也扩展出多个后端技术团队。在服务化演进的过程里,我们也进行了一些新的思考。
服务化的愿景「微服务」 是业内最近两三年业内很火的 buzzword,迁移到微服务架构,大多强调这些好处:
- 松耦合
- 独立发布
- 快速迭代
- 故障隔离
- 增加重用
经过服务的拆分,将复杂到难以移动的单体应用,拆分为多个可以独立部署的服务,单个服务的复杂性远远小于整体,这样不同服务的开发者可以并行开发,从而提高开发效率;因为服务的细粒度,可以 assign 给一个具体的人让他负责,随着业务的增长对服务做定向扩容;同时因为服务的隔离性,可以隔离故障,提高整体的稳定性。
不过在推动服务化的过程中,也感受到一些意外的痛点。
我们首先感受到的是服务数量的不可控。新 feature 经常会以新服务的形式开始开发,而非扩展现有的服务。随着产品的需求迭代,服务越来越碎片化,扩展现有服务变得越来越困难,因为很难选择加入到哪个服务更好。而且扩展一个其他人负责的服务,沟通成本不能忽视,这时出于产品进度的压力,另起炉灶开始一个新服务实在是最快的选择。
其次,部分业务基于 RPC 做水平分拆,原则上 RPC 的演进要保持向前兼容,而项目前期需求不稳定阶段,难免引进相当多的 break change,发包、上线、联调成本居高不下,也引进了额外的发布风险:上层服务和下层服务两者务必同时发布,但不能回滚其中之一。这在重要的功能开发中,成为了我们快速迭代的一个绊脚石。
拆分为 RPC 之后,通信介质发生了不同,而访问模式的适应不一定准备好。对有缓存覆盖的数据访问对象而言,N 1 式的访问模式不算问题,但是 N 1 query 对 RPC 却不能接受。服务的调用压力经常出乎维护者的意料,RPC 放大使得下层服务的波动放大,风险增加之余也增加了对资源的需求,而资源占用更多的同时,性能仍远低于过去对缓存的访问。
基于 SSO 的分拆RPC (远程过程调用)是服务化体系中基础的基础,但是慢慢的我们发现 RPC 并非分拆的唯一选择。基于 RPC 的水平分拆会引入中间层次,增加联调的环节,对于快速开发的新业务而言,无法忽视额外的联调成本。联调中发现问题时不得不在各个层次定位,难以自动化测试。经过今年几个快速迭代的新项目从主站的分拆,我们发现基于 SSO (单点登录)机制对 HTTP 接口做垂直拆分是一条可行的路,可灰度控制风险、对客户端透明。新项目对主站的耦合往往较低,只需要 get_member 等少量几个 RPC 接口,对外暴露的接口也非常少。新项目的迭代速度快,经过拆分,能够使服务与后端团队的完整业务相匹配,降低了主站联动的发布频率,某种程度上也减少了出于部署积压而堆积的发布风险。
这里我们得到的启发是,服务的分拆并非 RPC 不可。相反,我们希望看到更少的 RPC,更多的内聚。更少的 RPC 接口意味着更小的服务边界,更稳定的接口,更少的 break change。内聚意味着允许功能需求的独立演进,对其他业务的影响降到最低,也意味着内聚的业务模块内部,可以充分利用缓存来优化性能。
桌面端怎么拆?新项目之所以拆分顺利,也得益于它们当前只面向客户端,HTTP API 的拆分仍较为简单。然而桌面端的情形有所不同,一个页面往往不能单纯对应到一个服务上,比如问答页,除了问答服务,还需要评论、收藏、内容推荐等信息需要展现,这些页面组件背后的服务会趋于不同的工程师或团队维护。
这里可以注意到一件有意思的事情,在后端社区 buzzword 「服务化」 的同时,前端社区更加如火如荼地 buzzword 着 「前后端分离」 和 「组件化」。出于内容展现的复杂性,页面并不能成为分拆的好单位,但页面内的组件无疑更容易与后端的功能模块相匹配。「服务化」 和 「组件化」 是一对孪生子,分别允许后端团队和前端团队去隔离业务模块、独立演进。
知乎也正在进行着桌面端的前后端分离改造。随后桌面端不再特殊,允许后端面向 iOS / Android / Web 三端按同一套接口进行对接,也就可以像移动端 API 同样的方式,基于单点登录进行拆分了。
垂直拆分的风险当前感受到垂直拆分的主要风险在于保证不同垂直业务对同类型实体展现逻辑的一致性。其中 feed 对这点要求最高,因为它对所有实体类型都有引用。为此在拆分 feed 流之前,先行补充了所有实体的 swagger 文档,通过中心的 swagger 文档约束住同一实体在不同服务中的字段展现。
如何划分服务边界理想的世界里,服务边界恰好匹配于业务边界。然而工程师首先要承担业务需求的压力,只能抽时间重构拆分,业务边界也并不总是如新项目那样明晰。
这意味着要考虑优先级,也需要在拆分之前认真地思考业务的边界。排定优先级,考量拆分的收益与风险即可。划分业务的边界,则需要更多的思考拆分后的未来将如何沟通协作,然后再考虑技术因素。目前我们主要有这几个考量:
“
是否拥有独立团队来维护,或者是否拥有发展为一项独立业务的潜力;围绕领域而非 feature,有明确的维护团队,避免过于细粒度;拆分之后,能否改善现有的合作流程;能否帮助区分核心、非核心业务,改善稳定性;
”
以 feed 为例,它首先拥有独立团队维护,通过拆分,技术层面上允许 feed 团队重构掉下层服务与上层展现之间的冗余 RPC 调用,且调用模式较 uniform,在产品层面接受数据最终一致性的前提下可以通过 TTL 缓存提升性能,乃至按自己的业务场景做更细致的优化(优化结束后我们的某些接口 P95 性能加快了一倍);更重要的是对协作方式的影响,未来专栏、问答等生产信息的垂直业务,只提供一个 RPC 接口对接 feed 流即可,而不必集成到主站,这一来 「接入 feed」 流程的参与者,从 feed 组、垂直业务、主站三方,简化为 feed 组和垂直业务双方;此外 feed 通过 TTL 缓存,实质上冗余了一份垂直业务的数据,配合断路器的使用,依赖的垂直业务的抖动甚至崩溃在 feed 这边都可以优雅降级且保持正常展现了。将 feed 与主站的变更相隔离,也有助于改进作为一项核心业务的 feed 的稳定性。
服务分层:业务服务和公共服务在垂直业务之外,也存在多数业务都会重用的公共服务,如用户、话题、网页抓取、多媒体、推送等。业务服务和公共服务在关注点上有所不同:
我们希望业务服务快速迭代,更快、更好地响应多变的业务需求,更多地面向前端工程师;
我们希望公共服务稳定可靠,较少发生改动,但 SLA 要好,更多地为业务重用;
这里会形成一个自然的分层:上层业务求快、下层公共服务求稳。
经验与教训按领域思考合作的方式,而非技术角色分工: 首先思考团队的合作方式,面向领域去划分工作,减少跨服务的沟通环节,减少联调环节;
有目标地、适度地服务化: 服务化有成本和风险,在执行大规模的技术改进期间,要目标导向,小成本达成目标为上,不必一条路线走到底;
面向未来回到文章开头提到的这几点特质:松耦合、独立发布、快速迭代、故障隔离、增加重用,为了这些目标,我们还有很多工作要继续。没有完美的架构,真实世界也充满 trade off,但是围绕我们期望的目标,能一点点做工程改进,收集反馈不停地修正工作方法。这是一段漫漫长路,途中请切记最初的目标!