云原生运行时的下一个五年


零、引言

要说过去几年技术行业的 buzzword,那非『云原生』莫属,随着 Docker、Kubernetes 等技术的兴起,云原生时代的操作系统概念也呼之欲出。Kubernetes 在很好地解决了容器调度问题的同时也屏蔽了云厂商在资源层面的差异,就好比操作系统很好地解决了进程调度问题的同时也屏蔽了硬件设备的差异一样。而 Service Mesh 技术就可以类比是 TCP/IP,从而开发人员在编写应用程序时无需再关注服务之间的服务发现、服务路由、负载均衡、熔断、监控等逻辑,可以专注于业务实现。

过去两年多时间,蚂蚁集团也进行了大规模的 Service Mesh 落地实践,初步实现了基础设施和业务应用的解耦,我们也切实感受到了基础设施下沉后无论是对业务团队还是对基础设施团队所带来的研发和运维效率的提升。

然而我们的云原生探索并没有就此止步,在 Service Mesh 基础上,配置、消息乃至存储都在积极进行 Mesh 化改造,从而实现和业务应用的解耦。适逢其时,微软牵头的 dapr 横空出世,它提出了分布式应用运行时的概念,将各种基础设施服务抽象为标准化的接口来实现解耦,这会给微服务的云原生演进带来了什么样的变化?除了演进中的微服务,未来敏捷业务的一个重要方向——函数计算是否能结合云原生运行时实现更好的落地?本文会分享我们在此过程中的实践以及对未来的思考,希望能给大家带来一些启发。

一、从 Service Mesh 到应用运行时

1.1 微服务痛点

传统微服务体系的玩法一般是由基础架构团队提供一个 SDK 给业务应用使用,在 SDK 中会实现各种服务治理的能力。这种方式在一定程度上实现了团队间职责的解耦,然而由于 SDK 和业务应用的代码仍然是在一个进程中运行的,所以耦合度依旧很高,这就带来了一系列的问题,如升级成本高、版本碎片化严重、异构语言治理能力弱等。

在此背景下,Service Mesh 技术从 2018 年起逐渐崭露头角,引起了社区的广泛注意。如图 1 所示,通过 Service Mesh,可以把 SDK 中的大部分能力从应用中剥离出来,拆解为独立进程,以 Sidecar 的模式运行,从而可以让业务更加专注于业务逻辑,而基础架构团队则更加专注于各种通用能力建设,实现独立演进、透明升级,提升整体效率。

service-mesh-architecture

图 1 - Service Mesh 演进架构

1.2 Service Mesh 落地实践总结

蚂蚁在过往深受业务逻辑和基础设施耦合之痛,每年花在在中间件版本升级上就需要耗费数千人日,所以我们很快意识到了 Service Mesh 对蚂蚁微服务体系演进的价值,从 2018 年开始全力投入到这个方向。

考虑到对过往十多年微服务体系的平滑迁移需要大量的研发支持以及我们自身的技术人员储备,所以我们决策用 Go 语言开发了 MOSN 作为数据面,全权负责服务路由、负载均衡、熔断限流等能力的建设,大大加快了 Service Mesh 的落地进度,在半年内就实现了生产上线,2 年完成了核心链路的全面迁移。

目前 Service Mesh 支撑了生产环境数十万容器的日常运行,我们也初步实现了基础设施和业务应用的解耦,服务基础设施的升级能力也从 1 ~ 2 次/年提升为 1 ~ 2 次/月,不仅大大加快了迭代速度,同时节省了全站每年数千人日的升级成本。

1.3 初步泛 Mesh 化探索以及新的挑战

在泛 Mesh 化的大趋势下,我们在很短时间内也完成了 Cache、MQ、Config 等能力的下沉,从而实现了业务应用和基础设施的进一步解耦,享受到了快速迭代、异构治理等红利。

然而,新的挑战出现了。

generic-mesh-exploration

图 2 - 泛 Mesh 化探索

如图 2 所示,由于应用仍然依赖了各个基础设施的轻量 SDK,而每种 SDK 又往往通过私有协议和 Sidecar 交互,所以 SDK 中仍然保留了私有的通信和编解码逻辑,从本质上来说应用还是和基础设施存在较强的绑定,比如如果要把缓存从 Redis 迁移到 Memcache 的话,仍旧需要业务方升级 SDK。所以如何让应用和基础设施彻底解绑,能够无感知跨平台部署是我们面临的第一个挑战。

另一个挑战是虽然大部分的 SDK 能力已经下沉到了 Sidecar 中,但是我们仍然需要为每种语言开发一套 SDK 并实现私有的通信和编解码协议,所以异构语言接入的成本依旧较高,如何进一步降低异构语言的接入门槛是我们面临的第二个挑战。

二、重新定义基础设施边界

2.1 如何看待 dapr

上述挑战的根源其实还是业务应用和基础设施的边界没有定义清楚。适逢其时,微软牵头的 dapr 横空出世,它提出了分布式应用运行时的概念,将各种基础服务抽象为标准化的接口来实现解耦,我们也非常认同这个方向,所以首先对 dapr 进行了调研。

向上来说,dapr 对应用暴露的是一套基于能力设计的 API,应用无需感知底层基础服务的具体实现,通信层面采用了 HTTP 和 gRPC 两种主流协议,避免了繁琐的通信序列化协议的开发。

向下来说,dapr 对接了多款主流的中间件产品,基本覆盖了应用的日常需求,同时开发自定义的插件也比较容易,如果有需要可以很方便地实现。

dapr-architecture

图 3 - dapr 架构

图片来源:https://docs.dapr.io/concepts/overview/

然而当考虑如何在公司内部落地 dapr 时,我们发现 dapr 在产品设计上并不具备 Service Mesh 丰富的流量管控能力(见图 4),而这恰恰是我们在生产重度依赖的核心能力,所以无法简单的将 MOSN 替换为 dapr。另一种方案是使 dapr 和 MOSN 共存,也就是业务 Pod 中存在两个 Sidecar,然而这又会带来运维成本的飙升,可用性也会有所降低。

dapr-vs-service-mesh

图 4 - dapr 和 Service Mesh 的能力异同

图片来源:https://docs.dapr.io/concepts/service-mesh/

权衡再三,我们决定还是把应用运行时和 Service Mesh 两者结合起来,通过一个完整的 Sidecar 进行部署,在确保稳定性、运维成本不变的前提下,最大程度复用现有的各种 Mesh 能力,Layotto 就是在这样的背景下诞生。

2.2 Layotto 架构

layotto-architecture

图 5 - Layotto 架构

如图 5 所示,Layotto 构建在 MOSN 之上,在下层对接了各种基础服务,向上层应用提供了统一的、具备各种分布式能力的标准 API。对于接入 Layotto 的应用来说,开发者不需要再关心底层各种组件的实现差异,只需要关注应用需要什么样的能力,然后通过 gRPC 调用对应能力的 API 即可,这样可以彻底和底层基础服务解绑。

除了 Layotto 本身设计以外,项目还涉及两块标准化建设,首先想要制定一套语义明确、适用场景广泛的 API 并不是一件容易的事情,为此我们跟阿里、dapr 社区进行了合作,希望能够推进 Runtime API 标准化的建设,其次对于 dapr 社区已经实现的各种能力的组件而言,我们的原则是优先复用、其次开发,尽量不把精力浪费在已有的组件上面,重复造轮子。

一旦完成了 Runtime API 的标准化建设,接入 Layotto 的应用就天然具备了可移植性(见图 6),应用可以实现同一份代码在私有云以及各种公有云上部署,并且由于使用的是标准 API,应用也可以随意在 Layotto 和 dapr 之间自由切换。

layotto-portability

图 6 - Layotto 的可移植性

2.3 统一的边界:Layotto 的野望

除了基础服务之外,系统资源(如网络、磁盘等)、资源限制(如 CPU,堆,栈等),对于应用正常运行来说也是必不可少的。

application-runtime-dependency

图 7 - 应用的运行时依赖

以当前 Sidecar 思路的落地情况来看,无论是 dapr,MOSN 还是 Envoy,解决的都是应用到基础服务的问题,而对于系统调用、资源限制等方面都没有涉及,这也就意味着在这些方面无法实现很好的管控,会带来较大的安全隐患。如果能拦截这部分操作,让应用必须通过一个运行时去执行系统调用,那么就可以对执行操作的权限进行二次验证,更好的避免安全问题,这也是安全容器如 Kata 给我们带来的启示。

sidecar-solution

图 8 - Sidecar 的落地思路

Layotto 目前是以应用运行时的形态存在,但我们的目标是重新定义应用和所依赖的资源之间的边界,包括安全、服务、资源三大边界,未来也希望可以演进到应用的“真”运行时这一形态(见图 9),从而应用除了业务逻辑之外无需关注任何其他资源。

target-runtime-solution

图 9 - 真运行时形态

目标虽然已经明确,但是以什么样的形式来完成目标是我们必须要考虑的问题,仍旧以 Sidecar 的模型?还是其它形式?在 Service Mesh 的带动下,大家已经逐渐接受应用和基础设施之间借助 Sidecar 交互带来的收益,但要想继续通过 Sidecar 来对应用和操作系统之间的交互以及可以使用的最大资源进行限制恐怕就没那么简单,经过反复讨论以后,函数的模型进入了我们的视野。

三、未来五年:函数是不是下一站?

3.1 函数能不能成为云原生世界的一等公民?

虚拟机可以看做是对硬件进行了虚拟化,当红的容器则采用了对操作系统进行虚拟化改造的手法,按照这个趋势发展下去,未来可能会演进到更精细粒度的虚拟化,例如多个应用跑在一个进程中(nanoprocess)。

process-vs-nanoprocess

图 10 - process vs nanoprocess

图片来源:https://hacks.mozilla.org/2019/11/announcing-the-bytecode-alliance/

我们预期在 nanoprocess 技术成熟后,FaaS 会是一个很有价值的方向。如图 11 所示,多个函数运行在一个运行时基座上面,归属在一个进程中,在这种模型下函数之间的隔离性势必也是重点考虑的因素,所以使用哪种技术来作为函数实现的载体,让它们之间具有良好的隔离性、移植性、安全性是首要解决的问题,而这些又和当下很火的 WebAssembly 技术非常契合,所以自然也就成为了我们重点考虑的对象。

function-runtime

图 11 - Function 运行时

3.2 风口浪尖上的 WASM

WebAssembly,简称 WASM,是一个二进制指令集,最初是跑在浏览器上来解决 JavaScript 的性能问题,但由于它良好的安全性、高性能以及语言无关等优秀特性,很快人们便开始让它跑在浏览器之外的地方,随着 WASI 定义的出现,只需要一个 WASM 运行时,就可以让 WASM 文件随处执行。

webassembly-introduction

图 12 - WebAssembly 介绍

回到前面的问题,经过一番调研,我们发现 WebAssembly 的以下特性使之非常适合实现 nanoprocess 的场景:

  1. 沙箱机制:WASM 模块以沙箱形式运行,无法直接发起系统调用,例如访问文件系统、发起网络请求等,只有 WASM 运行时明确给予权限后才能经由 ABI(如 WASI)实现对外访问
  2. 内存模型:WASM 模块访问内存也是受限的,只能访问 WASM 运行时分配给它的内存块,所以不会对同一进程中其它的 WASM 模块产生影响

不过目前离大规模推广 WebAssembly 仍然还有一定的距离,例如多语言方面虽然对 C/C++/Rust/AssemblyScript 支持较好,Java/Go/JavaScript 等支持还比较有限、生态方面优雅的打印错误堆栈和 debug 能力还处于早期阶段、运行时方面目前主流的几款 WASM 运行时的能力支持还参差不齐等。不过考虑到它的标准化和厂商支持,相信随着社区的发展,上述问题都会逐步得到解决。

3.3 Layotto 与函数化应用的探索

如果未来把函数作为和当前微服务架构具有同等地位的另一种基础研发模型,我们就需要考虑整个函数模型的生态建设问题,而整个生态的建设其实就是围绕极致的迭代效率来打造,包括但不限于下面几点:

  1. 基础框架
  2. 开发调试

  3. 编译打包
  4. 调度部署

开发调试、编译打包这类生态体系相信随着 WASM 技术的发展会逐步得到解决,所以接下来重点介绍下我们对于未来函数化应用的基础框架和调度部署的思考以及探索。

先来看看基础框架方面,我们知道通过 WASI,WASM 提供了一套访问系统资源的接口。然而,实际情况下,一个应用往往还需要依赖许多其他资源才能正常工作,如调用服务,读取、写入缓存,发送、消费消息等。由于 Layotto 已经抽象了一套标准的 API 并且提供了 API 实现,所以我们就想何不直接将这套 API 暴露为 ABI,从而 WASM 模块可以借此实现对外部基础设施的访问?

如图 13 所示,运行在 Layotto 上的 WASM 模块可以通过 Runtime ABI 轻松实现访问 Layotto 封装的各种能力,如调用服务、读取配置等,为 Function 提供所需要的基础设施访问能力。

layotto-wasm-abi

图 13 - Layotto WASM ABI

解决了基础设施接口访问的问题,再来看下生命周期管理和资源调度方面的探索。

我们知道 K8S 目前已经成为容器管理调度的事实标准,所以我们优先探索的方向就是如何将函数调度融入到 K8S 生态。然而由于 K8S 的调度单位是 Pod,如何优雅的把调度 Pod 桥接到调度 WASM 上面也是一个棘手的问题。在做了一些调研后发现社区已经有一些将 K8S 和 WASM 整合的探索,基本的想法是开发一个 containerd shim 插件,这样当它收到创建容器的请求时,它可以启动一个 WASM 运行时来加载 WASM 模块。

由于我们希望实现 nanoprocess 的架构,所以做了一些改造。如图 14 所示,首先在 node 上启动一个 Layotto 运行时,然后我们自定义了一个 containerd shim layotto 的插件,当它收到创建容器的请求时,它会从镜像中提取 WASM 模块,然后交给 Layotto 运行。

有了这个解决方案,我们可以复用大部分 k8s 的能力,同时也能实现在 Layotto 中以 nanoprocess 形式运行多个 WASM 模块的效果。

layotto-nanoprocess-architecture

图 14 - Layotto nanoprocess 架构

最终使用的整个过程如图 15 所示,对于一个函数来说,首先把它编译成 WASM 模块,然后再构建成镜像,部署过程中只需要指定 runtimeClassName 为 layotto 即可,后续如创建容器、查看容器状态、删除容器等操作都完全兼容 K8S 的原生语义。

目前整套流程已经开源,感兴趣的同学可以参考我们的 QuickStart (https://mosn.io/layotto/#/zh/start/faas/start) 文档进行体验。

layotto-function-scheduling

图 15 - Layotto function 调度方式

四、展望未来

我们坚信随着基础设施边界的统一和函数化场景的广泛应用,业务研发将变得越来越简单。

future-dev-mode

图 16 - 未来研发模式

图 16 即我们设想的未来研发模式:开发人员打开 Cloud IDE,使用自己擅长的语言编写函数,快速完成编译、部署和测试验证,一键发布到多云环境。开发人员不再关心各个云环境之间的差异、也不用关心容量的问题,真正实现弹性、免运维、按需计费。当然,也不是所有的场景都能够函数化,一些复杂的有状态服务仍然会以独立进程的形式存在,这些应用可以通过 Sidecar 形式访问 Layotto 来实现对基础服务的统一访问,而安全性、资源限制等方面则可以借助于安全容器等技术来实现。

这一切目前看来可能还有些遥远,不过技术的发展总会出乎我们所料,相信随着社区的发展和技术的演进,上述愿景很快就能实现,让我们拭目以待!