WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
49 stars 12 forks source link

Microservices #104

Open WangShuXian6 opened 5 years ago

WangShuXian6 commented 5 years ago

Microservices

WangShuXian6 commented 4 years ago

起因

面对日渐壮大的应用,即便团队成员受过最专业的训练,他们可能也只是挣扎着来尽量维持之前的节奏和灵活性。 最坏的情况下,曾经简洁、稳定的产品变得越来越难以处理、越来越脆弱。 开发者们疲于处理服务故障,焦虑于发布新的版本,迟迟都不能交付新的功能或者补丁,无法再为顾客持续地交付更多价值。 不管是客户还是开发成员,都会因此而感到失落。

微服务为我们提供了一种更好地持续交付业务影响的方式。 相较于单体应用,使用微服务构建出来的应用是由一系列松耦合的、自治的服务组成的。 通过开发这些只做一件事的服务,开发者可以避免大型应用中所存在的缺乏活力和混乱的状态。 即便是对于已有的应用系统,开发者也可以一步步地将其中的功能抽取为独立的服务,这样可以使整个系统的可维护性更强。

在采用微服务以后,我们很快就意识到,开发更小的、更独立的服务只是保证关键业务型应用稳定运行的一部分工作而已。 毕竟,所有成功的应用在生产环境里所度过的时间要远远长于在代码编辑器里的时间。 如果想要通过微服务来交付价值,团队就不能只关注开发这一步,还需要在部署、监控和诊断这些运维领域具备专业能力。

WangShuXian6 commented 4 years ago

微服务应用

概念

微服务应用是一系列自治服务的集合,每个服务只负责完成一块功能,这些服务共同合作来就可以完成某些更加复杂的操作。 与单体的复杂系统不同,开发者需要开发和管理一系列相对简单的服务,而这些服务可能以一些复杂的方式交互。 这些服务之间的相互协作是通过一系列与具体技术无关的消息协议来完成的,这些协议可能是点到点形式的,也可能是异步形式的。

传统的软件工程实践倡导设计良好的系统都应该具备高内聚、低耦合的特点。 具备这些特性的系统更加易于维护,并且在面对变更时,也更加容易适应和扩展。

内聚度是用来衡量某个模块中的各个元素属于一个整体的紧密程度的指标, 耦合度则是衡量一个元素对另一个元素的内部运行逻辑的了解程度的指标。

将那些因相同原因而修改的内容聚合到一起,将那些因不同原因而修改的内容进行拆分。

在单体应用中,开发者会在类、模块、类库的层面来设计功能属性; 而在微服务应用中,开发者的目标则变成了可独立部署的功能单元——要为这些功能单元设计功能属性。 单个微服务应该是高内聚的:它应该只负责应用的某一个功能。 同样,每个服务对其他服务的内部运行逻辑知道得越少,就越容易对自己的服务或者功能进行修改,而不需要强迫其他服务一起进行修改。

微服务的三大关键特性

(1)每个微服务只负责一个功能。这个功能可能是业务相关的功能,也可能是共用的技术功能,比如与第三方系统(如证券交易所)的集成。

(2)每个微服务都拥有自己的数据存储,如果有的话。这能够降低服务之间的耦合度,因为其他服务只能通过这个服务提供的接口来访问它们自己所不拥有的数据。

(3)微服务自己负责编排和协作(控制消息和操作的执行顺序来完成某些有用的功能),既不是由连接微服务的消息机制来完成的,也不是通过另外的软件功能来完成的。

微服务的两个基本特性。

(1)每个微服务都是可以独立部署的。如果做不到这一点,那么到了部署阶段,微服务应用还是一个庞大的单体应用。

(2)每个微服务都是可代替的。每个微服务只具备一项功能,所以这很自然地限制了服务的大小。同样,这也使得每个服务的职责或者角色更加易于理解。

与 SOA 的区别

微服务与传统的面向服务架构(SOA)在思想上的一个关键区别就是微服务负责协调系统中的各个操作, 而SOA类型的服务通常使用企业服务总线(ESB)或者更复杂的编排标准来将应用本身与消息和流程编排拆分开。 在SOA模型下,服务通常缺乏内聚性,因为业务逻辑会不断地被添加到服务总线上,而非服务本身。

一个在线投资工具

出售股票的过程:

(1)用户创建一个订单,用来出售其账户里某只股票的股份;

(2)账户中的这部分持仓就会被预留下来,这样它就不可以被多次出售了;

(3)提交订单到市场上是要花钱的——账户要缴纳一些费用;

(4)系统需要将这个订单发送给对应的股票交易市场。

提交出售订单的流程

这可以看作整个微服务应用的一部分

应用中各微服务间的通信流程图:用户出售金融股票持仓

image

这个“在线投资系统”功能解耦的方式是很有意思的。 它能够帮助开发者在未来面对需求变更时更加灵活。 想象一下,当需要修改收费的计算方式时,开发者可以在不修改上下游服务的情况下,直接修改和发布fee服务。

再考虑一个全新的需求:用户下单以后,如果订单不符合正常的交易方式,系统需要向风控团队发送告警。 这也是容易实现的,只要基于order 服务发出的事件通知开发一个新的微服务,让这个新的服务来执行这个操作即可,同样不需要修改系统其他模块。


通过分解来实现扩展

应用扩展的三个维度

在《可扩展性的艺术》(The Art of Scalability)一书中,阿尔伯特(Abbott)和费舍尔(Fisher)定义了一个被称为“扩展立方体”的三维扩展方案

image

单体应用一般都是通过水平复制进行扩展的:部署多个完全相同的应用实例。这种方式也称作饼干模具(cookie-cutter)扩展或X轴扩展。

相反,微服务应用是一个Y轴扩展的例子,我们将整个系统分解为不同的功能模块,然后针对每个模块自己特有的需求来进行扩展。

Z轴是指对数据进行水平分区:sharding。不管是微服务应用还是单体应用,开发者都可以采用数据分区分片的方法。

“在线投资系统”案例的几个特点

金融预测是计算量特别繁重的功能,但是极少会被使用;

复杂的监管和业务规则控制着投资账户;

市场交易的规模是海量的,而且还要求极低的延迟。


如果采用微服务的方式开发,为了满足这些功能要求,我们可以针对每个问题选择最合适的技术工具,而不是像将方钉楔进圆洞那样死板地采用固有的技术和工具。

同样,自治和可独立部署意味着工程师可以分别管理这些微服务所对应的资源需求。

有意思的是,这也包含了一种与生俱来的减少故障的方式:如果金融预测服务出现了故障,它不会导致市场交易服务或者投资账户服务也产生连锁故障。

微服务应用具备一些很有意思的技术特性:按照单一功能来开发服务可以让架构师能够在规模和职责上很自然地划定界限;自治性使得开发者可以独立地对这些服务进行开发、部署和扩容。


微服务开发的五大文化和架构原则

自治性、可恢复性、透明性、自动化和一致性。

工程师在开发和运行微服务应用时,应该运用这些原则来推动自己做出技术和组织决策

1.自治性

微服务是自治的服务,每个服务的操作和修改都是独立于其他服务的。 为了保证自治性,开发者需要将服务设计得松耦合、可独立部署。

(1)松耦合——每个服务通过明确定义的接口或者发布的事件消息来与其他服务进行交互,这些交互独立于协作方的内部实现。比如我们在前面介绍过的order服务并不需要知道 account transaction服务的具体实现方式,如图1所示。

服务按照定义好的契约来通信以实现松耦合,契约隐藏了实现的细节

image

(2)可独立部署——不同的服务通常是由多个不同的团队并行开发的。强迫所有团队按照同样的步调或者按照专门设计的步骤进行部署,都会导致部署阶段有风险更大、更加使人焦虑。理想情况下,大家想要这些服务都能够快速、频繁地发布小的改动。

自治性也是一种团队文化。 将各个服务的责任和所有权委派给有责任交付商业价值的团队,这是至关重要的。 正如我们所确定的,组织设计会对系统设计产生影响。 清晰的服务所有权有助于团队基于他们本身所处的环境和目标来迭代开发和做出决策。 同样,当团队同时负责一个服务的开发和生产时,这种模式也能够促进提升团队端到端的主人翁意识,是非常合适的。

2.可恢复性

微服务与生俱来地具备故障隔离的机制: 如果开发者独立地部署这些微服务,那么当应用或者基础设施出现故障后,故障将只会影响到整个系统的一部分功能。 同样,部署的功能粒度越小,开发者越能更平缓地对系统进行变更,这样才不会在发布新功能的时候发布的是一个有风险隐患的大炸弹。

再次考虑一下那个“投资工具”,如果market服务不可用,系统将不能把订单发布到市场上。 但是用户仍然可以创建订单,当下游的功能恢复以后,market服务能够把这些订单筛选出来继续发到市场上。

尽管将应用拆分成多个服务能够隔离故障,但它还是会存在多点故障的问题。 同样,当故障发生的时候,开发者需要能够解释到底发生了什么问题以避免连锁反应。 这包括设计层面的(在可能的情况下支持异步交互以及适当地使用熔断器和超时), 还包括运维层面的(比如,使用可验证的持续交付技术和对系统活动进行稳定可靠地监控)。

3.透明性

最重要的一点,当故障发生时,开发者需要记得微服务应用是依赖于多个服务(而非单个系统)之间的交互及其表现的,而这些服务可能是由不同的团队开发的。 不管在什么时候,系统都应该是透明的、可观测的,这样既可以发现问题,也可以对问题进行诊断。

应用中的每个服务都会产生来自于业务、运营和基础设施的数据、应用日志以及请求记录。 因此,开发者需要搞清楚这些大量数据的含义。

4.自动化

通过开发大批的服务来缓解应用不断变大所带来的痛苦,这看似是有悖常理的。 事实上,相对于开发一个单体应用,微服务确实是一种更加复杂的架构。 通过采用自动化和在基础设施内保持服务之间的一致性,开发者可以极大地降低因这些额外的复杂性引入的管理代价。 开发者需要使用自动化来保证部署和系统运维过程中的正确性。

微服务架构的流行与两种趋势是同时发生的—— 一种趋势是DevOps技术得到主流接纳,其中的典型就是基础设施即代码(infrastructure-as-code)技术; 另一种趋势是完全通过API进行编程的基础设施环境(如AWS和Azure)的兴起。 这三者的同时发生并不是巧合。后两种趋势做了大量的基础工作,这才使得微服务在小型团队里具有可行性。

5.一致性

以恰当的方式调整开发工作是至关重要的。 开发者的目标应该是围绕业务概念来组织服务和团队,只有这样安排,服务和团队的内聚性才能更高。

SOA和微服务架构的对比

许多传统的SOA系统都是分别部署它们的技术层的——UI层、业务逻辑层、集成层和数据层。

image

一方面,在SOA中使用横向拆分是有问题的,因为这样会导致内聚的功能被分散到多个系统中。 新的功能可能需要协调发布到多个服务中,并且可能与其他在同一技术抽象层次的其他功能产生耦合,而这种耦合是不可接受的。

另一方面,微服务架构应该偏向于纵向拆分。 每个服务应该与一个独立的业务功能相匹配,并且将所有相关的技术层的内容封装在一起。

极少数情况下,构建一个实现了某个特定技术功能的服务也是合理的,比如多个服务都需要与某个第三方服务进行集成,那就可以将这一集成工作封装成一个服务。

向后兼容性

开发者应该时刻牢记是谁在消费这些服务。 为了保证系统的稳定性,开发者需要在开发过程中有足够的耐心来保持所开发服务的向后兼容性(不论是显式地兼容,还是同时运行多个版本的服务),这样就可以确保不需要强迫其他团队升级或者破坏服务之间已有的复杂交互。


软件系统的增长压力

压  力 描  述
规模 系统的处理规模可能大大超过了最初的技术选型的承载能力
新功能 新的功能可能和现有的功能关系并不紧密,或者换一种技术可能更容易解决问题
工程团队的壮大 随着团队越来越大,需要沟通的人和渠道也越来越多。新加入的开发者要花更多的时间来理解现有的系统,相应地,创造产品价值的时间也变少
技术债务 系统所增加的复杂度(包括之前的开发决策导致的债务)导致修改的难度提高
国际分发 国际分发使得数据一致性、可用性和延迟性面临巨大挑战

为什么微服务是一个明智的选择

1.技术差异性为微服务开路

有一些公司同时采用了多种不同的技术,这使得微服务成为很自然的选择。 即便开发者还没有完全决定使用微服务方案,运用微服务的一些原则也会让开发者在解决业务问题的时候有更大的技术选择范围。

2.开发冲突随着系统发展而增加

软件系统天生就具有复杂性,没有哪种方法论或者架构能够消除系统核心的这种本质复杂性(essential complexity)

可以通过采用恰当的开发方案来确保开发出的是一套良好的复杂系统,而从那种偶然的复杂性(accidental complexity) [6]中解脱出来。

软件开发的目标是持续地缩短交付周期来产生积极的商业价值。

冲突和风险会限制开发速度和敏捷性,进而会影响交付商业价值的能力。

随着单体应用越来越大,下面这些几个因素会导致冲突。

(1)变更周期耦合在一起,导致协作障碍并增大回滚的风险。

(2)在没有严格规范的团队中,软件模块和上下文边界含混不清,导致组件之间产生意料之外的紧耦合。

(3)应用的大小成为痛点:持续集成作业、系统发布(甚至是本地应用启动)都会变得越来越慢。

3.微服务降低冲突和风险

微服务通过三种方式来降低冲突和风险:

在开发阶段将依赖进行隔离和最小化;
开发者可以对单个的内聚组件进行思考,而非整个系统;
能够持续交付轻量的、独立的变更。

结论:

其一,开发小的、自治的服务能够降低在开发长期运行的复杂系统时出现的冲突;

其二,通过交付内聚的独立功能,开发者可以开发出一个面对变化时灵活、易扩展且具备可恢复性的系统,这有助于开发者在降低风险的同时交付商业价值。

成功的微服务迁移都是一点一点推进的,会在架构愿景、业务需求、优先级和资源约束之间进行平衡


微服务的挑战

过去的一些尝试(如SOA)已经被大家认为是不成功的。

没有哪一种技术是“银弹”,微服务架构,就极大地增加了系统中运行的模块的数量。在将功能和数据所有权分发到多个自治的服务上的同时,开发者也将整个应用的稳定性和安全操作的责任分配到了这些服务上。

设计挑战

1.划定微服务范围需要业务领域知识

每个微服务都只负责一个功能。 识别这些功能需要丰富的业务领域知识。 在应用生命周期的初期,开发者的领域知识充其量是不够完整的,而最糟糕的情况下,开发者了解的这些知识可能是错误的。

对问题领域理解不充分可能会导致错误的设计决定。 和单体应用中的模块相比,微服务应用的服务边界更加僵化。 这也就意味着,如果范围划定出错,可能给下游造成更高的代价:开发者可能需要在多个不同的代码库上进行重构;可能需要将数据从一个服务的数据库迁移到另一个服务中;可能没有发现服务间的隐式依赖,导致在部署阶段出现错误或者不兼容的问题。

错误地划分服务范围可能导致跨多个服务边界进行复杂且代价巨大的重构 image

但是,基于并不充分的业务领域知识做出设计决策的事情并不是微服务所独有的问题。区别只在于这些决策所造成的影响。

2.服务契约的维护

每个微服务都应该独立于其他服务的实现方式。这样才能实现技术多样性和自治性。 为了做到这一点,每个微服务应该对外暴露一个契约(类比于面向对象设计中的接口)——用于定义它所期望接受和返回的消息。一个良好的契约应该具有以下特点。

(1)完整:定义了交互所需的全部内容。

(2)简洁:除了必需的信息,没有多余的内容,这样消费者就能在合理的范围内组装消息。

(3)可预测:准确反映了所有实现的真实表现。

契约会成为服务之间的黏合剂。随着时间的推移,开发者会对契约逐渐做出调整,但是还需要保持对现有协作方的向后兼容性。稳定性和变化这两个矛盾体之间的紧张关系是很难把握分寸的。

3.微服务应用是多个团队设计的

每个团队负责不同的微服务,他们有自己的目标、工作方式和交付周期。如果开发者还需要和其他的独立团队协调时间表和优先级,就很难设计出一个内聚的系统。 因此,要协调任何庞大的微服务应用的开发,都需要跨多个团队在优先级和实践层面达成一致。

4.微服务应用是分布式系统

设计微服务应用也就意味着设计分布式系统

关于分布式系统的设计的谬论

网络是可靠的 网络延迟为0 带宽是无限的 数据传输成本为0


显然,开发者在非分布式系统中可以做出的那些假设(如方法调用的速度和可靠性)都不再合适,基于这些假设实现的系统会非常糟糕和不稳定。 开发者必须考虑到延迟性、可靠性以及应用中的状态一致性。

一致性难题

一旦应用成为一个分布式应用,应用的状态数据就会分布到许多地方—— 一致性就会成为难题。 开发者不再能保证操作的顺序。 在多个服务上进行操作时,开发者也不再能像ACID这样继续保证事务。 这还会影响到应用层面的设计:开发者需要考虑服务如何在不一致的状态下进行操作以及如何在事务失败的情况下进行回滚。

运维挑战

微服务方案本身会使系统中可能出现的故障点增多。

demo 提交出售订单时可能的故障点 可以看到,有些类型的故障可能会在多处发生。这些故障都会影响订单的正常处理流程。 image

生产环境中运行应用时,需要回答下面几个问题

(1)如果用户不能提交订单出现故障,如何判断是哪里发生了故障?

(2)如何在不影响下单操作的情况下部署一个新版本的服务?

(3)如何知道要调用哪些服务?

(4)如何在多个服务间测试应用是否正常工作?

(5)如果某个服务不可用,会发生什么事情?

1.难以实现的可观测性

在微服务应用中,透明性会变得更困难

这是因为开发者需要对整体有所了解。开发者需要将许许多多的碎片拼接起来形成整体的蓝图,所以需要将每个服务所生产的数据关联并连接到一起,进而在了解了交付商业价值整体的来龙去脉之后理解每个服务所做的工作。每个服务的日志提供了系统运行的部分视图,这是很有用的,但是开发者需要同时从微观细节和宏观整体两方面来更加全面地理解这个系统。

2.不断增加的服务使得故障点增多

任何可能出现故障的东西最终肯定会出现故障 开发者需要考虑如何让系统能够在单个组件出现问题的情况下继续运行。 这意味着,每个服务都需要更具鲁棒性(考虑到错误检查、故障切换、恢复), 同样,整个系统也应该运行更加可靠,即便单个组件做不到100%的可靠。


微服务开发生命周期

在个体层面,开发者应该熟悉每一个微服务——即便它比较小

在系统层面,选择微服务架构会对开发者设计和运行应用的方式产生重要影响。

微服务开发周期的三大迭代阶段:设计、部署和监控 image

每个阶段所做出的合理决定都有助于构建出具备可恢复性的应用

微服务设计

1.单体应用是否先行

虽然在开始的时候,微服务方案的开发速度会慢一些,但是能够降低未来开发的冲突和风险。同样,随着工具和框架越来越成熟,微服务最佳实践不再那么令人生畏,会变得越来越容易应用

2.服务的范围划定

为每个服务选择恰当水平的职责——功能范围——是设计微服务应用中最困难的挑战之一。开发者需要基于服务提供给组织的业务功能对其进行建模。

如果开发者想引入一个新的特殊类型的订单,如何对服务进行修改呢?开发者可以通过三种方式来解决这个问题: ①对现有的服务接口进行扩展;②添加一个新的服务接口;③添加一个新的服务。 image

每种方案都各有优缺点,也会影响到应用中各个服务之间的内聚和耦合性

3.通信

服务之间的通信可以是异步的,也可以是同步的。 虽然同步系统更易于进行问题排查,但是异步系统的解耦性更高,能够降低变更的风险,还能让系统更易于恢复。但是这种系统的复杂度比较高。 在微服务应用中,开发者需要在同步和异步消息之间进行平衡,以有效地对多个服务的行为进行协调。

4.可恢复性

在分布式系统中,一个服务不能完全信任它的协作方服务,这不一定是因为他们的代码很糟糕或者人为失误,还因为开发者不能想当然地认为服务之间的网络以及这些服务的行为是可靠的、可预测的。 服务在遇到故障的时候需要能够进行恢复。为了做到这一点,开发者需要通过在出现错误的时候进行回退、对于一些不友好的调用方要限制其请求速率、动态寻找健康服务等方式来使服务具有防御性。

微服务部署

在由大量的自治服务组成的系统中,如果这个服务是开发者开发的,就应该由开发者来运行它。 对服务的运行方式了解清楚,反过来有助于开发者在系统发展壮大以后做出更好的设计决策。

应用的特别之处是它所交付的商业价值。这来源于多个服务之间的协作。

开发者可以将每个服务所提供的特有功能标准化和抽象化,以保证团队聚焦于商业价值。

实现稳定可靠的微服务部署

1.微服务部署的人为操作标准化

完成这项工作最好的工具就是容器

容器将应用的打包过程、运行接口进行了标准化,并且为操作环境和代码提供了不可变(immutability)的特性。这使得它们成了在更高层次进行组合的强有力的构件。通过使用容器,开发者可以定义任何服务的完整执行环境并将它们相互隔离。

2.实现持续交付流水线

微服务的部署流水线概览

image

(1)制订一组软件必须通过的验证条件。在部署流程的每个环节,开发者都应该能够证明代码的正确性。

(2)代码从提交状态发布到生产环境上的流水线实现自动化。


服务监控

1.发现潜在的薄弱环节并进行重构

监控和报警系统使得开发者可以对问题进行诊断,并判断是什么问题导致了当次故障。 开发者可以通过自动化的机制来响应这些告警,比如在另一个机房创建一个新的容器实例,或者增加服务的运行实例的数量来解决负载问题。

为了将故障的影响最小化并避免在系统内产生连锁反应,开发者需要采用一些支持服务局部降级的方案来设计和调整服务间的依赖。即便一个服务不可用,也不应该导致整个应用垮掉。

2.了解数以百计的服务的行为

为了了解这些服务的行为,开发者需要在设计和实现这些服务时提高“透明性”的优先级。收集日志和一些数据指标,并将它们统一起来用于分析和告警。这样开发者在监控和分析系统的行为时,就可以诉诸于所构建的这个唯一的可信来源(single source of truth)。

开发者可以把每个服务看作一个“洋葱”。在“洋葱”的最里面,是这个服务所提供的特有业务功能。它的外面分别是各个工具层——业务指标、应用日志、运维指标和基础设施指标。这些工具可以让业务功能更易于观测。开发者可以在这些层之间跟踪每个请求,之后将从每层收集的数据推送到一个运维数据库用来进行分析和监控

一个业务功能的微服务由多个工具层所包围。请求会穿过这些工具层发送给微服务,而返回的结果也会穿过它们发送出去,这个过程中所收集的数据也会存储到一个运维数据库中 image

有责任感和运维意识的工程师文化

长期运行的系统并不仅仅是提出功能需求——然后进行设计、开发——最后把这些功能堆到一起,它还反映了开发者和运维人员的偏好、观点以及目标。

康威定律在某种层次上表达了类似的含义:

设计系统的组织……都是受到约束的,其设计出来的方案只是这些组织的沟通结构的翻版。

有意识地和组织结构相互依赖实现共生是一种很常见的微服务实践。为了能够从微服务中获益并充分地管理其复杂度,开发者需要制订一些对微服务应用有效的工作原则和做法,而不是继续采用以前开发单体应用时所使用的相同技巧。

WangShuXian6 commented 4 years ago

SimpleBank公司的微服务

业务范围 image

总览类型的领域模型都是不可或缺的第一步,但是在构建微服务时,这一步就显得至关重要。 如果不解业务领域,设计人员就可能在划定服务边界时做出错误的决策。 没有人希望自己所构建的服务是贫血的——只是执行些琐碎的增删改查(CRUD)操作。 这些贫血的服务通常是导致系统内部耦合严重的源头之一。 同时,我们要避免将太多的责任放到一个服务中,低内聚的服务会使得修改软件时效率更低,风险更大——而这恰恰是我们试图避免的。

最后,如果没有这种判断能力,开发者就可能成为过度工程化的牺牲品,他可能是盲目地选择了微服务的架构方案,而并不是以“产品或者业务领域的实际复杂性”为依据。

微服务是否是正确的选择

软件架构设计总是牵涉到现实主义和理想主义的矛盾和冲突——要在产品需要、发展压力、团队能力这些方面进行平衡。 错误的选择并不会立刻显现出后果

选择微服务架构时所要考虑的影响因素

影 响 因 素 影  响
业务领域复杂度 客观评估业务领域的复杂度是件很困难的事情,但是微服务能够解决受竞争压力所影响的系统复杂性问题,比如监管需求和市场范围
技术需求 开发者可以使用不同的编程语言(以及对应的技术生态)来开发系统的不同组件。微服务使得技术选型更具多样化
组织成长 快速成长的工程团队能够受益于微服务架构,因为在微服务架构下,对于已有代码库的依赖更小,新工程师可以快速得到提升,工作得更加高效
团队知识 许多工程师缺乏微服务和分布式系统的经验。如果团队缺少自信或者这方面的知识,最好在完全承诺实现之前,先构建一个用于概念验证的微服务

开发新功能

为了确保团队理解了微服务风格的要求和约束,开发一个最小可行产品(Minimum Viable Product,MVP)是非常重要的第一步。

如何开发这个功能。开发者需要回答下列3个问题。

(1)需要开发哪些服务。

(2)这些服务之间彼此如何合作。

(3)如何将功能公开出去。

SimpleBank公司为账户下单出售股票的流程 image

每个服务应该只负责一个功能。那么第一步就是确定需要实现的不同业务功能以及它们之间的相互关系。

通过领域建模识别微服务

为了确定所需要的业务功能,开发者需要提高对所开发软件的业务领域的了解程度。 这通常是产品发掘或者业务分析中最难的工作:调查研究,原型设计,与客户、同事或其他终端用户进行访谈等。

下单的业务功能

(1)记录出售订单的状态和历史。

(2)向客户收取下单的手续费。

(3)在客户的账户记录交易信息。

(4)将订单提交到市场上。

(5)向客户提供所持股份和订单的价值信息。

应用功能与SimpleBank公司的业务能力的对应关系

image

并不是要绝对化地将每个功能都映射为单个微服务。

服务应该通过区分修改原因来推动松耦合和单一职责

开发者在开始时可以直接将这些能力映射到微服务。每个服务应该体现业务所提供的能力,这样也就能实现体积和职责的平衡。开发者还应该思考一下有哪些推动微服务在未来进行变化的因素——它是不是真的只有单一职责。比如,开发者可能认为市场操作是订单管理的子集,因此不应该分成两个服务。但是市场操作这个领域变化的驱动因素是所支持的市场的功能和范围,而与订单管理关联更紧密的是产品的类型以及进行交易的账户。这两个领域并不会同时变化。将这两块分开后,就区分了变化范围并且能最大限度地提升内聚性

image

分解过细一方面会导致服务本身缺乏内聚,另一方面也会导致那些关联比较紧密的协作服务之间耦合过紧。同样,部署和监控太多的服务也会超出处于微服务实践初期的工程团队的工作能力。一条有用的经验就是宁可选择较大一些的服务,等功能变得更加特殊或者更加明确属于一个独立的服务时,再将功能从中拆分出去,这样会容易很多。

服务协作

服务协作可以是点到点方式的,也可以是事件驱动方式的。点到点的通信通常是同步的,而事件驱动的通信通常是异步的。

同步通信方式

(1)同步调用通常要比异步通信更加简单而且更便于排查分析。即便如此,也不要错误地认为它们和本地的进程内的函数调用有同样的特性——跨网络的请求明显要慢很多而且更加不可靠。

(2)即便不是所有编程环境,至少大部分都已经支持一种简单、与语言无关而又在开发者中有广泛认知度的传输机制:HTTP。HTTP主要用于同步调用,但是也可以将其用于异步调用。

order服务控制其他服务的动作以将订单提交到市场 image

1.服务契约

每个服务所接收的消息以及它返回的响应构成了服务与依赖该服务的上游协作服务之间的契约

随着服务数量的不断增多,开发者会意识到以机器可读的格式将服务交互的接口标准化的显著好处,比如,REST API可以使用Swagger/OpenAPI。同样,为每个服务加强一致性测试和发布标准化的契约,能够帮助组织机构中的工程师了解如何使用这些服务。

2.服务职责

order服务有许多职责。它直接操控下单流程所涉及的每个服务的动作。 从概念上讲这很简单,但是也有不利的一面。 最差的情况下,那些被调用的服务会变成“贫血式”的服务,大部分傻瓜式的服务被少数的聪明服务所控制,而这些聪明的服务越来越大。

这种方式会导致服务间耦合越来越紧。 如果开发者想要在下单流程中引入新的内容——比如想要在下的订单额度比较大时,通知客户的账户经理——开发者就必须将这部分修改部署到order服务中。 这增加了修改的代价。 理论上,订单服务不需要同步确认这一操作的结果——只要它收到了一个请求——它不需要了解下游的具体操作。

服务编排

在微服务应用中,服务自然会有不同层次的职责。但是开发者应该在编排(choreography)和编配(orchestration)之间进行平衡。在编排式的系统中,服务不需要直接向其他服务发送命令和触发操作。相反,每个服务拥有特殊的职责,也就是对某些事件进行响应和执行操作。

调整

(1)当有人创建订单时,可能还没开市,所以开发者需要记下订单的状态:创建成功,还是提交成功。订单提交发布的步骤并不需要同步。

(2)只有订单提交成功,才会收取手续费,所以收取手续费的步骤也不需要同步。实际上,应该是响应market服务进而执行收费操作,而不是由order服务来安排收费。

通过事件来对各个服务的功能进行编排,弱化order服务的协调者角色 image

事件的增加也相应地增加了架构上要关注的内容:开发者需要采取某些方法来保存这些事件并将它们开放给其他系统。为此,我们建议使用RabbitMQ或SQS这样的消息队列。

在这个设计方案中,我们从order服务中移除了如下职责。

(1)收费——order服务并不知道订单提交到市场以后还需要收费。

(2)下单——order服务并不直接与market服务进行交互。这样,开发者可以很容易地用一种不同的实现方案进行替换,甚至于都可以改成每个市场对应一个服务,而不需要对order服务进行任何修改。

order服务自身也需要通过订阅market服务发送的OrderPlaced事件来对其他服务的动作进行反应。开发者可以很容易地对其扩展来满足未来的需求,比如order服务可以订阅TradeExecuted事件来记录市场上这笔交易完成的时间,如果这笔交易没能在指定的时间内成交,order服务也可以订阅OrderExpired事件。

这一方案要比之前的同步协作的方案复杂很多,但是,在尽可能的情况下,采用这种编排的方案所开发的服务相互之间都是解耦的,相应地,也就可以更独立地部署这些服务,修改这些服务也更容易。但是有得就有失,我们也要付出一些代价:基础设施的队伍中又多了一个消息队列,我们需要持续地对其进行管理和扩容,并且它会成为一个单点故障源。

我们提出的这一设计方案还可以提高系统的可恢复性,比如,market服务出现的故障与order服务的故障是相互隔离的。如果订单发布失败,则可以晚一些等market服务恢复正常以后重新发送这个事件 [3];如果发送的次数过多,则可以直接终止。此外,采用这一方案会使得对系统的整个活动轨迹的跟踪变得困难很多,在考虑如何在生产环境监控这些服务时,工程师要考虑到这一点。


向外界开放服务

API网关,

将其作为底层各个服务对外的门面。这个网关会将各种各样的后端问题抽象化, 这样,这些前端应用既不再需要了解底层的这些微服务的存在, 也不需要了解为了完成各项功能这些微服务相互之间的交互方式。 API网关会作为代理接管那些发给底层服务的请求, 然后将底层服务的响应结果根据公开API的需要转换或者合并成新的数据格式。

用户界面(如网页端和移动端App)与API网关暴露的REST API进行交互。网关为底层的微服务提供了一个统一的对外门面并将请求代理给对应的后端服务 image

API网关模式是一种很简洁的方式,但是它也有一些缺点,因为作为众多底层服务唯一的组合对外入口,它会变得越来越大,而且可能会越来越笨重。 它会诱使开发者将业务逻辑添加到网关中,而非仅仅将其作为一个代理来对待。 它试图成为无所不能的可用于所有应用的服务,却又饱受其苦,毕竟不同应用的需求各有不同:移动客户端应用希望返回的数据体积更小、更精简,网页版的内部管理系统所需要的数据却又多得多。 在开发高度内聚的API时,开发者要同时平衡这些相互冲突是非常困难的。


将功能发布到生产环境中

将每个服务部署到一组虚拟机中。开发者可以使用负载均衡器来将请求均匀地分摊到每个网络服务的实例中, 或者开发者可以使用托管的事件队列服务(如AWS的Simple Queue Service)来让各个服务互相分发事件消息

在简单的微服务部署方案中,每个服务的请求都通过负载均衡器分发到不同的实例——这些实例运行在不同的虚拟机中。 同样,服务的多个实例都订阅了事件队列 image

确信服务是值得信任的

(1)可靠性——服务是否可用且没有错误呢?开发者可以依靠其部署流程来上线新功能而不引入缺陷或者导致服务不可靠吗?

(2)可扩展性——开发者了解服务所需要的资源和容量吗?如何在负载下保持响应能力呢?

(3)透明性——开发者是否可以通过日志或者数据指标来观测运行中的服务呢?

(4)容错性——开发者是否解决了单点故障的风险?如何应对所依赖的其他服务出现的故障?

在微服务生命周期的初期,开发者需要确立这三大基本准则:高质量的自动化部署、可恢复性和透明性。

高质量的自动化部署

传统的组织机构通常会通过引入比较官方的变更控制和审批流程来谋求稳定性。 这些流程的目的是管理和限制变更。这并不是盲目的冲动行事:如果这些变更引入的大部分bug 动辄会给公司造成成千上万乃至数百万的工程投入或者收入上的损失,那么开发者就应该严格控制这些变更。

在微服务架构中,这种方式就不可行了,因为整个系统处于一个持续演进的状态中——正是这种自由带来了实实在在的创新。 但是为了确保这种自由不会导致系统出现错误或者不可用,开发者就需要能够做到充分信任开发流程和部署工作。 同样,为了使这种自由处于第一优先级,开发者还需要尽可能地减少发布一个新服务或者修改一个已有服务所需要投入的工作。>我们可以通过标准化和自动化来实现稳定性的目标。

(1)将开发过程标准化。开发者应该评审代码的改动、编写对应的测试代码以及维护源代码的版本控制。但愿没有人对此要求表示意外或感到奇怪。

(2)将部署过程标准化和自动化。开发者应该彻底地验证所要提交到生产环境的代码变更,且要保证部署过程不需要工程师的介入,也就是说,要做成部署流水线。

可恢复性

想要确保软件系统在面对故障时是可恢复的,这是一项很复杂的任务。 系统之下的基础设施本来就是不可靠的,即便代码是完美无缺的,网络调用也会失败,服务器也会宕机。 作为服务设计的一部分,开发者需要思考服务本身以及服务的依赖项会怎样出现故障,然后提前做些工作来避免这些故障场景或者尽可能降低这些故障的影响。

SimpleBank公司的微服务应用的风险领域

领域 可能发生的故障
硬件 主机、数据中心组件、物理网络
服务间通信 网络、防火墙、DNS错误
依赖 超时、外部依赖、内部故障,比如数据库

透明性

微服务的行为和状态应该是可观测的。 不管在什么时候,开发者都应该能够判断服务是否健康以及处理请求是否符合预期。 如果某些内容影响到了某个关键指标——比如,订单提交到市场的时间过长——那么系统应该向工程师团队发送告警——这个告警需要有足够的理由才能发送。

有很多底层原因会导致服务超时:网络问题、服务内部依赖的问题(如数据库) 以及其他服务不正常的操作 image

开发者需要为微服务添加一套全面的工具。收集应用各个层面的活动数据对于了解微服务应用当前以及过去的运行表现是至关重要的。

搭建一套对微服务产生的基础日志进行聚合的基础系统,同时还要将这些数据发送到某个服务便于开发者进行查询和标记

开发者为每台虚拟机安装一个日志收集的代理应用。这个应用会将应用日志数据传到一个中心化的日志仓库中。开发者可以为日志创建索引、搜索,并能对其做进一步分析

image

应该有一套告警方案来确保所有服务都符合响应要求和服务目标

在这种场景中,最简单的方式就是,开发者应该有一套作用于每个微服务的反复执行的心跳检测机制,一旦服务完全没有响应,心跳检测就可以向团队发送告警。

除此之外,团队还应该对每个服务做出服务保证的承诺, 比如,对于关键服务,开发者会保证在99.99%的可用性基础上,95%的请求能够在100毫秒内返回。 如果没有达到这些阈值,就应该向服务所有者发送告警。

一套成熟的微服务监控方案还会处理业务指标、服务间链路追踪和基础设施指标。如果开发者想要信任自己的服务,就需要不断地研究这些数据的含义。


大规模微服务开发

技术分歧

虽然微服务使得不同的服务可以选择不同的语言和框架,但是我们也很容易明白,不选择一套合理的标准和限制,系统会杂乱和脆弱得难以想象。

很容易注意到,这种因没有统一规范而导致的挫折在规模较小的系统中就已经出现了。考虑下这两个服务——account transaction服务和order服务——它们分别是由两个不同的团队负责的。account transaction服务会为每个请求会生成结构良好的日志输出,其中包含有用的诊断信息,诸如计时、请求ID以及当前发布的修订ID。

service=api

git_commit=d670460b4b4aece5915caf5c68d12f560a9fe3e4

request_id=55f10e07-ec6c

request_ip=1.2.3.4

request_path=/users

response_status=500

error_id=a323-da321

parameters={ id: 1 }

user_id=123

timing_total_ms=223

第二个服务生成的则是难以解析的纯收文本消息:

Processed /users in 223ms with response 500

开发者会注意到,即便是最简单的日志消息格式,一致性和标准化也能够让在不同服务之间进行问题诊断和请求跟踪更加容易。通过在微服务系统的所有层次上达成一种合理的标准来解决分歧和杂乱扩展的问题是至关重要的。


孤立

在采用微服务方式的组织机构中,逆康威法则也是成立的:公司的结构是由产品的架构决定的。

开发团队会越来越趋向于微服务:他们会高度专业化地完成一件工作。每个团队只拥有或者负责少数几个关系密切的服务。总的来说,开发者将知道有关系统的所有信息。但是具体到每个开发者,他们只熟悉一个狭窄的专业领域

微服务本身的价值有限,不能孤立地发挥作用。因此,这些独立的团队必须紧密协作来构建无缝运行的应用程序,即使他们作为一个团队的整体的目标可能只与他们自己负责的更窄领域有关。同样,这种关注的狭窄性会使得团队容易只对他们本地局部的问题和参数设置进行优化,而非考虑整个组织机构的需求。极端情况下,这会导致团队之间发生冲突,进而降低部署速度以及产品的可靠性。


小结

(1)微服务非常适合于有多维复杂性的系统,比如产品的供应范围、全球部署和监管压力。

(2)在设计微服务时,了解产品业务领域是至关重要的。

(3)服务交互可以是编配型的,也可以是编排型的。后者会增加系统复杂度,但是能够降低系统中服务之间的耦合度。

(4)API网关是一种常见的模式,它将微服务架构的复杂性进行了封装和抽象,所以前端或者外部消费者不需要考虑这部分复杂度。

(5)如果开发者充分信任自己的服务能够处理生产环境上的流量压力,就可以说这个服务是生产就绪的。

(6)如果开发者可以可靠地部署和监控某个服务,就可以对这个服务更有信心。

(7)服务监控应该包括日志聚合以及服务层次的健康检查。

(8)微服务会因硬件、通信以及依赖项等原因而出现故障,并不是只有代码中的缺陷才会导致故障发生。

(9)收集业务指标、日志以及服务间的链路跟踪记录对于了解微服务应用当前和过去的运行表现是至关重要的。

(10)随着微服务以及支持团队的数量的不断增加,技术分歧以及孤立会日渐成为技术团队的挑战。

(11)避免技术分歧和孤立需要在不同团队间采用相似的标准和最佳实践,不管采用何种技术基础。

WangShuXian6 commented 4 years ago

设计

微服务应用的架构

整体架构

软件设计师希望所开发出来的软件是易于修改的。 想要做到这一点,开发方式就应该减少摩擦并将风险降至最低。

从单体应用到微服务
单体应用

在单体应用中,主要交付的就是一个应用程序。 这个应用程序可以被水平地分成几个不同的技术层。 在典型的三层架构的应用中,它们分别是数据层、业务逻辑层和展示层。 应用又会被垂直地分成不同的业务领域。 MVC模式以及Rails和Django等框架都体现了三层模型。 每一层都为其上一层提供服务:数据层提供持久化状态,业务逻辑层执行有效操作,而展示层则将结果展示给终端用户。

典型的单体应用三层架构 image

微服务

单个微服务和单体应用是很相似的:微服务会存储数据、执行一些业务逻辑操作并通过API将数据和结果返回给消费者。 每个微服务都具备一项业务能力或者技术能力,并且会通过和其他微服务进行交互来执行某些任务。 单个服务的抽象架构如图 image

在单体应用中,架构限定在整个应用本身的边界内; 而在微服务应用中,开发者是在对从规模到范围都在不断演变的内容进行规划。

用城市作类比的话,开发一个单体应用就像建造一幢摩天大厦, 而构建微服务应用则像开发一个社区:开发者需要建造基础设施(自来水管道、道路交通、电路线缆),还要规划未来的发展(小型企业区vs. 住宅区)。

这个类比强调不仅要考虑组件自身,还要考虑这些组件之间的连接方式、放置位置以及如何并行地构建它们。 开发者希望自己的方案能促使应用沿着良好的方向发展,而非强行规定或强迫应用采取某种结构。

微服务并不是孤立地运行的。 每个微服务都会和其他的微服务一起共存于一个环境中,而我们就在这个环境中开发、部署和运行微服务。 应用架构应该包含整个环境。

架构师的角色

架构师或者技术负责人的工作就是要确保系统能够不断演进,而不是采用了固化的设计方案。

如果微服务应用是一座城市的话,开发者就是市政府的规划师。

架构师的职责是确保应用的技术基础能够支持快节奏的开发以及频繁的变化。 架构师应该具备纵观全局的能力,确保应用的全局需求都能得到满足,并进一步指导应用的演进发展。

(1)应用和组织远大的战略目标是一致的。

(2)团队共同承担一套通用的技术价值观和期望。

(3)跨领域的内容——诸如可观察性、部署、服务间通信——应该满足不同团队的需要。

(4)面对变化,整个应用是灵活可扩展的。

架构师应该通过两种方式来指导开发: 第一,准则——为了实现更高一层的技术目标或者组织目标,团队要遵循的一套指南; 第二,概念模型——系统内部相互联系以及应用层面的模式的抽象模型。

架构准则

准则是指团队为了实现更高的目标而要遵循的一套指南(或规则)

准则是灵活的,它们可以并且应该随着业务优先级的变化以及应用的技术演进而变化。 例如,早期的开发过程会将验证产品和市场需求的匹配度作为更高优先级的工作,而一个更加成熟的应用可能需要更专注于性能和可扩展性。

微服务应用的4层架构

(1)平台层——微服务平台提供了工具、基础架构和一些高级的基本部件,以支持微服务的快速开发、运行和部署。一个成熟的平台层会让技术人员把重心放在功能开发而非一些底层的工作上。

(2)服务层——在这一层,开发的各个服务会借助下层的平台层的支持来相互作用,以提供业务和技术功能。

(3)边界层——客户端会通过定义好的边界和应用进行交互。这个边界会暴露底层的各个功能,以满足外部消费者的需求。

(4)客户端层——与微服务后端交互的客户端应用,如网站和移动应用。

上述架构层次如图3.4所示。不管底层使用了什么技术方案,开发者应该都能够将它应用到所有微服务应用中。

image

每一层都是建立在下一层次的功能之上的,比如,每个服务都会利用下层的微服务平台提供的部署流水线、基础设施和通信机制。要设计良好的微服务应用,需要在每个层级上都进行大量的投入并精心设计。

微服务平台

微服务需要由如下基础设施提供支持。

(1)服务运行的部署目标,包括基础设施的基本元件,如负载均衡器和虚拟机。

(2)日志聚合和监控聚合用于观测服务运行情况。

(3)一致且可重复的部署流水线,用于测试和发布新服务或者新版本。

(4)支持安全运行,如网络控制、涉密信息管理和应用加固。

(5)通信通道和服务发现方案,用于支持服务间交互。

image

一个具有鲁棒性的平台层既能够降低整体的实现成本,又能够提升整体的可稳定性,甚至能提高服务的开发速度。如果没有平台层,产品开发者就需要重复编写大量的底层的基础代码,无暇交付新的功能和业务价值。一般的开发者不需要也不应该对应用的每一层的复杂性都了然于胸。基本上,一个半独立的专业团队就可以开发出一套平台层,能够满足那些在服务层工作的团队的需求。

映射运行时平台

运行时平台(或者部署目标)——比如,像AWS的云环境或者像Heroku这样的PaaS平台——提供了运行多个服务实例以及将请求路由给这些实例的基础元件。除此之外,它还提供了相应的机制来为服务实例提供配置信息——机密信息和特定环境的变量。

开发者在这一基础之上来开发微服务平台的其他部分。观测工具会收集服务以及底层基础设施的数据并进行修正。部署流水线会管理这一应用栈的升级或回滚

在标准的云环境中运行微服务所需的部署配置

image

服务层

服务所存在的地方。在这一层,服务通过交互完成有用的功能——这依赖于底层平台对可靠的运行和通信方案的抽象,它还会通过边界层将功能暴露给应用的客户端。 我们同样还会考虑将服务内部的组件(如数据存储)也作为服务层的一部分。

业务特点不同,相应服务层的结构也会差异很大。 常见的模式:业务和技术功能、聚合和多元服务以及关键路径和非关键路径的服务。

功能

开发者所开发的服务实现的是不同的功能。

(1)业务能力是组织为了创造价值和实现业务目标所做的工作。划到业务功能的微服务直接体现的是业务目标。

(2)技术能力通过实现共享的技术功能来支持其他服务。

SimpleBank的order服务公开了管理下单的功能——这是一个业务功能; 而market服务是一个技术功能,它提供了和第三方系统通信的网关供其他服务(比如,market服务公开了市场信息数据或者贸易结算功能)

实现业务功能和技术功能的微服务 image

聚合与多元服务

在微服务应用的早期阶段,多个服务可能是扁平化的,每个服务的职责都是处于相似的层次的

随着时间的推移,这两种压力会导致服务出现层级结构的分化。靠近系统边界的服务会和某些服务交互以聚合它们的输出——我们将这种服务称为聚合器(aggregator)(图3.8),除此之外,还有些专门的服务会作为协调器(coordinator)来协调下层多个服务的工作。

聚合器服务通过将底层服务的数据进行关联来实现查询服务,协调器服务会向下游服务发出各种命令来编配它们的行动 image

在出现新的数据需求或者功能需求时,开发者要决定是开发一个新的服务还是修改已有的服务,这是开发者所面临的重大挑战。创建一个新的服务会增加整体的复杂度并且可能会导致服务间的紧耦合,但是将功能加到现有的服务又可能会导致内聚性降低以及难以替换。这是违背了微服务的基本原则的。

关键路径和非关键路径

随着系统的不断演进,有一些功能对顾客的需求和业务的成功经营来说越来越重要。比如,在SimpleBank公司的下单流程中,order服务就处于关键路径。一旦这个服务运行出错,系统就不能执行客户的订单。对应地,其他服务的重要性就弱一些。即便客户的资料服务不可用,它也不大会影响开发者提供的那些关键的、会带来收入的部分服务。

关键路径上的服务越多,系统出现故障的可能性就越高。因为不可能哪个服务是100%可靠的,一个服务累积的可靠性是它所依赖的那些服务的可靠性的乘积。

但是微服务使得我们可以清楚地确定这些路径,然后对它们单独处理,投入更多的精力来尽可能提高这些路径的可恢复性和可扩展性,对于不那么重要的系统领域,则可以少付出一些精力。

通信

通信是微服务应用的一个基本要素。微服务相互通信才能完成有用的工作。微服务会向其他微服务发送命令通知和请求操作,开发者选择的通信方式也决定着所开发的应用的结构。

在微服务系统中,网络通信也是一个很主要的不可靠因素

何时使用同步消息

同步消息通常是首先会想到的设计方案。它们非常适合于那些在执行新的操作前需要获取前一个操作的数据结果或者确认前一个操作成功还是失败的场景。

请求-响应的同步消息模式