【前面的话 】在前文 Sentinel进阶之熔断降级 中简单介绍了一下Sentinel
的流量控制,今天就来继续说一下Sentinel的系统自适应保护。
壹、概述 Sentinel 系统自适应保护从整体维度对应用入口流量进行控制,结合应用的 Load、总体平均 RT、入口QPS 和线程数等几个维度的监控指标,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
贰、背景 在开始之前,先回顾一下 Sentinel
做系统自适应保护的目的:
保证系统不被拖垮
在系统稳定的前提下,保持系统的吞吐量
长期以来,系统自适应保护的思路是根据硬指标,即系统的负载 (load1) 来做系统过载保护。当系统负载高于某个阈值,就禁止或者减少流量的进入;当load开始好转,则恢复流量的进入。这个思路给我们带来了不可避免的两个问题:
load 是一个“果”,如果根据 load 的情况来调节流量的通过率,那么就始终有延迟性。也就意味着通过率的任何调整,都会过一段时间才能看到效果。当前通过率是使load恶化的一个动作,那么也至少要过 1 秒之后才能观测到;同理,如果当前通过率调整是让 load
好转的一个动作,也需要1秒之后才能继续调整,这样就浪费了系统的处理能力。所以我们看到的曲线,总是会有抖动。
恢复慢。想象一下这样的一个场景(真实),出现了这样一个问题,下游应用不可靠,导致应用RT很高,从而load到了一个很高的点。过了一段时间之后下游应用恢复了,应用RT也相应减少。这个时候,其实应该大幅度增大流量的通过率;但是由于这个时候 load
仍然很高,通过率的恢复仍然不高。
TCP BBR
的思想给了我们一个很大的启发。我们应该根据系统能够处理的请求,和允许进来的请求,来做平衡,而不是根据一个间接的指标(系统 load)来做限流。最终我们追求的目标是 在系统不被拖垮的情况下,提高系统的吞吐率,而不是 load 一定要到低于某个阈值
。如果我们还是按照固有的思维,超过特定的 load 就禁止流量进入,系统 load 恢复就放开流量,这样做的结果是无论我们怎么调参数,调比例,都是按照果来调节因,都无法取得良好的效果。
Sentinel
在系统自适应保护的做法是,用 load1 作为启动控制流量的值,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定。
叁、系统规则 系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体Load、RT、入口QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效
。入口流量指的是进入应用的流量(EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的阈值类型:
Load
(仅对 Linux/Unix-like
机器生效):当系统 load1
超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt
计算得出。设定参考值一般是 CPU cores * 2.5
。
CPU usage
(1.5.0+ 版本):当系统 CPU
使用率超过阈值即触发系统保护(取值范围 0.0-1.0
)。
RT
:当单台机器上所有入口流量的平均RT
达到阈值即触发系统保护,单位是毫秒。
线程数
:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
入口 QPS
:当单台机器上所有入口流量的 QPS
达到阈值即触发系统保护。
肆、原理 先用经典图来镇楼:
我们把系统处理请求的过程想象为一个水管,到来的请求是往这个水管灌水,当系统处理顺畅的时候,请求不需要排队,直接从水管中穿过,这个请求的RT是最短的;反之,当请求堆积的时候,那么处理请求的时间则会变为:排队时间 + 最短处理时间。
推论一: 如果我们能够保证水管里的水量,能够让水顺畅的流动,则不会增加排队的请求;也就是说,这个时候的系统负载不会进一步恶化。
我们用 T 来表示(水管内部的水量),用RT来表示请求的处理时间,用P来表示进来的请求数,那么一个请求从进入水管道到从水管出来,这个水管会存在 P * RT
个请求。换一句话来说,当 T ≈ QPS * Avg(RT)
的时候,我们可以认为系统的处理能力和允许进入的请求个数达到了平衡,系统的负载不会进一步恶化。
接下来的问题是,水管的水位是可以达到了一个平衡点,但是这个平衡点只能保证水管的水位不再继续增高,但是还面临一个问题,就是在达到平衡点之前,这个水管里已经堆积了多少水。如果之前水管的水已经在一个量级了,那么这个时候系统允许通过的水量可能只能缓慢通过,RT会大,之前堆积在水管里的水会滞留;反之,如果之前的水管水位偏低,那么又会浪费了系统的处理能力。
推论二: 当保持入口的流量是水管出来的流量的最大的值的时候,可以最大利用水管的处理能力。
然而,和 TCP BBR 的不一样的地方在于,还需要用一个系统负载的值(load1)来激发这套机制启动。
这种系统自适应算法对于低 load 的请求,它的效果是一个“兜底”的角色。对于不是应用本身造成的 load 高的情况(如其它进程导致的不稳定的情况),效果不明显
。
伍、示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 public class SystemGuardDemo { private static AtomicInteger pass = new AtomicInteger(); private static AtomicInteger block = new AtomicInteger(); private static AtomicInteger total = new AtomicInteger(); private static volatile boolean stop = false ; private static final int threadCount = 100 ; private static int seconds = 60 + 40 ; public static void main (String[] args) throws Exception { tick(); initSystemRule(); for (int i = 0 ; i < threadCount; i++) { Thread entryThread = new Thread(new Runnable() { @Override public void run () { while (true ) { Entry entry = null ; try { entry = SphU.entry("methodA" , EntryType.IN); pass.incrementAndGet(); try { TimeUnit.MILLISECONDS.sleep(20 ); } catch (InterruptedException e) { } } catch (BlockException e1) { block.incrementAndGet(); try { TimeUnit.MILLISECONDS.sleep(20 ); } catch (InterruptedException e) { } } catch (Exception e2) { } finally { total.incrementAndGet(); if (entry != null ) { entry.exit(); } } } } }); entryThread.setName("working-thread" ); entryThread.start(); } } private static void initSystemRule () { List<SystemRule> rules = new ArrayList<SystemRule>(); SystemRule rule = new SystemRule(); rule.setHighestSystemLoad(3.0 ); rule.setHighestCpuUsage(0.6 ); rule.setAvgRt(10 ); rule.setQps(20 ); rule.setMaxThread(10 ); rules.add(rule); SystemRuleManager.loadRules(Collections.singletonList(rule)); } private static void tick () { Thread timer = new Thread(new TimerTask()); timer.setName("sentinel-timer-task" ); timer.start(); } static class TimerTask implements Runnable { @Override public void run () { System.out.println("begin to statistic!!!" ); long oldTotal = 0 ; long oldPass = 0 ; long oldBlock = 0 ; while (!stop) { try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { } long globalTotal = total.get(); long oneSecondTotal = globalTotal - oldTotal; oldTotal = globalTotal; long globalPass = pass.get(); long oneSecondPass = globalPass - oldPass; oldPass = globalPass; long globalBlock = block.get(); long oneSecondBlock = globalBlock - oldBlock; oldBlock = globalBlock; System.out.println(seconds + ", " + TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal + ", pass:" + oneSecondPass + ", block:" + oneSecondBlock); if (seconds-- <= 0 ) { stop = true ; } } System.exit(0 ); } } }
【后面的话 】最后是我自己实践的源码 ,包括流量控制和初始规则加载等等。