背景
最近公司做的项目需要和周边系统以接口形式进行对接,此时接口限流显得尤为重要。团队架构师给的建议就是通过阿里的Sentinel完成,为什么不用Hystrix,我没有深入研究,但是文章最后我会贴一张两者的对比图。
Sentinel组件化
Sentinel和spring类似,按需索取,需要什么组件,maven引入什么组件即可。Sentinel的github传送门(star:12.6k)
Sentinel-adapter:Sentinel的适配器,从源码我们可以看到Sentinel适配了dubbo、gateway、grpc、jax-rs、okhttp、reactor、sofa、spring-cloud-gateway、spring-webflux、spring-webmvc、web-servlet、zuul、zuul2;
Sentinel-benchmark:Sentinel的基准测试模块;
Sentinel-cluster:Sentinel的集群,从readme中可以看到:这个模块提供了Sentinel对集群流量控制的默认实现;
Sentinel-core:Sentinel的核心代码实现,就好比spring-core;
Sentinel-dashboard:Sentinel的仪表盘,该模块提供了轻量级可视化页面,可以在页面上查看、修改限流的相关数据;
Sentinel-demo:Sentinel的demo模块,这个模块提供了测试用例,同时告诉我们怎么使用Sentinel;
Sentinel-extension:Sentinel的扩展模块,提供了额外的扩招点和功能,优秀的框架都是面向接口、面向抽象编程;
Sentinel-logging:Sentinel的日志模块,使用的slf4j,可以在项目中自定义Sentinel的logger、appender;
Sentinel-transport:Sentinel的传输模块,提供了Sentinel监控服务端和客户端api接口以及给予不同类库和协议的实现。
Sentinel源码分析
这里主要分享限流和熔断比较关键的实现(项目主要用到了限流),分享源码前先明确下面两个概念:
- 限流:可以简单理解为限制流量,避免过多的客户端请求,导致服务不可用,主要有qps、并发限流(jdk提供了semphore信号量)、单机限流(谷歌提供的令牌同,我最初的想法是通过这个jar+注解实现的)等。
- 熔断降级:当链路中某个服务调用超时或异常比例升高,对这个资源的调用进行限制,让请求快速失败,当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断。
限流
限流入口
Entry entry = null;
try {
entry = SphU.entry(KEY);
//被保护的资源
.......
} catch (BlockException blockException) {
// 接口被限流的时候, 会进入到这里
System.out.println("---接口被限流了---, exception: ");
return ResponseEntity.ok("接口限流, 返回空");
} finally {
// 释放资源
if (entry != null) {
entry.exit();
}
}
根据Sentinel提供的demo模块可以快速找到限流入口,就是SphU.entry(KEY)代码,此处的key唯一确定限流规则,Sentinel会根据这个key去获取对应的限流规则;客户端请求达到限流规则,抛出BlockException,进入catch模块;在finally内退出entry释放资源。
entry实现
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException {
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
return new CtEntry(resourceWrapper, null, context);
}
if (context == null) {
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
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) {
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
- 代码进来先进行了必要参数的校验,此处的ResourceWrapper就是根据前面的key和EntryType构建的资源包装类
- 构建执行链,看到chain,大家想到了什么?
- 依次执行链上的插槽(slot我翻译为插槽,下同,哈哈哈)
构建执行链条
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
- 根据资源包装类从链map中获取执行链,获取到直接返回;如果没有获取到说明是第一次构建该资源的执行链
- 构建该资源对应的资源链,具体构建链代码如下:
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
continue;
}
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
- 加入链map,下次执行这条资源链就不需要再次计算链(如果没有这个map,每次接口调用都需要重新计算链,很浪费资源)
链条执行
关键类UML图如上,ProcessorSlot定义了链条上插槽方法,包括entry、fireEntry、exit、fireExit方法。链条上的所有插槽都通过抽象类AbstractLinkedProcessorSlot实现了ProcessoreSlot接口。
FlowSlot(限流插槽)实现如下:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
checkFlow(resourceWrapper, context, node, count, prioritized);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
- 限流插槽只实现了entry和exit方法,fireEntry和fireExit方法由抽象父类AbstractLinkedProcessorSlot实现,逻辑比较简单分别是执行链条上的下一个插槽方法和链条上下一个插槽的退出方法
- 限流插槽的真正的校验交给了FlowRuleChecker。先根据之前的key获取具体的限流规则,然后canPassCheck方法进行校验。代码如下:
public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
if (ruleProvider == null || resource == null) {
return;
}
Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
if (rules != null) {
for (FlowRule rule : rules) {
if (!canPassCheck(rule, context, node, count, prioritized)) {
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}
熔断降级
熔断插槽
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
- 熔断操作同样是通过继承AbstractLinkedProcessorSlot实现ProcessorSlot接口的
- 真正的熔断校验交给了DegradeRuleManager.checkDegrade
熔断check
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);
}
}
}
- 同样是先根据key获取熔断规则
- 通过passCheck是否需要熔断降级,符合规则就会抛出DegradeException
- 熔断降级可以通过@SentinelResource注解指定具体的回调函数
项目demo
本来想写我在项目服务中写的demo示例的,我主要使用了Sentinel的限流、熔断和仪表盘。由于篇幅原因就不写了,也算是偷个懒吧。
Sentinel和Hystrix对比
面向搜索引擎编程,内容可信度自己评估哈!!
彩蛋
最后彩蛋和本篇文章没有什么关系,只是我写帖子的时候女朋友看书告诉我说谷歌浏览器在没有网络情况下打开浏览器,会出现一个恐龙的小游戏,后知后觉的我只能说:牛皮!