超复杂调用网下的服务治理新思路

我个人对超复杂调用网给出一个定义:

  • 内网非测试的微服务达 1000 个以上

  • 至少存在一个微服务,且其实例数达到 300 个以上

  • 对外 API 普遍涉及至少 10 个微服务

在内部技术实践中,我们发现系统达到这个量级后,超复杂调用网就会产生许多棘手的问题。

第一个要点是微服务的数量。如果一个系统内的微服务数目只有几百个,那么绘制一张囊括所有微服务的调用图是有利于管理的;但如果超过了 1000 个,再把它们塞到一张图后整张图变得不可读,它的意义就不大了。

第二点,如果一个微服务的实例数只有几十个,这时实例的管理是比较简单的,如果实例数超过 300,那么团队不可避免地会需要使用一些分片策略或是长连接策略,它们都会带来一些特殊问题。

第三点是单个 API 涉及的微服务数量。如果 API 需要普遍涉及 10 个以上的服务,这时监控会面临更大的挑战。以字节跳动的场景为例,目前字节跳动内网的在线微服务数量在万级,其中最大的微服务大约有 1-2 万个实例,而单个 API 也普遍在后端关联了几十个甚至上百个微服务。面对这样的复杂度,有三个问题最为突出:

一是难以做容量预估。微服务已经达到了一定的复杂度,它们的调用关系是非常复杂的:一个核心服务的依赖链可能就有几百个,对每个依赖方做调研或去细致地跟进每个限流策略显然非常困难。另外,不同业务会通过不同活动实现业务增长,对核心服务来说,追溯每个业务的增长也是一个非常艰巨的任务。

二是会大幅提高服务治理难度。这里的服务治理包含限流、ACL 白名单、超时配置等,因为调用关系变得复杂,每个服务可能会调用几十个甚至上百个依赖服务,一些核心服务也会被几百个服务所依赖,这时如何梳理这些调用关系、配置多少限流、配置怎样的白名单策略,就成了团队需要深度探讨的问题。

三是容灾复杂度增大。在复杂的调用关系下,每个 API 会依赖大量的微服务,而每一个微服务都有一定概率产生故障。我们需要区分强依赖和弱依赖,并辅以特定的降级策略,才能够在不稳定的服务环境下获得尽可能稳定的对外效果。

业界尝试


那么对于这些复杂的治理难题,业界会有怎样的尝试呢?

第一种方式是鸵鸟心态。完全不做工作,这反而是业界最广泛的尝试。相信很多企业并不是没有受到超大规模调用网的侵扰,也不是没有对其做一些尝试,而是解决问题所产生的成本和损失实在是难以量化。

举个例子,一个核心服务有很多依赖方,其中一个依赖方的代码中存在严重的重试漏洞,瞬间产生大量重试把核心服务给压垮了,最终造成了系统级的灾难。这时我们可以去追溯问题的直接原因——代码质量问题,至于隔离没做好、超复杂调用关系没有梳理清楚等,这些会被归结为间接原因,往往可以不被追究。

第二种方式是精细化的监测与限流。业内一些开源组件在功能上确实做得比较出色。如左图是一个知名开源组件,它会对整个服务链路进行精细化监控。在这个示例里,每个三角形是一个 Gateway,中空圆形才真正的服务。它展示了从流量入口到每个微服务的整个链路,如果链路是绿色的,说明流量是健康的;链路是红色的,就说明流量存在异常。有了这样详细的拓扑图,开发者就可以看清它的依赖关系。

0ebb89906206338cf6a76af0a06d3da7.png

这看起来很美好,所以大概在两年前,我选取了一个中等规模的业务线,把所有依赖关系梳理出来,得到了上图中右侧这张图。里面每一个代号都是一个服务,每一条线都是这个服务的依赖关系——这实在是太复杂了。左图由于只有 4 个服务,整体比较清晰,但如果是几百个服务相互交织、相互依赖,用这种图来进行测算无疑是不可行的。

第三种方式是单元化,或称 SET 化,比较有代表性的是蚂蚁和美团。他们采用的主要方式是把每一个服务部署多份:set 1、set 2、set 3,流量通过单一的 shard key 进行 set 的选择。这样,set 之间就可以进行有效的资源隔离,在单个 set 产生问题时可以通过切流的方式容灾。

但它也有三方面的局限性。第一方面,SET 化需要有合适的分片键,如用地域或账号去切分,这需要和业务属性有匹配,并不是所有的业务都能找到这种合适的分片键。第二方面,这种方式需要的非全局数据比较多,譬如本地生活订单,用户在北京下单酒店的数据没必要经过深圳。但在抖音、今日头条这些综合信息服务场景中,非全局数据非常少,那些看似本地的数据如用户名、用户的粉丝数、近期的点赞列表,其实也是全局数据。最后一个方面,SET 化需要冗余,需要备份成本,大体量的公司不一定能够支撑。

第四种方式是 DOMA。它的英文全称是 Domain-Oriented Microservice Architecture。2020 年,Uber 提出了这个架构。下图是一个简单示例,其中绿色是 public interface,红色的是 private interface。如果有流量想访问域内的一个微服务,它必须要经过 Gateway Service 进行转发,然后才能访问。

20b90556ae03df2dba15145c477e6dbb.png

如果用户想要在域外访问这个数据库,我们需要通过左下角的 Query、ETL 把它转化成一个离线数据库。整个大框是一个 domain,它不同于 DDD 的 domain,它被称为服务域,可以理解成是一组服务的集合。字节跳动内部也参考了这种 domain 的思想,把一些服务聚合起来,产生特殊的化学反应。

但 DOMA 架构也存在一些问题,比如它过了一层 Gateway Service。我们在外层其实已经有一个从外网到内网的 Gateway,如果内网再放置过多 Gateway(尤其是中心化的),肯定会带来额外的性能消耗,并造成一定的延迟上涨,这也是字节跳动没有采取这种方式的原因。

字节跳动的探索和实践


对于超复杂调用网,字节跳动探索出了一些最佳实践,其中第一个核心叫做服务分层原则。

正如前文的微服务架构图所示,服务在经历从上到下的调用后出现了很复杂的调用关系,对此,我们可以依据康威定律对它做一些横向切分,对调用关系进行分层。

fdfb629134da0fdc0fc23561e41a09f7.png

康威定律是马尔文·康威于 1967 年提出的,指的是设计系统的架构受制于产生这些设计组织的沟通结构。举个例子,假设某家公司内部有四个团队,如上图所示,左侧团队和上方团队沟通较密切,上方团队和下方团队沟通较少,把这种关系映射到微服务架构中后也是类似的,上方微服务和左侧微服务的通信耦合性会大一些,和下方微服务的联系就会弱一些。

我们之前讨论过一个悖论:为什么企业的组织架构非常清晰,但是微服务设计就非常复杂?最终得出的结论是没有做好映射。字节跳动内部有很多团队分别负责业务、中台、基础架构等技术领域,在真实的微服务架构下,我们应该把它清晰地切分成不同层次。

如下图所示,首先是网关层。外网到内网之间需要有一个 Gateway 来处理一些基本事项,如参数基础校验、session 机制、协议转换等。

fbc8cd84463e5db22b4d55b7afd95337.png

第二层是 BFF 层。BFF 是近几年日趋流行的一个概念,全称是 Backend For Frontend(服务于前端的后端)。如过一个接口的对外主体业务逻辑是一致的,但在 iOS、Android、Web 等不同客户端的可能有一些细微差别,那么这些差别可以放在 BFF 层处理。

第三层是业务层。字节跳动有很多业务,如短视频、资讯、游戏、公益等,与特异业务功能直接相关的功能应当由这一层来实现。

第四层是中台层,这一层应用了 DDD 的思想,我们抽取了一些通用的特殊能力,对它们进行专业化的建模和封装,以实现大量基础能力的复用。

第五层是数据服务层,通过合理的封装,用户无需直接访问数据库的表即可更方便、更安全地使用数据。

最后一层是基础架构层,这层主要提供基础架构领域的各种能力,比如微服务基础组件、微服务基础依赖以及数据库或是消息队列等。

字节跳动之所以可以快速孵化新产品,业务层和中台层的建设是一个重要原因。比如新做一个教育应用,我们可以直接调用成熟的账号系统、支付系统、直播模块等,也可以通过向学员推送他可能感兴趣的视频,将他们转化成付费会员。由于存在这类专业领域的建模,在对微服务进行归类处理时,分层变得尤为重要。

这里有几个指导思想供大家参考:首先是分层原则需要结合业务灵活调整,DDD 只是一种指导思想,不能按照它的每一条规范去做;其次是在分层原则中,建议从上到下去进行访问,业务层的请求可以访问数据服务层,但数据服务层的请求不能访问中台层,逆向访问可能会产生循环依赖等严重问题;第三,对于调用关系异常复杂的业务层、中台层,我们给出了一种点线面结合的方法

  • 点:流量身份标记注入点

  • 线 1:流量身份标记沿调用链透传

  • 面:紧耦合的服务聚合为服务域

  • 线 2:部署和流量按域切分

b0e6a60e908760bd282bfd94c397005d.png

点在字节跳动内部被称为流量身份标记 TIM(Traffic Identity Mark)。流量从客户端进来后,我们会在 Gateway 层对 request 的各种参数进行检测,验证之后,一些需要在链路中传递的核心参数会被记录下来,供后续分流、核心服务调用使用。

这种做法有助于一些特殊链路数据保护策略的实现,如未成年人数据保护。未成年人发出的请求从一开始就带有相关参数,随着调用链向下传递,通过透传机制,核心的中台层和数据服务层依然能读到这些信息,并执行特殊的逻辑,以便对未成年人做好保护。

0af2b4ad62c875065e8cb8b3f4180059.png

有了点之后,如果想在下游核心业务中使用这些关键信息,就必须要求信息会向下透传。举个例子,假设抖音的一个请求带有流量身份标记 TIM1,那么该流量触达下游服务时仍应携带标记 TIM1;如果流量来自西瓜视频且携带了 TIM2,那么由这个请求触发下一个在线请求时,它也一定要携带这个 TIM2。这使得整个调用链可以完成串联,类似 Log ID、Trace ID。

所以这个地方有两个依赖,我们最好把 TIM 放在 Header 中,让它能更好地传递信息,并且使下游服务在不解析它的请求 Body 时,就能拿到 Header 中的信息来做流量调度等操作。在一个微服务内部,我们要通过 Context 机制,把入流量和出流量结合起来,把真正的标记传递过去,形成链路。


《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
合起来,把真正的标记传递过去,形成链路。

[外链图片转存中…(img-YeNlqXj3-1715413100897)]

[外链图片转存中…(img-1Nrm9YWN-1715413100898)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 25
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值