流控配置:
资源名的填写分feign/dubbo不同方式:
- feign 填写请求地址
- dubbo 填写sentinel注解的value值
@SentinelResource(value = "circle.checkPopulation", blockHandler = "checkPopulation", blockHandlerClass = {BlockExceptionHandler.class})
有种更加简单的方式,直接通过簇点链路菜单看各个资源地址,直接进行流控配置
针对来源:
可以针对不同调用系统进行细致化限流
feign方式 | dubbo方式 |
填写调用方 spring.application.name或者project.name 名字 | 填写调用方 dubbo.application.name |
流控模式:
- 直接模式,按照资源直接进行限流
- 关联模式,2个资源名使用一个限流配置
常见问题:
1.很多开发通过错误码来处理流程,而非通过异常。这种写法,导致 Sentinel 不能拦截到异常,无法触发降级。对于这种情况,有没有什么好的处理方法?
实际上 Sentinel 是通过 Tracer.trace(e)
来统计业务异常的,因此可以收到错误码就调用此函数来统计业务异常。
2.Web 端的资源目前都是根据某个特定的 URL 限流,可不可以根据前缀匹配限流?
对于 Sentinel Web Servlet Filter,可以借助 UrlCleaner
处理对应的 URL(如提取前缀的操作),这样对应的资源都会归到处理后的资源(如 /foo/1
和 /foo/2
都归到 /foo/*
资源里面)。UrlCleaner
实现 URL 前缀匹配只是个 trick,它会把对应的资源也给归一掉,直接在资源名粒度上实现模式匹配还是有很多顾虑的问题的。
源码分析:
Sentinel支持非常多的框架,看源码这里就可以看出来
Dubbo篇:
dubbo的限流是通过dubbo的全局上下文设置来透传的
在客户端直接会将参数封装到
其他的类型其实也差不多, 都是通过adapter包进行适配封装的
集群模式:
这些都是集群模式需要使用的代码
核心代码逻辑:
sentinel限流使用的是滑动窗口方式,没有使用redis的zset方式而是自己实现了一套
StatisticNode是统计的node,实现了三种度量统计方式,秒级/分钟级/线程级别
在这个方法内没有加入同步锁,用了大量java高并发集合的CAS操作来设置值
线程级别计数器:
- sentinel 处理流程是基于slot链(ProcessorSlotChain)来完成的,如限流熔断等,其中重要的一个slot就是StatisticSlot,它是做各种数据统计的,而限流熔断的数据判断来源就是StatisticSlot。
- StatisticSlot的各种数据统计都是基于滑动窗口来完成的
- StatisticSlot的滑动窗口需要了解统计指标的数据结构、滑动窗口的窗口定位,指标保存等概念
滑动窗口算法
把整个大的时间窗口切分成更细粒度的子窗口,每个子窗口独立统计。同时,每过一个子窗口大小的时间,就向右滑动一个子窗口。这就是滑动窗口算法的思路。
如上图所示,将一分钟的时间窗口切分成 6 个子窗口,每个子窗口维护一个独立的计数器用于统计 10 秒内的访问量,每经过 10s,时间窗口向右滑动一格。
回到简单窗口出现临界跳变的例子,结合上面的图再看滑动窗口如何消除临界突变。如果 0:50 到 1:00 时刻(对应灰色的格子)进来了 100 次请求,接下来 1:00~1:10 的 100 次请求会落到黄色的格子中,由于算法统计的是 6 个子窗口的访问量总和,这时候总和超过设定的阈值 100,就会拒绝后面的这 100 次请求。
Sentinel源码中的责任链:
# Sentinel default ProcessorSlots
com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot
com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot
com.alibaba.csp.sentinel.slots.logger.LogSlot
com.alibaba.csp.sentinel.slots.statistic.StatisticSlot
com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot
com.alibaba.csp.sentinel.slots.system.SystemSlot
com.alibaba.csp.sentinel.slots.block.flow.FlowSlot
com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot
-
NodeSelectorSlot ,这个就是为往context中设置当前resource对应的DefaultNode。
-
ClusterBuilderSlot 这个就是往context中设置ClusterNode。
-
LogSlot 打印日志的,主要是异常日志处理
-
StatisticSlot 这个slot很重要,指标收集作用
-
AuthoritySlot 黑白名单检查
-
SystemSlot 系统规则检查
-
FlowSlot 流控检查。
-
DegradeSlot 服务降级,熔断的检查。
总结一下总窗口时间跨度大小是 intervalInMs,滑动子窗口时间跨度大是 windowLengthInMs,那么总的窗口数量就是sampleCount:sampleCount = intervalInMs /windowLengthInMs
,总窗口数量和总窗口时间跨度大小是ArrayMetric的构造函数传进来的,具体是在StatisticNode
里新建的:
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,IntervalProperty.INTERVAL);
当前实现默认为 2,而总窗口大小默认是 1s,也就意味着默认的滑动窗口大小是 500ms;可以调用SampleCountProperty.updateSampleCount
来调整总的窗口数量,以此来调整统计的精度。在LeapArray
中有一个类型为AtomicReferenceArray
线程安全的滑动窗口的数组 array,数组中每个元素即窗口以WindowWrap<T>
表示,WindowWrap
有三个属性:
-
windowStart:滑动窗口的开始时间
-
windowLength:滑动窗口的长度。
-
value:滑动窗口记录的内容,泛型表示,关键的一类就是 MetricBucket,里面包含了一组 LongAdder 用于记录不同类型的数据,例如请求通过数、请求阻塞数、请求异常数等等。
滑动窗口算法的核心实现就是在LeapAyyay
根据当前时间获取滑动窗口的下标
long timeId = timeMillis / windowLengthInMs;
return (int)(timeId % array.length());
获取当前窗口的开始时间
windowStart = timeMillis - timeMillis % windowLengthInMs;
根据刚刚获取的窗口下标去数组里获取窗口元素
if (old == null) {
/*
* B0 B1 B2 NULL B4
* ||_______|_______|_______|_______|_______||___
* 200 400 600 800 1000 1200 timestamp
* ^
*
* time=888
*
*/
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs
, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
Thread.yield();
}
尝试创建一个新的窗口元素,然后尝试通过CAS来创建新的窗口元素,如果更新成功则返回新建的窗口,如果CAS添加元素失败则自旋,等待下次线程开始的时候重新获取老窗口重新获取老的窗口。
如果获取到的窗口的开始时间晚于当前窗口的开始时间,则说明获取到的窗口已经过期,我们就把原来的窗口覆盖掉,注意接下来这里应该是两步操作:
更新开始时间和重置值,整个步奏应该是原子的,所以需要用到锁来保证原子性:
else if (windowStart > old.windowStart()) {
/*
* (old)
* B0 B1 B2 NULL B4
* |_______||_______|_______|_______|_______|_______||___
* ... 1200 1400 1600 1800 2000 2200 timestamp
* ^
* time=1676
* 原来窗口的开始时间: 400, 说明老的已经过期我们重置
*/
if (updateLock.tryLock()) {
try {
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
updateLock
是ReentrantLock
锁,如果没有竞争到锁则自旋等待下次线程开始的时候重新获取老窗口重新重新获取老的窗口,注意ReentrantLock
默认是非公平锁,这里用的是默认的非公平锁,因为这里不关心获取锁的顺序但是关心性能,所以用非公平锁。为什么没有用synchronized
呢?因为这里没有获取到锁的线程有可能是另外一个线程在操作同一个窗口,当前没有竞争到锁的话应该自旋然后再次尝试获取窗口,也许获取到的就是新的窗口了,而不是让没有获取到锁的线程阻塞一直等待获取锁。
网上找到的一张图来总结Sentinel是怎么实现滑动窗口算法: