前言
阿里Sentinel 是面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。
阿里Sentinel和Hystrix有类似的功能,本文先从使用和源码角度分析,后续再说对比。
首先导入pom文件,这里有两个版本:
如果springboot基于1.5的则导入以下内容:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.4.0</version>
</dependency>
如果springboot基于2.0+的则导入以下内容:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>0.2.0.RELEASE</version>
</dependency>
两个内容都差不多,spring-cloud-starter-alibaba-sentinel更加智能化一些。本文以springboot 1.5为例。
阿里sentinel熔断主要是自己设定配置,包括流规则、降级规则,热点规则等。
添加配置信息类SentileConfig。
@Configuration
public class SentileConfig {
@Value("${csp.sentinel.api.port}")
private String port;
@Value("${csp.sentinel.dashboard.server}")
private String server;
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
@PostConstruct
private void initRules() throws Exception {
System.setProperty("csp.sentinel.api.port",port);
System.setProperty("csp.sentinel.dashboard.server",server);
FlowRule rule1 = new FlowRule();
rule1.setResource("flowtest");
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setCount(1); // 每秒调用最大次数为 1 次
List<FlowRule> rules = new ArrayList<>();
rules.add(rule1);
// 将控制规则载入到 Sentinel
com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager.loadRules(rules);
}
}
这里我将csp.sentinel.api.port和csp.sentinel.dashboard.server都配置到了类中,我们知道正常客户端启动时,需要加上连接sentinel server的参数,也就是在vm中启动时加上
-Dcsp.sentinel.dashboard.server=localhost:8848 -Dcsp.sentinel.api.port=8987。放到类中,我们就可以在application.yml中定义好了就可以了。springboot2.0+版本可以直接在application.yml中定义,springboot1.5的如果不想在启动参数中加,就需要加上 System.setProperty("csp.sentinel.api.port",port);
System.setProperty("csp.sentinel.dashboard.server",server);这两行代码。
配置类中定义了一个流控规则,名称为flowtest,方式为QPS,每秒最大次数为1次,之后将规则载入到Sentinel。
接着我们配置一个Controller。
@RestController
public class Controller {
@RequestMapping("/hello")
@SentinelResource(value = "flowtest",fallback = "error" ,blockHandler="blockHandler")
public String index(@RequestParam String name) {
return "hello,"+name+" hashcode:"+this.hashCode();
}
public String blockHandler(String a, BlockException e){
return "time out!!!"+a;
}
public String error(String a,String b){
return "error out!!!";
}
}
在RequestMapping请求中,添加了一个@SentinelResource注解,value=flowtest。这样就全都配置完成了。我们在请求的时候,根据刚才的设置,1s中只能有一个请求,否则就会触发熔断,调用blockHandler方法。
我们看到在配置类中,有一个SentinelResourceAspect的Bean对象,看一下这个类。
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
public SentinelResourceAspect() {
}
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
public void sentinelResourceAnnotationPointcut() {
}
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
Method originMethod = this.resolveMethod(pjp);
SentinelResource annotation = (SentinelResource)originMethod.getAnnotation(SentinelResource.class);
if (annotation == null) {
throw new IllegalStateException("Wrong state for SentinelResource annotation");
} else {
String resourceName = this.getResourceName(annotation.value(), originMethod);
EntryType entryType = annotation.entryType();
Entry entry = null;
Object var8;
try {
entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs());
Object result = pjp.proceed();
var8 = result;
return var8;
} catch (BlockException var13) {
var8 = this.handleBlockException(pjp, annotation, var13);
} catch (Throwable var14) {
Tracer.trace(var14);
throw var14;
} finally {
if (entry != null) {
entry.exit();
}
}
return var8;
}
}
}
可以看到这个类有@Aspect标识,并且切点是所有有@SentinelResource注解的方法。执行Around方法。方法中
entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs())是重点,进入方法。
public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
return Env.sph.entry(name, type, count, args);
}
//进入CtSph的entry方法
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
StringResourceWrapper resource = new StringResourceWrapper(name, type);
return this.entry(resource, count, args);
}
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
return this.entryWithPriority(resourceWrapper, count, false, args);
}
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, (ProcessorSlot)null, context);
} else {
if (context == null) {
//创建一个默认的sentinel_context,里面包含EntranceNode,根据熔断规则名创建
context = CtSph.MyContextUtil.myEnter("sentinel_default_context", "", resourceWrapper.getType());
}
//Constants.ON = true
if (!Constants.ON) {
return new CtEntry(resourceWrapper, (ProcessorSlot)null, context);
} else {
//创建链路
ProcessorSlot<Object> chain = this.lookProcessChain(resourceWrapper);
if (chain == null) {
return new CtEntry(resourceWrapper, (ProcessorSlot)null, context);
} else {
CtEntry e = new CtEntry(resourceWrapper, chain, context);
try {
//进入每个chain规则
chain.entry(context, resourceWrapper, (Object)null, count, prioritized, args);
} catch (BlockException var9) {
e.exit(count, args);
throw var9;
} catch (Throwable var10) {
RecordLog.info("Sentinel unexpected exception", var10);
}
return e;
}
}
}
}
在创建链路方法中,进入chain = this.lookProcessChain(resourceWrapper)方法。
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
//第一次进入为null
if (chain == null) {
Object var3 = LOCK;
synchronized(LOCK) {
chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
if (chain == null) {
if (chainMap.size() >= 6000) {
return null;
}
//创建chain
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap(chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
public static ProcessorSlotChain newSlotChain() {
if (builder != null) {
return builder.build();
} else {
resolveSlotChainBuilder();
if (builder == null) {
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default", new Object[0]);
builder = new DefaultSlotChainBuilder();
}
return builder.build();
}
}
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;
}
可以看到增加了8种slot,我们创建的就是flowslot方法,基于流控规则。之后就会进入
chain.entry(context, resourceWrapper, (Object)null, count, prioritized, args)方法。
我们创建的是FlowSlot,进入FlowSlot.entry方法。
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
//进入检验方法
this.checkFlow(resourceWrapper, context, node, count, prioritized);
//进入下一个chain
this.fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
//获取之前载入到Sentinel的规则
//即SentileConfig类中的, com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager.loadRules(rules);这句方法
Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
List<FlowRule> rules = (List)flowRules.get(resource.getName());
if (rules != null) {
Iterator var8 = rules.iterator();
while(var8.hasNext()) {
FlowRule rule = (FlowRule)var8.next();
//进入校验
if (!this.canPassCheck(rule, context, node, count, prioritized)) {
throw new FlowException(rule.getLimitApp());
}
}
}
}
boolean canPassCheck(FlowRule rule, Context context, DefaultNode node, int count, boolean prioritized) {
return FlowRuleChecker.passCheck(rule, context, node, count, prioritized);
}
static boolean passCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount, boolean prioritized) {
String limitApp = rule.getLimitApp();
if (limitApp == null) {
return true;
} else {
//是否为集群模式,我们创建的是针对单实例,进入passLocalCheck
return rule.isClusterMode() ? passClusterCheck(rule, context, node, acquireCount, prioritized) : passLocalCheck(rule, context, node, acquireCount, prioritized);
}
}
private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount, boolean prioritized) {
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
return selectedNode == null ? true : rule.getRater().canPass(selectedNode, acquireCount);
}
最后执行.canPass方法,canPass方法中有几种
我们使用的是默认的方法,还有按比例,温和启动等方式。 下面一个一个分析:
(一)DefaultController
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
int curCount = this.avgUsedTokens(node);
return (double)(curCount + acquireCount) <= this.count;
}
private int avgUsedTokens(Node node) {
if (node == null) {
return -1;
} else {
//grade模式 0=线程 1=活动窗口
return this.grade == 0 ? node.curThreadNum() : (int)node.passQps();
}
}
如果1s内的请求数量小于设置的数量,则通过。
(二)RateLimiterController(漏桶)
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
//获取当前时间
long currentTime = TimeUtil.currentTimeMillis();
//计算单位时间内每一次请求的花费时间
long costTime = Math.round(1.0D * (double)acquireCount / this.count * 1000.0D);
//这次请求预计花费的时间
long expectedTime = costTime + this.latestPassedTime.get();
//如果这次请求时间小于当前时间,说明之前的请求时间过早,加上这次的平均花费时间小于当前时间,则通过,并设置这次的请求时间为当前时间
if (expectedTime <= currentTime) {
this.latestPassedTime.set(currentTime);
return true;
} else {
//如果本次请求预计花费时间>当前时间,则设置等待时间
//等待时间=平均花费时间+上次请求时间-当前时间,就是超过平均速度了,要等一会
long waitTime = costTime + this.latestPassedTime.get() - TimeUtil.currentTimeMillis();
//如果大于最大等待时间,则返回false
if (waitTime >= (long)this.maxQueueingTimeMs) {
return false;
} else {
//没超过则再获取一次上次请求时间+平均花费时间
long oldTime = this.latestPassedTime.addAndGet(costTime);
try {
//再设置一次等待时间,因为有可能高并发
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime >= (long)this.maxQueueingTimeMs) {
this.latestPassedTime.addAndGet(-costTime);
return false;
} else {
//睡对应时间后放行
Thread.sleep(waitTime);
return true;
}
} catch (InterruptedException var15) {
return false;
}
}
}
}
(三)WarmUpController(令牌桶)
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
//获取通过数量
long passQps = node.passQps();
//获取上一个滑动窗口通过数量
long previousQps = node.previousPassQps();
//置令牌数量
this.syncToken(previousQps);
//获取当前令牌数量
long restToken = this.storedTokens.get();
//如果当前数量 大于 预警令牌数量,说明还处于冷启动状态
if (restToken >= (long)this.warningToken) {
long aboveToken = restToken - (long)this.warningToken;
//计算可通过数量
double warningQps = Math.nextUp(1.0D / ((double)aboveToken * this.slope + 1.0D / this.count));
if ((double)(passQps + (long)acquireCount) <= warningQps) {
return true;
}
}
//如果小于,则不超过阀值即可
else if ((double)(passQps + (long)acquireCount) <= this.count) {
return true;
}
return false;
}
//置令牌数量方法
protected void syncToken(long passQps) {
//获取当前时间
long currentTime = TimeUtil.currentTimeMillis();
//精确到秒
currentTime -= currentTime % 1000L;
//上一次填充时间
long oldLastFillTime = this.lastFilledTime.get();
//如果不在同一时间段
if (currentTime > oldLastFillTime) {
//获取当前令牌数量
long oldValue = this.storedTokens.get();
//计算新数量
long newValue = this.coolDownTokens(currentTime, passQps);
//填充补充的令牌
if (this.storedTokens.compareAndSet(oldValue, newValue)) {
//减去上一时间段通过的数量
long currentValue = this.storedTokens.addAndGet(0L - passQps);
if (currentValue < 0L) {
this.storedTokens.set(0L);
}
this.lastFilledTime.set(currentTime);
}
}
}
private long coolDownTokens(long currentTime, long passQps) {
//获取上一个时间段令牌数量
long oldValue = this.storedTokens.get();
long newValue = oldValue;
//如果当前桶中的数量小于预警令牌,说明一时间请求的数量过大,补充新的令牌数量
//比如count设置为10,预热时间为2分钟,则warningToken = 10,maxToken =20
//假设上一秒请求18个,还剩oldValue=2,那么这一秒补充2+(1)*10=12 个
if (oldValue < (long)this.warningToken) {
newValue = (long)((double)oldValue + (double)(currentTime - this.lastFilledTime.get()) * this.count / 1000.0D);
}
//如果上一秒请求数不多,剩余令牌数量大于预警令牌数,且前一次请求数小于阀值除以冷却系数,则直接补满
else if (oldValue > (long)this.warningToken && passQps < (long)((int)this.count / this.coldFactor)) {
newValue = (long)((double)oldValue + (double)(currentTime - this.lastFilledTime.get()) * this.count / 1000.0D);
}
//如果上一秒请求数不多,剩余令牌数量大于预警令牌数,且前一次请求数大于阀值除以冷却系数,则不补充。还是oldValue。
return Math.min(newValue, (long)this.maxToken);
}
附个请求示例方便理解: