当下微服务架构模式仍面临的挑战

微服务架构模式当下已是出于成熟期,按照鸿沟理论,最后一批保守主义者已经大规模采用了,表现为大量政企、央国企的IT应用基于微服务架构模式构建。微服务架构并不是完美的,大多数人都知道微服务架构存在“复杂性”的挑战,但是行业内没有很好的具象化这些挑战、影响以及应对策略。 近期谷歌发表了一篇论文,Towards Modern Development of Cloud Applications,翻译中文是“云应用的现代化开发模式”,文章中很准确的具象化了当前微服务架构范式存在的问题,并给出了一种新的方案。

论文原文翻译

概要

在编写分布式应用程序时,传统的明智做法是将您的应用程序拆分为可以分别拉起的独立服务。

这种方式的用意是好的,但像这样基于微服务的架构经常会适得其反,带来的挑战抵消了架构试图实现的好处。

从根本上说,这是因为微服务将逻辑边界(代码的编写方式)与物理边界(代码的部署方式)混为一谈

在本文中,我们提出了一种不同的编程方法,将两者(代码编写与部署方式)分离以解决这些挑战。 通过我们的方法,开发人员将他们的应用程序编写为逻辑上的单体,将有关如何分发和运行应用程序的决策放到一套自动化运行时 (runtime),并以原子方式部署应用程序

与当前的微服务开发模式相比,我们的原型应用最多可减少延迟 15 倍、成本最多减少了 9 倍。

微服务架构面临的挑战

微服务架构实施,普遍的做法是手动将您的应用程序拆分为可以独立部署的独立微服务。 通过对各种基础设施团队的内部调查,我们发现大多数开发人员出于以下原因之一将他们的应用程序拆分为多个二进制包:

  • (1) 提升性能。单独的二进制包可以独立扩展,从而提高资源利用率。

  • (2) 提升容错能力。一个微服务的崩溃不会导致其他微服务崩溃,从而限制了错误的传播范围。

  • (3) 改进抽象边界。微服务需要清晰明确的 API,并且代码纠缠的可能性会大大降低。

  • (4) 允许灵活的滚动发布。不同的二进制包可以以不同的速率发布,从而导致更敏捷的代码升级。

然而,将应用程序拆分为可独立部署的微服务并非没有挑战,其中一些直接与收益相矛盾。

  • C1:影响性能。序列化数据并通过网络发送数据的开销越来越成为瓶颈。当开发人员过度拆分他们的应用程序时,这些开销也会增加。
  • C2:损害正确性。推断每个微服务的每个已部署版本之间的交互是极具挑战性的。在对八个广泛使用的系统的 100 多个灾难性故障进行的案例研究中,三分之二的故障是由系统的多个版本之间的交互引起的。
  • C3:很难管理。开发人员必须按照自己的发布计划管理不同的二进制包,而不是使用一个二进制文件来构建、测试和部署。如果在本地运行一个应用程序,同时需要执行端到端的集成测试,那可是一个不小的工程。
  • C4:API 冻结。一旦微服务建立了 API,就很难在不破坏使用该 API 的其他服务的情况下进行更改。遗留的 API 不得不长期存在,只能不停的在上面打补丁。
  • C5:降低应用程序的开发速度。当开发活动影响多个微服务的更改时,开发人员无法以原子方式实施和部署更改。

解决方案

在本文中,我们提出了一种不同的编写和部署分布式应用程序的方法,一种解决 C1-C5 问题的方法。我们的编程方法包括三个核心原则:

  • (1) 以模块化的方式编写逻辑上划分为多个组件的单体应用程序。
  • (2) 利用运行时根据执行特征动态自动地将逻辑组件分配给物理进程。
  • (3) 以原子方式部署应用程序,防止应用程序的不同版本交互。

其他解决方案(例如 actor 系统)也尝试提高抽象度。但是,它们无法解决其中一项或多项挑战(第 7 节)。尽管这些挑战和我们的提案是在服务型应用 (serving application) 的背景下讨论的,但我们相信我们的观察和解决方案具有广泛的用途。

编程模型

  1. 组件:我们提案的关键抽象是组件 (component)。组件是一种长期存在的、可复制的计算代理,类似于 actor [2]。每个组件都实现一个接口(interface),与组件交互的唯一方法是调用其接口上的方法。组件可能由不同的操作系统进程托管(可能跨越多台机器)。组件方法调用在必要时变成远程过程调用,但如果调用者和被调用者组件在同一个进程中,则仍然是本地过程调用。

组件如图所示。示例应用程序包含三个组件:A、B 和 C。当部署应用程序时,运行时决定如何共同定位和复制组件。在此示例中,组件 A 和组件 B 位于同一个操作系统进程中,因此它们之间的方法调用作为常规方法调用执行。组件 C 不与任何其他组件位于同一位置,同时组件 C 被部署到了两台不同机器上,组件 C 之间的方法调用是通过跨网络 RPC 完成的

  1. 接口 为了具体起见,我们在 Go 中提供了一个组件 API,尽管我们的想法与语言无关。图-2 给出了一个“Hello, World!” 应用程序。组件接口表示为 Go 接口,组件实现表示为实现这些接口的 Go 结构。在图-2 中, hello 结构嵌入了 **Implements[ Hello] ** 结构来表示它是 Hello 组件的实现。

Init **初始化应用程序。Get[Hello] **将客户端返回给具有接口 Hello 的组件,必要时创建它。对 hello.Greet 的调用看起来像是常规方法调用,开发人员不需要关心任何序列化和远程过程调用相关内容。

运行时

在编程模型之下是一个负责分发(distributing)和执行(executing)组件的运行时。运行时做出关于如何运行组件的所有高级决策。例如,它决定将哪些组件放在一起并进行多副本部署。运行时还负责底层细节,例如将组件运行到物理资源以及在组件失败时重新启动组件。最后,运行时负责执行原子滚动更新,确保一个应用程序版本中的组件永远不会与不同版本中的组件进行通信。

运行时架构分为管控面(DeploymentManagement)与数据面(Application、Execution),他们之间通过一套标准API交互。Envolop作为桥梁连接管控面与数据面,所有的控制数据下发、可观测采集都通过Envelop完成。

评论:可行性有多高?

谷歌对当下微服务架构问题的总结是很到位的,但是给出的方案的可行性有多高?笔者表示持观望态度。

在大约5年前,我们为了解决同样的问题,在项目中有实施过类似方案:开发测试阶段按照微服务粒度实施,在交付阶段合并多个微服务为一个“服务”

这个方案是可行的,我们对每个微服务单独弹性的诉求不强烈。

回头看谷歌提出的方案,方案效果是在开发阶段不感知部署模型,也就是说开发时候可以是一个模块化的单体,运行阶段每个模块可以独立部署到一个进程,且能够根据模块之间调用频度(耦合度)调整部署位置,耦合紧密的亲和部署

这个方案里并没有提到,在开发阶段需要增加什么约束,才能实现其效果。如果没有任何约束,那么效果就是一个大单体内的模块是紧密耦合的,也不具备分离部署的条件。

或许,谷歌论文的架设前提开发模型与部署模型可分离,缺少具体的可实施建议。

Reference

Platform as a Runtime (PaaR) - Beyond Platform Engineering

Towards Modern Development of Cloud Applications

[Google Paper] 面向云时代的应用开发新模式

ServiceWeaver-提案的参考实现

关注公众号获得更多云最佳实践