阿里sentinel源码研究深入
1. 阿里sentinel源码研究深入
1.1. 前言
- 昨天已经把sentinel成功部署到线上环境,可参考我上篇博文,该走的坑也都走了一遍,已经可以初步使用它的限流和降级功能,根据我目前的实践,限流和降级规则似乎不能一同起效,还不知道原因,下面继续探索
1.2. 源码
1.2.1. 流控降级监控等的构建
- 首先客户端而言,我关注的是我写的代码
SphU.entry
,这明显是很关键的方法,下图的内容就是这里构建的
-Sentinel工作主流程就包含在上面一个方法里,通过链式调用的方式,经过了建立树状结构,保存统计簇点,异常日志记录,实时数据统计,负载保护,权限认证,流量控制,熔断降级等Slot
- 进入链式方法的入口为CtSph类,try方法大括号内
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
// This should not happen, unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception", e1);
}
1.2.2. 修改控制台规则是如何通知客户端的?
- 看sentinel-transport-simple-http包中的
HttpEventTask
类,它开启了一个线程,转么用来做为socket连接,控制台通过socket请求通知客户端,从而更新客户端规则,更改规则核心代码如下
// Find the matching command handler.
CommandHandler<?> commandHandler = SimpleHttpCommandCenter.getHandler(commandName);
if (commandHandler != null) {
CommandResponse<?> response = commandHandler.handle(request);
handleResponse(response, printWriter, outputStream);
} else {
// No matching command handler.
badRequest(printWriter, "Unknown command `" + commandName + '`');
}
通过命令模式,commandName为setRules时,更新规则
1.2.3. 既然它建立连接用的socket,为什么不用netty呢?
- 带着这个疑问,我本想在issues里找下,突然发现它的源码中有个
sentinel-transport-netty-http
这个包和sentinel-transport-simple-http
处于同级,官方的例子用的simple-http,但明显它也准备了netty-http,于是我替换成了netty-http,运行后效果和原先一样,至于效率上有没有提升,我就不清楚了_
1.2.4. 流量规则如何检查?
- 该规则检查类为
FlowRuleChecker
,在core核心包中,核心检查方法如下
private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
boolean prioritized) {
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
if (selectedNode == null) {
return true;
}
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
1.2.5. 熔断降级如何判断?
- 判断类为
DegradeRuleManager
,在core核心包,核心内容如下,再深入就是它判断的算法了,感兴趣的自己去看如下的passCheck
public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)
throws BlockException {
Set<DegradeRule> rules = degradeRules.get(resource.getName());
if (rules == null) {
return;
}
for (DegradeRule rule : rules) {
if (!rule.passCheck(context, node, count)) {
throw new DegradeException(rule.getLimitApp(), rule);
}
}
}
1.2.6. 默认的链条构建在哪?
- 核心类为
DefaultSlotChainBuilder
,构建了如下的slot
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
chain.addLast(new NodeSelectorSlot());
chain.addLast(new ClusterBuilderSlot());
chain.addLast(new LogSlot());
chain.addLast(new StatisticSlot());
chain.addLast(new SystemSlot());
chain.addLast(new AuthoritySlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
}
}
1.2.7. 既然已经知道了它是如何构建链式的处理节点的,我们是否何可自己重新构建?
- 发现类
SlotChainProvider
中的构建方法如下
private static void resolveSlotChainBuilder() {
List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>();
boolean hasOther = false;
for (SlotChainBuilder builder : LOADER) {
if (builder.getClass() != DefaultSlotChainBuilder.class) {
hasOther = true;
list.add(builder);
}
}
if (hasOther) {
builder = list.get(0);
} else {
// No custom builder, using default.
builder = new DefaultSlotChainBuilder();
}
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: "
+ builder.getClass().getCanonicalName());
}
- 也就是说,我们如果在
LOADER
中加入了其他的非默认实现就可以替代原来的DefaultSlotChainBuilder
,那LOADER
怎么来的?看代码,如下的全局变量,也就是需要自定义实现SlotChainBuilder
接口的实现类
private static final ServiceLoader<SlotChainBuilder> LOADER = ServiceLoader.load(SlotChainBuilder.class);
1.2.8. 如何实现SlotChainBuilder接口呢?
- 这里要注意的是它使用了
ServiceLoader
,也就是SPI
,全称Service Provider Interface
,加载它需要特定的配合,比如我自定义实现一个Slot
/**
* @author laoliangliang
* @date 2019/7/25 14:13
*/
public class MySlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
chain.addLast(new NodeSelectorSlot());
chain.addLast(new ClusterBuilderSlot());
chain.addLast(new LogSlot());
chain.addLast(new StatisticSlot());
chain.addLast(new SystemSlot());
chain.addLast(new AuthoritySlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
//自定义的
chain.addLast(new CarerSlot());
return chain;
}
}
/**
* @author laoliangliang
* @date 2019/7/25 14:15
*/
@Slf4j
public class CarerSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
log.info(JSON.toJSONString(resourceWrapper));
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
- 这里我自定义了
CarerSlot
,那是否能被加载到呢?事实上还不够,需要在META-INF/services/com.alibaba.csp.sentinel.slotchain.SlotChainBuilder
建这样一个文件,内容如下
- 好了,这样配置过后,它就能读到我们自定义的实现类代替它原先的类了
1.2.9. 该命令模式最初的初始化阶段在哪?
- 用过sentinel的都会感受到,只有当有第一个sentinel监控的请求过来时,sentinel客户端才会正式初始化,这样看来,这个初始化步骤应该在哪呢?
- 我通过不断反向跟踪上述的命令模式最初的初始化,找到了最初初始化的地方如下
public class Env {
public static final Sph sph = new CtSph();
static {
// If init fails, the process will exit.
InitExecutor.doInit();
}
}
- 有没有觉得很熟悉?doInit就是很多初始化的起点,当Env被调用时会运行static代码块,那么只有可能是sph被调用时
- 只要你debug过我上述第一条
SphU.entry
的源码,就会发现,如下,该方法一进入不就是先获取Env的sph,再调用的entry吗,所以初始化的地方也就找到了,第一次调用SphU.entry
的地方,或者你不用这个,使用的注解,里面同样有这个方法
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}
1.2.10. 注解是如何实现熔断降级的?
- 这个其实是比较容易理解的,既然通过
SphU.entry
包裹可以实现熔断降级,通过注解的形式包裹代码方法应该是比较容易的,那么在哪里实现和配置的呢 - 看过我前一篇文章的应该看到了,有存在如下配置
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
pushlish();
return new SentinelResourceAspect();
}
- 很明显的注解切面,通过spring注解的形式注入,我觉得这还是比较优雅的注入方式了,点进入就可以看到如下
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
public void sentinelResourceAnnotationPointcut() {
}
对@SentinelResource
注解进行了处理
1.2.11. 什么是直接失败?
- 这个很好理解,qps超过设置的值,直接失败
1.2.12. 什么是排队等待?
这个似乎看字面意思很好理解,但是一旦你点了这个选项,下面还有个参数的
所以这个排队等待是有超时时间的,达到峰值后匀速通过,采用的漏桶算法,流控图
1.2.13. 什么是慢启动模式?
- 以下是核心算法,
Warm Up
模式不看算法细节,看它的中文说明应该就能理解是怎么回事了吧;所谓慢启动模式,要求系统的QPS请求增速不能超过一定的速率,否则会被压制超过部分请求失败,应该是为了避免一启动就有大流量的请求进入导致系统一下子就宕机卡主或直接进入了熔断
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long) node.passQps();
long previousQps = (long) node.previousPassQps();
syncToken(previousQps);
// 开始计算它的斜率
// 如果进入了警戒线,开始调整他的qps
long restToken = storedTokens.get();
if (restToken >= warningToken) {
long aboveToken = restToken - warningToken;
// 消耗的速度要比warning快,但是要比慢
// current interval = restToken*slope+1/count
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
配置如下时,测试流控
流控图
1.2.14. 模式总结
你会发现直接失败和排队等待的区别在流控图上并不明显,那差别在哪呢?我重庆给个请求参数,5秒内模拟100个人轮流请求10次
sentinel控制台设置
流控图
- 总结:我设置了超时时间是5秒,而100个线程10次轮询也就是1000个请求,可以看出,它并不是一定要在5秒内解决这些请求,有了延时后,代表只要响应时间在5秒以内,不管多少请求都不会拒绝;
- 几个模式有利有弊,默认的快速失败使我们可以最大程度的控制系统的QPS,避免造成系统压力过大,但同时可能造成用于的体验效果变差
- 慢启动上面说过了
- 排队等待在设置合理的超时时间后可以最大程度的避免求情的失败,但同时可能造成线程压力过大
- 综上,在我看来排队等待模式是比较适合线上运行的,只是需要设置合理的超时时间,大公司机器不愁那就设小点,业界一般标准是200ms用户无感知,中小型可以设500ms甚至更大,看机器情况动态调整了
1.2.15. 提醒
- 像我是用apollo来持久化规则的,你也可以用nacos,redis,zookeeper等,当控制台未启动时,你启动客户端规则也会生效,只是没了控制台实时监控数据
阿里sentinel源码研究深入的更多相关文章
- 通俗易懂的阿里Sentinel源码分析:如何向控制台发送心跳包?
源码分析 public class Env { public static final Sph sph = new CtSph(); static { // 在Env类的静态代码块中, // 触发了一 ...
- OAuth2学习及DotNetOpenAuth部分源码研究
OAuth2学习及DotNetOpenAuth部分源码研究 在上篇文章中我研究了OpenId及DotNetOpenAuth的相关应用,这一篇继续研究OAuth2. 一.什么是OAuth2 OAuth是 ...
- Android开源项目 Universal imageloader 源码研究之Lru算法
https://github.com/nostra13/Android-Universal-Image-Loader universal imageloader 源码研究之Lru算法 LRU - Le ...
- zepto源码研究 - zepto.js - 1
简要:网上已经有很多人已经将zepto的源码研究得很细致了,但我还是想写下zepto源码系列,将别人的东西和自己的想法写下来以加深印象也是自娱自乐,文章中可能有许多错误,望有人不吝指出,烦请赐教. 首 ...
- dubbo源码研究(一)
1. dubbo源码研究(一) 1.1. dubbo启动加载过程 我们知道,现在流行注解方式,用spring管理服务,dubbo最常用的就是@Reference和@Service了,那么我首先找到这两 ...
- 【JavaScript】$.extend使用心得及源码研究
最近写多了js的面向对象编程,用$.extend写继承写得很顺手.但是在使用过程中发现有几个问题. 1.深拷贝 $.extend默认是浅拷贝,这意味着在继承复杂对象时,对象中内嵌的对象无法被拷贝到. ...
- underscore.js源码研究(8)
概述 很早就想研究underscore源码了,虽然underscore.js这个库有些过时了,但是我还是想学习一下库的架构,函数式编程以及常用方法的编写这些方面的内容,又恰好没什么其它要研究的了,所以 ...
- underscore.js源码研究(7)
概述 很早就想研究underscore源码了,虽然underscore.js这个库有些过时了,但是我还是想学习一下库的架构,函数式编程以及常用方法的编写这些方面的内容,又恰好没什么其它要研究的了,所以 ...
- underscore.js源码研究(6)
概述 很早就想研究underscore源码了,虽然underscore.js这个库有些过时了,但是我还是想学习一下库的架构,函数式编程以及常用方法的编写这些方面的内容,又恰好没什么其它要研究的了,所以 ...
随机推荐
- HTTP协议之chunk,单页应用这样的动态页面,怎么获取Content-Length的办法
当客户端向服务器请求一个静态页面或者一张图片时,服务器可以很清楚的知道内容大小,然后通过Content-Length消息首部字段告诉客户端需要接收多少数据.但是如果是动态页面等时,服务器是不可能预先知 ...
- url路由
注意: url(r'^index/', views.index) 第一个index是提交跳转的网址 (可修改) 第二个是自定义的方法 url(r'^index666/', views.ind ...
- Java String语法
String类代表字符串. Java程序中的所有字符串文字(例如"abc" )都被实现为此类的实例. 字符串不变; 它们的值在创建后不能被更改. 字符串缓冲区支持可变字符串. 因为 ...
- Struts CRUD
Struts CRUD 利用struts完成增删改查 思路: 1.导入相关的pom依赖(struts.自定义标签库的依赖) 2.分页的tag类导入.z.tld.完成web.xml的配置 3.dao层去 ...
- 用js写个原生的ajax过程
var xhr=new XMLHttpRequset(); xhr.addEventListener("load",loadHandler); xhr.open("GET ...
- 深入js系列-类型(对象)
开篇 值的传递方式 1.值传递 表示传递过程中复制了值 2.引用传递 表示传递过程中传递的是值的引用 js的传递方式 值传递 看下面的例子 // 这里值传递很容易理解 var a = 1 var b ...
- nuxtjs如何通过路由meta信息控制路由查看权限
我们知道NUXTJS可以通过路由中间件进行路由鉴权,中间件允许您定义一个自定义函数运行在一个页面或一组页面渲染之前. 但是我在实际使用过程中发现,中间件只有在路由跳转到路由中时才会进入,而在强制刷新网 ...
- nginx 动静分离之 tomcat
配置文件示例 server { listen ; server_name www.xxx.com; location ~* "\.(jpg|png|jepg|js|css|xml|bmp|s ...
- 重启nova-scheduler服务,报错Login was refused using authentication mechanism AMQPLAIN
问题描述 运行 systemctl restart openstack-nova-scheduler.service 失败,查看日志报错如下: 2019-12-22 14:52:27.426 1513 ...
- INSERT,UPDATE,DELETE时不写日志
我们在维护数据库的过程中,可能会遇到海量数据的存储和维护,但在有的情况下,需要先试验,然后再对实际的数据进行操作,那么在试验这个过程中,我们是不需要写日志的,因为当你对海量数据操作时,产生的日志可能会 ...