目录
限流
1. 固定时间窗口算法
2. 滑动时间窗口算法
3. 漏桶算法(leaky bucket)
4. 令牌桶算法(Token Bucket)
服务降级
1 、简介
2 、使用场景
3 、核心设计
3.1 分布式开关
3.2 自动降级
3.3 配置中心
3.4 处理策略
4 、高级特性
4.1 分级降级
4.2 降级权值
5 、总结与展望
开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。
缓存:缓存的目的是提升系统访问速度和增大系统处理容量。
降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。
限流:限流的目的是通过对并发请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以进行拒绝服务、排队或等待、降级等处理。
限流是限制系统的输入和输出流量,以达到保护系统的目的,而限流的实现主要是依靠限流算法,限流算法主要有4种:
固定时间窗口算法(计数器)
滑动时间窗口算法
令牌桶算法
漏桶算法
又称计数器算法。固定时间窗口算法就是统计记录单位时间内进入系统或者某一接口的请求次数,在限定的次数内的请求则正常接收处理,超过次数的请求则拒绝掉或者改为异步处理等限流措施。
时间窗口长度如果为1分钟,如图。
此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性即可轻松实现。
单机伪代码如下。
class CounterDemo {
public long timeStamp = getNowTime();
public int reqCount = 0;
public final int limit = 100; // 时间窗口内最大请求数
public final long interval = 1000; // 时间窗口ms
public boolean grant() {
long now = getNowTime();
if (now < timeStamp + interval) {
// 在时间窗口内
reqCount++;
// 判断当前时间窗口内是否超过最大请求控制数
return reqCount <= limit;
} else {
timeStamp = now;
// 超时后重置
reqCount = 1;
return true;
}
}
}
算法特点
实现简单。
时间窗口固定,每个窗口开始时计数为零,这样后面的请求不会受到之前的影响,做到了前后请求隔离。
因为两个时间窗口之间没有任何联系,所以调用者可以在一个时间窗口的结束到下一个时间窗口的开始这个非常短的时间段内发起两倍于阈值的请求。所以固定时间窗口算法无法限制窗口间突发流量。
滑动时间窗口算法其实是固定时间窗口算法的优化,主要是为了解决固定时间窗口算法无法限制窗口间突发流量的缺点。
上面的计数器的单位时间是1分钟,而在使用滑动时间窗口,可以把1分钟分成6格,每格时间长度是10s,每一格又各自管理一个计数器,单位时间用一个长度为60s的窗口描述。一个请求进入系统,对应的时间格子的计数器便会+1,而每过10s,这个窗口便会向右滑动一格。只要窗口包括的所有格子的计数器总和超过限流上限,便会执行限流措施。
由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
算法特点
因为窗口顺延,所以可以抵御窗口间突发流量(对比固定时间窗口算法)。
假如限流10万次/小时,如果某个调用者在前10分钟调用了10万次那么他必须再等待1小时才能发起下一次正常请求。所以没有做到前后请求隔离。
阿里开源的Sentinel,采用的是滑动窗口算法进行限流,可以阅读相关代码,加深对滑动时间窗口算法的理解。
漏桶算法其实很简单,可以粗略的认为就是注水漏水过程,往桶中以一定速率流出水,以任意速率流入水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。这个从桶底流出去的水就是系统正常处理的请求,从旁边流出去的水就是系统拒绝掉的请求。
单机伪代码如下。
class LeakyDemo {
public long timeStamp = getNowTime();
public int capacity; // 桶的容量
public int rate; // 水漏出的速度
public int water; // 当前水量(当前累积请求数)
public boolean grant() {
long now = getNowTime();
water = max(0, water - (now - timeStamp) * rate); // 先执行漏水,计算剩余水量
timeStamp = now;
if ((water + 1) < capacity) {
// 尝试加水,并且水还未满
water += 1;
return true;
} else {
// 水满,拒绝加水
return false;
}
}
}
算法特点
因为流出的速度是一定的,可以抵御突发流量,做到更加平滑的限流,而且不允许流量突发。
令牌桶算法是比较常见的限流算法之一,Google开源项目Guava中的RateLimiter使用的就是令牌桶算法。流程如下:
所有的请求在处理之前都需要拿到一个可用的令牌才会被处理。
根据限流大小,设置按照一定的速率往桶里添加令牌。
桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝。
请求到达后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除。
单机伪代码如下,分布式环境可以使用Redisson。
class TokenBucketDemo {
public long timeStamp = getNowTime();
public int capacity; // 桶的容量
public int rate; // 令牌放入速度
public int tokens; // 当前令牌数量
public boolean grant() {
long now = getNowTime();
// 先添加令牌
tokens = min(capacity, tokens + (now - timeStamp) * rate);
timeStamp = now;
if (tokens < 1) {
// 若桶中没有令牌,则拒绝
return false;
} else {
// 还有令牌,领取令牌
tokens -= 1;
return true;
}
}
}
算法特点
可以抵御突发流量,因为桶内的令牌数不会超过给定的最大值
可以做到更加平滑的限流,因为令牌是匀速放入的。
令牌桶算法允许流量一定程度的突发。(相比漏桶算法)
在时间点刷新的临界点上,只要剩余token足够,令牌桶算法会允许对应数量的请求通过,而后刷新时间因为token不足,流量也会被限制在外,这样就比较好的控制了瞬时流量。因此,令牌桶算法也被广泛使用。
什么是服务降级?当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。
如果还是不理解,那么可以举个例子:假如目前有很多人想要给我付钱,但我的服务器除了正在运行支付的服务之外,还有一些其它的服务在运行,比如搜索、定时任务和详情等等。然而这些不重要的服务就占用了JVM的不少内存与CPU资源,为了能把钱都收下来(钱才是目标),我设计了一个动态开关,把这些不重要的服务直接在最外层拒掉,这样处理后的后端处理收钱的服务就有更多的资源来收钱了(收钱速度更快了),这就是一个简单的服务降级的使用场景。
服务降级主要用于什么场景呢?当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,我们可以将一些 不重要 或 不紧急 的服务或任务进行服务的 延迟使用 或 暂停使用。
根据上述需求,我们可以设置一个分布式开关,用于实现服务的降级,然后集中式管理开关配置信息即可。具体方案如下:
服务降级-分布式开关
超时降级 —— 主要配置好超时时间和超时重试次数和机制,并使用异步机制探测恢复情况
失败次数降级 —— 主要是一些不稳定的API,当失败调用次数达到一定阀值自动降级,同样要使用异步机制探测回复情况
故障降级 —— 如要调用的远程服务挂掉了(网络故障、DNS故障、HTTP服务返回错误的状态码和RPC服务抛出异常),则可以直接降级
限流降级 —— 当触发了限流超额时,可以使用暂时屏蔽的方式来进行短暂的屏蔽
当我们去秒杀或者抢购一些限购商品时,此时可能会因为访问量太大而导致系统崩溃,此时开发者会使用限流来进行限制访问量,当达到限流阀值,后续请求会被降级;降级后的处理方案可以是:排队页面(将用户导流到排队页面等一会重试)、无货(直接告知用户没货了)、错误页(如活动太火爆了,稍后重试)。
微服务降级的配置信息是集中式的管理,然后通过可视化界面进行友好型的操作。配置中心和应用之间需要网络通信,因此可能会因网络闪断或网络重启等因素,导致配置推送信息丢失、重启或网络恢复后不能再接受、变更不及时等等情况,因此服务降级的配置中心需要实现以下几点特性,从而尽可能的保证配置变更即使达到:
服务降级-配置中心
启动主动拉取配置 —— 用于初始化配置(减少第一次定时拉取周期)
发布订阅配置 —— 用于实现配置及时变更(可以解决90%左右的配置变更)
定时拉取配置 —— 用于解决发布订阅失效或消失丢失的情况(可以解决9%左右的发布订阅失效的消息变更)
离线文件缓存配置 —— 用于临时解决重启后连接不上配置中心的问题
可编辑式配置文档 —— 用于直接编辑文档的方式来实现配置的定义
提供Telnet命令变更配置 —— 用于解决配置中心失效而不能变更配置的常见
当触发服务降级后,新的交易再次到达时,我们该如何来处理这些请求呢?从微服务架构全局的视角来看,我们通常有以下是几种常用的降级处理方案:
页面降级 —— 可视化界面禁用点击按钮、调整静态页面
延迟服务 —— 如定时任务延迟处理、消息入MQ后延迟处理
写降级 —— 直接禁止相关写操作的服务请求
读降级 —— 直接禁止相关度的服务请求
缓存降级 —— 使用缓存方式来降级部分读频繁的服务接口
针对后端代码层面的降级处理策略,则我们通常使用以下几种处理措施进行降级处理:
抛异常
返回NULL
调用Mock数据
调用Fallback处理逻辑
我们已经为每个服务都做好了一个降级开关,也已经在线上验证通过了,感觉完全没问题了。
场景一:某一天,运营搞了一次活动,突然跑过来说,现在流量已经快涨到上限了,有没有批量降级所有不重要服务的方式?开发一脸懵逼的看着,这又不是操作DB,哪里有批量操作呀。
场景二:某一天,运营又搞事了,说我们等下要搞一个活动,让我们赶紧提前把不重要的服务都降级了,开发又是一脸懵逼,我怎么知道要降级哪些服务呀。
反思:服务降级的功能虽然是实现了,可是没有考虑实施时的体验。服务太多,不知道该降级哪些服务,单个操作降级速度太慢……
当微服务架构发生不同程度的情况时,我们可以根据服务的对比而进行选择式舍弃(即丢车保帅的原则),从而进一步保障核心的服务的正常运作。
如果等线上服务即将发生故障时,才去逐个选择哪些服务该降级、哪些服务不能降级,然而线上有成百上千个服务,则肯定是来不及降级就会被拖垮。同时,在大促或秒杀等活动前才去梳理,也是会有不少的工作量,因此建议在开发期就需要架构师或核心开发人员来提前梳理好,是否能降级的初始评估值,即是否能降级的默认值。
为了便于批量操作微服务架构中服务的降级,我们可以从全局的角度来建立服务重要程度的评估模型,如果有条件的话,建议可以使用 层次分析法(The analytic hierarchy process,简称AHP) 的数学建模模型(或其它模型)来进行定性和定量的评估(肯定比架构师直接拍脑袋决定是否降级好很多倍,当然难度和复杂度也会高许多,即你需要一个会数学建模人才),而层次分析法的基本思路是人对一个复杂的决策问题的思维和判断过程大体上是一样的。
以下是个人给出的最终评价模型,可作为服务降级的评价参考模型进行设计:
我们利用数学建模的方式或架构师直接拍脑袋的方式,结合服务能否降级的优先原则,并根据台风预警(都属于风暴预警)的等级进行参考设计,可将微服务架构的所有服务进行故障风暴等级划分为以下四种:
评估模型:
蓝色风暴 —— 表示需要小规模降级非核心服务
黄色风暴 —— 表示需要中等规模降级非核心服务
橙色风暴 —— 表示需要大规模降级非核心服务
红色风暴 —— 表示必须降级所有非核心服务
设计说明:
故障严重程度为:蓝色<黄色<橙色<红色
建议根据二八原则可以将服务划分为:80%的非核心服务+20%的核心服务
以上模型只是整体微服务架构的服务降级评估模型,具体大促或秒杀活动时,建议以具体主题为中心进行建立(不同主题的活动,因其依赖的服务不同,而使用不同的进行降级更为合理)。当然模型可以使用同一个,但其数据需要有所差异。最好能建立一套模型库,然后实施时只需要输入相关服务即可输出最终降级方案,即输出本次大促或秒杀时,当发生蓝色风暴时需要降级的服务清单、当发生黄色风暴时需要降级的服务清单……
微服务架构中有服务权值的概念,主要用于负载时的权重选择,同样服务降级权值也是类似,主要用于服务降级选择时的细粒度优先级抉择。所有的服务直接使用以上简单的四级划分方式进行统一处理,显然粒度太粗,或者说出于同一级的多个服务需要降级时的 降级顺序 该如何?甚至我想要人工智能化的 自动降级,又该如何更细粒度的控制?
基于上述的这些AI化的需求,我们可以为每一个服务分配一个降级权值,从而便于更加智能化的实现服务治理。而其评估的数值,同样也可以使用数学模型的方式进行 定性 与 定量 的评估出来,也可以架构师根据经验直接拍脑袋来确定。
以上提供了半实际与半理论的服务降级方案,使用者可以根据其公司的实际情况进行适当的选择,而完整的方案,笔者目前也没有发现有实施过的,但可以建议有长远服务治理规划的大厂进行完整方案的研究与实施,会对未来人工智能万物互联的时代有较好的治理价值存在(个人看法)。而小厂出于成本和其发挥的价值的考虑,不建议使用这么复杂的方案,但可以实现分布式开关和简单分级降级的功能特性。