Sentinel1.8.0源码分析
Sentinel介绍
sentinel主要用来做资源保护,也就是限流降级熔断调度,一般我们说的限流主要是针对于接口来说的,当接口的处理能力达到了上线过后,如果这个时候接口还继续接受请求,那么导致的结果就是服务崩溃,无法正常的处理业务,而sentinel的主要作用就是最服务要调用的接口进行保护,根据接口的可承受请求数进行保护,当请求达到一定的级别过后进行限流,并且友好的返回给调用方,简单来说sentinel做的事情就是对接口做资源保护,那么它的做法其实很简单,一般我们能想到的都是做一个aop切面,切的就是具体受保护的资源,在真正调用的时候先对资源进行保护,如果没有达到限流的标准,那么就放行这个请求,如果达到了限流的阈值,那么就抛出一个BlockException,如果远程业务报错,那么抛出一个业务异常,那么在sentinel中都可以是对异常进行拦截处理;简单来说sentinel自动注入了两个类,一个是切面AOP(SentinelResourceAspect),这是一个AOP,它的切点是@SentinelResource,一个是HandlerInterceptor,sentinel对它的实现是SentinelWebInterceptor,其实这两个的作用都是拦截到请求,然后做资源保护,根据一定的限流规则确定是否放行,如果放行则正常调用,如果不放行,那么抛出BlockException,区别是切面AOP处理的是@SentinelResource注解,而拦截器主要处理的是controller中的restful服务地址,如果你在sentinel中配置了restful地址的限流,那么这个拦截器就会进行资源保护从而进行限流。
Sentinel涉及的限流算法
我们来看一下,目前已知的有哪些限流算法,比较常用的就是计数器限流、滑动时间窗口、漏桶算法、令牌桶算法,这几个限流算法是比较常见的,但是他们都有特点以及不足呢?
计数器法
计数器法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个 请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter。

具体算法的伪代码:
/**
* 最简单的计数器限流算法
*/
public class Counter {
public long timeStamp = System.currentTimeMillis(); // 当前时间
public int reqCount = 0; // 初始化计数器
public final int limit = 100; // 时间窗口内最大请求数
public final long interval = 1000 * 60; // 时间窗口ms
public boolean limit() {
long now = System.currentTimeMillis();
if (now < timeStamp + interval) {
// 在时间窗口内
reqCount++;
// 判断当前时间窗口内是否超过最大请求控制数
return reqCount <= limit;
} else {
timeStamp = now;
// 超时后重置
reqCount = 1;
return true;
}
}
}
滑动时间窗口算法
滑动时间窗口,又称rolling window。为了解决计数器法统计精度太低的问题,引入了滑动窗口算法。下面这张图,很好地解释了滑动窗口算法:

在上图中,整个红色的矩形框表示一个时间窗口,在我们的例子中,一个时间窗口就是一分钟。然后我们将时间窗口进行划分,比如图中,我们就将滑动窗口划成了6格,所以每格代表的是10秒钟。每过10秒钟,我们的时间窗口就会往右滑动一格。每一个格子都有自己独立的计数器counter,比如当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1。
计数器算法其实就是滑动窗口算法。只是它没有对时间窗口做进一步地划分,所以只有1格。
由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
具体算法的伪代码:
/**
* 滑动时间窗口限流实现
* 假设某个服务最多只能每秒钟处理100个请求,我们可以设置一个1秒钟的滑动时间窗口,
* 窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数
*/
public class SlidingTimeWindow {
//服务访问次数,可以放在Redis中,实现分布式系统的访问计数
Long counter = 0L;
//使用LinkedList来记录滑动窗口的10个格子。
LinkedList<Long> slots = new LinkedList<Long>();
public static void main(String[] args) throws InterruptedException {
SlidingTimeWindow timeWindow = new SlidingTimeWindow();
new Thread(new Runnable() {
@Override
public void run() {
try {
timeWindow.doCheck();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
while (true){
//TODO 判断限流标记
timeWindow.counter++;
Thread.sleep(new Random().nextInt(15));
}
}
private void doCheck() throws InterruptedException {
while (true) {
slots.addLast(counter);
if (slots.size() > 10) {
slots.removeFirst();
}
//比较最后一个和第一个,两者相差100以上就限流
if ((slots.peekLast() - slots.peekFirst()) > 100) {
System.out.println("限流了。。");
//TODO 修改限流标记为true
}else {
//TODO 修改限流标记为false
}
Thread.sleep(100);
}
}
}
漏桶算法
漏桶算法,又称leaky bucket。

从图中我们可以看到,整个算法其实十分简单。首先,我们有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。
我们将算法中的水换成实际应用中的请求,我们可以看到漏桶算法天生就限制了请求的速度。当使用了漏桶算法,我们可以保证接口会以一个常速速率来处理请求。所以漏桶算法天生不会出现临界问题。
具体的伪代码如下:
/**
* 漏桶限流算法
*/
public class LeakyBucket {
public long timeStamp = System.currentTimeMillis(); // 当前时间
public long capacity; // 桶的容量
public long rate; // 水漏出的速度(每秒系统能处理的请求数)
public long water; // 当前水量(当前累积请求数)
public boolean limit() {
long now = System.currentTimeMillis();
water = Math.max(0, water - ((now - timeStamp)/1000) * rate); // 先执行漏水,计算剩余水量
timeStamp = now;
if ((water + 1) < capacity) {
// 尝试加水,并且水还未满
water += 1;
return true;
} else {
// 水满,拒绝加水
return false;
}
}
}
令牌桶算法
令牌桶算法,又称token bucket。同样为了理解该算法,我们来看一下该算法的示意图:

从图中我们可以看到,令牌桶算法比漏桶算法稍显复杂。首先,我们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的,token以 一个固定的速率r往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。每当一个请求过来时,就会尝试从桶里移除一个令牌,如果没有令牌的话,请求无法通过。
具体的伪代码如下:
/**
* 令牌桶限流算法
*/
public class TokenBucket {
public long timeStamp = System.currentTimeMillis(); // 当前时间
public long capacity; // 桶的容量
public long rate; // 令牌放入速度
public long tokens; // 当前令牌数量
public boolean grant() {
long now = System.currentTimeMillis();
// 先添加令牌
tokens = Math.min(capacity, tokens + (now - timeStamp) * rate);
timeStamp = now;
if (tokens < 1) {
// 若不到1个令牌,则拒绝
return false;
} else {
// 还有令牌,领取令牌
tokens -= 1;
return true;
}
}
}
限流算法小结
计数器 VS 滑动窗口:
计数器算法是最简单的算法,可以看成是滑动窗口的低精度实现。
滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。
也就是说,如果滑动窗口的精度越高,需要的存储空间就越大。
漏桶算法 VS 令牌桶算法:
漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。
因为默认的令牌桶算法,取走token是不需要耗费时间的,也就是说,假设桶内有100个token时,那么可以瞬间允许100个请求通过。
当然我们需要具体情况具体分析,只有最合适的算法,没有最优的算法。
@SentinelResource注解的源码解析
前面已经知道了@SentinelResource注解是通过spring对的自动装配进行注入的,自动注入的类是SentinelAutoConfiguration,在这个自动装配类中通过@Bean导入了一个Bean,这个bean是一个Aop的切面,这个切面切的就是@SentinelResource注解,这个切面的类的名字是SentinelResourceAspect,我们关注下它的大概的逻辑:
SentinelResourceAspect

在这里有几行关键的代码
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
Object result = pjp.proceed();
第一行代码很显然就是sentinel的资源保护的具体逻辑,这个逻辑中对受保护的资源进行过滤判断,判断是否达到了限流或者降级的条件,如果达到了限流的条件,那么就会抛出BlockException,所以简单的来理解就是在真正的调用之前pjp.proceed()方法之前进行资源保护降级限流,如果没有降级和限流,则正常执行代码逻辑,否则进行降级限流,如果降级限流的逻辑中达到了限流的条件则抛出BlockException,这样就表示已经被限流了,我们可以编写ExceptionHandler来获取这个异常并且向客户端做一个友好的提示。所以接下来我们就要详细的分析SphU.entry这个方法的具体逻辑是怎么实现的了。
com.alibaba.csp.sentinel.CtSph#entryWithType
限流逻辑的真正入口
@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
Object[] args) throws BlockException {
//这里将资源的名称,资源的entryType,资源的类型封装成了一个Wrapper,然后调用
StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
//这里的prioritized是false,表示不安优先级来
return entryWithPriority(resource, count, prioritized, args);
}
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
// The {@link NullContext} indicates that the amount of context has exceeded the threshold,
// so here init the entry only. No rule checking will be done.
return new CtEntry(resourceWrapper, null, context);
}
if (context == null) {
// Using default context.
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// Global switch is close, no rule checking will do.
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
/**
* 这里是整个架构的核心所在,这里是在构建一个处理链,这个处理链是一个单向链表结构,类似于Filter一样,构建这个链条的
* 原因是对业务进行解耦,像限流资源保护有很多,比如限流、降级、热点参数、系统降级等等,如果都写在一起就耦合很严重,我们知道oop的
* 思想就是让每个类确定各自的职责,不要让他做不相干的事情,所以这里将业务进行全面解耦,然后在解耦的同事又通过链式编程将它们窜起来
*/
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
/*
* Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
* so no rule checking will be done.
*/
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
//resourceWrapper是受保护的资源封装的一个Wrapper对象,chain就是这个资源要经过的过滤slot
//封装成了一个CtEntry
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
//启用链条的调用,我们看了之前的代码知道了chain的类型是DefaultProcessorSlotChain,所以调用的开始路径是DefaultProcessorSlotChain的
//entry方法,在这里方法里面开始chain调用
chain.entry(context, resourceWrapper, null

本文深入解析Sentinel中的限流算法,涵盖计数器法、滑动时间窗口、漏桶和令牌桶,探讨它们的原理、实现和应用场景,带你理解如何在Sentinel中实现资源保护策略。
最低0.47元/天 解锁文章
1071

被折叠的 条评论
为什么被折叠?



