多运行时微服务架构

服务化演进中的问题

自从数年前微服务的概念被提出,到现在基本成了技术架构的标配。微服务的场景下衍生出了对分布式能力的大量需求:各服务之间需要相互协作和通信,以及共享状态等等,因此就有了各种中间件来为业务服务提供这种分布式能力。

我们熟知的”Spring Cloud 全家桶“正是凭借着对各种中间件优秀的集成与抽象能力,成为了当时炙手可热的项目。然而随着业务的快速发展,组织规模的不断扩大,微服务越来越多,系统规模越来越大则是服务化体系架构演进的必然。这就带来了两方面复杂度的上升:

  • 服务治理与接入的复杂度: 服务治理代表了系统中服务资源的地图及其获取途径,例如通过注册发现服务提供图谱能力,路由、网关、负载均衡服务提供获取途径。服务接入则代表了如何使用系统中的服务能力,例如通过中间件提供的API 协议或是封装的 SDK 来接入该中间件。各种业务服务越多、中间件越复杂,整个系统服务治理与接入的复杂度就会急剧上升。
  • 团队协作的复杂度: 该复杂度主要体现在团队的认知负载上,复杂的依赖、沟通、协作将明显拖慢交付进度。正如康威定律所述的,由于服务复杂度的上升,团队之间的交互成本也随之上升。 当系统中的中间件都通过 SDK 作为其外化能力的控制方式,来封装协议、数据结构与操作方法。随着中间件数量和种类不断增多,大量孤立的 SDK 被绑定在业务服务上,导致两方面问题:
  1. 版本升级困难:SDK 与业务服务的强依赖性导致想要升级 SDK 版本变得异常复杂与缓慢
  2. 业务服务难以异构:SDK 所支持的语言反向限制了业务服务所能选择的语言,例如 Spring Cloud 几乎没有官方的多语言支持

如何治理这种不断上升的复杂度呢?复杂问题归一化是一种不错的手段。

什么是多运行时微服务架构?

多运行时微服务架构(Multi-Runtime Microservice Architecture)也被简称为多运行时架构,是由 Red Hat 的首席架构师 Bilgin Ibryam 在 2020 年初所提出的一种微服务架构形态,它相对完整地从理论和方法的角度阐述了多运行时架构的模型(实际上,在 2019 年末,微软的 Dapr v0.1.0 就已经发布)。Bilgin Ibryam 梳理了分布式应用的各类需求后,将其划分到了四个领域内:

(来源:Multi Runtime Microservices Architecture

  • 生命周期:即应用从开发态到运行态之间进行打包、部署、扩缩容等需求。
  • 网络:分布式系统中各应用之间的服务发现、容错、灵活的发布模式、流量管理、跟踪和遥测等需求。
  • 状态:我们期望服务是无状态的,但业务本身一定需要有状态,因此包含对缓存、编排调度、幂等、事务等需求。
  • 绑定:与外部服务之间进行集成可能面临的交互适配、协议转换等需求。

Bilgin Ibryam 认为,应用之间对分布式能力的需求,无外乎这四大类。且在 Kubernetes 成为云原生场景下运行时的事实标准后,对生命周期这部分的需求已经基本被覆盖到了。因此实际上我们更关注的是如何归一化其他三种需求。

与单机应用的类比

单机应用一般大都是以用户态进程的形式运行在操作系统上。显然,与微服务类似,单机应用的核心关注点也是业务逻辑,与业务关系不大的支撑能力,都要依赖操作系统来完成。因此上述由 Bilgin 归纳的分布式应用四大类需求,其实我们很容易就可以和单机应用进行合理的类比:

支撑能力 单机应用 分布式应用
生命周期 用户态进程 Kubernetes
网络 网络协议、域名解析、路由服务 服务发现/注册、负载均衡、流量管理
状态 文件系统 数据库、对象存储、块存储
绑定 标准库、系统调用 事件分发、分布式事务、消息路由

从上述类比来看我们发现,单单是 Kubernetes 可能还不足以称为是 “云原生操作系统”,除非有一种解决方案,能在分布式环境下,把其他几项支撑能力也进行归一化整合。

Service Mesh

Service Mesh 在近几年的高速发展,让我们认识到网络相关的需求是如何被归一化并与业务本身解耦的:
通过流量控制能力实现多变的发布模式以及对服务韧性的灵活配置,通过安全能力实现的开箱即用的 mTLS 双向认证来构建零信任网络,通过可观察性能力实现的网络层Metrics,LoggingTracing 的无侵入式采集。而上述服务治理能力,全部被代理到 Sidecar 进程中完成。这就实现了 codebase level 的解耦,网络相关的分布式能力完全抛弃 SDK

伴随着 Service Mesh 的成功,我们不禁会想到,是否可以将另外的两种需求——状态和绑定 ——也进行 Mesh 化改造呢?

分布式能力 Mesh 化

基于对 Service Mesh 的拓展,我们大可以将其他的能力也进行 Mesh 化,每一类能力都以 Sidecar 的形式部署和运作:

在业界也有不少从某些能力角度切入的方案:istio(networking)、dapr(state,binding)、kubernetes(lifecycle)。我们可以发现,各类方案都有自己的一套对某些能力需求的 Mesh 化方案,合理地选择它们,的确满足了分布式能力 Mesh 化的要求,但却引入了新的问题:

  • 复杂度从业务服务下沉到了 Mesh 层:多种 Mesh 化方案之间缺乏一致性,导致选型和运维的成本很高
  • 多个 Sidecar 进程会带来不小的资源开销,很多解决方案还需要搭配控制面进程,资源消耗难以忽视

对业务复杂度上升的归一化,现在变成了对 Mesh 复杂度上升的归一化。

Multi-Runtime = Micrologic + Mecha

Bilgin Ibryam 在多运行时微服务架构中,对前述讨论的各种问题点进行了整合,提出了 Micrologic + Mecha 的架构形态:

Micrologic 中只包含业务逻辑,尽可能的把分布式系统层面的需求剥离出去,放到 Mecha 中。从 Mecha 的命名就可以明白它的功能:由提供各种分布式能力的 “机甲” 组成的 Sidecar 进程,与 “裸奔的” 业务逻辑一起部署。因为是 Micrologic 进程和 Mecha 进程共同部署的这种多个 “运行时” 的架构,所以称之为 “多运行时架构”。Mecha 不仅成功地将分布式能力从耦合的业务进程中抽取出来,还整合了方案,避免了多种方案混合的额外成本。可以说 Mecha 在本质上提供了一个分布式能力抽象层。

因此与其叫 “多运行时架构”,不如叫 “面向能力的架构”。

微软的尝试:Dapr

Dapr 是微软主导开发并开源的一种 Mecha runtime,从宏观上看它处在整个架构的中间层:

自上而下分别是业务层、Dapr Runtime层、基础设施层。Dapr 通过 HttpgRPC API 向业务层提供分布式能力抽象,通过称为 “Component” 的接口定义,实现对具体基础设施的插件式管理。

云和边缘的微服务构建块(Building Blocks)

作为一个合格的 Mecha,最关键的就是如何定义分布式能力抽象层。如何把各类中间件提供的分布式能力定义清楚是一项挑战。Dapr 中定义的分布式能力抽象层,称为 Building Blocks。顾名思义,就是一系列的 “构建块”,每一个块定义了一种分布式能力。

其中有一些 Blocks 的能力由 Dapr 自己就能实现,有一些则需要由实际的基础设施或中间件来实现。选取几个典型举例说明:

  • Service-to-service Invocation:提供服务间调用的能力,其中也隐含了服务的注册与发现。该 Block 的能力由 Dapr 直接实现。
  • State management:提供状态管理能力,最简单的就是存取状态。该 Block 需要其他基础设施通过 Component 的形式实现,例如定义一个 Redis Component
  • Publish and subscribe:提供消息发布和订阅的能力,这是非常典型的一种分布式能力。也需要通过基础设施来实现,如定义一个 Kafka Component
Dapr 的限制与挑战

Dapr 期望通过定义一个能容纳所有需求的分布式能力抽象层,来彻底解放业务逻辑。从归一化的角度看,不得不说这是一种大胆而富有野心的尝试,理想条件下的确能非常优雅地解决问题。但现实总是充斥着各种跳脱出理想的情况,Dapr 在推广的过程中遇到了很多限制与挑战。

与 Service Mesh 整合

作为面向开发侧提供的能力抽象层,Dapr 在网络能力上包含了 mTLS、ObservabilityResiliency(即超时重试熔断等),但并没有包含诸如负载均衡、动态切换、金丝雀发布等运维侧的流量管理能力。

因此对于不断走向成熟的业务系统,可能既要 Service Mesh 在运维侧的流量管理能力,又要 Dapr 在开发侧的分布式抽象能力,不管谁先谁后,都将面临一个问题:怎样搭配使用它们才是正确的?某些场景下可以做适配,如:

  • 对于 distributed tracing 的能力,如果采用 Service Mesh 来实现,则需要考虑将原本 Dapr 直连的中间件也加入 mesh 网络,否则会 trace 不到。但从 distributed tracing本身功能角度讲,更应该使用 Dapr
  • mTLS 应该只在 Dapr 或者 Service Mesh 中开启,而不应该都开启。

DaprService Mesh 配合使用中难以避免的是开销的问题,包括资源开销和性能开销。每个应用 Pod 携带两种 sidecar,再加上 DaprService Mesh 自己的控制面应用(高可用方案主备或多副本),这些资源开销是无法忽略,甚至是非常可观的。而由于 Service Mesh 网络代理的流量劫持,网络调用需要先经过 Dapr sidecar,再经过网络代理 sidecar,被代理两次,也会造成一定的性能开销。

简单计算一下就会发现,当拥有 1000 个业务实例时,dapr + istioSidecar 进程可能会消耗 800+ vCPU60+ GiB 内存。随着分布式能力抽象层的不断扩展,到底哪些属于开发侧,哪些属于运维侧,也许不会像现在这样泾渭分明了。因此已经有对 Multi-RuntimeService Mesh 能力边界越来越模糊的讨论。

Sidecarless?

从上一节的表格我们发现,资源消耗以及性能的问题其实不只是 Dapr 下的场景,实际上它是 sidecar 模式自有的限制,因此在 Service Mesh 领域的讨论中,已经有提出 Sidecarless 的概念了,即通过 DaemonSet 而不是 Sidecar 的形式来部署网络代理。

对于网络代理的 Sidecarless 化,支持方认为它能带来高性能、低资源消耗的优点,而反对方则认为它会导致安全性与隔离性差、故障的爆炸半径过大等缺点。那么,Mecha 是否也可能会走向 Sidecarless 呢?

就像今年 Cilium 发布支持 Service Mesh 能力的办法,通过 eBPF 在内核态实现 L3 L4 层能力,而对应的 L7 层能力则交给用户态的 Envoy 处理这种将问题一分为二的思想,也许多运行时架构的未来方案也可能是折中或是多种方式结合的。例如采用在 Node 上按 Service AccountNamespace 运行多实例,或是轻量级 Sidecar 做协议转换+DaemonSet 做流量管理和网络调用。当然 DaemonSet 也有其固有的缺陷,资源被共享从而降低消耗的同时,故障也被共享了,而且故障产生的伤害面也变大了,此外还会导致 DaemonSet 被应用使用的争抢问题,以及应用之间的数据暴露风险。到底后续将会如何演进,我们拭目以待。

定义抽象能力的(API)的困境

分布式能力抽象层,是对分布式场景下需求的抽象性定义,抽象作为一种共识,其要义就在于保留共性而排除个性。但实际当中会发现,同类型中间件的差异化恰恰体现在了一些高级的、细分的专有特性上,很多业务对中间件选型的原因也在于这些专有特性上。这就引出了一个困境:抽象能力所覆盖的需求,其丰富程度与可移植性成反比。

就如上图所示,如果抽象能力范围只覆盖到红色的部分,则组件 ABC 的专有特性都无法被引入,而如果抽象能力范围覆盖到绿色,那么就无法迁移到组件C。定义抽象能力的困境,本质上是一种对能力收敛的权衡,这种权衡可能是与具体的业务需要高度相关的。然而,在企业实际的场景下,这个“全集”的规模可能并不一定像我们想象的那么庞大,因此就有可能提供额外的一种思路,即对分布是抽象层进行扩展,将有限规模的“个性”全部包含进去,形成 “并集” 从而规避上述问题。

蚂蚁金服的方案:layotto

蚂蚁金服作为 Dapr 的早起使用者,在落地的过程中结合遇到的问题及业务思考,在 2021 年年中推出了自研的 Mecha 方案:layotto

由于 layotto 在运行态上是与 MOSN 绑定在一个 Sidecar 内的,因此就减少了一部分前文提到的两个 Sidecar 之间通信的开销。当然 layotto 可以这样做也有一部分原因在于 MOSN 本身已经在蚂蚁内部大规模落地,同时蚂蚁也有足够的研发强度来支撑 layotto 的开发。

“私有协议”与“可信协议”

Layotto 的开发者,在讨论多运行时架构以及 layotto 落地实践的文章中,尝试对可移植性的概念进行了扩展,将支撑分布式能力的协议划分为“可信协议”与“私有协议”。其中,可信协议指代的是一类影响力很大的协议如 Redis 协议、S3 协议、SQL 标准等。这一类协议由于用户众多,且被各类云厂商所支持,因此可以认为它们本身就具有可移植性。私有协议则指代一些企业内部自研的、闭源或影响力小的开源软件提供的协议。显然这一类协议才更需要考虑抽象与可移植性。因此实际上的所谓分布式能力抽象层可能会是如下图所示的样子:

各类可信协议不再二次抽象,而是直接支持,对其余的私有协议再进行抽象。这种直接支持开源协议的思路,部分缓解了定义抽象能力的困境问题。

灵活的扩展模型

前文提到的 API 扩展形成 “并集”,Layotto 通过提供 In-Tree 形式的私有 API 注册点,实现了不修改 Layotto 代码就能扩展 API 能力:

从代码角度看,Layotto 是通过暴露 API 注册钩子、启动入口,来允许用户自行扩展代码,之后再调用启动函数启动进程。这样扩展 API 代码与 Layotto package 级隔离,但编译后可形成同一个二进制文件。另外,通过 MOSNWASM 插件能力,Layotto 也支持通过 WASM 镜像来扩展 API Filter

未来展望

虽然多运行时架构这种理念从提出到现在只有两年,但已经很少有人会否认它所带来的价值,不论是 Dapr 还是 layotto 的快速发展,都明确了头部企业对这一领域的投资逻辑。

当然目前从理论到实践可能都不够成熟,大家在落地实践的过程中也都会或多或少遇到前文提到的一些局限。但这些局限所处的层次大都是工程化、技术选择等具体的问题,相信随着各方技术的不断整合,实践的不断完善,问题都能解决。对多运行时架构实践的未来,结合当下的限制、挑战以及趋势,我们也许能勾勒出某种未来可能的架构形态:

在这一架构形态下:

  • 分布式能力抽象层提供标准能力抽象,以及灵活扩展的私有协议的能力
  • 既成标准协议(对前文 “可信协议” 的另一种提法)作为 “既成的” 抽象能力,在Mecha 层只做协议转换或直接透传
  • Mecha 与网络代理层进程级耦合,各类特性不再明确区分开发侧与运维侧
  • 进程在 Node 上按租户/namespace 以及高可用要求划分多实例
  • 接入现代化的可观测性体系,提升对故障的洞察分析能力,降低由于架构分层带来的问题诊断困难

总之,不管是架构形态怎么变、能力怎么抽象,让业务逻辑不断内聚,越来越面向接口、面向能力编程的趋势不会改变,服务化体系的未来值得期待。