sentinel是一个功能全面的、面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。下面说一下它的工作原理及使用方法
sentinel工作原理
当sentinel作用在调用的接口上时,会将这个接口抽象成一种资源,调用方需要申请这种资源,使用的方法是SphU.entry(),如果能够申请成功,则说明没有被限流,否则会抛出BlockException,表面已经被限流了。
从SphU.entry()方法往下执行会进入到Sph.entry(),在entry中,会为每一次申请创建一个context,这个context就是记录本次请求的上下文,然后还会为每一种资源创建一组插槽,这一组插槽就是责任链模式,每一个插槽实现一个功能,请求会进行插槽一个一个执行,这些插槽主要有以下几个:
NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
StatistcSlot 则用于记录,统计不同纬度的 runtime 信息,流量统计作用;
FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流;
AuthorizationSlot 则根据黑白名单,来做黑白名单控制;
DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级;
SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;
其中,ClusterBuilderSlot会为每一种资源申请一个全局的ClusterNode,这个ClusterNode会在StatistcSlot中记录每个线程对这个接口的调用情况(调用总数、成功次数、失败次数等)。StatistcSlot采用的是滑动窗口统计方法,它有两个重要的参数:windowLength(窗口长度), intervalInSec(时间间隔)。如窗口长度为500ms,时间间隔是1s,这样系统就会申请两个窗口,然后计算当前时间戳为time,进一步确定落在哪个窗口,具体逻辑如下:
long timeId = time / windowLength;
//计算在窗口数组中的索引
int idx = (int)(timeId % array.length());
// 计算这个时间戳在对应窗口的开始时间
long time = time - time % windowLength;
while (true) {
WindowWrap<Window> old = array.get(idx);
if (old == null) { //开始的时候数组为空,申请一个窗口
WindowWrap<Window> window = new WindowWrap<Window>(windowLength, time, new
Window());
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
Thread.yield(); //多个线程执行,修改失败重新执行一次就可以了
}
} else if (time == old.windowStart()) { //恰好找到该窗口,直接返回
return old;
} else if (time > old.windowStart()) {
if (addLock.tryLock()) {
try {
// 旧的窗口过时了,需要申请新的窗口,并将原先的窗口替换
return resetWindowTo(old, time);
} finally {
addLock.unlock();
}
} else {
Thread.yield();
}
} else if (time < old.windowStart()) {
// 当前时间小于时间窗口,不可能走到这个分支
return new WindowWrap<Window>(windowLength, time, new Window());
}
}
可见通过时间窗口,就可以将一次请求对应到一个窗口,进而计算单位时间的流量,进行流控,同时熔断降级也是通过计算单位时间的异常来实现的,所以时间窗口是十分核心的模块。随着统计的不断进行,当一个窗口时间小于当前时间时,会被系统从当前窗口数组中移除,放入到一个list中,本地会启动一个定时任务,周期性的将list中的窗口数据刷新到本地文件中。当dashboard发送获取统计数据的请求后,本地就会返回文件中的数据给dashboard,这样用户就可以看到统计数据。
sentinel的使用
目前使用sentinel主要用4种方式。
1、在代码中直接使用
在代码中直接调用函数SphU.entry(),由于这种方式是直接入侵代码,所以不推荐
2、通过注解添加
在调用的接口上面添加注解@SentinelResource,这样当调用该接口时,spring会通过AOP的方式对含有SentinelResource注解的接口进行增强,采用环绕通知@Around进行切片捕捉,在执行目标方法前执行SphU.entry()。这是一种比较好的方法,但是不适合大规模配置,如果接口众多,一个一个添加注解很麻烦。
3、在filter中添加
目前我们项目中调用接口时,通过filter时,会获取调用接口的名称,让好通过名称为资源命名,这样每个接口就是一种资源,这样想到于在每个接口上执行SphU.entry(),这样就实现了对每个接口对流量监控
4、在dashboard中添加配置
本地代码中加入sentinel的包,相当与client。在服务器上部署了dashboard,可以通过在dashboard中的配置页面来实现sentinel配置的添加。本地的sentinel通过zk来监听最新的配置消息,当配置修改时,zk通过主动推送的方式将配置消息发送给client。
sentinel集群实现
sentinel中限流、熔断降级可以在本地进行,也可以几个实例组成一个集群来统一控制。在集群模式中,一个实例做service,其余做client,通过netty进行socket通信。client端遇到流量统计、流控、熔断降级都是发送请求到service,由service统一进行流量统计、流控、熔断降级的处理。client的服务发现通过dashboard的动态配置来完成。
sentinel本地与dashboard通信
sentinel本地和dashboard通过http协议通信,实现方式:本地的sentinel-transport-netty-http模块通过netty实现简单的http服务(采用netty提供的http编码和解码方式:HttpRequestDecoder和HttpResponseEncoder),通过SPI获取本地的继承CommandHandler的类,这些类中找到含有注解CommandMapping的类,CommandMapping的name字段就是http中的url,创建一个map,记录url到Handler的映射关系,当本地服务器接收到dashboard的请求时,从里面的url中找到handler,进行请求的处理。其实就是类似于Spring MVC里面的MappingHandler。
总结
sentinel的功能强大,使用简单,可帮助大家快速搭建线上监控系统。