读构建可扩展分布式系统:方法与实践08微服务

1. 微服务

1.1. 微服务的起源可以追溯到2008年左右

  • 1.1.1. 在Amazon,​“两个比萨原则”成为一个单系统组件团队规模的管理原则,后来被称为微服务

    • 1.1.1.1. 每个内部团队都应该小到可以用两个比萨饼喂饱
  • 1.1.2. Amazon和Netflix是微服务架构的先驱,他们在2009年左右大规模采用了微服务架构

  • 1.1.3. Uber已将其微服务架构发展为基于相关服务的集合(称为域)

1.2. 认为微服务在某种意义上比服务更小,是对微服务的一种误解

1.3. 微服务是细粒度、高内聚、松耦合服务的一种设计和部署方法,服务被组合起来满足系统的需求

  • 1.3.1. 定义特征是它们的范围,围绕业务能力组织

  • 1.3.2. 细粒度服务或微服务是独立部署的,并且必须在必要时相互通信和协调以处理各个系统请求

  • 1.3.3. 微服务架构是分布式系统

1.4. 微服务是一种流行的现代架构风格,在合适的场景中具有大量工程优势

  • 1.4.1. 每个微服务对系统其他部分来说都是黑盒子,它可以内部选择最适合团队和应用程序需求的架构和技术栈

  • 1.4.2. 主要的系统新功能可以构建为微服务并组合到应用程序架构中,同时最小化对系统其余部分的影响

1.5. 有时微服务并不总是正确的方法

1.6. 采用微服务需要引入新的设计和开发实践,以创建一组细粒度、内聚的组件来满足你的应用程序需求

1.7. 微服务之间经常协作,需要进行通信来满足单个请求

  • 1.7.1. 缓慢的响应会导致调用服务中的背压,最终一个故障会导致所有相关服务崩溃

1.8. 微服务要求开发过程广泛自动化

  • 1.8.1. DevOps是一个发展快速且技术丰富的领域,搜索引擎是查找构成现代化DevOps管道的各种构建、配置、测试、部署和监控平台信息的最佳途径

2. 转向微服务

2.1. 基于微服务的架构在很多方面受益于过去10年出现的软件工程和技术创新的融合

2.2. 单体应用

  • 2.2.1. 自IT系统问世以来,单体架构风格一直主导着企业应用程序

  • 2.2.2. 将应用程序分解为多个逻辑模块或服务,它们作为单个应用程序构建和部署

  • 2.2.3. 单体架构鼓励创建可在服务实现之间共享的可重用业务逻辑和DAO

    • 2.2.3.1. DAO映射到数据库实体,所有服务实现共享一个数据库
  • 2.2.4. IBM WebSphere和Microsoft.NET等主流平台都支持将所有服务构建和部署为单个可执行包,即术语“单体”​(完整的应用程序)的来源

    • 2.2.4.1. API、业务逻辑、数据访问等都包含在一个部署件中
  • 2.2.5. 由于应用程序在一台(可能非常强大的)服务器上运行,因此系统和错误监控也得到了简化

  • 2.2.6. 垂直扩展(向上扩展)是提高单体应用响应能力和容量的最简单方法

  • 2.2.7. 水平扩展(向外扩展)也是可行的

    • 2.2.7.1. 可以提供两个或多个单体副本,并使用负载均衡器来分发请求

    • 2.2.7.2. 有状态和无状态服务都适用,只要负载均衡器支持有状态设计的会话保持特性

  • 2.2.8. 问题

    • 2.2.8.1. 代码库的复杂度

      2.2.8.1.1. 随着应用程序和工程团队规模的增长,添加新功能、测试和重构变得越来越困难

      2.2.8.1.2. 技术债务不可避免地会累积

      2.2.8.1.3. 如果没有持续一致的重构努力来维持架构的完整性和代码质量,工程就会变得更加困难

    • 2.2.8.2. 水平扩展

      2.2.8.2.1. 你可以通过在多个节点上复制应用程序来水平扩展以增加容量

      2.2.8.2.1.1. 每次都要复制整个应用程序(单体)

      2.2.8.2.2. 随着请求量的快速增长,它们提供了方案来解决扩展单体工程几乎不可避免地面临的挑战

2.3. 打破巨型单体

  • 2.3.1. 微服务架构将应用程序功能分解为多个独立的服务,这些服务在必要时进行通信和协调

  • 2.3.2. 每个微服务都是完全独立的,在需要的地方封装自己的数据存储,并提供用于通信的API

  • 2.3.3. 优势

    • 2.3.3.1. 代码库

      2.3.3.1.1. 遵循两个比萨原则,单个服务的复杂程度不应超过小型团队可以构建、发展和管理的程度

      2.3.3.1.2. 团队可以完全自主地选择自己的开发栈和数据管理平台

      2.3.3.1.3. 一个设计良好的微服务支持的功能范围更窄、更加内聚,这会降低代码的复杂度,加快新功能的开发节奏

      2.3.3.1.4. 按需独立部署微服务的修订版

      2.3.3.1.5. 如果微服务支持的API是稳定的,则更改对依赖的服务是透明的

    • 2.3.3.2. 水平扩展

      2.3.3.2.1. 可以通过扩展单个微服务来满足请求量和延迟要求

      2.3.3.2.2. 其他轻负载的服务可以简单地在单个节点上运行或以低成本进行复制,以消除单点故障并提高可用性

      2.3.3.2.3. 如何将系统功能分解为单独的服务,是转向微服务架构的关键设计决策之一

      2.3.3.2.3.1. DDD(Domain-Driven Design,领域驱动设计)提供了一种合适的方法来识别微服务

      2.3.3.2.3.2. 微服务必须自包含的性质可以很好地映射到DDD中有界上下文的概念

  • 2.3.4. 微服务本质上是分布式的,总有一个平衡的考量

    • 2.3.4.1. 需要分析和调整领域模型的纯度,以满足分布式通信的成本以及系统管理和监控的复杂性

    • 2.3.4.2. 在耦合的微服务之间复制数据

      2.3.4.2.1. 服务能够在本地访问它需要的数据,从而简化设计并减少数据访问响应时间

      2.3.4.2.2. 复制数据也需要权衡其利弊

      2.3.4.2.2.1. 需要额外的存储容量和开发工作来确保所有重复数据的一致性状态

      2.3.4.2.2.2. 当数据发生变化时,可以立即启动数据副本的更新,以尽量减少重复数据不一致的时间间隔

      2.3.4.2.2.3. 如果业务上下文允许,可以运行定期复制(例如,每小时或每天)​,可能是在请求负载较低时调用计划任务执行

      2.3.4.2.2.4. 随着应用程序对性能和可扩展性的需求不断增长,与系统重大重构所带来的问题相比,复制数据的成本和复杂性通常很小

  • 2.3.5. 部署微服务

    • 2.3.5.1. 为了支持频繁更新并从小型团队提供的灵活性中获益,你需要能够简单快速地部署新的微服务版本

    • 2.3.5.2. 无服务器处理平台是一种有吸引力的微服务部署方法

      2.3.5.2.1. 微服务可以在你选择的无服务器平台上构建,并公开API

    • 2.3.5.3. 无服务器平台的优点

      2.3.5.3.1. 部署简单

      2.3.5.3.1.1. 只需将微服务的新可执行包上传到为函数配置的端点

      2.3.5.3.2. 按使用量付款

      2.3.5.3.2.1. 如果你的服务只有少量请求,那么你的成本会很低,甚至为零

      2.3.5.3.3. 易于扩展

      2.3.5.3.3.1. 你选择的平台会处理函数的扩展

    • 2.3.5.4. 当你将所有微服务都部署到无服务器平台上时,你将为客户端暴露多个需要被调用的端点

      2.3.5.4.1. 将后端更改直接暴露给客户端从来不是一个好主意

    • 2.3.5.5. API网关模式

      2.3.5.5.1. 将客户端与实现应用功能的微服务的底层架构隔离开来

      2.3.5.5.2. NGINX Plus

      2.3.5.5.3. Kong

      2.3.5.5.3.1. Kong API网关是无状态的,因此可以部署多个实例并使用负载均衡器分发请求

      2.3.5.5.4. 特定云供应商的托管产品

      2.3.5.5.5. 以毫秒级低延迟将传入的客户端API请求代理到实现API的后端微服务

      2.3.5.5.5.1. 由API网关处理的面向客户端的API与后端微服务API之间的映射是通过管理工具或配置文件执行的

  • 2.3.6. 微服务原则

    • 2.3.6.1. 围绕业务领域进行建模

      2.3.6.1.1. 有界上下文的概念为微服务的范围提供了一个起点

      2.3.6.1.2. 在微服务之间存在耦合并且可能引入性能开销的情况下,可能需要重新考虑业务领域边界

    • 2.3.6.2. 高度可观测

      2.3.6.2.1. 监控每项服务对于确保它们按预期运行、以低延迟处理请求以及记录错误情况至关重要

      2.3.6.2.2. 在分布式系统中,可观测性是高效运行的基本特征

    • 2.3.6.3. 隐藏实现细节

      2.3.6.3.1. 微服务是黑盒子

      2.3.6.3.2. 它们的API是其保证支持的契约,但是API内部如何执行不会对外公开

    • 2.3.6.4. 全面去中心化

      2.3.6.4.1. 处理需要多次调用下游微服务的客户端请求,它们通常被称为工作流

    • 2.3.6.5. 隔离故障

      2.3.6.5.1. 一个微服务的故障不应传播到其他微服务并导致应用程序崩溃

    • 2.3.6.6. 独立部署

      2.3.6.6.1. 每个微服务都应该是可独立部署的,以便团队能够在不依赖于其他团队进度的情况下进行增强和修改

    • 2.3.6.7. 自动化文化

      2.3.6.7.1. 想要从微服务获得好处,开发和DevOps的工具及实践是绝对必要的

      2.3.6.7.2. 自动化使得修改已部署的系统变得更快、更可靠

    • 2.3.6.8. 工作流

      2.3.6.8.1. 编制和编排通常用于实现需要访问多个微服务的用例

    • 2.3.6.9. 点对点编排

      2.3.6.9.1. 所需的微服务之间直接通信即可满足请求

      2.3.6.9.2. 每个自治微服务共享处理工作流的责任和知识

      2.3.6.9.3. 通信可以是同步的,也可以是异步的

    • 2.3.6.10. ⑩集中编制

      2.3.6.10.1. 实现工作流的逻辑被嵌入单个组件中,通常是专用的微服务

      2.3.6.10.2. 该服务与域服务通信并将结果发送回用户

      2.3.6.10.3. 两种方法都需要权衡取舍

3. 微服务的弹性

3.1. 分布式系统的一个老生常谈的话题是,在绝大多数时间里,系统运行不会出现灾难性错误

  • 3.1.1. 受益于快速可靠的网络、很少崩溃的机器和磁盘、用于托管微服务及进行消息传递的基础平台和数据库非常强大

  • 3.1.2. 你的系统仍然必须为可能发生的间歇性故障做好准备,通常是在最不方便的时候

3.2. 级联故障

  • 3.2.1. TCP请求将超时并向调用方抛出错误

  • 3.2.2. 级联故障的潜在本质是,它们是由所依赖的服务响应缓慢触发的

  • 3.2.3. 如果下游服务只是由于系统崩溃或暂时性网络故障而失败或不可用,调用者会立即收到错误并可以做出相应的响应

  • 3.2.4. 对于逐渐变慢的服务,情况并非如此

    • 3.2.4.1. 请求返回结果,只是响应时间更长

    • 3.2.4.2. 如果不堪重负的组件继续受到请求的轰炸,它就没时间恢复并且响应时间继续增长

    • 3.2.4.3. 这种情况通常会因客户端在请求失败时立即重试而加剧

      3.2.4.3.1. 立即重试在不堪重负的微服务上维持负载,带来的结果是可预测的,即另一个异常

      3.2.4.3.2. 重试只是保持压力

      3.2.4.3.3. 在两次重试之间插入不断增加的延迟

      3.2.4.3.3.1. 有助于减轻下游的负载,但这个延迟会计入调用者所经历的延迟,因此通常无法解决问题

  • 3.2.5. 级联故障在分布式系统中很常见

    • 3.2.5.1. 无论是由不堪重负的服务引起,还是由错误情况(例如应用程序代码错误或网络问题)引起,你都需要采取明确的步骤来防范它们
  • 3.2.6. 避免级联故障的模式包括使用超时和断路器发起快速失败

  • 3.2.7. 快速失败模式

    • 3.2.7.1. 服务慢的核心问题是它们会长时间占用系统资源来处理请求

    • 3.2.7.2. 请求线程会暂停,直至它收到响应

    • 3.2.7.3. 少量请求花费的时间比平均响应时间长得多

      3.2.7.3.1. 使用百分位数来量化慢速请求的百分比

      3.2.7.3.2. 百分位数提供了比平均值更丰富、更准确的微服务响应时间视图

    • 3.2.7.4. 服务器的垃圾回收、数据库争用、过多的上下文切换、系统页面错误和网络请求丢失都是造成这种长尾的常见原因

    • 3.2.7.5. 长响应时间从来都不是好事,无论是技术上还是用户感受上

      3.2.7.5.1. 消除长响应时间的常见方法是快速失败

      3.2.7.5.1.1. 当请求花费的时间超过某个预设的时间限制时,客户端不会等待它完成,而是会向其调用者返回一个错误

      3.2.7.5.1.2. 在服务器上启用节流

3.2.7.5.1.2.1. 如果请求负载超过某个阈值,则立即使请求失败并返回HTTP503错误

3.2.7.5.1.2.1.1. 这向客户端表明该服务不可用

  • 3.2.7.6. 节流或限制速率是许多负载均衡器和API网关技术中可用的功能

    3.2.7.6.1. 当达到定义的限制时,负载均衡器将拒绝请求,保护它控制的资源免于过载

    3.2.7.6.1.1. 这使服务能够以一致的低响应时间处理请求

    3.2.7.6.2. 一种稍微复杂的方法可以使用滑动窗口算法跟踪平均响应时间或P99等指标

  • 3.2.7.7. 请求失败时还需要考虑一件事

    3.2.7.7.1. 微服务的一个原则是故障隔离

    3.2.7.7.2. 这意味着部分系统的故障不会导致整个应用程序不可用

  • 3.2.8. 断路器模式

    • 3.2.8.1. 如果微服务由于过载或网络不稳定而开始抛出错误,那继续向API发送请求是毫无意义的

    • 3.2.8.2. 可以使用断路器模式来实现这个功能,它可以保护远程端点在某些错误情况发生时不会不堪重负

      3.2.8.2.1. 客户端可以使用断路器来保护服务器免于过载

      3.2.8.2.2. 断路器配置为监视某些条件,例如端点的错误响应率或每秒发送的请求数

    • 3.2.8.3. 对于几乎肯定会失败的操作,断路器是减少资源使用率的重要方式

    • 3.2.8.4. 当服务(有望)恢复时,断路器会自动重置并恢复正常操作

    • 3.2.8.5. 断路器对于故障隔离非常有效

      3.2.8.5.1. 它们保护客户端免受依赖服务的错误操作影响,并允许服务恢复

      3.2.8.5.2. 在读取密集型场景中,当断路器打开时,请求通常可以返回默认或缓存的结果

      3.2.8.5.2.1. 有效地向客户端隐藏了故障,并且不会降低服务吞吐量和响应时间

    • 3.2.8.6. 请确保已将断路器触发器绑定到监控和日志记录基础架构中,以便诊断故障原因

3.3. 舱壁模式

  • 3.3.1. 舱壁(bulkhead)一词的灵感来自大型造船实践

  • 3.3.2. 船体内部被分成几个物理分区,确保在船体的一部分发生泄漏时只有一个分区被淹没,船会继续漂浮,这是相当重要的

  • 3.3.3. 舱壁是一种损害限制策略

    • 3.3.3.1. 微服务中预留一定数量的线程来处理特定的请求

    • 3.3.3.2. 舱壁模式将远程资源调用隔离在它们自己的线程池中,故单个过载或失败的服务不会消耗应用服务器中可用的所有线程

  • 3.3.4. 可用于确保在对微服务中某个API的请求激增期间,不会耗尽所有可用的资源

  • 3.3.5. 通过设置应用服务器中特定API可以要求的最大线程数量限制,可以保证其他API的处理能力