前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、概念

1、服务降级

说明:主逻辑失败采用备用逻辑的过程

故事的背景是这样的:由于小强在工作中碰到一些问题,于是想请教一下业界大牛小壮。于是发生了下面的两个场景:

案例:小强在拿起常用手机拨号时发现该手机没有能够拨通,所以就拿出了备用手机拨通了某 A 的电话,这个过程就叫做降级

2、服务熔断

说明:因短期内多次失败,而被暂时性的忽略,不再尝试使用,而是直接使用能够正常访问的链路进行访问

故事的背景是这样的:由于小强在工作中碰到一些问题,于是想请教一下业界大牛小壮。于是发生了下面的两个场景:

阶段1:由于每次小壮的解释都属于长篇大论,不太容易理解,所以小强每次找小壮沟通的时候都希望通过常用手机来完成,因为该手机有录音功能,这样自己可以慢慢消化。

阶段2:由于上一次的沟通是用备用电话完成的,小强又碰到了一些问题,于是他又尝试用常用电话拨打,这一次又没有能够拨通,所以他不得不又拿出备用手机给某 A 拨号,就这样连续的经过了几次在拨号设备选择上的“降级”。

阶段3:小强觉得短期内常用手机可能因为运营商问题无法正常拨通了,所以,再之后一段时间的交流中,小强就不再尝试用常用手机进行拨号,而是直接用备用手机进行拨号,这样的策略就是熔断

3、服务降级 & 服务熔断 区别

1、触发原因不太一样,服务熔断一般是某个服务(上游服务)故障引起,而服务降级一般是从整体负荷考虑;

2、管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分

4、可用性的定义

一个完善的架构应该具备3个能力,也就是身体的“三高”:1、高性能;2、高可用;3、易扩展。

理解高可用时,通常参考两个关键指标:

⬤ 平均故障间隔(Mean Time Between Failure,简称 MTBF):表示两次故障的间隔时间,也就是系统正常运行的平均时间,这个时间越长,说明系统的稳定性越高;

⬤ 故障恢复时间(Mean Time To Repair,简称 MTTR):表示系统发生故障后恢复的时间,这个时间越短,说明故障对用户的影响越小。

说明:只有当系统故障间隔时间越长,且恢复时间越短,系统的整体可用性才会更高。

可用性(Availability)的计算公式:Availability = MTBF / (MTBF + MTTR) * 100% ()

image-20240111164357666

二、流量治理

流量治理的目的是为了优化网络流量的分配和管理,以确保网络资源得到有效利用,并提高网络的性能和稳定性。通过流量治理,可以实现以下目的:

目的 说明
优化网络性能 通过限制或调整流量,可以减少网络拥堵和延迟,提高数据传输速度和响应时间。
安全防护 对网络流量进行监控和管理,以识别和阻止恶意流量、DDoS 攻击等网络安全威胁。
成本控制: 通过流量治理,可以有效控制网络资源的使用,降低网络运营成本。
提高用户体验 通过合理的流量治理策略,可以确保用户能够快速、稳定地访问所需的网络资源,提升用户体验。
故障容错和弹性 在网络或服务出现问题时,通过动态路由和流量重定向等机制,实现故障转移和自我恢复,以维持服务的持续可用性;

三、流量治理的手段

1、熔断器

场景:当 A 的其中一个依赖服务 B 出现故障,只能被动地等待依赖服务 B 报错或者请求超时;这个时候:

A 下游连接池会被逐渐耗光;

⬤ 入口请求大量堆积,CPU、内存等资源被逐渐耗尽,最终导致服务宕掉。

⬤ 依赖 “A” 服务的下游服务,也会因为相同的原因出现故障,一系列的 级联故障 最终会导致整个系统不可用;

方案:合理的解决方案是 引入熔断器 **和优雅降级**,通过尽早失败来避免局部不稳定而导致的整体雪崩。

熔断本质上是一种快速失败策略。旨在通过及时中断失败或超时的操作,防止资源过度消耗和请求堆积,从而避免服务因小问题而引发的雪崩效应。

1)一刀切-熔断器

当请求失败比率达到一定阈值之后,熔断器开启,并休眠一段时间(由配置决定)。这段休眠期过后,熔断器将处于半开状态,在此状态下将试探性地放过一部分流量,如果这部分流量调用成功后,再次将熔断器关闭,否则熔断器继续保持开启并进入下一轮休眠周期。

关闭:默认状态。允许请求到达目标服务,同时统计在窗口时间内的成功和失败次数,如果达到错误率阈值将会切换为“打开”状态;

打开:对应用的请求会立即返回错误响应或执行预设的失败降级逻辑,而不调用目标服务

半开:进入“打开”状态会维护一个超时时间,到达超时时间后开始进入该状态,允许应用程序一定数量的请求去调用目标服务。

⬤ 熔断器会对成功执行的调用进行计数,达到配置的阈值后会认为目标服务恢复正常,此时熔断器回到“关闭”状态;

⬤ 如果有请求出现失败的情况,则回到“打开”状态,并重新启动超时计时器,再给系统一段时间来从故障中恢复。

image-20240111172928918

2)弹性熔断-熔断器

客户端自行限制请求速度,限制生成请求的数量,在熔断器 Open 状态下 仍然可以放行少部分流量,超过这个数量的请求直接在本地回复失败,而不会真正发送到服务端。

关键词  
requests 客户端请求总量
accepts 成功的请求总量 - 被 accepted 的量

客户端请求被拒绝的概率(Client request rejection probability,以下简称为 p)

1、在通常情况下(无错误发生时) requests == accepts

2、当后端出现异常情况时,accepts 的数量会逐渐小于 requests

3、当后端持续异常时,客户端可以继续发送请求直到 requests = Kaccepts ,一旦超过这个值,客户端就启动自适应限流机制,新产生的请求在本地会被概率(以下称为 p)丢弃;

4、当客户端主动丢弃请求时,requests 值在某个时间点会超过 Kaccepts,使 p 计算出来的值大于 0,此时客户端会以此概率对请求做主动丢弃;

5、当后端逐渐恢复时,accepts 增加,(同时 requests 值也会增加,但是由于 K 的关系,K * accepts 的放大倍数更快),使得 ( requestsK × accepts ) / ( requests + 1) 变为负数,从而 p == 0,客户端自适应限流结束。

p 基于如下公式计算(其中 K 为倍率 - multiplier,常用的值为 2)

降低 K 值会使自适应限流算法更加激进(允许客户端在算法启动时拒绝更多本地请求);

增加 K 值会使自适应限流算法变得保守一些(允许服务端在算法启动时尝试接收更多的请求,与上面相反)。

2、 隔离

微服务系统中,隔离策略是流量治理的关键组成部分,其主要目的是避免单个服务的故障引发整个系统的连锁反应。

image-20240111174605303

1)动静隔离

动静隔离通常是指将系统的动态内容和静态内容分开处理

a、动态内容

1、指需要实时计算或从数据库中检索的数据,通常由后端服务提供;

2、可以通过缓存、数据库优化等方法来提高动态内容的处理速度。

b、静态内容

1、指可以直接从文件系统中获取的数据,例如图片、音视频、前端的 CSSJS 文件等静态资源;

2、可以存储到 OSS 并通过 CDN 进行访问加速。

image-20240111174752901

2)读写隔离

读写隔离通常是指将读操作和写操作分离到不同的服务或实例中处理,大部分的系统里读写操作都是不均衡的,写数据可能远远少于读数据;读写隔离得以让读服务和写服务独立扩展。

a、写服务

⬤ 负责处理所有的写操作,例如创建、更新和删除数据;

⬤ 通常会有一个或多个数据库或数据存储,用于保存系统的数据。

b、读服务

⬤ 负责处理所有的读操作,例如查询和检索数据;

⬤ 可以有独立的数据库或数据存储,也可以使用缓存来提高查询的性能。

c、事件驱动

⬤ 当写服务处理完一个写操作后,通常会发布一个事件,通知读服务数据已经发生变化;

⬤ 读服务可以监听这些事件,并更新其数据库或缓存,以保证数据的一致性。

d、独立扩展

⬤ 通过 CQRS 模式,读服务和写服务可以独立地进行扩展;

⬤ 如果系统的读负载较高,可以增加读服务的实例数量;如果写负载较高,可以增加写服务的实例数量。

image-20240111175038135

3)核心隔离

核心隔离通常是指将资源按照 “核心业务”与 “非核心业务”进行划分,优先保障“核心业务”的稳定运行AI助手

4)热点隔离

热点隔离通常是指一种针对高频访问数据(热点数据)的隔离策略

  • 可以帮助微服务系统更高效地处理热点数据的访问请求;

  • 需要有机制来识别和监控热点数据;

    • 分析系统的历史访问记录;
    • 观察系统的监控告警信息等。
  • 将访问频次最高的 Top K 数据缓存起来,可以显著减少对后端存储服务的访问压力,同时提高数据访问的速度;

  • 可以创建一个独立的缓存服务来存储和管理热点数据,实现热点数据的隔离。

5)用户隔离

用户隔离通常是指按照不同的分组形成不同的服务实例。这样某个服务实例宕机了也只会影响对应分组的用户,而不会影响全部用户

1、每个租户有独立的服务与数据库(网关根据 tenant_id 识别出对应的服务实例进行转发)

2、每个租户有共享的服务与独立的数据库(用户服务根据 tenant_id 确定操作哪一个数据库)

3、每个租户有共享的服务与数据库(用户服务根据 tenant_id 确定操作数据库的哪一行记录)

6)进程隔离

进程隔离通常是指系统中每一个进程拥有独立的地址空间,提供操作系统级别的保护区。一个进程出现问题不会影响其他进程的正常运行,一个应用出错也不会对其他应用产生副作用

容器化部署便是进程隔离的最佳实践:

7)线程隔离

线程隔离通常是指线程池的隔离,在应用系统内部,将不同请求分类发送给不同的线程池,当某个服务出现故障时,可以根据预先设定的熔断策略阻断线程的继续执行

⬤ 接口 A 和 接口 B 共用相同的线程池,当 接口 A 的访问量激增时,接口 C 的处理效率就会被影响,进而可能产生雪崩效应;

⬤ 使用线程隔离机制,可以将 接口 A 和 接口 B 做一个很好的隔离。

8)集群隔离

集群隔离通常是指将某些服务单独部署成集群,或对于某些服务进行分组集群管理,具体来说就是每个服务都独立成一个系统,继续拆分模块,将功能微服务化:

image-20240111175756230

9)机房隔离

机房隔离通常是指在不同的机房或数据中心部署和运行服务,实现物理层面的隔离。机房隔离的主要目的有两个:

1、解决数据容量大、计算和 I/O 密集度高的问题。将不同区域的用户隔离到不同的地区,比如将湖北的数据存储在湖北的服务器,浙江的数据存储在浙江的服务器,这种区域化的数据管理能有效地分散流量和系统负载;

2、增强数据安全性和灾难恢复能力。通过在不同地理位置建立服务的完整副本(包括计算服务和数据存储),系统可以实现异地多活或冷备份。这样,即使一个机房因自然灾害或其他紧急情况受损,其他机房仍能维持服务,确保数据安全和业务连续性。

3、重试

如何在不可靠的网络服务中实现可靠的网络通信,这是计算机网络系统中避不开的一个问题。微服务架构中,一个大系统被拆分成多个小服务,小服务之间大量的 RPC 调用,过程十分依赖网络的稳定性。网络是脆弱的,随时都可能会出现抖动,此时正在处理中的请求有可能就会失败。

对于网络抖动这种情况,解决的办法之一就是重试。但重试存在风险,它可能会解决故障,也可能会放大故障。对于网络通信失败的处理一般分为以下几步:

感知错误:通过不同的错误码来识别不同的错误,在 HTTPstatus code 可以用来识别不同类型的错误。

重试决策;这一步主要用来减少不必要的重试,比如 HTTP4xx 的错误,通常 4xx 表示的是客户端的错误,这时候客户端不应该进行重试操作,或者在业务中自定义的一些错误也不应该被重试。根据这些规则的判断可以有效的减少不必要的重试次数,提升响应速度。

重试策略;重试策略就包含了重试间隔时间,重试次数等。如果次数不够,可能并不能有效的覆盖这个短时间故障的时间段,如果重试次数过多,或者重试间隔太小,又可能造成大量的资源(CPU、内存、线程、网络)浪费。

对冲策略:对冲是指在不等待响应的情况主动发送单次调用的多个请求,然后取首个返回的回包。

1)重试方式

常见的重试主要有两种方式:同步重试、异步重试

a、同步重试

⬤ 程序在调用下游服务失败的时候重新发起一次;

⬤ 实现简单,能解决大部分网络抖动问题,是比较常用的一种重试方式。

b、异步重试

如果服务追求数据的强一致性,并且希望在下游服务故障的时候不影响上游服务的正常运行,此时可以考虑使用异步重试。

⬤ 将请求信息丢到消息队列中,由消费者消费请求信息进行重试;

⬤ 上游服务可以快速响应请求,由消费者异步完成重试。

c、最大重试次数

无限重试可能会导致系统资源(网络带宽、CPU、内存)的耗尽,甚至引发重试风暴,应评估系统的实际情况和业务需求来设置最大重试次数:

1、设置过低,可能无法有效地处理该错误;

2、设置过高,同样可能造成系统资源的浪费。

d、退避策略

⬤ 一方面要考虑到本次请求时长过长而影响到的业务的忍受度;

⬤ 一方面要考虑到重试对下游服务产生过多请求带来的影响。

退避策略基于重试算法实现。重试算法有多种,思路都是在重试之间加上一个间隔时间

策略 说明
线性间隔 每次重试间隔时间是固定的,比如每 1s 重试一次。
线性间隔+随机时间 有时候每次重试间隔时间一致可能会导致多个请求在同一时间请求;
加入随机时间可以在线性间隔时间的基础上波动一个百分比的时间。
指数间隔 间隔时间是指数型递增,例如等待 3s、9s、27s 后重试。
指数间隔+随机时间 在指数递增的基础上添加一个波动时间。

e、重试风暴

1、DB 负载过高时,Service CDB 的请求出现失败;

2、因为配置了重试机制,Service CDB 发起了最多 3 次请求;

3、链路上为了避免网络抖动,上游的服务均设置了超时重试 3 次的策略;

4、这样在一次业务请求中,对 DB 的访问可能达到 3^(n) 次。

image-20240111182242320

解决方案:

1、限制单点重试

⬤ 一个服务不能不受限制地重试下游,很容易造成下游服务被打挂;

⬤ 除了设置最大重试次数,还需要限制重试请求的成功率。

2、引入重试窗口

⬤ 基于断路器的思想,限制 请求失败/请求成功 的比率,给重试增加熔断功能

⬤ 常见的实现方式是引入滑动窗口。

3、限制链路重试

⬤ 多级链路中如果每层都配置重试可能导致调用量指数级扩大;

⬤ 核心是限制每层都发生重试,理想情况下只有最下游服务发生重试;

Google SRE 中指出了 Google 内部使用特殊错误码的方式来实现。

该方法可以有效避免重试风暴,但请求链路上需要上下游服务约定好重试状态码并耦合对应的逻辑,一般需要在框架层面上做出约束。

f、对冲策略

有时候我们接口只是偶然会出问题,并且我们的下游服务并不在乎多请求几次,那么我们可以考虑对冲策略 AI 助手,对冲是指在不等待响应的情况下主动发送单次调用的多个请求,然后取首个返回的回包

4、降级

虽说故障是不可避免的,要达到绝对高可用一般都是使用冗余+自动故障转移,这个时候其实也不需要降级措施了。

与限流的区别

1、降级依靠牺牲一部分功能或体验保住容量,而限流则是依靠牺牲一部分流量来保住容量。

2、一般来说,限流的通用性会更强一些,因为每个服务理论上都可以设置限流,但并不是每个服务都能降级,比如 O2 系统中的登录服务和用户服务,就不可能被降级(没有这两个服务,用户都没法使用系统了)。

image-20240111183140736

1)自动降级

1、适合触发条件明确可控的场景,比如请求调用失败次数大于一定的阈值,服务接口超时等情况;

2、对于一些旁路服务,服务负载过高也可以直接触发自动降级。

2)手动降级

1、降级操作都是有损的,部分情况下需要根据对业务的影响程度进行手动降级;

2、通常需要先制定降级的分级策略,影响面由浅至深。

3)执行降级

image-20240111183359428

5、超时

超时是一件很容易被忽视的事情

早期架构发展阶段,大家或多或少有过遗漏设置超时或者超时设置太长导致系统被拖慢甚至挂起的经历,随着微服务架构的演进,超时逐渐被标准化到 RPC 中,并可通过微服务治理平台快捷调整超时参数 ,

1、传统超时会设定一个固定的阈值,响应时间超过阈值就返回失败。在网络短暂抖动的情况下,响应时间增加很容易产生大规模的成功率波动

2、服务的响应时间并不是恒定的,在某些长尾条件下可能需要更多的计算时间,为了有足够的时间等待这种长尾请求响应,我们需要把超时设置足够长,但超时设置太长又会增加风险,超时的准确设置经常困扰我们

1)超时策略

1、固定超时时间

2、EMA 动态超时。

2)超时控制

超时控制的本质是 fail fast,良好的超时控制可以尽快清空高延迟的请求,尽快释放资源避免请求堆积。

a、服务间超时传递

如果都使用每个 RPC 服务设置的固定超时时间,这里以上图为例

1、A -> B,设置的超时时间为 3s

2、B 处理耗时为 2s,并继续请求 C

3、如果使用了超时传递那么 C 的超时时间应该为 1s,这里不采用所以超时时间为配置的 3s

4、C 继续执行耗时为 2s,此时最上层(A)设置的超时时间已截止;

5、C -> D 的请求对A 来说已经失去了意义。

image-20240111190908852

b、EMA 动态超时

如果我们的微服务系统对这种短暂的时延上涨具备足够的容忍能力,可以考虑基于 EMA 算法动态调整超时时长。

EMA 算法引入“平均超时”的概念,用平均响应时间代替固定超时时间,只要平均响应时间没有超时即可,而不是要求每次请求都不能超时。

⬤ 当平均响应时间(EMA)大于超时时间限制(Thwm),说明平均情况表现很差,动态超时时长(Tdto)就会趋近于超时时间限制(Thwm),降低弹性;

⬤ 当平均响应时间(EMA)小于超时时间限制(Thwm),说明平均情况表现很好,动态超时时长(Tdto)就可以超出超时时间限制(Thwm),但会低于最大弹性时间(Tmax),具备一定的弹性。

总而言之:

1、总体情况不能超标;

2、平均情况表现越好,弹性越大;

3、平均情况表现越差,弹性越小。

c、超时策略的选择

超时策略的选择:剩余资源 = 资源容量 - QPS 单次请求消耗资源请求持续时长 – 资源释放所需时长

- 固定超时( 关键路径选择固定超时; EMA动态超时(非关键路径开启 EMA 动态超时,防止一直出问题导致服务耗时增加、吞吐量降低。
优点 稳定 可以根据耗时动态调整超时时间
缺点 如果某个服务一直出问题超时,会导致服务吞吐量降低 服务有损

3)超时建议

根据服务调用方应用的级别及其定义的可用率 SLA,可依据如下建议设置其超时时间:

调用方应用级别 可用率SLA 超时时间建议值(ms,按十位取整)
0级 核心系统 >=99.99% 超时时间 <= MAX(TP9999*1.2,TP9999+20ms)
1级 次核心系统 >=99.9% 超时时间 <= MAX(TP999*1.2,TP999+10ms)
2级 业务支撑系统 >=99% 超时时间 <= MAX(TP99*1.2,TP99+10ms)
3级 一般系统 >=99% 超时时间 <= MAX(TP99*1.2,TP99+10ms)

⬤ 超时时间原则上仅受调用方应用级别、可用率 SLA,以及调用该服务的性能数据影响,而不受服务提供方应用级别影响

漏斗检测

微服务架构中,用户的每次操作,都会产生多次请求,依次经过各个节点,比如购物车->库存->商品读服务等,形成一条调用链(调用树)。调用链中的不少节点,需要设置请求超时时间。一般来说,整个调用链上各节点的超时时间应该呈现“漏斗状”,即越下游的超时时间越小,如下图所示:

image-20240924163657668

图中展示了 ABCD四个节点,箭头表示调用关系,箭头上的数字表示请求设置的超时(单位ms)。 场景1调用链的超时设置是符合“漏斗状”的,而场景2则不符合,不符合“漏斗状”的超时设置至少有以下两个风险:

1. 资源暴涨:当某类请求在 D 中的发生故障导致请求超时,B 调用 C100ms 时超时失败,但 C 调用 D 的请求还在继续,资源没有释放。特别当 B 或者其上游有重试的情况下,C 的资源压力会成倍增长,从而影响所有经过 C 的请求。这种资源暴涨会影响所有下游节点,并随着链路的长度影响越来越大。

2. 降级失效:如果 C 有降级的业务需求,当 C 调用 D 发生超时失败时转而调用其他兜底服务,因 B 调用 C 已超时从而无法正常返回兜底数据。

综上所述,合理设置链路上各个节点的超时时间非常重要,为了合理设置链路超时,应用健康度上线了链路超时检测,能自动检测出超时设置不符合“漏斗状”的链路,并提示风险,如下图所示:

6、限流

image-20240111191705941

ContactAuthor