弹力设计

前言

系统可用性测量

  • 弹力定义

    • 对于分布式系统的容错涉及,在英文中又叫 Resiliencey(弹力)。
    • 指系统在不健康、不顺、甚至出错的情况下有能力 hold 得住,挺得住,还有能在这种逆境下力挽狂澜的能力。
  • 做设计的原则

    • 找到设计目标,或是一个基准线。
    • 通过基准线或目标指导设计,使设计明确,可测试,可测量。
  • 系统可用性的计算公式

    • 弹力设计 - 图1
  • MTTF 是 Mean Time To Failure,平均故障前的时间
  • MTTR 是 Mean Time To Recovery,平均修复时间

故障原因

  • 服务不可用的因素

    • 一是有计划的

      • 日常任务:备份,容量规划,用户和安全管理,后台批处理应用。
      • 运维相关:数据库维护、应用维护、中间件维护、操作系统维护、网络维护。
      • 升级相关:数据库、应用、中间件、操作系统、网络,包括硬件升级。
    • 二是无计划的

      • 系统级故障,包括主机、操作系统、中间件、数据库、网络、电源以及外围设备。
      • 数据和中介的故障,包括人员误操作、硬盘故障、数据乱了。
      • 自然灾害、人为破坏、以及供电问题。
    • 归类

      • 网络问题。

        • 网络链接出现问题,网络带宽出现拥塞。
      • 性能问题

        • 数据库慢SQL
        • Java Full GC
        • 硬盘 IO 过大
        • CPU 飙高
        • 内存不足
      • 安全问题

        • 被网络攻击,如 DDoS
      • 运维问题

        • 系统总是在被更新和修改,架构也在不断地被调整,监控问题。
      • 管理问题

        • 没有梳理出关键服务以及服务的依赖关系,运行信息没有和控制系统同步。
      • 硬件问题

        • 硬盘损坏、网卡出问题、交换机出问题、机房掉电、挖掘机问题

故障不可避免

  • 故障是分布式的,多米诺骨牌式的。

    -

  • 两个意识

      1. 故障是正常的,而且是常见的。
      1. 故障是不可预测突发的,而且相当难缠。
  • 观念

    • Design for Failure

    • 不要尝试着去避免故障,而是要把处理故障的代码当成正常的功能做在架构里写在代码里。

    • 所谓弹力,能上能下。

      • 在好的情况下,故障修复对于用户和内部运维来说是完全透明的,系统自动修复不需要人的干预。
      • 如果修复不了,系统能够做自我保护,不让事态变糟糕。

一、隔离设计 Bulkheads

系统分离两种方式

  • 服务种类
  • 多租户

服务种类隔离

  • 图解

    -

  • 优点

    • 各板块隔离,一个板块的故障不影响另一个板块
  • 缺点

    • 同时获取多板块数据,要调多次服务,会降低性能

    • 数据抽取到数仓,增加数据合并复杂度

    • 跨板块业务,故障的连锁效应

      • 解决1:交互步骤保存
      • 解决2:高可用消息中间件
    • 多板块分布式事务,需要两阶段提交的方案。

用户的请求分离

  • 图解

    -

  • 多租户的三种做法

    • 独立的服务和数据。
    • 独立数据分区,共享服务。
    • 共享的服务和数据分区。
  • 多租户各方案优缺点

    -

  • 常用方案

    • 一般共享服务,独立数据分区。
    • 重要租户独立的服务和数据。

隔离设计的重点

  • 定义好隔离业务的大小和粒度。

  • 考虑系统复杂度、成本、性能、资源使用的问题,找到合适的均衡方案。
    定义好要什么和不要什么。因为,我们不可能做出一个什么都能满足的系统。

  • 隔离模式配套重试、异步、流控、熔断等设计模式。

  • 运维复杂度的提升,需要自动化运维工具。

  • 服务监控系统。

二、异步通讯设计 Asynchronous

同步调用

  • 缺点

    • 影响吞吐量
    • 消耗系统资源
    • 只能一对一
    • 多米诺骨牌效应

异步调用

  • 三种实现方式

    • 请求响应
    • 直接订阅
    • 间接订阅
  • 事件驱动设计

    • EDA:服务间通过交换信息来完成交流和整个流程的驱动。

    • 五个好处

      • 服务依赖消除,服务平等可重用可被替换。
      • 服务开发测试运维故障处理高度隔离
      • 服务不会相互 block
      • 服务间增加 Adapter 容易
      • 服务吞吐解除
    • 三个缺陷

      • 架构复杂,业务流程不易管理。
      • 事件乱序,需要状态机控制。
      • 事务处理复杂,对一致性技术有要求。

三、幂等性设计 Idempotency

幂等性

  • 含义:一个调用被发送一次和多次所产生的副作用是一样的。
  • 服务调用的三种结果:成功、失败和超时。超时是我们需要解决的问题。

超时问题解决手段

  • 超时后查询调用结果。

  • 被调用服务实现幂等性。

    • 分布式系统需要全局 ID

幂等性接口的处理流程

  • 目标:过来已经收到的交易。
  • 方式:需要一个存储来记录收到的交易。
  • 查询弊端:大比例会多查一次,处理流程变慢。
  • 查询弊端的优化:新增场景,使用 DB 的唯一性约束;更新场景,匹配目标状态进行更新。

HTTP 的幂等性

  • GET 获取资源,是幂等的。
  • HEAD 探活使用,是幂等的。
  • OPTIONS 获取 URL 支持的方法,是幂等的。
  • DELETE 删除资源,副作用相同,是幂等的。
  • POST 创建资源,URI 非资源本身,副作用不同,不是幂等的。
  • PUT 创建/更新操作,URI 即资源本身,有副作用,是幂等的。

POST 多次提交的幂等性设计

  • 首先,表单隐藏 token,识别重复提交的信息。(前端生成唯一ID 把 POST 变成 PUT)。
  • 然后,用户提交后,后端会把用户数据和 token 存在 db 中。重复提交的情况,数据库 token 会做排他限制,从而做到幂等性。
  • 稳妥做法:PRG 模式(POST/Redirect/Get);表单设置过期。

四、服务的状态 State

状态的含义

  • 程序的一些数据或是上下文
  • 举例:1. 用户登录的 session 判断请求合法性
  1. 多服务的业务流程中使用运行上下文 Context

无状态的服务 Stateless

  • 分布式服务设计的最佳实践。扩展性和运维方便。
  • 随意增加和减少结点,随意搬迁。
  • 降低代码复杂度和 bug 数。
  • 思维方式和「函数式编程」一致。

有状态的表现

  • 程序调用的结果。
  • 服务组合下的上下文。
  • 服务的配置。

有状态如何向无状态转换

  • 把状态保存到其他的地方。如分布式存储或分布式文件系统。
  • 耦合第三方有状态存储服务的缺点:1. 有依赖;2. 增加网络开销,服务响应变慢。
  • 对第三方存储服务的要求: 高可用高扩展
  • 对无状态服务的要求:为减少网络开销,增加缓存机制。
  • 「转移责任」的玩法。

有状态服务的优点

  • 数据本地化(Data Locality)。状态和数据本机保存,低时延。
  • 高可用性和强一致性。CAP 中的 AC。

无状态服务和有状态服务的区别

  • 无状态服务需要把数据同步到不同的结点上。

  • 有状态服务通过 Sticky Sessions 做数据分片。

    • 或是持久化长连接
    • 或是哈希通过 uid 求模,方便水平扩展。

服务状态的容错设计

  • 底层分布式存储系统运行时进行多结点的复制。

五、补偿事务 Compensating Transaction

业务流程中一组服务的一致性策略

  • 如果一个步骤失败,要么回滚到以前的服务调用。
  • 要么不断重试保证所有的步骤都成功。

ACID

  • 原子性(Atomicity):整个事务中的所有操作,要么全部完成,要么全部失败,不可能停滞在中间某个环节。
  • 一致性(Consistency):在事务开始之前和结束以后,数据库的完整性约束没有被破坏。
  • 隔离性(Isolation):两个事务的执行互不干扰,不会发生交互。
  • 持久性:事务完成,该事务对数据库所做的更改便持久地保存在数据库之中,并不会被回滚。

ACID 的特点和举例

  • 特点:保证数据库的一致性。
  • 举例:银行系统的转账业务。

ACID 的变种 BASE

  • Basic Availability:基本可用。允许系统的短暂不可用,后面可快速恢复。
  • Sofa-state:软状态。介于有状态和无状态的服务的一种中间状态。为提高性能,可让服务暂时保存一些状态或数据,但不是强一致性性。
  • Eventual Consistency:最终一致性,短暂时间不一致,最终整个系统看到的数据是一致的。

BASE 的特点和举例

  • 特点:BASE 系统允许系统出现暂时性问题,更有弹力。Design for Failure。保证在短时间内,就算是有数据不同步的风险,也应该允许新的交易发生,而后面在业务上将可能出现问题的事务给处理掉,以保证最终的一致性。
  • 举例:网上卖书。ACID:锁库存—>释放—>他人购买;不可能有多个用户下单,性能不佳。BASE:不真正分配库存,异步处理订单,发现没库存,通知用户没有购买成功,

ACID 和 BASE 的区别

  • ACID 是酸,BASE 是碱。
  • ACID 强调一致性,CAP 中的 C,BASE 强调可用性,CAP 中的 A。

业务补偿

  • 无法做到强一致性:跨系统,系统不是一个公司提供,多方协调,相互依赖。

  • 举例:
    一、出门旅游。1.请假;2. 订机票;3.订酒店;4.租车。
    二、线上运维发布新服务或对已有服务水平扩展。1. 找机器;2. 初始化环境;3. 部署应用;4. 健康检查;5. 接入流量。

  • 实现业务补偿的步骤

    • 服务幂等化。一个事务失败或超时,可通过重试达到目标状态。
    • 状态可回滚。如果达不到目标状态,整个状态可恢复至初始状态。
    • 业务可更新。请求有变化,启动整个事务的业务更新机制。
  • 好的业务补偿机制的要点。

    • 业务的起始状态定义。清楚的描述要达到什么样的状态(比如:请假、机票、酒店这三个必须成功,租车是可选的),以及如果其中的条件不满足,那么我们要回退到哪一个状态。
    • 状态拟合。业务跑起来的时候,串行或并行地做这些事,努力达到目标状态,达不到就通过补偿机制回滚到之前的状态。
    • 事务可修改。对于已经完成的事务进行整体修改,可以考虑成个修改事务。
  • 业务补偿的设计重点

    • 业务流程涉及的服务方支持幂等性,上游有重试机制。
    • 维护和监控整个过程的状态。业务流程的控制方完成,即工作流引擎。类比旅行代理机构,既能满足需求,有问题也会帮我们回滚和补偿。
    • 设计业务正向流程的时候,也需要设计业务的反向补偿流程。
    • 业务补偿的业务逻辑是强业务相关,难以通用。
    • 下层的业务方最好提供短期的资源预留机制。电商把货品库存占 15 分钟支付的时间。

六、重试设计 Retry

重试的场景

  • 语义:当前故障是暂时的,不是永久的,需要重试。
  • 定义需要重试的情况:调用超时、被调用端返回了某种可以重试的错误(如繁忙中、流控中、维护中、资源不足等)
  • 定义不需要重试的情况:业务级错误(无权限,非法数据)、技术上的错误(如 http 503)

重试的策略

  • 间歇性重试:避免因为重试过快而导致网络负担加重。
  • 指数级退避:每一次重试休息时间会成倍增加。
  • 目的:让被调用方能够有更多的时间来从容处理请求。类似 TCP 的拥塞控制。

Spring 的重试策略

  • Spring Retry

    • NeverRetryPolicy

    • AlwaysRetryPolicy

    • SimpleRetryPolicy

    • TimeoutRetryPolicy

    • CircuitBreakerRetryPolicy

    • CompositeRetryPolicy

      • 乐观组合重试策略
      • 悲观组合重试策略
  • Backoff 的策略

    • NoBackOffPolicy
    • FixedBackOffPolicy
    • UniformRandomBackOffPolicy
    • ExponentialBackOffPolicy
    • ExponentialRandomBackOffPolicy

重试设计的重点

  • 确定什么样的错误下需要重试。

  • 重试的时间和重试的次数。

    • 前端交互:快速失败报错,网络错误请重试。
    • 流控,使用指数退避的方式,以避免造成更多的流量。
  • 超过重试次数,或是一段时间,重试没有意义。

  • 考虑被调用方是否有幂等涉及。

    • 如果没,重试不安全,会导致一个相同操作被执行多次。
  • 重试代码简单且通用,不用侵入业务代码。

    • 模式一:代码级,类似注解。
    • 模式二:Service Mesh 方式

七、熔断设计 Circuit Breaker

名词来源

  • 电路世界的熔断机制,电闸的保险丝,自我保护装置。
  • 分布式系统中,无意义重试达到一定阈值,要开启熔断操作,保护后端不会过载。
  • 闭合:正常
    断开:故障
    半开:故障后检测故障是否已被修复的场景。

熔断器模式的作用

  • 防止应用程序不断尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费 CPU 时间去等待长时间的超时产生。
  • 使应用程序能够诊断错误是否已经修正。如果已经修正,应用程序会再次尝试调用操作。
  • 使得系统更加稳定和有弹性,在系统从错误中恢复的时候提供稳定性,并且减少了错误对系统性能的影响。
  • 快速拒绝那些有可能导致错误的服务调用,而不会去等待操作超时或者永远不返回结果来提高系统的响应时间。

熔断器模式图解

  • 熔断器模式的状态机实现

  • 闭合(Closed 状态)

    • 错误计数器

      • 调用失败加1
      • 超过阈值,断开。
      • 特定时间间隔自动重置。
    • 超时时钟

      • 时钟超过该时间,半断开。
  • 断开(Open)状态

    • 对请求立即返回错误响应,不调用后端服务。
    • 缓存机制可以拦一层。
  • 半开((Half-Open)

    • 一定数量的请求调用
    • 调用成功,闭合,错误计数器重置。
    • 调用失败,断开,重置计数器
    • 作用:有效防止正在恢复中的服务被突然而来的大量请求再次拖垮。
  • 图解

Hystrix 中熔断的实现逻辑

    1. 有请求来了,allowRequest() 处于非熔断,放行,到达熔断时间陪,放行,否则返回出错
    1. 每次调用都有两个函数 markSuccess(duration) 和 markFailure(duration) 来统计一下在一定的 duration 内有多少调用是成功还是失败的。
    1. 判断是否熔断的条件 isOpen(),是计算一下 failure/(success+failure) 当前的错误率,如果高于一个阈值,那么打开熔断,否则关闭。
    1. Hystrix 会在内存中维护一个数组,其中记录着每一个周期的请求结果的统计。超过时长长度的元素会被删除掉。

熔断设计的重点

  • 错误类型。
  • 日志监控
  • 测试服务是否可用
  • 手动重置
  • 并发问题
  • 资源分区
  • 重试错误的请求

八、限流设计 Throttle

限流策略

  • 拒绝服务
  • 服务降级
  • 特权请求
  • 延时处理
  • 弹性伸缩

限流的实现方式

  • 计数器方式

    • 请求来加一
    • 请求完减一
    • Counter 大于阈值,拒绝请求
  • 队列算法

    • 通用

      -

    • 优先级队列

      -

    • 权重队列(避免低优先级被饿死)

      -

    • 队列流控

      • 以队列的方式来处理请求,如果处理过慢,会导致队列满,开始触发限流。
      • 缺点:队列长度控制流量,配置上难操作队列过长,导致后端服务在队列没有满时就挂掉了。
      • 这样的模型不能做 push,而是 pull 方式会好一些。
    • 漏斗算法 Leaky Bucket

      • 原理:进水量如同访问流量,出水量如同系统处理请求。访问流量过大,漏斗积水,水太多就会溢出。

      • 实现方式:队列,请求过多,队列积压请求,队列满,开始拒绝请求。举例:TCP,请求数量过多,会有一个 sync backlog 的队列缓冲请求,或是 TCP 的滑动窗口用于流控的队列。

      • 队列请求+限流器,让 Processor 以一个均匀的速度处理请求。

        -

    • 令牌桶算法 Token Bucket

      • 原理:有一个中间人。在桶内按一定速率放入一些 token,然后处理程序要处理请求时,需要拿到 token,才能处理;如果拿不到,则不处理。

      • 图解

        -

      • 令牌桶和漏斗的差异

        • 处理请求的方式:前者是在流量小的时候攒钱,流量大的时候快速处理。
          后者是以一个常量和恒定的速度处理。
        • 令牌桶可以做成第三方服务,在分布式系统中对全局进行流控。
  • 基于响应时间的动态限流

    • 限流值配置相关的因素

      • 数据库
      • API 性能
      • 服务的自动化伸缩
    • 限流方式的原理

      • 限流的值很难被静态地设置成恒定的一个值。

      • 动态限流:不设定特定流控值,能够动态感知系统压力来自动化限流。

      • 设计典范:TCP 协议的拥塞控制算法

        • rtt 探测网络延时和性能,从而设定相应的滑动窗口的大小,以让发送的速率和网络的性能相匹配。
    • 限流实践步骤

      • 记录每次调用后端请求的响应时间。
      • 在一个时间区间内(比如过去 10s)的请求计算一个响应时间的 P90 或 P99 值,也就是把过去 10s 内的请求的响应时间排个序
      • 看 90% 或 99% 的位置是多少
      • P90 或 P99 超过设定阈值,自动限流
    • 限流设计要点

      • 要计算一定时间内的 P90 或 P99。本身耗 CPU,因为大量排序。

        • 解决方案一:不记录所有请求,采样。
        • 解决方案二:蓄水池的近似算法。

九、降级设计 degradation

目的

  • 解决资源不足和访问量过大的问题。
  • 有限资源扛住大量请求。

方式

    1. 降低一致性。
    • a、简化流程一致性。异步简化电商下单交易系统。
    • b、降低数据一致性。缓存或去数据。Cache Aside 模式或是 Read Through 模式
    1. 停止次要功能。
    • 峰值来临前暂停次要功能,峰值过后恢复功能。如 电商的搜索功能,用户的评论功能。
    • 限制功能流量或是退化成简单功能,停止功能做兜底。
    • 带来的用户体验问题要做有限的补偿。
    1. 简化功能。
    • API 多版本数据返回。比如:正常商详页的商品和评论都返回前端,降级场景只返回商品信息。
    • 释放更多资源给交易系统这种强依赖数据库资源的业务使用。

要点

    1. 要牺牲业务功能或流程以及一致性,需要对业务进行梳理分析,在业务接受的范围内做到功能降级。
    1. 定义降级的关键条件,做好相应的应急预案。
    • 条件:吞吐量过大、响应时间过慢、失败次数过多、网络或服务故障。
    • 预案要快速自动化或半自动化执行。
    1. 业务功能梳理:mast-have or nice-to-have or 死保 or 可以牺牲。
    1. 牺牲一致性或业务流程:读操作,使用缓存;写操作,异步调用;记流水,方便对账。
    1. 降级开关设置成配置型,可推送。API 有所区分,由上游调用者来驱动。
    1. 降级属于应急情况,长期不用容易出现问题或 bug,需要做演练。

总结

弹力设计总图

  • 首先,服务不能是单点,所以架构上要冗余服务,即有多个服务的副本。

    • 负载均衡+服务健康检查:Nginx
    • 服务发现+自动路由+健康检查:ZooKeeper
    • 自动化运维:Kubernetes 服务调度、伸缩和故障迁移
  • 然后,需要隔离业务,隔离业务就要对服务进行解耦和拆分。

    • bulkheads 模式:业务分片、用户分片、数据库拆分。
    • 自包含系统:单体到微服务的中间状态,把一组密切相关的微服务拆分出来,做到无外部依赖。
    • 异步通讯:服务发现、事件驱动、消息队列、业务工作流。
    • 自动化运维:需要一个服务调用链和性能监控的监控系统。
  • 最后,需要能让整个架构接受失败的相关处理设计,即容错设计。

    • 错误方面:调用重试+熔断+服务的幂等性设计。
    • 一致性方面:强一致性使用两阶段提交、最终一致性使用异步通讯方式。
    • 流控方面:使用限流+降级技术。
    • 自动化运维方面:网关流量调度,服务监控。

弹力设计开发和运维

  • 运维工具需要两个系统

    • 服务监控系统。类似 APM。
    • 服务调度系统。类似 Docker + KUbernetes。
  • 统一标准的开发工具

    • Spring Cloud
  • Spring Cloud 与 Kubernetes 的区别

    • Spring Cloud 是丰富且集成良好的 Java 库,作为应用栈的一部分解决所有运行时问题。
    • Kubernetes 针对容器,不针对语言。是以通用的方式为所有语言解决分布式计算问题。
    • 二者是微服务的最佳实践。

XMind - Evaluation Version