为什么要进行限流?
RPC 是解决分布式系统架构通讯的一大利器,而分布式系统设计需要面临高并发问题。在这样的情况下,我们提供的每个服务节点都可能由于访问量过大而引起一系列问题,比如业务处理耗时过长、CPU 飚高、频繁 Full GC 以及服务进程假死宕机等问题。在实际生产环境中,我们要保证服务的稳定性和高可用特性,就需要业务提供方能够进行自我保护,从而保证在高访问量、高并发的场景下,系统依然能够稳定,高效运行。
服务端的自我保护实现 在 RPC 框架中集成限流功能,可以根据实际情况配置限流阈值;我们还可以在服务端添加限流逻辑,当调用端发送请求过来时,服务端在执行业务逻辑之前先执行检查限流逻辑,如果发现访问量过大并且超出了限流条件,就让服务端直接降级处理或者返回给调用方一个限流异常。
在Dubbo框架中, 可以通过Sentinel来实现更为完善的熔断限流功能,服务端是具体如何实现限流逻辑的?
方法有很多种, 最简单的是计数器,还有平滑限流的滑动窗口、漏斗算法以及令牌桶算法等等。而Sentinel采用滑动窗口来实现的限流。
windowStart: 时间窗口的开始时间,单位是毫秒
windowLength: 时间窗口的长度,单位是毫秒
value: 时间窗口的内容
初始的时候arrays数组中只有一个窗口,每个时间窗口的长度是500ms,这就意味着只要当前时间与时间窗口的差值在500ms之内,时间窗口就不会向前滑动。 时间继续往前走,当超过500ms时,时间窗口就会向前滑动到下一个,这时就会更新当前窗口的开始时间,只要不超过1000ms,则当前窗口不会发生变化。当前时间如果超过1000ms时,就会再次进入下一个时间窗口,此时arrays数组中的窗口将会有一个失效,会有另一个新的窗口进行替换: 以此类推随着时间的流逝,时间窗口也在发生变化,在当前时间点中进入的请求,会被统计到当前时间所对应的时间窗口中。计算qps时,会用当前采样的时间窗口中对应的指标统计值除以时间间隔,这个就是具体的qps。
调用方的自我保护
一个服务 A 调用服务 B 时,服务 B 的业务逻辑又调用了服务 C,而这时服务 C 响应超时了,由于服务 B 依赖服务 C,C 超时直接导致 B 的业务逻辑一直等待,而这个时候服务 A 继续频繁地调用服务 B,服务 B 就可能会因为堆积大量的请求而导致服务宕机,由此就导致了服务雪崩的问题。
在一个服务作为调用方去调用另外一个服务时,为了防止被调用的服务出现问题而影响到整个服务,调用方的服务也需要进行自我保护, 最有效的方式就是熔断处理。 熔断机制: 熔断器的工作机制主要是关闭、打开和半打开这三个状态之间的切换。在正常情况下,熔断器是关闭的;当调用方调用下游服务出现异常时,熔断器会收集异常指标信息,当达到熔断条件时熔断器打开,这时调用端再发起请求是会直接被熔断器拦截,并快速地执行失败逻辑;熔断器经过一段时间后,会尝试转为半打开状态,这时熔断器允许调用方发送一个请求给服务端,如果这次请求能够正常地得到服务端的响应,则将状态置为关闭状态,否则设置为打开。
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时,对这个资源的调用进行限制,让请求快速失败,它可以支持以下降级策略: (带过, 可以演示RPC-DUBBO工程的熔断限流功能)
更多资料,参考Sentinel官方文档。
- 平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
- 异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
- 异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。
熔断降级源码
DegradeRule.passCheck方法:
@Override public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) { if (cut.get()) { return false; } ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource()); if (clusterNode == null) { return true; } if (grade == RuleConstant.DEGRADE_GRADE_RT) { // 按平均响应时间降级 double rt = clusterNode.avgRt(); if (rt < this.count) { passCount.set(0); return true; } // Sentinel will degrade the service only if count exceeds. // 超出最大RT时间进行降级 if (passCount.incrementAndGet() < RT_MAX_EXCEED_N) { return true; } } else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { // 按照异常比例降级 double exception = clusterNode.exceptionQps(); double success = clusterNode.successQps(); double total = clusterNode.totalQps(); // if total qps less than RT_MAX_EXCEED_N, pass. if (total < RT_MAX_EXCEED_N) { return true; } double realSuccess = success - exception; if (realSuccess <= 0 && exception < RT_MAX_EXCEED_N) { return true; } if (exception / success < count) { return true; } } else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) { // 按照异常数降级 double exception = clusterNode.totalException(); if (exception < count) { return true; } } if (cut.compareAndSet(false, true)) { ResetTask resetTask = new ResetTask(this); // 设定重置时间窗调度任务 pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS); } return false; }