迁移指南【Migration Cookbook】
本文章参考自Migrating to Cloud-Native Application Architectures
- Stine M. Migrating to cloud-native application architectures[M]. O’Reilly Media, 2015.
作者:Matt Stine,Pivotal的技术产品经理,拥有15年企业IT和众多业务领域的经验。Matt 强调精益/敏捷方法、DevOps、架构模式和编程范例,他正在探究使用技术组合帮助企业IT部门能够像初创公司一样工作。云原生的推广者。
中文版:《迁移到云原生应用架构》
宋净超(Jimmy Song),Tetrate 布道师,云原生社区 及 ServiceMesher 创始人,CNCF Ambassador
现在我们已经定义了云原生应用架构,并简要介绍了企业在采用它们时必须考虑做出的变化,现在是深入研究技术细节的时候了。对每个技术细节的深入讲解已经超出了本报告的范围。本章中仅是对采用云原生应用架构后,需要做的特定工作和采用的模式的一系列简短的介绍,文中还给出了一些进一步深入了解这些方法的链接。
3.1 分解架构
在和客户讨论分解数据、服务和团队后,客户经常向我提出这样的问题,“太棒了!但是我们要怎样实现呢?” 这是个好问题。如何拆分已有的单体应用并把他们迁移上云呢?
事实上,我已经看到了很多成功的例子,使用增量迁移这种相当可复制的模式(pattern of incremental migration
),我现在向我所有的客户推荐这种模式。SoundCloud 和 Karma 就是公开的例子。
本节中,我们将讲解如何一步步地将单体服务分解并将它们迁移到云上。
3.1.1 新功能使用微服务形式
您可能感到很惊奇,第一步不是分解单体应用。我们假设您依然要在单体应用中构建服务。 事实上,如果您没有任何新的功能来构建,那么您甚至不应该考虑这个分解。(鉴于我们的主要动机是速度,您该如何维持原状还能获取速度呢?)
所以不要继续再向单体应用中增加代码,将所有的新功能以微服务的形式构建。这是第一步就要考虑好的,因为从头开始构架一个服务比分解一个单体应用并提出服务出来容易和快速的多。
然而有一点不可避免,就是新构建的微服务需要与已有的单体应用通信才能完成工作,这个问题怎么解决?
3.1.2 隔离层
Eric Evans(Addison-Wesley)的领域驱动设计(DDD)讨论了隔离层的思想。 其目的是允许两个系统的集成,而不允许一个系统的领域模型破坏另一个系统的领域模型。 当您将新功能集成到微服务中时,不希望这些新服务与整体的紧密结合,让他们深入了解整体的内部结构。 隔离层是创建 API 协议的一种方式,使得整体架构看起来像其他微服务。
Evans 将隔离层的实施划分为三个子模块,前两个代表着经典设计模式。
Facade
表现层
- 表现层的目的是为了简化与单体应用接口集成的过程。单体应用设计之初很可能没有考虑这个集成,因此我们引入了表现层来解决这个问题。它没有改变单体应用的模型,这很重要,注意不要将转换和集成问题耦合到一起。
Adapter
适配器
- 我们用适配器来定义 service,用来提供我们需要的新功能。它知道如何获取系统请求并使用协议将请求发送给单体应用的表层。
Translator
转换器
- 转换器的职责是在单体应用与新的微服务之间进行请求和响应的领域模型转换。
这三个松耦合的组件解决了以下三个问题:
- 系统集成
- 协议转换
- 模型转换
剩下的是通信链路的位置。在 DDD 中,Evans 讨论了两种选择。当您无法访问或更改遗留系统时,第一,facade to system。而我们的重点在于我们控制的整体,所以我们将倾向于 Evans 的第二个建议,adapter to facade。使用这种替代方法,我们将表现层构筑到单体中,允许在适配器和表现层之间进行通信,因为在为此专门构建的组件之间创建连接更容易。
最后,要注意隔离层可以促进双向通信。 正如我们新的微服务可能需要与整体进行通信以完成工作一样,反之亦然,特别是当我们进入下一阶段时。
3.1.3 扼杀单体应用
我从 Martin Fowler 的题为 “strangling the monolith” 的文章中借用了 “扼杀巨石” (strangling the monolith
)的想法。 在这篇文章中,Fowler 解释了逐渐创造 “围绕旧系统边缘的新系统,让它几年来慢慢增长,直到旧系统被扼杀” 的想法。这种情况同样适用于我们。通过提取的微服务和其他隔离层的组合,我们将围绕现有单体的边缘构建一个新的云原生系统。
有两个标准帮助我们选择要提取哪些组件:
- SoundCloud 指出了第一个标准:识别单体中的有界上下文(
bounded contexts
)。如果您回想起我们之前讨论有界上下文,它需要一个内部一致的领域模型。我们的单体领域模型极有可能不是内部一致的。现在是开始识别子模型的时候了,里面有我们要提取的候选者。 - 第二个标准是优先考虑的:在众多的候选者中我们应该首先提取哪一个呢?我们可以回顾一下迁移到云原生架构的第一个原因:创新速度。什么候选微服务将最受益于创新速度?我们显然希望选择那些正在改变我们当前业务需求的服务。看看单体应用的积压。确定需要更改的代码的区域,以便提交更改的要求,然后在进行所需更改之前提取适当的有界上下文。
3.1.4 潜在的结束状态
我们怎么知道何时结束?下面有两个基本的结束状态:
- 单体架构已经被完全扼杀。所有的有界上下文都被提取为微服务。最后一步是确定消除不再需要隔离层的机会。
- 单体架构被扼杀到了这样一个点:额外服务提取的成本超过必要开发努力的回报。 单体的一些部分可能相当稳定 —— 它们几年来都没有改变,还是一直都在运行得好好的。迁移这些部分可能没有太大的价值,维持必要的隔离层与其集成的成本足够低,我们可以长期负担。
3.2 使用分布式系统
我们开始构建由微服务组成的分布式系统时,我们还会遇到在开发单体应用时通常不会遇到的非功能性要求。有时,使用物理定律就可以解决这些问题,例如一致性、延迟和网络分区问题(consistency, latency, and network partitions
)。然而,脆弱性和可管理性(brittleness and manageability
)的问题通常可以使用相当通用的模式来解决。在本节中,我们将介绍帮助我们解决这些问题的方法。
这些方法来自于 Spring Cloud 项目和 Netflix OSS 系列项目的组合。
3.2.1 版本化和分布式配置
在 “12 因素应用 ”中我们讨论过通过操作系统级环境变量为应用注入对应的配置,强调了这种配置管理方式的重要性。这种方式特别适合简单的系统,但是,当系统扩大后,有时我们还需要附加的配置能力:
- 为调试一个生产上的问题而变更运行的应用程序日志级别
- 更改 message broker 中接收消息的线程数
- 报告所有对生产系统配置所做的更改以支持审计监管
- 运行中的应用切换功能开关
- 保护配置中的机密信息(如密码)
为了支持这些特性,我们需要配置具有以下特性的配置管理方法:
- 版本控制
- 可审计
- 加密
- 在线刷新
Spring Cloud 项目中包含的一个可提供这些功能的配置服务器。此配置服务器通过 Git 本地仓库支持的 REST API 呈现了应用程序及应用程序配置文件(例如,可用开 / 关切换的一组配置作为一组,如 “deployment” 和 “staging” 配置)(下图)。
{
“label”: “”,
“name”: “default”,
“propertySources”: [
{
“name”: “https://github.com/mstine/config-repo.git/applica tion.yml”,
“source”: {
“greeting”: “ohai”
}
}
]
}
示例服务器的默认配置文件如下:
{
"label": "",
"name": "default",
"propertySources": [
{
"name": "https://github.com/mstine/config-repo.git/applica tion.yml",
"source": {
"greeting": "ohai"
}
}
]
}
- 该配置中指定了后端 Git 仓库中的
application.yml
文件。- greeting 当前被设置为 ohai。
上例中的配置是自动生成的,无需手动编码。我们可以看到,通过检查它的 /env 端点(下例),greeting 的值被分发到 Spring 应用中
"configService:https://github.com/mstine/config-repo.git/applica tion.yml": {
"greeting": "ohai"
},
该应用接收到来自配置服务器的 greeting 的值:ohai
现在我们就可以无需重启客户端应用就可以更新 greeting 的值。该功能由 Spring Cloud 项目中的一个名为 Spring Cloud Bus 的组件提供。该项目将分布式系统的节点与轻量级消息代理进行链接,然后可以用于广播状态更改,如我们所需的配置更改(下图)。该项目将分布式系统的节点与轻量级消息代理进行链接,然后可以用于广播状态更改,如我们所需的配置更改(下图)。
只需通过对参与总线的任何应用程序的 /bus/refresh 端点执行 HTTP POST(这显然应该进行适当的安全性保护),指示总线上的所有应用程序使用配置服务器中的最新的可用值刷新其配置。

3.2.2 服务注册/发现
当我们创建分布式系统时,代码的依赖不再是一个方法调用。相反,应用它们必须通过网络调用。我们该如何布线,才能使组合系统中的所有微服务彼此通信?
云中的(下图)的同样架构模式是有一个前端(应用程序)和后端(业务)服务。后端服务往往不能直接从互联网访问,而是通过前端服务访问。服务注册提供的所有服务的列表, 使它们可以通过一个客户端库到达前端服务(路由和负载均衡),客户端库执行负载均衡和路由到后端服务。
在使用服务定位器和依赖注入模式的各种形式之前,我们已经解决了这个问题,面向服务的架构长期以来一直使用各种形式的服务注册表。我们将采用类似的解决方案,利用 Eureka,这是一个 Netflix OSS 项目,可用于定位服务,以实现中间层服务的负载平衡和故障转移。为了使用 Netflix OSS 服务,Spring Cloud Netflix 项目提供了基于注释的配置模型,这大大简化了开发人员在开发 Spring 应用程序时对 Eureka 的心力耗费。
只需简单得在代码中添加 @EnableDiscoveryClient 注释,应用程序就可以进行服务注册和发现。
@SpringBootApplication
@EnableDiscoveryClientpublic//@EnableDiscoveryClient 开启应用程序的服务注册发现。
class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
该应用程序就能够通过利用 DiscoveryClient 与它的依赖组件通信。
下例是应用程序查找名为 PRODUCER 的注册服务的一个实例,获得其 URL,然后利用 Spring 的 RestTemplate
与之通信。
- 开启的
DiscoveryClient
通过 Spring 注入。getNextServerFromEureka
方法使用 round-robin 算法提供服务实例的位置
3.2.3 路由和负载均衡
基本的 round-robin 负载平衡在许多情况下是有效的,但云环境中的分布式系统通常需要更高级的路由和负载均衡行为。这些通常由各种外部集中式负载均衡解决方案提供。然而,这种解决方案通常不具有足够的信息或上下文,来在给定的应用程序尝试与其依赖进行通信时做出最佳选择。此外,如果这种外部解决方案故障,可能会影响到整个架构。
云原生的解决方案通常将路由和负载均衡的职责放在客户端。Ribbon Netflix OSS 项目就是其中的一种。
Ribbon 提供一组丰富的功能集:
- 多种内建的负载均衡规则:
- Round-robin 轮询负载均衡
- 平均加权响应时间负载均衡
- 随机负载均衡
- 可用性过滤负载均衡(避免跳闸线路和高并发链接数)
- 自定义负载均衡插件系统
- 与服务发现解决方案的可拔插集成(包括 Eureka)
- 云原生智能,例如可用区亲和性和不健康区规避
- 内建的故障恢复能力
跟 Eureka 一样,Spring Cloud Netflix 项目也大大简化了 Spring 应用程序开发人员使用 Ribbon 的心力耗费。开发人员可以注入一个 LoadBalancerClient
的实例,然后使用它来解析应用程序依赖关系的一个实例(例 3-5),而不是注入 DiscoveryClient
的实例(用于直接从 Eureka 中消费)。
- 由 Spring 注入的
LoadBalancerClient
。- choose 方法使用当前负载均衡算法提供了服务的一个示例地址。
Spring Cloud Netflix 通过创建可以注入到 Bean 中的 Ribbon-enabled 的 RestTemplate bean 来进一步简化 Ribbon 的配置。RestTemplate 的这个实例被配置为使用 Ribbon(示例 3-6)自动将实例的逻辑服务名称解析为 instanceURI。
- 注入的是
RestTemplate
而不是LoadBalancerClient
。- 注入的
RestTemplate
自动将 http://producer 解析为实际的服务实例的 URI。
3.2.4 容错
分布式系统比起单体架构来说有更多潜在的故障模式。由于传入系统中的每一个请求都可能触及几十甚至上百个不同的微服务,因此这些依赖中的某些故障实质上是不可避免的。
如果不进行容错,30 个依赖,每个都是 99.99% 的正常运行时间,每个月将导致 2 个小时的停机时间(99.99%^30=99.7% 的正常运行时间 = 2 小时以上的停机时间)。
——Ben Christensen,Netflix 工程师
如何避免这类故障导致级联故障,给我们的系统可用性数据带来负面影响?Mike Nygard 在他的 Pragmatic Programmers 中提出了几个可以觉得该问题的几个模式,包括:
Circuit breakers 熔断器
当服务的依赖被确定为不健康时,使用熔断器来阻绝该服务与其依赖的远程调用,就像电路熔断器可以防止电力使用过度,防止房子被烧毁一样。熔断器实现为状态机(图 3-5)。当其处于关闭状态时,服务调用将直接传递给依赖关系。如果任何一个调用失败,则计入这次失败。当故障计数在指定时间内达到指定的阈值时,熔断器进入打开状态。在熔断器为打开状态时,所有调用都会失败。在预定时间段之后,线路转变为 “半开” 状态。在这种状态下,调用再次尝试远程依赖组件。成功的调用将熔断器转换回关闭状态,而失败的调用将熔断器返回到打开状态。
Bulkheads 隔板
隔板将服务分区,以便限制错误影响的区域,并防止整个服务由于某个区域中的故障而失败。这些分区就像将船舶划分成多个水密舱室一样,使用隔板将不同的舱室分区。这可以防止当船只受损时造成整艘船沉没(例如,当被鱼雷击中时)。软件系统中可以用许多方式利用隔板。简单地将系统分为微服务是我们的第一道防线。将应用程序进程分区为 Linux 容器,以便使用单个进程无法接管整个计算机。另一个例子是将并行工作划分为不同的线程池。
Netflix 的 Hystrix 应用了这些和更多的模式,并提供了强大的容错功能。为了包含熔断器的代码,Hystrix 允许代码被包含到 HystrixCommand 对象中。
- run 方法中封装了熔断器
Spring Cloud Netflix 通过在 Spring Boot 应用程序中添加 @EnableCircuitBreaker 注解来启用 Hystrix 运行时组件。然后通过另一组注解,使得基于 Spring 和 Hystrix 的编程与我们先前描述的集成一样简单(例 3-8)
- 使用
@HystrixCommand
注解的方法封装了一个熔断器。- 当线路处于打开或者半开状态时,注解中引用的
getProducerFallbac
k 方法,提供了一个优雅的回调操作
Hystrix 相较于其他熔断器来说是独一无二的,因为它还通过在其自己的线程池中操作每个熔断器来提供隔板。它还收集了许多关于熔断器状态的有用指标,其中包括:
- 流量
- 请求率
- 错误百分比
- 主机报告
- 延迟百分点
- 成功、失败和拒绝
这些 metric 会被发送到事件流中,然后被 Netflix OSS 项目中的另一个叫做 Turbine 的组聚合。每个单独的和聚合后的 metric 流都可以在强大的 Hystrix Dashboard(图 3-6)中以可视化的方式呈现,该页面提供了很好的分布式系统总体健康状态的可视化效果。
API 网关 / 边缘服务
在 “移动应用和客户端多样性” 中我们探讨过服务器端聚合与微服务生态系统。为什么有这个必要?
延迟
移动设备通常运行在比我们家用设备更低速的网络上。即使是在家用或企业网络上, 为了满足单个应用屏幕的需求,需要连接数十(或者上百)个微服务,这样的延迟也将变得不可接受。很明显,应用程序需要使用并发的方式来访问这些服务。在服务端一次性捕获和实行这些并发模式,会比在每一个设备平台上做相同的事情,来得更廉价、更不容易出错。
延迟的另一个来源是响应数据的大小。在 Web 服务开发领域,近年来一直趋向于 “返回一切可能有用的数据” 的做法,这将导致响应的返回数据越来愈大,远远超出了单一的移动设备屏幕的需求。移动设备开发者更倾向于通过仅检索必要的信息而忽略其他不重要的信息,来减少等待时间。
往返通信
即使网速不成问题,与大量的微服务通信依然会给移动应用开发者造成困扰。移动设备的电池消耗主要是因为网络开销造成的。移动应用开发者尽可能通过最少的服务端调用来减少网络的开销,并提供预期的用户体验。
设备多样性
移动设备生态系统中设备多样性是十分巨大的。企业必须应对不断增长的客户群体差异,包括如下这些:
- 制造商
- 设备类型
- 形式因素
- 设备尺寸
- 编程语言
- 操作系统
- 运行时环境
- 并发模型
- 支持的网络协议
这种多样性甚至扩大到超出了移动设备生态系统,开发者目前可能还会关注家用消费电子设备不断增长的生态系统,包括智能电视和机顶盒。
API 网关模式(图 3-7)旨在将客户端的这些需求负担从设备开发者转移到服务器端。API 网关仅仅是一类特殊的满足单个客户端应用程序的微服务(如特定的 iPhone App),并为其提供一个到后端的入口。每个请求同时访问数十(或数百)个微服务,汇总响应并转化,以满足客户应用的需求。在必要时,它们还进行协议转换(例如,HTTP 到 AMQP)。
API 网关可以使用任何支持 web 编程和并发模式的语言、运行时、框架,和能够目标微服务进行通信的协议来实现。热门的选择包括 Node.js (由于其反应式编程模型)和 Go 编程语言(由于其简单的并发模型)。
在这次讨论中,我们将坚持使用 Java,并给出一个 RxJava 例子,一个 Netflix 开发的 Reactive Extensions 的 JVM 实现例子。如果仅使用 Java 语言所提供的原生方法来组成多个工作或数据流是一个很大的挑战,而 RxJava 是一种致力于缓解这种复杂性的技术(还包括 Reactor) 技术之一。
在这个例子中我们将创建一个类似 Netfilx 的网站,它可以在页面上展现一个电影目录,用户可以为这些电影创建评分并进行评论。而且,当用户在浏览某一个标题的页面时,页面上会给予用户相关推荐。为了提供这些能力,需要开发 3 个微服务:
- 目录服务
- 查看服务
- 推荐服务
我们期望的该移动应用的服务的响应如例 3-9 所示
{
"mlId": "1",
"recommendations": [
{
"mlId": "2",
"title": "GoldenEye (1995)"
}
],
"reviews": [
{
"mlId": "1",
"rating": 5,
"review": "Great movie!",
"title": "Toy Story (1995)",
"userName": "mstine"
}
],
"title": "Toy Story (1995)"
}
例 3-10 中的代码利用了 RxJava 的 Observable.zip 方法来并发访问每个服务。在接到三个响应后,代码将它们传递给 Java 8 的 Lambada 表达式处理并生成一个 MovieDetails 实例。 该实例可以被序列化并产生如例 3-9 中的响应。
这个例子仅涉及了 RxJava 所有可用功能的一些皮毛,读者可以在 RxJava 的 wiki 上查看进一步信息。
3.3 本章小结
本章中我们讨论了两种帮助我们迁移到云原生应用架构的方法:
分解原架构
我们使用以下方式分解单体应用:
- 所有新功能都使用微服务形式构建。
- 通过隔离层将微服务与单体应用集成。
- 通过定义有界上下文来分解服务,逐步扼杀单体架构。
使用分布式系统
分布式系统由以下部分组成:
- 版本化,分布式,通过配置服务器和管理总线刷新配置。
- 动态发现远端依赖。
- 去中心化的负载均衡策略
- 通过熔断器和隔板阻止级联故障
- 通过 API 网关集成到特定的客户端上
还有很多其他的模式,包括自动化测试、持续构建与发布管道等。欲了解更多信息,请阅读 Toby Clemson 的 《 Testing Strategies in a Microservice Architecture》,以及 Jez Humbl 和 David Farley(AddisonWesley)的《Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation》。