在过去几年里,蚂蚁集团的基础设施进行了广受瞩目的大规模 Mesh 化改造,成为业内服务网格化的一个标杆,在收获了基础架构团队掌握数据平面带来业务敏捷性的同时,也享受到了将基础设施 SDK 从应用中剥离出来后带来的应用和基础设施双方更高的易维护性。然而服务网格并不是银弹,大规模落地后也面临着新的问题。
适逢其时,微软牵头的 Dapr 横空出世,把分布式应用运行时的概念带入了大众视野,我们也尝试使用这种思路来解决 Mesh 化后遗留的问题,本文通过回顾蚂蚁集团一路从微服务架构到 Service Mesh 再到分布式应用运行时的整个演进历程,结合生产落地过程中遇到的各种问题及思考,尝试探讨一下未来五年,云原生运行时可能的发展方向。
1.从 Service Mesh 到应用运行时
2018 年,蚂蚁集团在 Service Mesh 刚刚开始流行的时候就在这个方向上大力投入,如今已三年有余,SeviceMesh 早已在公司内大规模落地,支持了生产环境数十万容器的日常运行,2019 年下半年,Dapr 项目正式开源并持续火爆,应用运行时的概念引起了人们的关注,蚂蚁集团也踏上了从 Service Mesh 到应用运行时的演进道路。
蚂蚁 Service Mesh 实践的收获与遗留问题
在传统的微服务架构下,基础架构团队一般会为应用提供一个封装了各种服务治理能力的 SDK,这种做法虽然保障了应用的正常运行,但缺点也非常明显,每次基础架构团队迭代一个新功能都需要业务方参与升级才能使用,而遇到 bugfix 版本,往往需要强推业务方升级,这里面的痛苦程度基础架构团队的每一个成员都深有体会。
伴随着升级的困难,随之而来的就是应用使用的 SDK 版本差别非常大,生产环境同时跑着各种版本的 SDK,这种现象又会让新功能的迭代必须考虑各种兼容,时间一长就会让代码维护非常困难,有些祖传逻辑更是不敢改也不敢删。
同时这种“重”SDK 的开发模式,导致异构语言的治理能力始终很难对标主语言,各种保障高可用的能力都无法作用于异构语言应用。
后来有人提出了 Service Mesh 的理念,这种理念旨在把服务治理能力跟业务解耦,让两者通过进程级别的通信方式进行交互。在这种架构下,各种服务治理能力从应用中剥离,运行在独立的进程中,让业务团队跟基础架构团队可以各自进行迭代更新,大幅度提升效率。同时 SDK 因为功能减少而变“轻”则降低了异构语言的接入门槛,让这些语言开发的应用有机会对标主语言的治理能力。
在看到 Service Mesh 理念的巨大潜力之后,蚂蚁集团很快就在这个方向上进行了大力投入,如下图所示,首先是使用 Go 语言自研了可以对标 envoy 的数据面。然后把 RPC 中各种治理能力下沉了到 MOSN 中,这样 RPC 的 SDK 变“轻”,而其他基础设施的 SDK 则依旧维持现状。
在 RPC 能力完成 Mesh 化改造之后,我们进行了快速推广,如今达到了数千个应用,数十万个容器的生产规模,与此同时,全站升级频率最快可以达到 1~2 次 / 月,这跟传统微服务架构下 1~2 次 / 年的升级频率相比达到了一个质的提升。
蚂蚁初步泛 Mesh 化的探索
在 RPC 能力完成 Mesh 化改造并且验证了这种架构的可行性,以及体验到 Mesh 化带来的迭代效率大幅度提升的收益以后,我们正式走上了整个基础设施泛 Mesh 化改造的道路。
如上图所示,在泛 Mesh 化改造的大趋势下,除了 RPC 以外,缓存、消息、配置等一些常用的基础设施能力迅速从应用中剥离,下沉到 MOSN 中,这套架构极大的提高了整个基础架构团队的迭代效率。
正如软件工程没有银弹说的那样,随着泛 Mesh 化落地规模逐渐扩大,我们逐渐意识到它遗留的问题。
如上图所示,在这种架构下,虽然应用跟基础设施之间加了一层网络代理,但对于基础设施协议部分的处理依然保留在 SDK 中,这就导致应用本质上还是要面向某个基础设施做开发,比如想使用 Redis 作为缓存实现,那么应用需要引入 Redis 的 SDK,未来如果想切换到 Memcache 等其他缓存实现,则必须对应用进行改造,除了替换 SDK 之外,甚至还涉及到调用 API 的调整,因此这种架构完全无法满足当前公司面临的同一个应用在多个平台部署的需求。
与上述问题类似,泛 Mesh 化改造后,“轻”SDK 的低开发成本让各种异构语言都有机会接入到整个基础设施体系中来,享受多年基础设施建设的红利。但由于 SDK 里仍然保留了通信、序列化等协议的处理逻辑,因此随着接入的语言越来越多样化,这里依然存在不能忽视的开发成本。
换句话说,泛 Mesh 化改造带来的“轻”SDK 跟传统微服务架构相比虽然降低了异构语言接入基础设施的门槛,但是随着接入语言越来越多样,依赖的中间件能力越来越丰富,我们还需要尝试进一步降低这种门槛。
如果对上述两个问题做一层抽象,本质上都可以归结为应用跟基础设施之间的边界不够清晰,或者说应用中始终嵌入了某种基础设施实现中特有的处理逻辑,导致两者一直耦合在一起,因此如何定义应用跟基础设施之间的边界,让两者彻底解绑是我们当下必须要思考解决的问题。
2. 重新定义基础设施边界
如何看待 Dapr
Dapr 项目由微软牵头,于 2019 年下半年正式开源,作为分布式应用运行时的一种实现方案登上舞台,引起了广泛关注,它向我们展示了如何定义应用跟基础设施之间的边界。
上图是 Dapr 官方提供的架构图,跟 Service Mesh 架构类似,Dapr 采用 Sidecar 的模型部署在应用跟基础设施之间,但与之不同的是,Dapr 基于 HTTP/gRPC 这类标准协议向应用提供了一套语义明确、面向能力的 API,让应用可以不再关心基础设施的实现细节,只需专注业务本身依赖哪些能力即可。
目前 Dapr 已经提供了较为丰富的 API,包括状态管理、发布订阅、服务调用等常见基础设施能力,基本能够覆盖业务日常开发的需求,并且每种能力都对应了多种具体的基础设施实现,开发者可以按需自由切换且这种切换对应用完全透明。
除了能力之外,Dapr 官方也给出了 Dapr 跟 Service Mesh 之间的异同点,如下图所示,两者虽然有一些交集,但本质不同,Service Mesh 强调的是透明的网络代理,它并不关心数据本身,而 Dapr 强调的是提供能力,是真正站在应用的角度来思考如何降低应用的开发成本。
Dapr 本身的优势非常明显,不过 Service Mesh 所提供的丰富的网络治理能力也是保障应用生产稳定性的关键点,与此同时 Dapr 跟基础设施的交互其实也无法离不开网络,因此有没有一种解决方案可以让应用运行时跟 Service Mesh 双剑合璧,降低应用开发成本的同时保留丰富的网络治理能力?
Layotto:ServcieMesh & 应用运行时双剑合璧
Layotto 作为 Dapr 之外的一个应用运行时实现方案,目的就是希望把应用运行时跟 Service Mesh 两者的优势结合起来,因此 Layotto 是建立在 MOSN 之上,分工上希望让 MOSN 来处理网络部分,而自己负责向应用提供各种中间件能力。
此外基于蚂蚁集团内部的生产运维经验,Layotto 还抽象了一套面向 PaaS 的 API,主要目的是希望把应用跟 Layotto 本身的运行状态透出给 PaaS 平台,让 SRE 可以快速了解应用的运行状态,降低日常运维的成本。
API 标准化:跨平台部署利器
对于跟应用交互所使用的这套 API,Layotto 希望在 Dapr 的基础上结合实际生产使用的场景进行扩展改造,同时也会跟阿里、Dapr 一起合作,争取定义一套能力通用,覆盖场景广的标准 API。而一旦完成标准化建设,对于所有基于这套 API 开发的应用来说,它们不仅不需要为适配各种平台之间的差异而烦恼,甚至也可以在 Layotto 跟 Dapr 之间无缝切换,彻底打消商业化用户对产品绑定带来的顾虑。
为什么不在一个 sidecar 里解决所有?
Dapr 项目给我们最大的启示在于它定义了应用跟基础设施之间的边界,但应用需要的不仅仅是这些。
Dapr 为我们提供了很好的思路,是一个好的开端,但还不能够完全覆盖我们想要的东西,我们希望可以完全定义应用跟依赖资源之间的边界,可以覆盖系统资源、基础设施、资源限制等多个环节,成为应用的“真”运行时,应用除了业务逻辑之外无需关注任何其他资源。
以当前 Sidecar 思路的落地情况来看,无论是 Dapr、MOSN 还是 envoy,解决的都是应用到基础设施的问题,而对于系统调用,资源限制等方面仍旧由应用自己完成,这部分操作不需要经过任何中间环节,而没有被接管就意味着很难统一治理,类似网络流量如果没有统一的出入口,治理起来自然会困难重重。同时如果不能对应用可访问的系资源进行精细化控制,那始终会存在安全隐患。
统一的边界:Layotto 的野望
虽然 Layotto 的初始阶段跟 Dapr 类似,是作为应用运行时的形态存在,但一个更大的目标是尝试定义应用跟所有依赖资源之间的边界,我们统称为安全、服务、资源三大边界,未来希望可以演进到应用的“真”运行时这一形态。
定义清楚边界带来的直接收益是可以彻底解放业务的开发者,让他们可以专注于业务本身,现在一名业务开发人员想要上手写代码,不仅要熟悉本身的业务逻辑,还需要熟悉缓存、消息、配置等各种各样基础设施的实现细节,成本非常高,而一旦把边界定义清楚以后,业务开发人员就再也不需要关注这些,这本身会降低业务开发人员的上手门槛,进而降低整体的开发成本。
目标虽然已经明确,但是 Layotto 以什么样的存在形式来达到这个目标是我们首先面临的问题,在 Service Mesh 的推动下,大家已经逐渐接受应用跟基础设施之间借助 Sidecar 交互带来的收益,但要想继续通过 Sidecar 的形式来对应用跟操作系统之间的交互以及应用可使用的最大资源进行限制恐怕就没那么简单了,因此我们迫切需要一种全新的部署模型来达成目标,经过反复讨论以后,函数计算这种研发模式进入我们的视野。
3. 未来五年:函数是不是下一站?
Layotto 与蚂蚁函数化的明天
相信大家对函数计算并不陌生,但函数本身除了作为独立进程运行以外还有没有其他更好的方式可以尝试?为了回答这个问题,我们首先回顾一下虚拟化技术的发展。
如上图所示,以前的虚拟机时代,人们在一套硬件上面独立运行多个操作系统,这种模式可以抽象为对硬件进行了虚拟化,而现在大火的容器技术则是在一个操作系统上通过 namespace、cgroup 等技术手段来运行多个容器,这种模式相比于虚拟机来说,可以看作是对操作系统进行了虚拟化,但因为容器技术采用的是共享内核的方式,因此在安全方面一直被人诟病,这也是 Kata 之类的安全容器诞生的一个背景。
此外,在社区中还有一种 Unikernel 的技术在发展,它的一个主体思路是应用可以独占内核,但这部分内核不是一个完整的操作系统,而是仅仅包含了应用运行所需要的部分,在应用开发完成后会跟内核一起编译成镜像直接在硬件上面运行。
之所以会有多种虚拟化方案,其实是因为对不同的资源进行虚拟化带来的收益不相同,比如容器跟虚拟机相比就有更快的启动速度以及更高的资源利用率。对比上述三种技术,我们可以得出技术人员一直尝试在隔离性、安全性、轻量化三个方向上寻找一个平衡点,希望可以最大程度的整合它们各自的优势。因此我们期望的函数模型也能够整合这三者各自的优势。
跳过,再跳过,函数能不能成为云原生时代的一等公民?
最终我们期望的函数模型如下图所示:
向上来说:
函数本身可以使用任何语言进行开发,这样才能更好的满足越来越多样的业务诉求。
多个函数运行在一个运行时基座上面,它们都跑在一个进程里,在这种模型下函数之间的隔离性势必也是重点考虑的因素。
向下来说:
函数运行过程中不能直接访问下层资源,必须借助基座发起请求,包括系统调用、基础设施等。
运行时基座可以对函数运行过程中可使用的资源进行精细化的控制,保证按需使用。
为了实现上述目标,我们需要找到一种技术来作为函数的载体,让一个进程中的不同函数之间具有良好的隔离性,移植性,安全性。正因为如此,当下越来越火的 WebAssembly 技术成为了我们重点考虑的对象。
风口浪尖上的 WebAssembly
WebAssembly,简称为 Wasm,虽然最初定位是希望让服务端编程语言运行在浏览器中来解决 JavaScript 的性能问题,但由于这项技术具备了各种优秀的特性,因此人们迫切的希望它可以在浏览器之外的环境中运行,类似于 Node.js 可以让 JavaScript 跑在服务器上一样,WebAssembly 社区也提供了多种运行时来支持在服务器上运行 *.wasm 文件。
WebAssembly 作为当前一项炙手可热的技术宠儿,与生俱来就有其他技术不可能替代的优势:
1.语言无关,跨平台
WebAssembly 作为一套指令集,理论上可以支持从任意语言编译过来,同时在设计之初就把在不同 CPU 架构上运行作为基本目标。
2.安全性,体积小
wasm 模块在运行时可以执行的系统调用、可访问的磁盘文件都是需要宿主明确授权才行,这带来了良好的安全性。
编译成的 wasm 文件本身体积很少,这就带来了更快的传输、加载速度。
3.沙箱执行环境
多个 wasm 模块都运行在自己的沙箱环境中,它们之间具有良好的隔离性,互不影响。
这项技术虽然有巨大的发展潜力,不过就目前而言,对于实际在后端生产环境落地来说还有很多的不足:
1.多语言的支持程度
WebAssembly 目标是可以支持从各种语言编译得到,但就目前来说各种主流语言对它的支持程度大不相同,支持的比较好是 C/C++/Rust 等编译语言,可惜这些语言对于开发普通的业务逻辑来说,上手成本是个很大问题。而对于业务场景中主流的 Java、Go 来说,它们对 WebAssembly 的支持程度非常有限,并不足以支撑这项技术在生产环境中落地。
2.生态建设
在实际生产环境中,定位线上问题是我们日常都会面对的,Java 有自带的各种命令及 arthas 等三方工具,Go 的 pprof 也是很优秀的性能分析利器,但运行中的 Wasm 如何进行排查,比如优雅的打印错误堆栈或者 debug 目前还处于早期阶段。
3.各种运行时能力参差不齐
正如前面“统一边界”中提到的,运行中的函数需要让它进行安全的系统调用,并且限制可以使用的最大资源,目前几款主流的 Wasm 运行时对这些能力的支持层次不齐,有的只支持部分功能,这些都是在真实的生产场景落地之前必须要解决的问题。
虽然 WebAssembly 在大火的同时也有很多不足,但随着社区的发展,相信上述提到问题都会逐步解决,重要的是我们相信这项技术的前景,我们也会参与到整个 WebAssembly 社区的推广建设中。
Layotto 与蚂蚁函数化应用的明天
如果未来以函数作为跟当前微服务架构具有同等地位的另一种基础研发模型,我们就需要考虑整个函数模式的生态建设问题,而这个建设其实就是围绕极致的迭代效率来打造,包括但不限于下面几点:
1.基础框架
得益于 WebAssembly 这项技术的支持,函数本身是可以使用多种主流语言进行开发,但为了更好的管理每个函数,仍旧需要让业务同学在开发过程中遵循一定的模板,如函数加载时会执行一个 start() 方法,可以做一些初始化工作,卸载时会执行一个 destroy() 方法,这可以做一些清理工作。
2.开发调试
现在多数的开发调试工作大家仍旧习惯在本地 IDE 进行,但本地开发其实有很多不便之处,比如需要进行各种配置,或者当我们需要协作时,往往需要以投屏的方式让别人参与进来,现在 CloudIDE 日渐成熟,相信随着发展可以更好的解决上述问题。
3.打包部署
现在主流的应用在部署时会打包成 war、jar 或者直接编译成目标操作系统的可执行文件,在函数体系中,应用则是编译成 *.wasm 文件,默认就可以在各种操作系统上面运行。
4.生命周期管理 + 资源调度
现在 Kubernetes 已经成为了容器管理调度的事实标准,函数调度如何融入到 Kubernetes 生态也是我们探索的一大重点。
在运行模型上,如下图所示,Layotto 除了支持 Sidecar 模式之外,借助于 WebAssembly 技术可以让 wasm 形态的函数直接跑在 Layotto 上,这种模式下,函数跟 Layotto 之间的交互是采用本地调用的方式来完成,我们把这一层 API 称为 Runtime ABI,它是由 Runtime API 演变而来,比如函数想要从缓存中查询某个 key,只需调用一个 proxy_get_state 的本地方法即可完成.
关于调度,目前 Kubernetes 已经成为了事实标准,因此需要解决的其实是 wasm 形态的函数如何融入 Kubernetes 的生态。这里我们需要重点考虑两个问题:
1.wasm 跟镜像之间的关系是什么?
Kubernetes 是以镜像为基础来创建 Pod,而函数编译的产物是 wasm 文件,我们需要有把两者融合在一起的妥善方案。
2.如何让 Kubernetes 管理部署 wasm?
Kubernetes 的调度单位是 Pod,如何优雅的把调度 Pod 桥接到调度 wasm 上面并且让多个 wasm 函数运行在一个进程里也是一个棘手的问题,
在调研过社区的一些探索方案之后,我们给出了一套自己的实现方案。
整体来说 Kubernetes 支持开发者基于 Containerd 的 OCI 规范对容器运行时进行扩展,目前基于这套规范已经有了 Kata、gVisor 等知名的安全容器实现方案,因此在 Layotto 中我们也同样采用了基于 Containerd 的扩展实现容器运行时方案。整体方案上有两个关键点:
1.镜像构建阶段
对于编译好的 .wasm 文件,我们把它打在一个镜像里,然后 push 到镜像仓库用于后续调度使用。
2.调度部署阶段
我们自己实现了一个叫做 containerd-shim-layotto-v2 的插件,在 Kubernetes 收到调度 Pod 的请求以后它会把真正的处理逻辑交给 kubelet,然后再经过 Containerd 转交给我们的自定义插件,该插件会从目标镜像中提取出 .wasm 文件让 Layotto 加载运行。目前 Layotto 集成了 wasmer 作为 wasm 的运行时。
整套调度方案最终的使用效果如下图所示,对于一个开发好的函数来说,首先把它编译成 *.wasm 文件,然后再构建成镜像,部署过程中只需要在 yaml 文件中指定 runtimeClassName 为 Layotto 即可。后续如创建容器、查看容器状态、删除容器等操作都保留了 Kubernetes 的语义,这对于 SRE 同学来说完全没有额外的学习成本。
目前整套流程已经开源在 Layotto 社区,感兴趣的同学可以参考我们的 QuickStart 文档进行体验。最后我们来畅想一下未来可能的研发模型。
首先在研发阶段,开发人员可以自由选择适合业务场景的语言编写代码,而对于开发工具来说,除了本地 IDE 以外可能越来越多的人会选择 CloudIDE 来开发,这将很大的提高开发人员的协作效率。
然后是部署阶段,对于一些轻量的业务场景,可能会按照函数模型进行部署,而对于传统的业务,可能会保留 BaaS 模型,同时如果有更高的安全性诉求,一种可行方案是把业务部署在 Kata 之类的安全容器中。最后,随着 Unikernel 技术的成熟,可能会有越来越多的人在这个方向上进行尝试,比如把 Layotto 打在 kernel 中跟应用一起编译部署。
更重要的一点是,不管未来使用哪种模型部署,基于 Layotto 与生俱来的移植性,运维人员可以把应用随意部署在任何平台上,而这种切换对于开发人员来说完全透明。
最后在向用户提供服务的阶段,随着函数服务启动的速度越来越快,可以做到收到请求以后再加载运行函数并且对它们可使用的资源进行严格精确的控制,真正做到按需计费。
4. 开源与共赢
前文中提到的未来研发模式中其实依赖很多技术领域的发展,这些技术的成熟依赖整个技术社区的发展,这也是 Layotto 选择开源的一个重要背景,因此我们跟多个社区都进行了交流,希望一起推动未来研发模型依赖的各项技术走向成熟。
Dapr 社区:API 标准化
Dapr 除了定义应用跟基础设施之间的服务边界以外,它还有一套 Runtime API 被人们广为接受,上图是我们在内部实际落地过程中针对这些 API 提出的各种修正建议,我们希望跟 Dapr 社区、阿里巴巴一起对这套 API 进行标准化建设。
WebAssembly 社区:生态建设
对于 WebAssembly 社区来说,我们会持续关注这项技术的整个生态发展,大体上分为以下几类:
1.多语言支持
前面提到现在对 WebAssembly 支持较好的语言对于开发业务逻辑来说成本较高,因此希望随着社区的发展可以更好的支持如 Java,Go,JS 等常见的业务开发语言。
2.WASM ABI
这里主要是定义 wasm 函数跟 Layotto 之间进行交互所使用的 API。社区中已有了一些尝试,我们希望在此基础上增加 Runtime ABI 的定义,让函数可以更方便的调用基础设施。
3.生态建设
我们希望 WebAssembly 技术具备更好的排查定位问题的能力,更精细的可使用资源控制手段,更多实用的高级特性。
Layotto 社区:微服务 & 函数的探索
Layotto 社区则会聚焦于未来研发模式的探索,主要分为以下两类:
Sidecar 模型
在这种模型下,应用跟 Layotto 之间通过基于 gRPC 协议的 Runtime API 进行交互,这也是当下最容易落地的一种模型。
FaaS 模型
在这种模型下,Layotto 会借助 WebAssembly 技术让多个函数在同一个进程中运行,在此基础上尝试定义函数运行过程中所依赖的安全、服务、资源三大边界。
5. 后记
我们尝试基于现阶段微服务架构理论的发展以及实际生产落地中解决各种问题所积累的宝贵经验来思考云原生运行时的下一个五年会如何发展,但这只是我们目前探索的一个方向,我们也相信存在其他的可能性,但有一点是很明确的,那就是云原生运行时的下一个五年不是等来的,是众多技术人员一起努力探索出来的。
本文整理自蚂蚁集团技术专家马振军在 QCon 全球软件开发大会(上海站)2021 上的演讲《云原生运行时的下一个五年》。
作者简介
马振军,花名“古今”,取自《增广贤文》中“观今宜鉴古,无古不成今”。在基础架构领域耕耘多年,目前在蚂蚁集团中间件团队负责 MOSN、Layotto 等项目的开发工作。对云原生、容器运行时、WebAssembly 等技术领域的发展有浓厚兴趣。
活动推荐
5 月 12-14 日,QCon 全球软件开发大会将落地北京。大会也设置了多个云原生相关的专题,其中「云原生架构变革」专题已邀请到百度主任研发架构师郑然担任出品人,专题将重点关注多云 / 混合云架构管理(研发管理、流量治理、监控告警、成本优化)、多运行时架构、在离线混部、Serverless、云边一体、智能 Operator、亲和性部署等技术的最新进展,点击此处了解更多内容。
评论