弹力设计
前言
系统可用性测量
弹力定义
- 对于分布式系统的容错涉及,在英文中又叫 Resiliencey(弹力)。
- 指系统在不健康、不顺、甚至出错的情况下有能力 hold 得住,挺得住,还有能在这种逆境下力挽狂澜的能力。
做设计的原则
- 找到设计目标,或是一个基准线。
- 通过基准线或目标指导设计,使设计明确,可测试,可测量。
系统可用性的计算公式
- MTTF 是 Mean Time To Failure,平均故障前的时间
- MTTR 是 Mean Time To Recovery,平均修复时间
故障原因
服务不可用的因素
一是有计划的
- 日常任务:备份,容量规划,用户和安全管理,后台批处理应用。
- 运维相关:数据库维护、应用维护、中间件维护、操作系统维护、网络维护。
- 升级相关:数据库、应用、中间件、操作系统、网络,包括硬件升级。
二是无计划的
- 系统级故障,包括主机、操作系统、中间件、数据库、网络、电源以及外围设备。
- 数据和中介的故障,包括人员误操作、硬盘故障、数据乱了。
- 自然灾害、人为破坏、以及供电问题。
归类
网络问题。
- 网络链接出现问题,网络带宽出现拥塞。
性能问题
- 数据库慢SQL
- Java Full GC
- 硬盘 IO 过大
- CPU 飙高
- 内存不足
安全问题
- 被网络攻击,如 DDoS
运维问题
- 系统总是在被更新和修改,架构也在不断地被调整,监控问题。
管理问题
- 没有梳理出关键服务以及服务的依赖关系,运行信息没有和控制系统同步。
硬件问题
- 硬盘损坏、网卡出问题、交换机出问题、机房掉电、挖掘机问题
故障不可避免
故障是分布式的,多米诺骨牌式的。
-
两个意识
- 故障是正常的,而且是常见的。
- 故障是不可预测突发的,而且相当难缠。
观念
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 判断请求合法性
- 多服务的业务流程中使用运行上下文 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 中熔断的实现逻辑
- 有请求来了,allowRequest() 处于非熔断,放行,到达熔断时间陪,放行,否则返回出错
- 每次调用都有两个函数 markSuccess(duration) 和 markFailure(duration) 来统计一下在一定的 duration 内有多少调用是成功还是失败的。
- 判断是否熔断的条件 isOpen(),是计算一下 failure/(success+failure) 当前的错误率,如果高于一个阈值,那么打开熔断,否则关闭。
- 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
目的
- 解决资源不足和访问量过大的问题。
- 有限资源扛住大量请求。
方式
- 降低一致性。
- a、简化流程一致性。异步简化电商下单交易系统。
- b、降低数据一致性。缓存或去数据。Cache Aside 模式或是 Read Through 模式
- 停止次要功能。
- 峰值来临前暂停次要功能,峰值过后恢复功能。如 电商的搜索功能,用户的评论功能。
- 限制功能流量或是退化成简单功能,停止功能做兜底。
- 带来的用户体验问题要做有限的补偿。
- 简化功能。
- API 多版本数据返回。比如:正常商详页的商品和评论都返回前端,降级场景只返回商品信息。
- 释放更多资源给交易系统这种强依赖数据库资源的业务使用。
要点
- 要牺牲业务功能或流程以及一致性,需要对业务进行梳理分析,在业务接受的范围内做到功能降级。
- 定义降级的关键条件,做好相应的应急预案。
- 条件:吞吐量过大、响应时间过慢、失败次数过多、网络或服务故障。
- 预案要快速自动化或半自动化执行。
- 业务功能梳理:mast-have or nice-to-have or 死保 or 可以牺牲。
- 牺牲一致性或业务流程:读操作,使用缓存;写操作,异步调用;记流水,方便对账。
- 降级开关设置成配置型,可推送。API 有所区分,由上游调用者来驱动。
- 降级属于应急情况,长期不用容易出现问题或 bug,需要做演练。
总结
弹力设计总图
首先,服务不能是单点,所以架构上要冗余服务,即有多个服务的副本。
- 负载均衡+服务健康检查:Nginx
- 服务发现+自动路由+健康检查:ZooKeeper
- 自动化运维:Kubernetes 服务调度、伸缩和故障迁移
然后,需要隔离业务,隔离业务就要对服务进行解耦和拆分。
- bulkheads 模式:业务分片、用户分片、数据库拆分。
- 自包含系统:单体到微服务的中间状态,把一组密切相关的微服务拆分出来,做到无外部依赖。
- 异步通讯:服务发现、事件驱动、消息队列、业务工作流。
- 自动化运维:需要一个服务调用链和性能监控的监控系统。
最后,需要能让整个架构接受失败的相关处理设计,即容错设计。
- 错误方面:调用重试+熔断+服务的幂等性设计。
- 一致性方面:强一致性使用两阶段提交、最终一致性使用异步通讯方式。
- 流控方面:使用限流+降级技术。
- 自动化运维方面:网关流量调度,服务监控。
弹力设计开发和运维
运维工具需要两个系统
- 服务监控系统。类似 APM。
- 服务调度系统。类似 Docker + KUbernetes。
统一标准的开发工具
- Spring Cloud
Spring Cloud 与 Kubernetes 的区别
- Spring Cloud 是丰富且集成良好的 Java 库,作为应用栈的一部分解决所有运行时问题。
- Kubernetes 针对容器,不针对语言。是以通用的方式为所有语言解决分布式计算问题。
- 二者是微服务的最佳实践。
XMind - Evaluation Version