微服务实战 - [摩根・布鲁斯] 学习笔记

注:本文为个人学习笔记,与OSCHINA作者EWind911是同一人。

第一部分 概述

第 1 章 微服务的设计与运行

1. 微服务既是一种架构风格,也是一系列文化习惯的集合。它以五大核心原则为支撑:

自治性:每个服务的操作和修改以及部署都是独立的,每一个服务都应该具有高内聚低耦合的特性。

可恢复性:即使出现故障,也只会影响系统的部分功能。这就把故障隔离到了具体的微服务上,利于快速的修复。

透明性:开发者任何时候都可以知道程序正在做什么,这就意味着需要需要有明确的运行数据、应用日志、请求记录等来表明服务正在干什么。

自动化:通过采用自动化和在基础设备内保持服务之间的一致性,开发者可以极大地降低因这些额外的复杂性引入的管理代价。开发者需要使用自动化来保证部署和系统运维过程中的正确性

一致性:开发者的目标应该是围绕业务概念来组织服务和团队,只有这样,服务和团队的内聚性才高。

2. 微服务减少了开发冲突,实现了自治性,技术灵活性以及松耦合

3. 微服务的设计过程需要丰富的业务领域知识,开发者在团队之间的平衡优先级

4. 服务向其他服务暴露契约。

设计良好的契约应该是简介的、完整的和可预测的。例如 REST API

5. 设计良好的微服务应该持续地在系统中交付价值

6. 自动化和可验证的发布操作能够让部署过程更加可靠和没有 “意外” 发生,进而降低风险

7. 故障是不可避免的。

微服务设计应该是透明的,易观测的,这样团队才能主动地管理、了解和真正的拥有服务运维

8. 采用微服务的团队需要在运维方面比较成熟

团队需要关注于服务的整个生命周期,而不是只关注设计和开发阶段。

第 2 章 SimpleBank 公司的微服务

为了更好的诠释微服务,全书围绕 SimpleBank 这样一个投资领域的公司来进行阐述。SimpleBank 团队希望,不管一个人有多少钱,他都能够享受到智能化的金融投资服务。他们相信,不管是购买股票、出售基金还是外汇交易,都应该像开储蓄账户一样简单。下面是 SimpleBank 公司的功能模型:

在设计 SimpleBank 的微服务时,需要注意以下几点:

1. 为什么选择微服务?微服务非常适用于多维复杂性的系统

在 SimpleBank 公司,考虑到传统金融软件的风险和惰性,不能够持续的交付新功能以及巨大的开发、运行成本。采用微服务能够减少阻力和持续交付价值。

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

根据业务领域拆分,我们可以大致的把微服务分成 user(用户)、order(订单)、accountTransaction(交易记录)、fee(费用)、market(交易市场)。

3. 除了微服务还需要什么?API 网关是一种常见的模式,他将微服务架构的复杂性进行了封装和抽象

前端不需要知道微服务干了什么。前端只需要访问网关,就能得到自己想要的东西,不需要考虑微服务的复杂度。

4. 如何让新功能上线?如果开发者信任自己的服务能够处理生产环境上的流量压力,就可以说这个服务是生产就绪的

为了实现生产就绪,开发者应该考虑如下几个方面:

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

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

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

容错性:开发者是否解决了单点故障的风险?如何应对依赖的服务出现的故障?

5. 怎么样保证服务正常运行?服务监控应该包裹日志聚合以及服务层次的健康检查和告警

第 1 章也说到,微服务应该保证服务的透明性,时时刻刻都可以知道系统正在干什么。为什么需要健康检查和告警?健康检查可以代替开发者的专注力,在服务运行时,自动检查服务是否健康,并在出错时第一时间通知开发者对错误进行处理。例如:order 微服务直接对接了用户的下单需求,所以要求 95% 的请求都在 100ms 内返回结果。超过 5% 没有到达阈值,就证明服务正在以一种不健康的状态运行,可能是服务器压力过大,可能是数据库过载亦或是代码导致的运行错误。此时需要马上有人工介入来检查并解决问题。

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

开发者应该在开发时,就考虑到这些情况,并且做一定的补偿机制让出现错误的数据在系统恢复正常后依旧能归正,而不是数据丢失。

7. 收集业务指标、日志以及服务间的链路追踪记录对于了解微服务应用当前和过去的运行表现时至关重要的

8. 随着微服务以及支持团队的数量不断增加,技术分歧以及孤立会日渐成为技术团队的挑战,不论采用任何技术基础都应该避免技术分歧和孤立

技术分歧:不同的团队,使用的技术和规范不一致。维护和支持多种不同方案所付出的努力是相当巨大和令人恐惧的,应该拒绝这样的行为。达成一种合理的标准来解决分歧和杂乱扩张的问题是至关重要的。

孤立:开发团队即使只负责自己相关的微服务,也应该了解整个功能、产品的相关信息。不同的团队之间必须紧密协作来构建无缝运行的应用程序。

第二部分 设计

第 3 章 微服务应用的架构

1. 微服务应用和单体应用的区别

单体应用像是一栋摩天大厦。而微服务应用像是一个社区,开发者需要建造基础设备(自来水管道、道路交通、电路线缆),还要规划未来的发展。它最终的样子不是指定好的,而是由一系列的准则和高层的概要模型来指导实现的。

单个的微服务单体应用还是很相似的:微服务会存储数据,执行一些业务逻辑操作并通过 API 将数据和结果返回给消费者。

2. 指导微服务架构的准则会反应出组织的目标并对团队的实践产生影响

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

准则是指团队为了实现更高的目标而要遵循的一套指南或规则。它用于知道团队如何实践。

  • 开发实践必须符合那些公认的外部标准(如 ISO2)
  • 所有数据必须是可转移的,并且在存储数据的时候要有有效期限制
  • 必须要能够在应用中清晰的跟踪和回溯追查个人信息

3. 微服务应用由四层组成:平台层、服务层、边界层和客户端层

  • 平台层:为微服务平台提供了工具、基础架构和一些高级的基础部件,以及支持微服务的快速开发、运行和部署。可以理解为用于部署的流水线、用于监控的日志服务、用于承载微服务的服务器等工具。

  • 服务层:各个微服务在这里借助平台层的支持来相互作用,以提供业务和技术功能。
  • 边界层:客户端会通过定义好的边界和应用进行交互。边界会暴露底层的各个功能,以满足外部消费者的需求。比如 api 网关、服务于前端的后端(BFF)、消费者驱动网关(如 GraphQL)。

  • 客户端层:与微服务后端交互的客户端应用,如网站和移动应用。

4. 微服务通信:同步通信与异步通信

  • 同步通信通常是微服务开发的第一选择,它很适合命令型的交互。但是会增加微服务间的耦合性和不稳定性。它非常适合用于那些在执行新的操作前,需要获取前一个操作的数据结果或者确认前一个操作成功或失败的场景。
  • 异步通信更加灵活,能够适应快速的系统演化并降低微服务间的耦合性。但会让微服务应用的复杂度增加。它适用于当前服务不需要了解下游的消费者时的场景。异步通信常常需要一个通信代理,常用的工具如 Kafka、RabbitMQ 和 Redis 等。

5. 客户端由愈发臃肿庞大的风险,同样可以把微服务的原则用于前端应用 -- 微前端

随着前端应用的不断发展,它们开始和大规模的后端开发一样面临协作和摩擦问题。如果可以像拆分后端一样拆分前端一样,那就太好了。

但是这样做仍然面临许多新的挑战:不同的组件间保持视觉和交互的一致性需要很大的精力来开发和维护;当需要从多个源头加载 JavaScript 时,Bundle 的大小很难管理,它会影响加载时间;接口重载和重绘可能会导致整理的性能变差。虽然为前端不是很普遍,但是人们还是使用了一些技术方案来解决:

  • Web 组件通过清晰的、事件驱动的的 API 来提供 UI 片段
  • 使用客户端包含(client-side include)技术来集成片段
  • 使用 iframe 来将微 app 放置到不同的屏幕区域
  • 在缓存层使用 ESI(edge side include)来集成组件

第 4 章 新功能设计

1. 新功能需求

在上一篇文章微服务实战 - [摩根・布鲁斯] 学习笔记 part1 和前面的内容,我们已经设计了 SimpleBlank 的基础应用。现在 SimpleBlank 公司发现大部分客户并不想由他们自己来选择投资产品,更愿意让公司替代他们来做这份苦差事。那么我们接下来就设计一下如何开发新功能。这个新功能建立在第 2 章和第 3 章讨论的一部分服务的基础上:order 服务、user 服务、market gateway 服务、account transaction 服务、fee 服务、market data 服务。

SimpleBank 公司的客户是想要通过预付费或者定期支付的方式来投资,而且希望能够在一段时间后能够看到他们财富有所增长,或者达到某个目标,比如攒够了购房定金。就目前而言,SimpleBank 公司的客户需要自行选择如何利用自己的钱来进行投资 -- 即便他们对
投资一无所知。无知的投资者可能选择有高预期回报的资产,却没有意识到预期回报越高通常意味着风险也就越高。

为了解决这个问题,SimpleBank 公司会让用户从许多预先制订好的多种投资策略(investment stategy)中选择一种,然后以客户的名义进行投资。投资策略取决于不同的资产类型债券、股票、基金等的占比情况), 这些资产的比例是根据风险水平和投资时间来设计的。客户向自己的账户充值以后,SimpleBank 会自动将这笔钱按照对应的策略来进行投资。这个过程可以用下图来概括。

2. 新功能划分

在设计这个新功能应该归属于哪一个服务,我们可以按照以下几个方法进行划分:

  • 按业务功能划分

业务能力是指组织为了创造价值和实现业务目标所做的事情。现目前,我们对 Simple Blank 公司的业务能力:订单管理、分类交易账簿、费用收取和向市场下单这些业务进行了区分,如图:

业务能力与领域驱动设计(Domain-Driven Design,DDD)方法有着紧密的联系。其中最有用的概念就是引入限界上下文(bounded context)。一个领域的任何解决方案都是由若干个限界上下文组成。用 user 服务举例,它不需要关心 user 在创建订单时,会发生的任何行为。它只关心用户的注册、登录、用户信息的修改等。

想要按照业务能力来设计服务的话,最好从一个领域模型开始。领域模型就是对限界上下中业务所要执行的功能以及所涉及的实体的描述。一个简单的投资策略包含两部分:名称和一组按百分比分配的资产。SimpleBank 公司的管理员负责创建策略。下图初步列出了这些实体。

这些实体的设计模型图会有助读者理解服务所要拥有和保存的的数据。只有 3 个实体,另外,我们至少已经确定了两个新的服务:用户管理 (usermanagement) 和资产信息 (asset information)。用户 (user) 和资产 (asset) 实体分别属于截然不同的两个限界上下文。我们可以将策略和账户关联起来,然后用他们来生成订单。账户和订单时两个截然不同的上下文,但是投资策略既不属于账户上下文,也不属订单上下文。当策略发生变化时,这个变化不会影响账户和订单他们自己的功能。所以我们需要一个新的服务,上下文与现有功能的关系如图:

  • 按用例划分

这种方式在以下场景中非常有效:功能并不明确属于特定领域,或者需要和多个业务领域交互;所需要实现的用例非常复杂将其放到其他服务中会违背单一责任的原则。

我们可以将客户的需求理解为按照投资策略下单:客户可以将他的钱投入某个投资策略中,这样系统就会生成相应的的订单。比如,如果客户投人 1000 美元,而这个策略指定了 20% 的资金要投到 ABC 股票,那么就会生成一个购买 200 美元
ABC 股票的订单。下面是一个完整的业务流程图:

  • 按易变性划分

在理想世界中,我们可以通过组合复用已有服务来完成任何功能的开发。这可能听起来有点不切实际,但是考虑如何最大限度地提高所构建服务的复用性,从而实现长期价值,是非常有意义的。

除了考虑系统的功能,我们还应该考虑应用未来可能发生的变化。这也被称为易变性。将很可能变化的部分封装起来,有助于确保领域内的不确定性因素不会对其他领域产生消极影响。读者会发现这与面向对象编程中的稳定依赖原则 (stable dependencies principle) 很类似:"包只应该依赖于比自己更加稳定的包"。

SimpleBank 公司的业务领域存在许多维度的易变性。比如,向市场下单是易变的:不同类型的订单需要提交到不同的市场中;SimpleBank 为每个市场要调用不同的 API(比如,通过第三方代理商交互或者直接与交易中心交互);随着 SimpleBank 提供的金融资产的范围扩大,这些市场也可能发生变化。

将与市场交互的功能作为服务的一部分会增加系统的耦合度,并极大提高系统的不稳定性。反之,我们可以将市场服务进行拆分,最终开发多个服务来满足不同市场的需要。该方案如图 4.16:


 

3. 划分新功能注意事项

  • 限界上下文通常是与服务边界相对应的,在思考服务的未来发展时,这是一种很有效的方法。
  • 通过对易变领域的深人思考,开发者可以将那些会一起变化的领域封装起来,以提高应对未来变化的适应能力。
  • 如果服务划分不好,后期修正的代价是特别大的,因为到那时,开发者需要重构多个代码库,由此产生的工作量会变得特别大。
  • 可以将技术功能封装成一个服务,这么做既能够简化业务能力,又可以对业务功能提供支撑,并能最大限度地提高服务的可用性。
  • 如果服务边界还不够明确,我们宁可选择粗粒度的服务,但是要主动在报务内部采用模块化的方案来为未来的拆分做准备。比如我们对 order 服务的定义,包含了订单所有的生命周期。但是对于订单的回滚,也可以单独抽离一个服务去处理这个需要集成很多服务的功能。

4. 处理不确定性

  • 日益壮大的微服务,总有一天会面临臃肿庞大的情况。我们需要对这样的微服务进行拆分。反之,长久不变更或者是被某一位服务紧密联系的小服务,也可以合并到其他的微服务中。这就意味着对废弃掉的服务需要下线。服务下线是一件特别有挑战性的工作,但是随着微服务应用的不断发展,我们未来终有天会需要这么做。特别注意:对于服务下线或者是剥离新的服务,我们要遵循快速开发部署和持续的交付新功能。而不是一次性把所有的功能全部移交到新的服务。
  • 在大型组织机构中,将所有权拆分到多个团队中是很有必要的,但是这又会引入新的问题:控制变弱、设计受限、开发速度不一致。代码开放化、接口明确化、沟通持续化以及放宽对 DRY(don't repeat yourself,在微服务方案中会存在一些重复的工作,这是不可避免的)原则的要求都可以缓解团队之间的紧张关系。

第 5 章 微服务的事务与查询

许多单体应用在修改应用状态时都是依靠事务来保证一致性和隔离性的。要实现这两点很简单:应用通常只和单个数据库交互,使用支持启动、提交和回滚这些事务操作的框架来实现强致性保证。每个业务逻辑事务会牵涉到许多不同的实体,比如下单操作会涉及更新交易记录、预定股票仓位和缴纳手续费。

在微服务应用中,就没有这么幸运了。正如前面所介绍的那样,每个独立服务只负责特定的功能。数据的所有权是去中心化的,每个数据源只有一个所有者。这种层面的解耦有助于实现服务自治,但同时也牺牲了某些之前所具备的安全性,从而使得应用层面数据的一致性成为问题。数据所有权的去中心化还使得数据的获取变得更加复杂。之前只需要在数据库层面进行关联的查询操作现在需要调用多个服务才能实现。在某些使用场景中,这还是能够接受的,但是当数据集特别大时,这种方案就会变得非常麻烦。

1. 分布式事务

假设现在有一个 SimpleBank 的客户 A,A 需要卖出自己的一些股票。那么系统就会涉及到如下操作:A 创建订单、应用验证和预定股票仓位、应用向 A 收取手续费以及应用将订单提交到市场上。假设我们现在 order 服务负责处理卖股票的流程。在收费时,fee 服务发生了故障,此时,系统处于一种不一致的状态:股票已经预留了,订单也已经色到建了,但是公司没有收到 A 的手续费。我们不能就这样撒手不管了 -- 所以,order 服务需要开始修正,它会指示 account transaction 服务弥补并取消预留的股票。这可能看起来很简单,但是当牵涉的服务越来越来越多、事务的执行时间越来越长或者操作还会进一步交叉触发下游新的的事务时,一切都变得越来越复杂了。当然,面对这个问题,也可以使用二阶段提交(two phase commit,2PC)协议。但是在微服务中,我们需要尽量减少分布式事务的存在。

在上图中,order 服务负责编配其他服务的行为,调用一系列功能最终订单被发布。如果任何步骤出现故障,order 服务就负责启动其他服务的回滚。这种交互方式易于分析和推理,因为整个业务是有逻辑和顺序性的。但是这会导致 order 和其他服务紧密耦合,降低服务的独立性,同时也会使被编配的服务缺乏自治性,越来越趋于贫血模型。

2. 基于事件的通信

基于出售股票的例子,我们可以使用事件消息来重新设计这个场景:

  • 当用户通过界面发起出售请求时,应用发布一个 OrderRequessted 事件。
  • order 服务接收这个事件后进行处理,然后向事件队列发出一个 OrderCreated 事件。
  • transaction 和 fee 服务都会接收到这个事件通知,这两个服务会执行它们相应的操作,然后在执行完成以后分别发出一个通知事件。
  • market 服务等待两个通知事件:收费确认事件和股票预定成功事件。一旦接收到这两个事件,market 服务就可以向股票交易市场提交订单了。这步操作完成后,market 服务就会向事件队列发送一个最终的事件消息。

事件使得开发者可以用一种乐观的方式来实现高可用。比如,即便 fee 服务出现故障,order 服务仍旧能够创建订单。当 fce 服务恢复后,它可以继续处理积压的事件。我们可以将这个方法扩展到回滚的场景中:如果由于金额不足导致 fee 服务收费失败,fee 服务可以发送一个 ChargeFailed 事件,然后其他服务就可以消费该事件来取消下单操作。这种方式称为编排。每个服务可以在不了解整个流程结果的情况下响应各种事件,独立执行各种操作(这种模式也叫做 Saga 模式)。这些服务就如同舞蹈演员一般:他们知道每一段音乐的舞步和要做的动作,不需要有人显式地请求或者命令他们,就会按照音乐的变化给出相应的反应。相应地,这种设计方式解除了服务之间的耦合,提升了各个服务的独立性,并且简化了独立部署变更的复杂度。

3. 查询和命令分离

我们可以将前面的使用事件消息来构建数据视图的方案作进一步归纳。在许多系统中,查询和写数据有很大的不同 -- 写数据影响的是单一的、高度规范化的实体,而查询通常是会从一系列的数据源中获取非规范化的数据。有些查询模式会受益于使用与写数据完全不同的数据存储,比如,开发者可能会使用 PostgreSQL 作为持久化的事务存储,但是使用 Elasticsearch 作为索引查询的数据存储。这种命令 - 查询职责分离(CQRS)模式是一种应用于这种场景的通用模型,它显式地将系统中的读(查询)和写(命令)进行分离。

在微服务应用中,CQRS 有两大核心优势:第一,可以针对特定的查询请求优化其查询模型来提升它们的性能,并消除了对跨服务的 join 关联的需要;第二,有助于在服务和整体应用层面实现关注点分离。但是在 CQRS 中服务的命令状态天然地会先于查询状态而得
到更新,由于这种复制延迟(replication lag)的存在使得开发者需要考虑最终一致性。因为查询模型是通过事件来更新的,所以对数据的查询可能返回的是过期的数据。如果在一些场景中,要求数据及时性,可以采用以下三种方式解决:

  • 乐观更新:提前更新预期结果的界面,等待结果返回再更新页面。
  • 轮询:设置命令的有效时间,在有效时间内轮询结果,知道返回直接过。
  • 发布 - 订阅:不同于一直轮询结果,页面也可以在查询模型上订阅一些事件,例如通过 web socket。在这种情况下,只有在读模型发出了 “updated” 事件后,界面才会更新

第 6 章 设计高可靠服务

没有哪个微服务是一座不与外界联系的孤岛,每个微服务总归要隶属于某个更大的系统。工程师们所开发的大部分服务都会被其他上游合作方服务所依赖,同样,这些服务也会依赖于其他下游合作方服务来完成对用户有益的功能。一个服务如果想要可靠地、始终如一地完成它的工作,就需要能够充分信任这些合作方。彻底消除微服务应用中的故障是不可能的 -- 与之对应的投人将是一个无底洞!反之,我们的重点应该放在设计出能够容忍依赖项出现故障的微服务,让这些微服务能够优雅地从故障中恢复正常或者能够减轻这些故障对其功能的影响。

1. 可靠性定义

对于微服务,我们可以假设,有些时间里它们能够成功执行对应的工作 -- 这就是所谓的正常运行时间 (uptime)。同样,我们也可以放心地假设:因为故障是不可避免的,所以有些时间里,服务并不能完成对应的工作,这就是所谓的故障时间 (downtime)。我们就可以使用正常运行时间和故障时间来计算可用性 (availability):服务正常运行时间的百分比。服务可用性是种衡量服务可预期的可靠情况的指标。

高可用通常用 "9" 来表示。两个 9 表示 99%,5 个 9 表示 99.999%。生产环境上的关键服务中,如果可靠性低于这个值,是非常不正常的。假设 market 服务的可用性为 99.9%,这看似很靠谱,但 0.1% 的不可用时间会随着请求量的增加而变得越来越显著:每 1000 个请求有 1 个失败,但是每一百万次请求将会有 1000 次失败。这些失败会直接影响到 market 服务,除非设计出一个方案能够减轻依赖服务故障对调用方服务的影响。同样的,假设某个服务依赖了 6 个与 market 服务同样可用性的服务,那么他的可用性就是 0.999⁶=0.994。

如果我们不能 100% 相信网络、硬件、其他服务甚至自己所负责的服务的可靠性,在设计服务时,我们需要采取一种防御型的方式来满足 3 个目标:

  • 对于无法避免的故障要降低其发生率;
  • 对于无法预测的故障要控制其连锁影响,不要产生系统层面的影响;
  • 故障发生后,能够快速恢复(理想情况下可以自动恢复)

2. 故障来源

连锁故障是分布式应用中一种很常见的故障类型。连锁故障是一个 "正反馈" 的例子:某个事件对系统产生了干扰进而造成一些影响,而这个影响反过来又强化了最初的干扰程度。任何故障都可能造成连锁故障。

  • 硬件:服务运行依赖的底层物理基础设施和虚拟化的基础设施
  • 通信:不同服务间的写作及服务与外部间的协作
  • 依赖:服务自身所以来的服务的故障
  • 内部:服务本身代码错误

3. 设计可靠的通信方案

  • 重试
    • 如果故障是孤立暂时性的,那么重试是一个很合理的选择。当异常行为发生时,重试操作一方面能够减轻对终端用户的影响,另一方面还能减少运维工作人员的介入。为重试方案做好规划是很重要的:每次重试要花费几十毫秒时间,所以服务消费方只能在超过合理的响应时间之前进行一定次数的重试。
    • 如果故障是持续性的,比如 order 服务承载力下降了,后续的重试都会让系统稳定性下降。假设每次失败后都会重试 5 次,那么每个失败请求又会产生 5 个重试,这会导致重试越来越多,最坏的情况是,order 服务被重试方案压死了。
  • 后备方案
    • 优雅降级:如果 order 服务故障,不能再为客户提供提交订单的功能。为了解决这个问题,我们应该设计一个备用方案。比如客户还能查看现有订单记录,以及订单详情,虽然不能继续创建新的订单,但至少可以查看历史数据,这比什么都做不了好太多了。
    • 缓存:如果 order 服务故障,客户不再能查看历史订单。为了解决这个问题,我们可以将客户之前查询的订单缓存起来,从而减少对 order 服务的需要。
    • 功能冗余:如果 order 服务故障,客户不再能查看历史订单。我们可以向其他数据源发起请求来代替。在一个系统中,存在功能冗余有很多驱动因素:外部整合、结果相似但是性能特征不同的第法,甚至于已经废弃但是还在继续运行的老的功能。在全球的分布式部署中,开发者甚至可以借助于其他区域 (region) 的服务作为后备方案。
    • 桩数据:最后,尽管在当前这个具体的场景中桩数据方案并不合适,但是我们可以在其他一些场景中用桩数据作为后备方案。想象一下亚马逊的 “向开发者推荐” 模块:如果由于某些原因后端不能获取到个性化的推荐信息,那么这时使用一组非个性化的数据作为备用要比在界面上显示一块空白优雅得多。
  • 超时:我们可以在 HTTP 请求方法中设置超时时间。对于 HTTP 调用,如果迟迟没有收到任何响应数据,应该超时终止;但是如果只是响应结果下载比较慢,这种情况下不应该超时终止。设置期限时间是个难题,如果时间过长,调用服务就在消耗没必要的资源;如果设置过短,又会导致失败率增高,如图 6.13

  • 断路器(熔断器):断路器是一种暂时停止向发生故障的服务发起请求来避免连锁故障的方法(此处不再详解)。注意:设计断路器时,有两大准则:
    • 在发生问题时,远程通信应该快速失败,而不要浪费资源等待永远不会到来的响应结果。
    • 如果所依赖的服务持续出现故障,最好在该依赖服务恢复之前停止进步发起请求。
  • 异步通信:当我们不需要立刻得到响应,也不需要响应始终保持一致时,我们就可以使用异步通信术来降低直接的服务调用的数量,相应地,这也能够提高系统整体的可用性 -- 尽管这是以业务逻辑变得更加复杂为代价的。正如在其他部分讲到的那样,通信代理会变成故障单点为了确保扩容、监控和运维的有效性,开发者需要特别小心这部分内容。

4. 最大限度提高服务可靠性

  • 负载均衡与服务健康:
    • 负载均衡:在生产环境中通过部署多个服务实例来确保冗余度和水平扩展
    • 服务健康:设计和部署的每个服务都应该实现合适的健康检查方案,如果某个实例不正常,这个实例就不应该再接受任何请求。健康检查基于以下两个标准:
      • A。存活性:只简单检查服务应用是否启动起来和是否正常运行。
      • B。就绪性:检查服务是否准备好处理通信数据。
  • 限流:限制一个时间窗口内对协作服务的请求频率或者总有效请求量。有助于确保服务不会过载,这种限制可以是无差别的(超过某个数量以后的所有请求全部丢弃),也可以设计的很复杂(丢弃那些频率低的服务客户端的请求,优先处理关键接口的请求,丢弃低优先级的请求)
  • 验证可靠性和容错性
    • 压力测试:对服务流量的现状和预期增长情况建模;估算数据请求流量所需要的服务容量;针对估算的容量通过压力测试来验证所部署服务的实际容量;根据业务和服务指标来视情况重新估算。
    • 混沌测试:
      • a. 为正常的系统行为定义一个可量化的稳定状态;
      • b. 假设实验组和对照组的功能都保持稳定;
      • c. 引入能够体现现实世界故障的可变因素;
      • d. 否定第二步

5. 默认安全

采用一些标准 -- 不管是通过框架还是代理 -- 来帮助工程师快速开发出默认具有容错性的服务。

第 7 章 构建可复用的微服务框架

在组织全面拥抱微服务以后,随着组织内的团队规模越来越大,其中的每个团队都很可能开始专注于一组特定的编程语言和工具。有时候,即便使用同样的编程语言,不同团队也会在实现同一个目标时选择不同的工具组合。尽管这并没有错,但是这会导致开发者换到其他团队的难度增大。创建新服务的习惯以及代码结构会有很大差异。即使这些团队最终能够采用各种不同方式解决这个问题,我们仍然相信潜在的重复性要好于多出来一个信息沟通和同步的环节。

严格规定团队能够使用的工具和编程语言,并强制不同团队采用同一套标准方式来创建服务,会损害团队的工作效率和创新能力,最终导致所有问题都采用同样的工具。幸运的是,我们可以让团队在为服务自由选择编程语言的同时遵循一些通用的实践方案。我们可以针对所使用的每种语言封装一套工具集,同时确保工程师能够访问那些能够方便各个团队人遵守实践的资源。如果团队 A 决定使用 Elixir 来创建通知管理服务,而团队 B 决定使用 Python 来开发一个图像分析服务,他们应该都有对应的工具来让这两个服务可以向通用的度量指标收集基础服务发送度量指标数据。

开发者应该以相同的格式将日志集中保存到同一个地方,像断路器、功能标志的功能以及共用相同的事件总线的能力也应该是现成可用的。这样,团队不仅可以做出选择,还可以使用这些工具来与运行其服务的基础设施保持一致。这些工具就组成了服务底座。我们可以在服务底座的基础之上构建新的服务,而不需要做太多的前期调查和准备工作。将普遍关注的内容和架构选型抽象化,同时还能够加快团队启动新服务的速度。

  1. 微服务底座能够加快新服务的启动速度,扩大试验领域和降低低风险。
  2. 使用服务底座能够让开发者将与某些基础设施相关的代码实现抽取出来。
  3. 服务发现、可观测性以及不同的通信协议都是服务底座所关注的内容,服务底座需要把供这些功能。
  4. 如果有适合的工具存在,我们可以为下单出售股票这样的复杂功能快速开发出原型。
  5. 虽然微服务架构经常和使用任意语言开发系统的可能性联系起来,但是在生产环境中这些系统需要保证并提供机制来让运行和维护都是可管理的。
  6. 微服务底座能够实现上述这些保证,同时能够让开发者快速启动和开发以验证想法的正确性,如果验证通过,就可以部署到生产环境。

第三部分 部署

第 8 章 微服务部署

  1. 部署新的应用和变更必须要标准化简单明确,以免在微服务开发过程中出现摩擦,并提高稳定性可用性
    • 由于微服务应用是以部署单元级别进行演进,因此部署新服务的成本必须小到可以忽略不计,能够让工程师快速创新、引进新内容并向用户交付价值。
    • 小版本发布能够降低风险和提高可预测性,减少可能的暴露范围可以加快发布速度、简化监控工作,并对应用的正常运行产生更小的干扰。
    •  自动化(验证过程以及上线过程)推动部署节奏和一致性。
  2. 微服务可以运行在任何地方,但是理想的部署平台需要支持一系列功能
    • 运行时管理,比如自动愈合和自动扩容。这样服务环境就可以动态地响应失败或者负载变化,而不需要人为干预(如果某个服务实例出现故障,它应该会被自动替换掉)。
    • 日志和监控,用来监测服务的运行情况并方便工程师对服务执行的过程有深入了解支持安全运维,比如网络控制、密码凭据管理以及应用加固。
    • 负载均衡、DNS 以及其他路由组件可将用户侧的请求以及微服务之间的请求路由分发出去。
    • 部署流水线,安全地将服务从代码交付到生产环境中运行。
  3. 实例组、负载均衡器以及健康检查能够让所部署的服务实现自愈自动扩容
  4. 服务工件必须是不可变的可预测的,以将风险控制到最小,降低认知难度,简化理装抽象。
    • 服务工件是服务中一个确定的、不可修改的软件包。它们主要关注的是代码打包,而非应用程序层面更广泛的本质需求:如果开发者对同一个代码提交记录并执行构建流程,会生成相同的工作。
    • 理想的微服务部署工件允许开发者打包特定版本的已编译代码,指定任何二进制依赖项,并为启动和停止该服务提供标准的操作抽象概念。这应该是与环境无关的:开发者应该能够在本地测试和生产中运行相同的工件。
  5. 可以将服务打包为特定于语言的包、操作系统软件包、虚拟机模板或者容器镜像,再部署到主机上。
  6. 可以使用金丝雀部署或蓝绿部署来降低意外缺陷对可用性的影响,部署模式:
    • 金丝雀部署:在服务中添加一个新实例来验证 n+1 版本的可靠性,然后再全面推出。
    • 蓝绿部署:创建一个运行新版本代码的并行服务组(绿色集合),开发者逐渐将请求从旧版本(蓝色集合)中转移出去。
    • 滚动部署:在启动新实例(版本为 n+1)时,逐步将旧实例从服务中剔除。确保在部署期间最小比例的负载容量得到保证。

第 9 章 基于容器和调度器的部署

本章中,主要介绍了如何使用 Kubernetes 和 Docker 进行微服务的部署,此处不做过多阐述,主要总结在部署过程中应该注意的事项:

  1. 将微服务打包为不可变的、可执行的工件能够让开发者通过基本操作(增加或移除容器)来对部署过程进行编排。
  2. 为了方便服务开发和部署,调度器和容器会将底层的机器管理概念抽离出去。
  3. 调度器的工作是设法将应用的资源需求与集群机器的资源使用情况匹配起来,同时对运行中的服务进行健康检查以确保它们正确运行。
  4. Kubernetes 具备了微服务部署平台的理想特性,包括密码凭据管理里、服务发现和水平扩容。
  5. Kubernetes 用户定义了所期望的集群服务状态(或者规格),而 Kubernetes 会不停地执行 “观测 - 比较 - 执行” 这一循环操作来计算如何达到所期望的状态。
  6. Kubernetes 的逻辑应用单元是 pod:一个容器或者在一起执行的多个容器。
  7. 复制集管理 pod 组的生命周期。如果现有的 pod 出现故障,复制集会启动新的 pod。
  8. Kubernetes 中的部署对象被设计用来通过对复制集中的 pod 执行滚动更新来保持服务可用性的。
  9. 开发者可以使用服务对象来对底层的 pod 进行分组并供集群内外的其他应用访问。

第 10 章 构建微服务交付流水线

本章中,主要介绍了如何使用 Jenkis 进行部署流水线的搭建,此处不做过多阐述,主要总结在部署过程中应该注意的事项:

  1. 微服务部署过程应该满足两大目标:节奏安全一致性
  2. 部署新服务所花费的时间通常是微服务应用中的一大阻碍。
  3. 对微服务而言,持续交付是理想的部署实践方式,它通过快速交付小版本的经过验证的变更集来降低风险
  4. 良好的持续交付流水线能够确保部署过程的可见性、部署结果的正确性,并能够向工程师团队反馈丰富的信息。
  5. Jenkins 是非常流行的自动化构建工具,它使用脚本语言将不同的工具联系到一起并组合成交付流水线。
  6. 预发布环境是非常有价值的,但是当面对大量的独立变更时,维护好预发布环境也面临着巨大的挑战。
  7. 读者可以在各个服务上复用声明式流水线步骤;积极推动标准化能够提高不同团队间部署过程的可预测性。
  8. 为了对发布和回滚提供细粒度的控制,读者应该将部署这一技术活动与功能发布的业务活动分开管理

第四部分 可观测性和所有权

第 11 章 构建监控系统

  1. 可靠的微服务监控系统包括度量指标、链路追踪和日志。
  2. 从微服务中收集丰富的数据有助于开发者发现故障、调查问题,并理解整个应用的表现。
    • 在第 3 章中,我们讨论了架构层次:客户端、边界层、服务层和平台层。也应该把这 4 层都监控起来,因为开发者不可能在完全隔离的情况下确定一个特定组件的执行情况。网络同题最有可能影响服务的运行。如果开发者只在服务层面上收集度量指标,那么所能知道的也就只能是这个服务现在没有请求可处理。除此之外,对于问题出现的原因一无所知。而如果开发者同时还收集了基础设施层的度量指标,就可以了解到那些极有可能影响其他众多组件的问题。        
  3. 在收集度量指标时,开发者应该重点关注四大黄金标志:时延、错误量、通信量(吞吐率)和饱和度。
    • 时延
      时延这一标志衡量的是从请求发给指定的服务到该服务完成请求所花费的时间。开发者可以从这一标志中得出很多信息。如果服务的时延不断增加,那么开发者就可以推断出该服务的服务质量在不断下降。不过,在将此标志与 "错误量" 这一标志关联起来时,开发者需要格外小心。设想一下,正在处理一个请求,同时应用响应得特别快,但返回的是一个错的信息。在这种情况下,时延是一个很小的值,但返回的结果并不是所期望的内容。将出错请求的时延值排除在这个公式之外是很重要的,因为这些数据可能会对人们产生误导。
    • 错误量
      错误量这一标志计算的是那些执行不成功且没有生成正确结果的请求数量。错误量可以是显式的,也可以是隐式的 -- 比如,HTTP500 错误量和有内容不正确的 HTTP200 错误量。后一种错误量并不容易被监控到,因为开发者不能简单地依赖 HTTP 状态态码,所以只能通过查找其他组件中的错误内容来判断是否出错。开发者一般通过端到端测试我者契约式测试来发现这种错误。
    • 通信量
      通信量这一标志衡量的是对系统的需求量。所监测的系统类型不同,度量的内容 (如每秒的请求量、网络 I/0 等) 也会差异很大。
    • 饱和度
      饱和度这一标志衡量的是在指定时间点服务的承载能力。它主要应用于那些受限的资源 (如 CPU、内存和网络)。
  4. Prometheus 和 StatsD 是两种常见的和具体语言无关的从微服务中收集度量指标的工具。
  5. 开发者可以使用 Grafana 将度量指标数据以图表的形式展示出来,创建人类可读的仪表盘和触发告警。
  6. 基于度量指标的告警体现的是系统不正常的症状而非原因,这些告警更具有持久性和可维护性。
  7. 定义良好的告警应该有明确的优先级,能够逐层升级到对应的人员,具有可操作性并且包含简洁而有价值的信息。
  8. 从多个服务中收集和聚合的数据能够让开发者将完全不同的度量指标关联起来并进行比较,从而对系统有进一步全面的了解。

第 12 章 使用日志和链路追踪了解系统行为

本章中,主要介绍了如何通过 ELK 解决方案进行日志和链路追踪的搭建,此处不做过多阐述,主要总结在部署过程中应该注意的事项:

  1. 读者可以使用 Elasticsearch、Kibana 和 Fluentd(ELK)一起搭建一套日志基础设施,并使用 Jaeger 搭建一套分布式链路追踪系统。
  2. 日志基础设施可以生成、转发和存储索引的日志数据以方便检索和关联不同请求。
  3. 日志中有用的信息
    • 时间戳
      为了能够将数据关联起来并适当排序,开发者需确保将时间截附加到日志记录上。时间戳应该尽可能地精确和详细,比如使用的由 4 位数字表示的年份和最精确的时间单位。每个服务都应该提供自己的时间戳,最好以毫秒为单位。时间戳还应该包含时区,建议开发者尽可能使用 GMT/UTC 来收集数据。
    • 标识符
      不管在什么时候,开发者都应该在要记录的数据中尽可能多地使用唯一的标识符。当开发者交叉引用来自多个数据源的数据时,请求 ID、用户 ID 和其他唯一标识是非常重要的。有了这些标识符,开发者就能够非常高效地将不同数据源的数据分组。唯一标识符和时间戳结合起来使用是一种非常强大的手段,可以帮助开发者了解系统统中的事件流。
    • 来源
      确定给定日志记录的来源能够简化必要情况下的调试难度,开发者可以使用的典型数据来源包括主机名、类名或模块名、函数名和文件名。在调用某个指定函数的执行时间增大时,从这个来源中收集的信息能够让开发者推断出应用出现的性能问题,因为开发者可以推断出执行时间,即使这不是实时的。尽管它无法代替度量指标的收集,但是也能够有效地帮助开发者发现系统瓶颈和潜在的性能问题。
    • 日志等级或类别
      每个日志条目都应该包含一个类别。这个类别可以是要记录的数据类型,也可以是日志等级一般来说,通常使用的日志等级有 ERROR、DEBUG、INFO 和 WARN。开发者可以按照这些类别对数据进行分组。有一些工具可以使用 ERROR 级别的消息解析日志文件检索,然后将这些消息发送给错误报告系统。这是开发者利用日志等级或者类别自动实现错误报告流程而不需要显式指令的最佳示例。
  4. 分布式链路追踪能够让开发者跟踪不同微服务之间的请求的执行过程。
  5. 除了度量指标集合,链路追踪能够让开发者更好地了解系统的运行方式,发现潜在问题并随时对系统进行审查。

第 13 章 微服务团队建设

  1. 构建优秀的软件不仅和选择什么方案实现有关,还与有效的沟通、协调和协作有关。
  2. 应用架构和团队结构有着共生的关系。可以使用后者来改变前者。
  3. 如果想让团队变得高效,就应该将他们组织起来,最大化地实现自治、所有权以及端到端职责。
    • 所有权
      拥有强烈主人翁意识的团队具有较高的内在动力,并对其所负责的颈域承担很大的责任。由于微服务应用通常都有很长的生命周期,因此长期负责某个领域的团队在对该领域的知识和了解越来越深的同时还需要为代码演进提供支持。
    • 自治
      这 3 个原则体现了微服务本身的一些原则,这并非巧合。能够自主工作的团队 -- 对其他团队的依赖有限 -- 可以减少工作中的摩擦。这种类型的团队内部高度一致,但与其他团人的耦合度较低。
    • 端到端职责
      开发团队应该对产品拥有完整的 “设想 - 构建 - 运行” 循环。通过对所构建内容的控制,团队可以做出理性的、局部的优先级决策;展开实验;并能在很短的时间周期内用真正的代码和用户验证所提出的想法。
  4. 在微服务交付方面,跨职能团队比传统的职能团队速度更快、更有效率。
    • 在按职能划分的方式中,我们按照专业来将人员分组,并采用职能化的汇报线方式,最后将这些人员分配到有时间限制的项目中。大多数组织投入资金支持的的项目都是为了实现特定的需求,并有时间限制。衡量项目是否成功的依据就是他们有无准时交付并完成需求。如图可以更简单的理解职能团队

    • 跨职能搭建的团队是由具有不同技能集的人员组成的团队,通常与长期的产品目标或远大的使命保持一致。在需求范围内,可以自由安排项目的优先级,并根据需要构建的功能完成这些任务。衡量团队是否成功的依据通常是其对业务关键性能指标(KPI)和结果的影响。如图可以更简单的理解跨职能团队

  5. 较大型的工程组织应该建立一套具有基础设施、平台和产品团队人的分层模型。较低层次的团队为较高层次的团队提供服务以保证其能够更有效地工作。
  6. 社区实践(比如协会和分会),可以分享职能知识。
  7. 微服务应用很难全部装进人的大脑,这给全局决策和值班的工程师带来了挑战。一个成功的值班轮换机制应该具有以下特点:
    • 包容性
      每个能做的人都应该做,包括副总裁和总监。
    • 公平性
      正常工作时间之外的值班工作应获得报酬。
    • 可持续性
      应该有足够多的工程师进行轮换,以避免过度疲劳和破坏工作与生活的平衡或办公室的日常工作。
    • 反射性
      团队应该不断查看告警和监控页面,确保只有重要的告警出现时才会唤醒别人。在这个模型中,我们将告警分散到各个团队,因为运行大规模软件是非常复杂的工作。运维工作可能超出任何一个团队中工程师的工作范围或知识范围。许多运维任务 -- 例如维护 Elasticsearch 集群、部署 Kafka 实数据库调优 -- 都需要特定的专业知识,而期望产品工程师都具备这方面的知识是不合理的。运维工作的节奏也与产品交付的节奏有所不同
  8. 架构师应该指导和影响应用的演进,而不是支配应用的方向和结果。
  9. 内部开源模型能改善跨团队协作,削弱占有欲,降低巴士因子①的风险
  10. 设计评审能提高微服务的质量、可访问性和一致性。下图列出了标准设计评审文档中的组成部分

  11. 微服务文档应该包括概述、操作手册、元数据和服务契约。

以上所有内容,为《微服务实战》- [摩根・布鲁斯] 学习笔记的全部内容。若感兴趣,强烈推荐通读全书。若有错误,烦请指正。


巴士因子是软件开发中关于软件项目成员之间信息集中及共享度的一个衡量指标。一个项目至少失去若干关键成员的参与(“被巴士撞了”,指代职业和生活方式变动、婚育、意外伤亡等任意导致缺席的缘由)即导致项目陷入混乱、瘫痪而无法存续时,这些成员的数量即为巴士因子。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值