搜公众号
推荐 原创 视频 Java开发 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库
Lambda在线 > 蜂鸟看世界 > 迁移到云原生应用架构(三)迁移手册

迁移到云原生应用架构(三)迁移手册

蜂鸟看世界 2019-02-10
举报

原文:Migrating to Cloud-Native Applicatin Architectures, Matt Stine, 2015, Chapter3 Migration Cookbook 翻译:梧晓馨 摘要: 本章节介绍了两套操作步骤,协助云原生应用架构转型: 1)分解单体应用

  • 以微服务的方式构建所有新功能需求

  • 利用防损层集成新建微服务和原单体应用

  • 识别边界上下文,提取微服务,遏制原单体应用 2)分布式系统

  • 利用配置服务器和管理总线,实现配置的版本管理、分发和刷新

  • 动态发现远程依赖服务

  • 分散负载均衡决策

  • 利用熔断和隔离防止级联故障

  • 利用API网关集成支持多样化客户端

自动化测试和构建持续交付流水线等问题需要进一步研究。感兴趣的读者可以扩展阅读如下参考文献:

  • “Testing Strategies in a Microservice Architecture”, Toby Clemson

  • 《Continuous Delivery: Reliable Software Release through Build, Test, and Deployment Automation》, Jez Humble, David Farley (Addision-Wesley)

前两个章节,定义了云原生应用架构并从宏观层面高度概括阐述了企业采用云原生应用架构应进行哪些调整和变更。本章节,探索技术规范。每一条技术规范都可以展开论述自成一章,但是本文不论述规范标准,而是通过一组短小精悍的cookbook式的规则介绍,阐释云原生应用架构转型应实施哪些特定的操作和策略模式。

3.1 分解

探讨完拆解数据模式、服务和团队的需求之后,客户通常会问:“非常好!那么怎样才能从我们的技术现状转型为您所描述的目标状态呢?”这是一个很好的问题。我们应该如何分解现有的单体应用并迁移上云? 在咨询的过程中,我发现许多公司以“逐步迁移”的方式成功完成了转型。这种迁移模式完全可以被其他公司复制,也是我目前向所有客户推荐的一种模式。SoundCloud和Karma的企业咨询案例可以作为参考。(http://bit.ly/sc-monolith-1;http://bit.ly/sc-monolith-2;http://bit.ly/karma-build-micro) 本小节,我们将分步骤,逐步介绍分解单体应用并将它们迁移上云的全过程。

1)以微服务的方式开发实现新功能

首先,第一步并不是直接分解单体应用。我们假定原单体应用有一组待开发构建的新功能集。事实上,如果你的应用没有任何需要构建的新功能,那么这个单体应用很可能就不需要做分解。(我们做服务分解向云原生转型的主要目的是加快更新迭代速度,如果服务本身就没有更新迭代的需求,又何谈加快更新迭代的速度呢?)

……在解决架构转型问题时,最佳的方案并不是直接分解母体应用,而是不再对母体应用引入任何新的功能。所有新功能特性均以微服务的方式开发构建…… ——Phil Calcado,SoundCloud

因此,在团队决定向云原生架构转型后,应立刻停止向原单体应用中增加任何新代码。所有新功能特性,均以微服务的方式实现。先做好这件事,因为从零构建新服务远比从一个“大泥球”(a big ball of mud, http://www.laputan.org/mud/)中分解服务容易的多。

不可避免地,这些新建微服务需要与原单体应用进行交互通信,以完成特定任务。如何解决这个问题呢?

2)防损层

“我们的大部分业务逻辑仍然是Rails单体应用(备注:Ruby on Rails,是一个高效的Web应用程序编程框架,基于MVC设计模式实现,于2004年7月提出),因此几乎所有的微服务都需要与之交互。”——Phil Calcado,SoundCloud

著名建模专家Eric Evans,在其著作《Domain-Driven Design: Tackling Complexity in the Heart of Software》(Addison-Wesley)中探讨了“防损层”(anti-corruption layer)的观点。其目标是:允许两个系统集成,其中一个系统的领域模型不能损伤另一个系统的领域模型。当将新功能特性构建为微服务时,一定不希望对其暴露过多原单体应用的内部信息,造成新的微服务与原单体应用紧耦合。“防损层”是一种创建API合约的方式,使得原单体应用和新建的微服务一样,通过API接口与外部交互。

Evans将防损层的实现划分成了三个子模块,前两个子模块体现了经典的设计模式(《Design Patterns: Elements of Reusable Object-Oriented Software》[Addision Wesley]):

  • 前端(Facade)

前端模块旨在简化微服务与单体应用接口集成的过程。很可能单体应用在设计的时候就没有考虑这个问题,因此,引入“前端”子模块负责解决该问题。至关重要的是,“前端”模块不改变单体应用的模型,并仔细避免将协议转换的关注点和系统集成的关注点耦合在一起。

  • 适配器(Adapter)

适配器,用于定义“服务”,实现新功能特性的需求。基于协议接收来自系统的请求,并向单体的“前端”模块发送请求。

备注:由于大量业务逻辑实现在原单体应用中,因此,微服务往往需要请求原单体应用获取功能支撑。

  • 翻译器(Translator)

翻译器的职责是在原单体应用的领域模型和微服务的领域模型之间转换请求和响应。 这三个松耦合的组件解决了三个问题:

  • 系统集成

  • 协议转换

  • 模型转换

剩下的任务便是确定与系统交互通信的链接位置。在DDD模型中,Evans探讨了两个可选方案。第一个,是“前端”到系统。该方案适用于无法访问或改变遗留系统的情况。这里,我们的关注点是对原单体应用实施控制,因此,倾向于Evan的第二个建议——“适配器”到系统。基于该可选方案,我们构建原单体应用的前端模块,并规约“前端”模块与“适配器”模块之间的交互通信。据推测,在两个围绕交互目标显式开发实现的模块之间建立交互通信链接,更加易于实现。

最后,防损层可以促进双向通信。正如实现新特性的微服务需要请求原单体应用实现某项任务目标,反之亦然,尤其体现在下一个转型任务中。

3)遏制模块化单体架构

“完成架构转型后,团队便可以在更灵活的环境中构建新功能特性,并实施一些软件增强手段。一个重要的遗留问题是:如何从所谓的“母舰”单体Rails应用中抽取功能特性呢?——Phil Calcado, SoundCloud

这里借用一下Martin Fowler在“StranglerApplication”一文中发表的“遏制单体”的思想。Fowler在文中阐释了“基于遗留系统的边界逐步创建一个新系统,经过若干年的缓慢发展,直至遗留系统被遏制。” 我们所实施的转型操作正是如此。通过抽取微服务和附加的防损层,我们基于现存的单体应用边界构建了一个新的云原生系统。

以下两个标准协助我们做出选择,应该抽取哪些组件:

  • SoundCloud确定了第一个标准:识别单体应用内部的边界上下文。回顾前面讨论过的边界上下文,要求领域模型是内部一致的。极有可能的是,原单体应用的领域模型内部不一致。识别内部一致的子模型,构成待抽取成微服务的候选组件。

  • 第二个标准是处理优先级:先抽取哪个候选组件?答案是转型微服务架构的首要原因:创新速度。哪些候选组件最能够受益于高效的创新速度?显然,我们要选择那些在当前业务模型下更新需求最多的组件。检查原单体应用的待办需求列表。识别出交付变更应修改的代码区域范围,在修改代码之前,抽取合适的边界上下文。

4)可能的终态

如何确定任务的结束点?有两个基本的终态:

  • 原单体应用被完全遏制并淘汰。所有的边界上下文都被抽取为微服务。最后的步骤是消除那些不再需要的防损层。

  • 原单体应用被遏制到一个点,服务抽取的成本已经超过重新开发新的微服务。单体应用的一些功能已经完全稳定了——若干年不再需要更新,正常地执行任务。把这些功能抽取为微服务已经没有多大的价值,并且运维所需防损层与之集成的成本非常低,完全可以长期维护。

3.2 分布式系统

构建由微服务构成的分布式系统时,将面临一些非功能性的要求,这些要求在开发单体应用时一般不存在。某些情况下,一致性、延迟和网络分区等物理规律会妨碍解决这些问题。但是,脆弱性和可管理性问题一般可以通过合理使用一些普遍的案例模式来解决。本小节阐述解决这些关注点的关键操作。

这些关键操作源于Spring Cloud项目和Netflix OSS系列项目。

1)以版本方式管理的分布式配置

Chapter1“12因素应用”中讨论了为应用设置恰当配置管理的重要性,指出通过操作系统层环境变量注入配置信息。该方法适用于简单的系统,一旦扩展为大规模的系统,很多时候,需要附加的配置能力:

  • 更新一个运行态应用系统的日志级别,以进行生产调试

  • 更新从消息代理接收消息的线程数目

  • 报告生产系统的所有配置更新,支持监管审计

  • 切换运行态应用系统的功能on/off开关

  • 关键配置信息的隐私保护(如:passwords)

为了支持这些能力,我们需要一个配置管理方案,包含如下功能:

  • 版本管理

  • 可审计

  • 加密

  • 无需重启的刷新

Spring Cloud项目包含一个配置服务器(Config Server)提供上述功能。配置服务器将应用和应用概述文件(如:可以统一进行on/off切换的一组配置集,如“开发”概述文件、“测试”概述文件)的配置信息,表示为一个REST API,由Git仓库作为后端存储支撑。如图3-1。

下面是一个简单的Config Server默认应用程序概要描述文件配置。(示例3-1)

示例3-1、一个简单Config Server的默认应用程序概述文件配置 { “label”: “”, “name”: “default”, “propertysources”: [ { “name”: “https://github.com/mstine/config-repo.git/application.yml”, “source”: { “greeting”: “ohai" } } ] }
备注:

  • 配置信息存储在后端Git库中,文件为application.yml

  • “greeting”值设置为“ohai”

示例3-1中的配置不是手工编码的,而是自动生成的。通过检查“/env”端点,可以看到“greeting”的值已经被分发到Spring应用程序中。

示例3-2、一个Config Server客户端的环境变量 “configservice: https://github.com/mstine/config-repo.git/application.yml”: { “greeting”: “ohai" }, 备注:

  • 应用程序接收来自Config Server的配置信息,其中,“greeting”值为“ohai”

我们所要做的就是,在不重启客户端应用程序的前提下,更新“greeting”。Spring Cloud的另一个项目模块Spring Cloud Bus实现该功能。Spring Cloud Bus利用一个轻量级的消息代理链接分布式系统的各个nodes,并通过消息代理向所有nodes广播状态更新,如:预期的配置更新等。(图3-2)

迁移到云原生应用架构(三)迁移手册

对云总线上的任意一个node上的应用程序“/bus/refresh”端点(该节点需要配置了合适的安全策略),执行HTTP POST操作,可以触发云总线上链接的所有node,利用Config Server上的最新配置信息,刷新它们的应用程序配置。

2)服务注册/发现

分布式系统,各个节点之间的代码依赖通过网络调用实现,而不再是一个方法调用。如何促使集成的应用各个微服务之间友好通信呢?

迁移到云原生应用架构(三)迁移手册

如图3-3所示,云中的一个通用架构模式是设置前端服务(应用)和后端服务(业务)。后端服务通常不允许直接从互联网访问,访问必须经过前端服务。服务注册提供所有服务的服务列表,使得前端服务可以通过一个客户端库访问这些服务,该客户端库通过负载均衡将访问请求发送给后端服务。

在应用各种服务定位器和依赖注入模式之前,我们已经解决了这个问题,面向服务的架构长期利用各种形式的服务注册。此处,我们利用Eureka实现一个类似的解决方案。Eureka是一个Netflix OSS项目,用于负载均衡和中间层服务故障转移时定位需要的服务。Spring Cloud Netflix项目简化了Spring应用程序调用Eureka的操作,该项目提供了一个基于注释的配置模型,来访问Netflix OSS服务。

一个基于Spring Boot的应用可以通过简单地添加“@EnableDiscoveryClient”声明加入服务注册和发现。(示例3-3)

示例3-3、一个打开服务注册/发现功能的Spring Boot应用 @SpringBootApplication @EnableDiscoveryClient public class Application { public static void main(String[ ] args) { SpringApplication.run(Application.class, args); }  } 备注:

  • “@EnableDiscoveryClient”打开该应用的服务注册/发现功能

这样,应用程序便能够利用DiscoveryClient与其依赖的其他服务通信。示例3-4中,应用程序检索注册名为PRODUCER的服务实例,获取其URL,然后通过Spring的RestTemplate模版与之通信。

示例3-4、使用DiscoveryClient定位一个producer服务 @Autowired DiscoveryClient discoveryClient; @RequestMapping(“/“) public String consume( ) { InstanceInfo instance = discoveryClient.getNextServerFromEureka(“PRODUCER”, false); RestTemplate restTemplate = new RestTemplate( ); ProducerResponse response = restTemplate.getForObject(instance.getHomePageUrl( ), ProducerResponse.class); Return “{\”value\”: \” “ + response.getValue( ) + “\”}"; }
备注:

  • Spring将DiscoveryClient注入应用中

  • getNextServerFromEureka方法使用一个轮询(round-robin)算法返回一个PRODUCER服务实例

3)路由和负载均衡

基本的轮询负载均衡适用于许多场景,但是云环境中的分布式系统通常需要实施一些更高级的路由和负载均衡处理操作。这些处理操作通常由各种外部集中负载均衡解决方案实现。一般,这类解决方案不会充分处理足够的信息或上下文,从而做出最佳的负载均衡决策。同时,一旦这类外部解决方案返回“处理失败”,这些失败可能会造成整个架构范围内的级联故障传递。

云原生解决方案通常将路由和负载均衡解决方案转交给客户端处理。一个典型的客户端解决方案是Ribbon Netflix OSS项目。(图3-4)

迁移到云原生应用架构(三)迁移手册

Ribbon提供了一组丰富的功能特性,包括:

  • 内嵌多种负载均衡规则

  • 轮询

  • 加权平均响应时间

  • 随机

  • 可用筛选(避免产生环路或高并发的链接数)

  • 可自定义的负载均衡规则插件系统

  • 可插拔式地集成服务发现解决方案(包括Eureka)

  • 云原生智能,如:域亲和、不健康域规避

  • 内嵌的故障恢复

通过应用Eureka,Spring Cloud Netflix项目很大程度地简化了Spring应用开发者处理Ribbon的工作量。开发者可以注入一个LoadBalancerClient实例,无需再注入一个DiscoveryClient实例(用来直接调用Eureka),然后,使用LoadBalancerClient解析应用的依赖。(示例3-5)

示例3-5、使用LoadBalancerClient定位一个producer服务 @Autowired LoadBalancerClient loadBalancer; @RequestMapping(“/“) public String consume( ) { ServiceInstance instance = loadBalancer.choose(“producer”); URI producerUri = URI.create(“http://${instance.host}:${instance.port}”); RestTemplate restTemplate = new RestTemplate( ); ProducerResponse response = restTemplate.getForObject(producerUri, ProducerResponse.class); return “{\”value\”: \”” + response.getValue( ) + “\"}”; } 备注:

  • Spring负责注入LoadBalancerClient

通过创建一个在Ribbon中设置开启的RestTemplate bean——该模版bean可以被注入beans——Spring Cloud Netflix进一步简化应用对Ribbon的处理操作。该RestTemplate实例被配置,基于Ribbon自动解析逻辑的服务名,获取实例的URIs。(示例3-6)

示例3-6、使用在Ribbon中开启的RestTemplate @Autowired RestTemplate restTemplate; @RequestMapping(“/“) public String consume( ) { ProducerResponse résponse = restTemplate.getForObject(“http://producer”, ProducerResponse.class); return “{\”value\”: \” “ + response.getValue( ) + “\"}" } 备注:

  • Spring直接注入RestTemplate,而不是一个LoadBalancerClient

  • 注入的RestTemplate自动将“http://producer”解析为一个真正的服务实例URI

4)容错

相比单体应用,分布式系统潜在的故障模式更多。由于每一个入口请求目前必须潜在地访问几十个(甚至数百个)不同的微服务。基本上可以肯定,其中一个或多个依赖服务将会发生某种访问失败。

 
   
   
 
  1. 若不实施容错措施,30个依赖服务,每个服务的服务保障指标为49,每个月将造成2+小时的宕机时长(99.99%^30^=99.7%uptime=2+ hours downtime in a month)。——Ben Christensen, Netflix Engineer

如何阻止故障在全系统内级联传播而造成如此过长的不可用时间呢?Mike Nygard在其著作《Release It!Design and Deploy Production-Ready Software》中描述了几个有帮助的容错模式,包括:

  • 熔断(Circuit breakers)

熔断是一种服务隔绝机制。当服务的某一个依赖项状态为不健康时,通过阻止远程调用的方式,将服务与其所有依赖项隔绝开。工作原理与电源熔断器保护房屋免于因过度用电造成火灾一样。熔断器实现为一个有限状态机(图3-5)。当处于关闭状态时,直接将调用请求发送给依赖项。如果调用失败,说明故障发生。当故障计数在特定时间段内达到一个指定的阈值时,触发熔断器,打开状态。在打开状态下,所有调用被直接终止,返回失败。经过一个预先设置好的时间段,熔断器将状态转变为“半开”。在半开状态下,再次尝试向依赖项发送调用请求。若调用请求响应成功,熔断器关闭状态;若调用请求失败,熔断器重新打开状态,阻止所有请求。

迁移到云原生应用架构(三)迁移手册

  • 隔离(Bulkheads)

隔离对服务进行分区,明确错误,防止因某一区域发生故障造成整个服务失败。起名源于船只通过密封分区隔离出多个水舱。这是一种保护机制,防止被鱼雷击中后整艘船沉没。软件系统可以通过多种方式利用这种机制。简单地分解微服务,是第一道防线。将应用分解后的进程装进Linux容器限制进程的资源占用,是第二道。还有一种方法是将并行任务划分为不同的线程池。

Netflix在Hystrix中实现了一个非常强大的容错库,利用以上模式以及其他模式。Hystrix允许代码封装为HystrixCommand对象,从而将代码封装到熔断机制中。

示例3-7、使用一个HystrixCommand对象 public class CommandHelloWorld extends HystrixCommand { private final String name; public CommandHelloWorld(String name) { super(HystrixCommandGroupKey.Factory.asKey(“ExampleGroup”)); this.name = name; } @Override protected String run( ) { //run( )方法中的代码用一个熔断器进行封装 return “Hello “ + name + “!”;
} }

Spring Cloud Netflix在Spring Boot应用中添加一个“@EnableCircuitBreaker”声明,打开Hystrix运行时组件功能。然后,利用一组声明使得基于Spring和Hystrix编程可以与我们之前描述的集成操作一样容易。(示例3-8)

示例3-8、使用@HystrixCommand @Autowired RestTemplate restTemplate; @HystrixCommand(fallbackMethod = “getProducerFallback”) public ProducerResponse getProducerResponse( ) { return restTemplate.getForObject(“http://producer”, ProducerResponse.class); } public ProducerResponse getProducerFallback( ) { return new ProducerResponse(42); } 备注:

  • @HystrixCommand声明的方法,使用一个熔断器进行封装

  • 声明中引用到的getProducerFallback方法,当熔断器处于打开或半开状态时,该方法将执行一个优雅的回退操作

与其他熔断器相比,Hystrix是独一无二的。Hystrix同时实现了隔离机制,在独立的线程池中运行每一个熔断器。同时,Hystrix收集许多关于熔断器状态的有用指标,包括:

  • 流量卷

  • 请求率

  • 错误百分比

  • 宿主机报告

  • 延迟百分比

  • 请求成功、失败、拒绝

这些指标以事件流的形式发出,与另一个Netflix OSS项目Turbine集成。单个或复合的指标流通过功能强大的Hystrix Dashboard进行可视化显示(图3-6),从而可观测分布式系统的全局健康状态。

迁移到云原生应用架构(三)迁移手册

5)API网关/边界服务

在“Chapter1 移动应用和客户端多样化”中,我们探讨了微服务生态中服务端集成和转换的思想。其必要性是什么?

  • 网络延迟

移动设备的网络带宽往往比台式设备低很多。单个应用界面显示需要访问数十个甚至数百个微服务,可能带来不可接受的延迟,哪怕在家庭或办公的网络环境中。并发访问这些服务的需求变得清晰起来。相比在每个设备平台上处理请求,在服务器端一次性捕获并实现这些并发模式,代价更低,更不容易出错。

另一个延迟原因是响应大小。近几年,web服务开发倾向于“返回所有你可能会用到的信息”,因此响应数据包比显示一个应用屏幕要大得多。移动设备开发者更倾向于只获取需要的信息,降低延迟。

  • 来回请求

假设网络带宽不再是一个问题,与大量微服务通信也会给移动开发者带来问题。使用网络功能会大量消耗移动设备的电源储备。移动开发者希望在满足用户体验的基础上,尽量少地请求服务端,避免应用网络功能。

  • 设备多样性

移动设备生态呈现出严重的多样性。业务必须应对不断增长的客户差异,包括:制造商、设备类型、表单元素、设备大小、编程语言、操作系统、运行环境、并发模式、支持的网络协议。

多样性甚至超出了移动设备的范畴,开发者还要面对不断增长的家居设备生态,如:智能电视和机顶盒。

API网关模式(图3-7)将这些负担从设备开发端转移到服务器端。API网关是一类特殊的微服务,响应来自某特定客户端应用(如:一个特定的iphone app)的请求,提供一个后端入口点。每个请求并发访问数十或数百个微服务,集成响应结果,根据客户端需求转换响应结果。同时,根据需要进行协议转换,如:HTTP到AMQP。

API网关可以基于任何语言、运行时,或者支持web编程、并发模式以及与目标微服务之间通信协议的编程框架来实现。譬如:Node.js(交互编程模式)和Go语言(简单并发模式)。

本章节讨论中,使用Java并以RxJava为例。RxJava是Netflix项目中交互扩展的JVM实现。单纯使用Java原语并发地组合多个工作流或数据流,是一项挑战。RxJava正是解决该问题的技术之一。这类技术还包括Reactory。

举例,我们构建一个Netflix网站,为用户提供电影目录,并允许用户对电影进行打分和点评。此外,当用户检索特定的电影标题时,如果用户喜欢这个电影,为他推荐可能想看的电影。我们开发了三个微服务,提供这些功能:

  • catalog服务

  • reviews服务

  • recommendations服务

该移动应用所预期的响应,如示例3-9所示。

示例3-9、应用预期的响应信息 { “ml-Id”: “1”, “recommendations”: [ { “ml-Id”: “2”, “title”: “GoldenEye(1995)" } ],  “reviews”: [ { “ml-Id”: “1”, “rating”: 5, “review”: “Great movie!”, “title”: “Toy Story(1995)”, “userName”: “mstine" } ], “title”: “Toy Story(1995)" }

代码见示例3-10,利用RxJava的Observable.zip方法并发访问每个服务。接收到三个请求响应后,代码将这些响应结果发送给Java 8 Lambda,基于结果信息创建一个MovieDetails实例。MovieDetails示例序列化地生成示例3-9所示的响应结果。

示例3-10、并发访问三个微服务,集成响应结果 Observable details = Observable.zip( catalogIntegrationService.getMovie(ml-Id), reviewsIntegrationService.reviewsFor(ml-Id), recommendationsIntegrationService.getRecommendations(ml-Id), (movie, reviews, recommendations) -> { MovieDetails movieDetails = new MovieDetails( ); movieDetails.setMlId(movie.getMlId( )); movieDetails.setTitle(movie.getTitile( )); movieDetails.setReviews(reviews); movieDetails.setRecommendations(recommendations); return movieDetails; } );

上面的案例几乎算不上RxJava的入门。如有需要,可以检索RxJava的Wiki了解更多功能。



版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《迁移到云原生应用架构(三)迁移手册》的版权归原作者「蜂鸟看世界」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读

关注蜂鸟看世界微信公众号

蜂鸟看世界微信公众号:hummingbirdWorldview

蜂鸟看世界

手机扫描上方二维码即可关注蜂鸟看世界微信公众号

蜂鸟看世界最新文章

精品公众号随机推荐

举报