多运行时微服务架构实践

2020/4/22 11:35:06 资讯频道 1658

创建良好的分布式应用程序并非易事:这样的系统通常会遵循 12 要素应用程序和微服务原则。它们必须是无状态的、可扩展的、可配置的、独立发布的、容器化的、可自动化的,有时甚至是事件驱动的和 serverless。创建之后,它们应该很容易进行升级,并且可以承受长期的维护。用今天的技术在这些互相竞争需求中找到一个良好的平衡点仍然需要付出艰苦的努力。

在本文中,我将探讨分布式平台该如何演化以实现这种平衡,更重要的是,在分布式系统的发展中还需要做些什么才能简化可维护的分布式架构的创建。

1. 分布式应用程序的需求

为了讨论方便起见,我把现代分布式应用程序的需求分为 4 类,分别是生命周期(lifecycle)、网络(networking)、状态(state)和绑定(binding),并简要地分析一下最近几年中它们的发展情况。

?¤?è??è???—???????????????????è·μ

生命周期

让我们从基础开始。当我们编写功能时,编程语言会指定生态系统中的可用库、打包格式和运行时。例如,Java 使用.jar 格式,所有的 Maven 依赖项作为一个生态系统,而将 JVM 作为运行时。如今,随着发布周期变得更短,生命周期中更为重要的是以自动化的方式部署的能力、从错误中恢复的能力和扩展服务的能力。这组能力广泛地代表了应用程序生命周期的需求。

网络

如今,从某种意义上说,几乎所有的应用程序都是分布式应用程序,因此,它们都需要网络。但是,现代分布式系统需要从更广泛的角度去掌握网络。从服务发现和错误恢复,到实现现代软件发布技术和各种跟踪及遥测。为了满足我们的需要,我们甚至会在这个类别中包含不同的消息交换模式、点对点和发布 / 订阅方式,以及智能路由机制。

状态

当我们在讨论状态时,通常是在讨论服务的状态以及为什么无状态是更好的方案。但是,管理我们服务的平台本身需要状态。进行可靠的服务编排和工作流、分布式单例、临时调度(即 cron jobs)、幂等性(idempotency)、状态化的错误恢复、缓存等等都需要它。这里所列出的功能都依赖于底层的状态。尽管实际状态管理不属于本文讨论的范围,但是,我们感兴趣的是分布式原语及其依赖于状态的抽象。

绑定

分布式系统的组件不仅要相互通信,而且要和现代的或遗留的外部系统集成。这需要连接器(connector)能够转换各种协议、支持不同的消息交换模式,如轮询、事件驱动、请求 / 答复、转换消息格式,甚至能够执行自定义的错误恢复程序和安全机制。

排除一次性用例的情况,上面提到的内容代表了创建良好分布式系统通用原语的一个很好的集合。如今,很多平台都提供这类功能,但是,我们在本文中要讲的是,在过去的 10 年中,我们使用这些功能的方式发生了什么变化,以及它在未来 10 年里会怎样变化。为了进行对比,我们来看看过去 10 年中基于 Java 的中间件是如何满足这些需求的。

2. 传统中间件的局限性

在知名的传统解决方案中,有一个方案满足了上述需求的老一代版本,那就是企业服务总线(ESB)及其变体,如面向消息的中间件、轻量级的集成框架等等。ESB 是一个中间件,它支持利用面向服务的架构(即经典的 SOA)实现异构环境之间的互操作性。

尽管 ESB 可以提供良好的功能集,但是,ESB 的主要挑战是单体架构以及业务逻辑和平台之间紧密的技术耦合,这导致了技术和组织的集中化。在这类系统中开发和部署服务时,它和分布性系统框架会紧密耦合,从而限制了服务的演化。在软件生命的后期这才会变成一个明显的问题。

ESB 面对每类需求都有一些问题和局限性,这使得 ESB 在现代已经不再是可行的方案了,详述如下。

生命周期

传统的中间件通常只支持的一个语言运行时,(比如 Java),这就限定了软件该如何打包、哪些库可用、它们打补丁的频率等等。业务服务必须使用这些库,这些库与平台紧密耦合并使用相同的语言编写。在实践中,这会导致服务和平台的同步升级,从而妨碍服务和平台之间实现独立和定期的版本发布。

网络

尽管传统的中间件拥有高级的功能集,这些功能注重与其他内部及外部服务的交互,但是,它有一些重大的缺陷。网络功能集中于一种主要的编程语言及其相关的技术。对于 Java 语言,即 JMS、JDBC、JTA 等等。更重要的是,网络问题和语义也深深地刻入了业务服务中。一些库具有解决网络问题的抽象(如曾经流行一时的 Hystrix 项目),但是,该库的抽象将自身的编程模型、交换模式和错误处理语义及库本身“泄漏”到了服务之中。尽管可以方便地在一个位置编写和读取混合了网络功能的完整业务逻辑,但是,这样会把两个问题紧密地耦合到一个实现中,最终会形成绑定在一起的演化路径。

状态

为了进行可靠的服务编排、业务流程管理和实现模式(如 Saga 模式以及其他运行缓慢的流程),平台需要在后台保持持久状态。类似的,临时操作(如触发定时器和 cron job)都是基于状态构建的,并需要在分布式环境中对数据库进行集群和弹性处理。这里主要的约束是,与状态交互的库和接口没有完全抽象出来,也没有与服务运行时完全解耦。通常,这些库必须配置数据库细节,并且它们位于服务中,从而把语义和依赖项问题泄露到了应用程序域中。

绑定

使用集成中间件的主要驱动因素之一是能够使用不同协议、数据格式和消息交换模式连接各种其他系统。然而,这些连接器必须与应用程序共存的事实意味着,依赖项必须与业务逻辑一起更新和打补丁。这样的话,数据类型和数据格式必须在服务内进行来回地转换。这意味着,必须根据消息交换模式构造代码和设计流程。上述的这几个样例场景说明了在传统中间件中,即使是抽象的端点也会如何影响服务的实现。

3. 云原生的趋势

传统的中间件功能非常强大。它拥有所有必要的技术特性,但是,它缺乏快速改变和扩展的能力,而这是现代数字业务需求所要求的。这是微服务架构及其设计现代分布式应用程序的指导原则正在解决的问题。

微服务背后的思想以及其技术需要推动了容器和 Kubernetes 的普及和广泛使用。这开启了一种新的创新方式,会影响未来几年分布式应用程序开发的方式。我们来看看 Kubernetes 及其相关的技术是如何影响每组需求的。

生命周期

容器和 Kubernetes 把我们打包、分发和部署应用程序的方法演化成与编程语言无关的格式。关于 Kubernetes 模式和 Kubernetes 对程序开发人员的影响,有很多文章,这里我简单说一下。请注意,对 Kubernetes 来说,需要管理的最小原语是容器,并且,它专注于在容器级别和流程模型上交付分布式原语。这意味着,在管理应用程序的生命周期方面、健康检查、恢复、部署和扩展都做得很好,但是,在改进分布式应用程序的其他方面(如灵活的网络、状态管理和绑定)做得并不好,这些分布式应用程序生存于容器内部。

我们可能会指出,Kubernetes 拥有有状态的工作负载、服务发现、cron job 及其他功能。这是事实,但是,所有这些原语都在容器级别,并且在容器内部,开发人员仍然必须使用特定于编程语言的库来获取更细粒度的功能,这些功能我们已经在本文的开头部分列出。这就是驱动 Envoy、Linkerd、Consul、Knative、Dapr、Camel-K 等等项目的原因。

网络

事实证明,由 Kubernetes 提供的围绕服务发现的基本网络功能是一个良好的基础,但是,对于现代应用程序来说还不够。随着微服务数量的增加和部署的加快,无需接触服务就能实现更先进的发布策略、管理安全、指标、追踪、从错误中恢复、模拟错误等功能变得越来越有吸引力,并产生了一种新的软件类别,称为服务网格。

这里有一个更令人兴奋的趋势,那就是将网络相关的关注点从包含业务逻辑的服务转移出来,放到一个单独的运行时中,这可能是边车,也可能是节点级别的代理。今天,服务网格可以进行更高级的路由,有助于测试、处理安全的某些问题,甚至可以使用特定于应用程序的协议(例如,Envoy 支持 Kafka、MongoDB、Redis、MySQL 等)。尽管,服务网格作为一种解决方案,还没有得到广泛的采用,但是,它触及了分布式系统真正的痛点,我相信,它将找到适合自己生存的形式。

除了典型的服务网格外,还有其它项目,如 Skupper,它证实了把网络能力放入外部运行时代理的趋势。Skupper 通过第 7 层虚拟网络解决了多集群通信的难题,并提供了高级路由及连接功能。但是,它在每个 Kubernetes 命名空间运行一个实例,而不是把 Skupper 嵌入业务服务运行时。

总之,容器和 Kubernetes 在应用程序的生命周期管理中向前迈出了重要的一步。服务网格及相关技术触及了真正的痛点,并为把应用程序更多的责任向外转移到代理打下了基础。我们来看看接下来的事。

状态

我们在前面列出了依赖于状态的主要集成原语。状态的管理比较困难,应该把它委派给专门的存储软件和托管服务。这不是本文的主题,但是,使用状态,以语言无关的抽象方式来帮助集成用例才是主题。如今,大家做了很多努力,试图在语言中立的抽象背后面提供状态化的原语。对于基于云的服务来讲,状态化工作流的管理是必备功能,例如 AWS 的 Step 函数、Azure Durable 函数等。在基于容器的部署中,CloudState 和 Dapr 都依赖边车模型以提供分布式应用程序中状态化抽象更好的解耦。

我希望,把前面列出的状态化功能也都抽象到一个独立的运行时。这意味着,工作流管理、单例、幂等、事务管理、cron job 触发器和状态化错误处理都在边车(或主机级别的代理)中可靠地发生,而不是存在于服务中。业务逻辑不需要在应用程序中包括这类依赖项和语义,它可以从绑定环境中声明式地请求这类行为。例如,边车可以充当 cron job 触发器、幂等消费者和工作流管理器,并且自定义业务逻辑可以作为回调调用或在工作流、错误处理、临时调用或唯一幂等请求等的某些阶段插入。

另一个状态化的用例是缓存。无论是服务网格层执行的请求缓存,还是用 Infinispan、Redis、Hazelcast 等进行的数据缓存,有一些把缓存功能推到应用程序运行时之外的示例。

绑定

尽管我们一直在讨论从应用程序运行时解耦所有分布式需求这个主题,但是这种趋势也伴随着绑定。连接器、协议转换、消息转换、错误处理和安全中介都可以移出服务运行时。我们还没到那个阶段,但是,有几个项目在朝这个方向进行尝试,如:Knative 和 Dapr。把所有这些职责移出应用程序运行时将导致更小型的专注于业务逻辑的代码。这类代码将生存于独立于分布式系统需求的运行时中,可以作为预打包的功能使用。

Apache Camel-K 项目采用了另一种有趣的方法。该项目依赖于一个智能 Kubernetes 操作符(Kubernetes Operator,该操作符利用来自 Kubernetes 和 Knative 附加的平台能力构建应用程序运行时),而不是将运行时代理和主应用程序放到一起。在这里,该代理是负责应用程序要求的操作符,其中包括分布式系统原语。区别在于,有些分布式原语被添加到应用程序运行时中,而有些是在平台(也可能包括边车)中实现的。

4. 未来架构的趋势

从广义上看,我们可以总结为,通过把功能移到平台级别,分布式应用程序的商业化达到了新领域。除了生命周期之外,现在我们可以将网络观察、状态抽象、声明性事件以及端点绑定也视为有现成解决方案的领域,而 EIP 则是该列表上的下一个。有趣的是,商业化正在使用进程外模型(边车)进行功能扩展,而不是运行时库或单纯的平台功能(如新 Kubernetes 功能)。

通过把所有传统中间件功能(也即 ESB)移到其他运行时,我们正在接近实现整个循环,不久之后,我们在服务中唯一要做的就是编写业务逻辑。

?¤?è??è???—???????????????????è·μ

与传统 ESB 时代相比,这个架构更好地解耦了业务逻辑和平台,但还没完全解耦。很多分布式原语,如经典的企业集成模式(Enterprise Integration Pattern,简称 EIP): 拆分器、聚合器、过滤器、基于内容的路由器;以及流处理模式(映射、过滤、折叠、联接、合并、滑动窗口)仍然需要包含在业务逻辑运行时中,很多其他应用程序依赖于多个不同的及重叠的平台附加组件中。

如果我们把在不同领域中进行创新的不同云原生项目叠加起来,那么,我们最终会看到这张图,如下所示:

?¤?è??è???—???????????????????è·μ

这里的图只是用于说明,它故意选择了有代表性的项目,并把它们映射为分布式原语的一种。实际上,我们不会同时使用所有这些项目,因为它们中的一些是重叠的,并且工作负载模型也是不兼容的。如何来讲解这张图?

  • Kubernetes 和容器在多语言应用程序的生命周期管理中取得了巨大的飞跃,并为未来的创新奠定了基础。
  • 服务网格技术在 Kubernetes 之上利用高级的网络功能进一步进行了改善,并开始涉足应用程序方面。
  • 尽管 Knative 凭借其快速扩展主要专注于 serverless 类型的工作负载,但是,它也满足了服务编排和事件驱动的绑定需求。
  • Dapr 建立在 Kubernetes、Knative 和服务网格的思想之上,并深入应用程序运行时以解决状态化工作负载、绑定和集成的需求,充当现代分布式中间件。

这张图能够帮助我们看到,很可能在将来,我们最终会使用多运行时来实现分布式系统。多运行时,不是因为有多个微服务,而是因为每个微服务将由多个运行时构成,很可能会有两个,即自定义业务逻辑运行时和分布式原语运行时。

5. 引入多运行时微服务

以下是正在形成的多运行时微服务架构的概述。

你是否记得在电影《阿凡达》中,科学家开发的增强机动平台(Amplified Mobility Platform,简称 AMP)“机甲战衣(mech suits)”以进入荒野去探索潘多拉星球(Pandora)?这个多运行时架构和 Mecha- 战衣类似,都能为人类驱动者提供超能力。在电影中,我们把人类放入战衣以获得力量及毁灭性武器。在这个软件架构中,我们让业务逻辑(指的是微逻辑)形成应用程序的核心,而边车 mecha 组件提供强大的开箱即用的分布式原语。这个微逻辑和 mecha 功能组合起来形成一个多运行时微服务,该微服务使用进程外功能以满足其分布式系统的需求 。最棒的是,Avatar 2 即将出手来帮助推广这个架构。这样我们终于可以在所有的软件大会上用非凡的机甲照片来替换老式的挎斗摩托车照片了;)。下面,我们来看看这个软件架构的细节。

这是一个有两个组件的模型,类似于客户端 - 服务器架构,其中每个组件都是独立的运行时。它和纯粹客户端 - 服务器架构的区别在于,这两个组件都位于同一个主机中,并且在它们之间有可靠的网络,这一点无需担心。两个组件同等重要,它们可以在任何一个方向上初始化操作并充当客户端或服务器。组件之一称为微逻辑(Micrologic),它包含了几乎所有从分布式系统关注点中剥离出来的最小业务逻辑。在一起的另一个部件叫 Mecha,它提供了我们一直在本文中讨论的所有分布式系统功能(生命周期除外,它是平台的功能)。

?¤?è??è???—???????????????????è·μ

微逻辑和 Mecha(即边车模型)可能是一对一的部署,也可以多个微逻辑运行时共享 Mecha。第一种模型最适合 Kubernetes 等环境,而后者适用于边缘部署。

微逻辑运行时特征

我们简要地探讨一下微逻辑运行时的一些特征:

  • 微逻辑组件本身不是微服务。它包含微服务会有的业务逻辑,但是,该逻辑只能和 Mecha 组件结合使用。另一方面,微服务是自包含的,整体功能的片段或部分处理流没有分散到其他的运行时。微逻辑及其对应的 Mecha 组合形成了微服务。
  • 这也不是函数或 serverless 架构。serverless 主要以其托管的快速扩展和归零能力而闻名。在 serverless 架构中,函数只实现单个操作,因为这是扩展的单位。在这一方面,函数与微逻辑不同,微逻辑实现多个操作,但是,实现不是端到端的。更重要的是,操作的实现分散于 Mecha 和微服务运行时。
  • 这是客户端 - 服务器架构的特殊形式,针对无需编码就使用众所周知的分布式原语进行了优化。另外,如果我们假设 Mecha 扮演服务器的角色,那么,每个实例必须专门配置如何与每个的客户端一起工作。它不是一个旨在支持多个客户端的同时又是一个典型的客户端 - 服务器架构的通用服务器实例。
  • 微逻辑中的用户代码没有直接和其他系统交互,也没有实现任何分布式系统原语。它通过事实标准(如 HTTP/gRPC、CloudEvents 规范)和 Mecha 交互,并且,在配置好的步骤和机制的指导下,Mecha 利用丰富的功能与其他系统通信。
  • 尽管微逻辑只负责实现从分布式系统问题中剥离出来的业务逻辑,但是,它仍然必须至少实现一些 API。它必须允许 Mecha 和平台通过预定义的 API 和协议(例如,遵循 Kubernetes 部署的云原生设计原则)与之交互。

Mecha 运行时特征

以下是一些 Mecha 运行时的特征:

  • Mecha 是一个通用的、高度可配置的、可重用的组件,提供分布式原语作为现成的功能。
  • Mecha 的每个实例必须配置成与一个微逻辑组件(边车模型)一起工作,或者配置成与一些组件共享。
  • Mecha 不对微逻辑运行时做任何假设。它与利用开放协议和格式(如 HTTP/gRPC、JSON、Protobuf、CloudEvents)的多语言微服务或甚至单体系统一起工作。
  • Mecha 用简单文本格式(如 YAML、JSON)进行声明式配置,这些格式规定了要启用什么功能以及如何把它们绑定到微逻辑端点上。对于特定的 API 交互操作,可以为 Mecha 附加规范,如 OpenAPI、AsyncAPI、ANSI-SQL 等。对于由多个步骤构成的状态化工作流,可以使用如 亚马逊状态编程语言(Amazon State Language)的规范。对无状态集成,可以用类似 Camel-K YAML DSL 的方法使用企业集成模式(Enterprise Integration Patterns,简称 EIPs)。这里的关键之处是,所有这些都是简单的、基于文本的、声明式的、多语言定义的,Mecha 可以不需要编码就能实现。请注意,这些是未来派的预测,目前,没有用于状态化编排或 EIP 的 Mecha,但是,我期望现有的 Mecha(Envoy、Dapr、Cloudstate 等)很快开始添加这类功能。Mecha 是应用程序级别的分布式原语抽象层。
  • 与其依赖于用于不同目的(如网络代理、缓存代理、绑定代理)的多个代理,不如有一个提供所有这些功能的 Mecha。某些能力的实现(如存储、消息持久性、缓存等)将会有其他的云或自建的服务插入并支持。
  • 关于生命周期管理的一些分布式系统问题由管理平台(比如,Kubernetes 或其他云服务)而不是由使用 Open App Model 等通用开放规范的 Mecha 运行时来提供是合理的。
  • 这个架构的主要好处是什么?

    它的好处是业务逻辑和不断增长的分布式系统问题之间的松耦合。软件系统的这两个元素有完全不同的动态。业务逻辑总是唯一的,代码是自定义的和内部编写的。它经常更改,具体取决于组织的优先级和执行能力。另一方面,分布式原语是用来解决本文所列的那些问题的,并且大家都知道。它们由软件供应商开发并作为库、容器或服务使用。代码随着供应商的优先级、发布周期、安全补丁、开源管理规则等进行更改。这两者相互之间没有什么可见性,也无法相互控制。

    ?¤?è??è???—???????????????????è·μ

    微服务原则有助于通过限界上下文解耦不同的业务领域,其中每个微服务可以独立地发展。但是,微服务架构没有解决业务逻辑与中间件问题耦合带来的难题。对于某些缺少集成场景的微服务来说,这可能不是一个大问题。但是,如果我们的领域涉及复杂的集成(每个人的情况都越来越复杂),遵循微服务原则将无助于我们避免与中间件耦合。即使中间件被表示为包含在我们的微服务中的库,在我们开始迁移和改变这些库的时候,耦合也将变得明显。我们需要的分布式原语越多,就越容易耦合到集成平台中。通过预定义的 API 而不是库,把中间件作为独立的运行时 / 进程使用,有助于松耦合并实现每个组件的独立发展。

    这也是一种为供应商分发和维护复杂中间件软件的更好的方法。只要与中间件是通过开放 API 和标准的进程间通信进行交互的,那么,软件供应商就可以按其节奏自由地发布补丁和更新。用户可以自由地使用其喜欢的编程语言、库、运行时、部署方式和流程。

    这个架构的主要缺点是什么?

    进程间通信。事实上,分布式系统的业务逻辑和中间件机制(Mecha 就来自 mechanics 这个词)处于不同的运行时,这需要采用 HTTP 或 gRPC 调用而不是进程内的方式调用。但是,请注意,这不是一个转到另一台主机或数据中心的网络调用。微逻辑运行时和 Mecha 应该在同一个主机中共存,因此延迟很低,并且出现网络问题的可能性最小。

    复杂性。接下来的问题是,是否值得进行如此复杂的开发并维护这样的系统以获得收益。我认为,答案越来越倾向于“是”。分布式系统的需求在增加,发布周期变得越来越短。该架构针对该领域进行了优化。前段时间,我曾经写道,未来的开发人员必须具备混合开发技能。该架构进一步证实并加强了这个趋势。应用程序的一部分将用更高级的编程语言编写,而部分功能将由必须进行声明式配置的现成组件提供。这两个部分的连接没有发生在编译期,也没有在启动时通过进程内依赖项注入而实现,而是在部署的时候通过进程间通信发生的。该模型可实现更高的软件重用率和更快的变更速度。

    微服务之后并不是函数

    微服务架构有个明确的目标。它对变更进行优化。通过把应用程序切分到业务域,该架构借助服务为软件演化和可维护性提供了最优的服务边界,这些服务是解耦的,由独立的团队管理,并以独立的节奏发布。

    如果我们来看看 serverless 架构的编程模型,会发现它主要是基于函数的。函数针对可扩展性进行了优化。借助函数,我们把每个操作分解为一个独立的组件,从而可以根据需要,快速且独立地扩展。在这个模型中,部署粒度是一个函数。选择函数的原因是,它的代码结构的输入的速度与扩展行为直接相关。这个架构针对极端的可扩展性而不是复杂系统的长期维护性进行了优化。

    我们从 AWS Lambda 的流行程度及其完全托管的运维来看,serverless 的其他方面又是什么呢?“AWS Serverless”针对供应速度进行了优化,以弥补缺乏控制和锁定的代价。但是,完全托管不是应用程序架构,而是软件消费模型。它在功能上是正交的,与使用基于 SaaS 的平台类似,在理想情况下适用于任何架构,无论是单体、微服务、Mecha 还是函数。在很多方面,AWS Lambda 类似于完全托管的 Mecha 架构,但有一个很大的不同:Mecha 没有强制使用函数模型,相反,它允许围绕业务域使用更具凝聚力的代码构造,而与所有中间件无关。

    ?¤?è??è???—???????????????????è·μ

    另一方面,Mecha 架构针对中间件的独立性优化了微服务。尽管微服务彼此独立,但是,它们严重依赖嵌入式分布式原语。Mecha 架构把这两个问题分成独立的运行时,允许它们通过独立的团队进行独立的发布。这种解耦改善了日常的运维(如补丁和更新)并提高了业务逻辑内聚单位的长期维护性。Mecha 架构是微服务架构的自然发展,根据引发最大摩擦的边界来分割软件。与函数模型相比,该优化以软件重用和演化的方式提供了更多的好处,函数模型通过以代码的过度分布为代价为优化提供了极大的可扩展性。

    6. 结论

    分布式应用程序有很多要求。创建有效的分布式系统需要多种技术和好的集成方法。尽管传统的单体中间件提供了分布式系统要求的所有必需的技术功能,但是,它缺乏快速变化、适应和扩展的能力,而这些是业务所需要的。这就是为什么基于微服务架构促成容器和 Kubernetes 的快速普及背后的原因,借助云原生领域最新的发展,现在,我们回到了原点,把所有传统中间件功能迁移到平台和现成的辅助运行时。应用程序功能的商品化主要使用进程外模型而不是运行时库或纯平台功能进行功能扩展。这意味着,将来我们很可能使用多运行时来实现分布式系统。多运行时,不是因为有多个微服务,而是因为每个微服务将有多运行时构成,有用于自定义微业务逻辑的运行时以及用于分布式原语的现成的可配置的运行时。