一、Sentinel 是什么?
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
-
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
-
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
-
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
-
完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 的主要特性:
Sentinel 的开源生态:
二、如何使用Sentinel
我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。使用Sentinel 来进行资源保护,主要分为两个步骤:
- 定义资源
- 定义规则
先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。
对于主流的框架,我们提供适配,只需要按照适配中的说明配置,Sentinel 就会默认定义提供的服务,方法等为资源。三、示例
3.1、Sentinel编码限流
先来简单的体验下 Sentinel 吧,在你的Maven项目中增加 Sentinel 的依赖:
com.alibaba.csp sentinel-core 1.4.0
Sentinel中需要限流的称之为资源,对资源进行处理,下面来看最简单的一段代码:
package com.example.demo.service;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;import com.alibaba.csp.sentinel.Entry;import com.alibaba.csp.sentinel.SphU;import com.alibaba.csp.sentinel.slots.block.BlockException;import com.alibaba.csp.sentinel.slots.block.RuleConstant;import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;public class LimitByCoding { public static void main(String[] args) throws InterruptedException { initFlowRules(); long startTime = System.currentTimeMillis(); for (int i = 0; i < 1000; i++) { /* 您的业务逻辑 - 开始 */ System.out.println("hello world"); } long endTime = System.currentTimeMillis(); System.out.println(endTime - startTime); long startTime2 = System.currentTimeMillis(); for (int i = 0; i < 1000; i++) { Entry entry = null; try { entry = SphU.entry("HelloWorld"); /* 您的业务逻辑 - 开始 */ System.out.println("hello world"); /* 您的业务逻辑 - 结束 */ } catch (BlockException e1) { /* 流控逻辑处理 - 开始 */ System.out.println("block!"); /* 流控逻辑处理 - 结束 */ } finally { if (entry != null) { entry.exit(); } } } long endTime2 = System.currentTimeMillis(); System.out.println(endTime2 - startTime2); } private static void initFlowRules() { Listrules = new ArrayList<>(); FlowRule rule = new FlowRule(); rule.setResource("HelloWorld"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // Set limit QPS to 20. rule.setCount(20); rules.add(rule); FlowRuleManager.loadRules(rules); }}
第一行中初始化限流的规则,创建了一个资源叫 HelloWorld,设置了这个资源的QPS 为 20。
结果:
上面的例子中次数为1000次,只输出一句话,不做任何限制,执行完成的时间大概在9毫秒左右。
下面加上限流的逻辑,执行完成的时间基本上就超过91毫秒了,可见限流起了作用。
在业务开始前使用SphU.entry();方法标识开始,结束使用entry.exit();,如果触发了流控逻辑就会抛出BlockException异常让用户自行处理。
代码运行之后,我们可以在日志 ~/logs/csp/${appName}-metrics.log.xxx 里看到下面的输出:
|--timestamp---|------date time--|-resource-|p |block|s |e|rt 1529998904000|2018-06-26 15:41:44|HelloWorld|20|0 |20|0|0 1529998905000|2018-06-26 15:41:45|HelloWorld|20|5579 |20|0|728 1529998906000|2018-06-26 15:41:46|HelloWorld|20|15698|20|0|0 1529998907000|2018-06-26 15:41:47|HelloWorld|20|19262|20|0|0 1529998908000|2018-06-26 15:41:48|HelloWorld|20|19502|20|0|0 1529998909000|2018-06-26 15:41:49|HelloWorld|20|18386|20|0|0
-
p:通过的请求,
-
block:被阻止的请求
-
s:成功执行完成的请求个数
-
e:用户自定义的异常
-
rt:平均响应时长。
3.2、Sentinel注解限流
这种做法不好的地方在于每个需要限制的地方都得写代码,从 0.1.1 版本开始,Sentinel 提供了 @SentinelResource 注解的方式,非常方便。
要使用注解来保护资源需要引入下面的Maven依赖:
com.alibaba.csp sentinel-annotation-aspectj 1.4.0
完整的pom.xml
4.0.0 org.springframework.boot spring-boot-starter-parent 1.5.19.RELEASE com.dxz LimitDemo 0.0.1-SNAPSHOT LimitDemo Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat com.alibaba.csp sentinel-annotation-aspectj 1.4.0 org.springframework spring-core 4.3.7.RELEASE org.springframework spring-beans 4.3.7.RELEASE org.springframework spring-context 4.3.7.RELEASE org.springframework spring-aop 4.3.7.RELEASE org.springframework spring-expression 4.3.17.RELEASE org.springframework.boot spring-boot-starter slf4j-api org.slf4j 1.7.10 ch.qos.logback logback-classic 1.2.2 org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin
引入之后我们需要配置SentinelResourceAspect切面让其生效,因为是通过SentinelResourceAspect切面来实现的,我这边以Spring Boot中使用进行配置示列:
package com.example.demo.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;@Configurationpublic class AopConfiguration{ @Bean public SentinelResourceAspect sentinelResourceAspect() { return new SentinelResourceAspect(); }}
然后在需要限制的方法上加SentinelResource注解即可:
package com.example.demo.service;import org.springframework.stereotype.Component;import com.alibaba.csp.sentinel.annotation.SentinelResource;import com.alibaba.csp.sentinel.slots.block.BlockException;import com.example.demo.ExceptionUtil;@Componentpublic class BuzService { @SentinelResource(value = "get", blockHandler = "exceptionHandler") public String get(String id) { return "http://cxytiandi.com"; } @SentinelResource(value = "get2", blockHandler = "handleException", blockHandlerClass = { ExceptionUtil.class }) public String get2() { return "http://cxytiandi.com"; } public String exceptionHandler(String id, BlockException e) { e.printStackTrace(); return "错误发生在" + id; }}
SentinelResource:value
表示资源名,必填项
SentinelResource:blockHandler
处理 BlockException 的方法名,可选项。若未配置,则将 BlockException 直接抛出。
-
blockHandler 函数访问范围需要是 public
-
返回类型需要与原方法相匹配
-
参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException
-
blockHandler 函数默认需要和原方法在同一个类中
如果你不想让异常处理方法跟业务方法在同一个类中,可以使用 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
业务方法:
@SentinelResource(value = "get2", blockHandler = "handleException", blockHandlerClass = { ExceptionUtil.class }) public String get2() { return "http://cxytiandi.com"; }
异常处理类:
package com.example.demo;import com.alibaba.csp.sentinel.slots.block.BlockException;public final class ExceptionUtil { public static String handleException(BlockException ex) { System.err.println("错误发生: " + ex.getClass().getCanonicalName()); return "error"; }}
如何测试?
我们可以在Spring Boot的启动类中定义规则,然后快速访问接口,就可以看出效果啦,或者用压力测试工具ab等。
package com.example.demo;import java.util.ArrayList;import java.util.List;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import com.alibaba.csp.sentinel.slots.block.RuleConstant;import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;@SpringBootApplicationpublic class LimitDemoApplication { public static void main(String[] args) { initFlowRules(); SpringApplication.run(LimitDemoApplication.class, args); } private static void initFlowRules() { Listrules = new ArrayList<>(); FlowRule rule = new FlowRule(); rule.setResource("get"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(1); rules.add(rule); rule = new FlowRule(); rule.setResource("get2"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(1); rules.add(rule); FlowRuleManager.loadRules(rules); }}
结果:
源码分析
只需要配置了SentinelResourceAspect就可以使用注解,我们来简单的看下SentinelResourceAspect的源码
@Aspect public class SentinelResourceAspect extends AbstractSentinelAspectSupport { @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)") public void sentinelResourceAnnotationPointcut() { } @Around("sentinelResourceAnnotationPointcut()") public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable { // 获取当前访问的方法 Method originMethod = resolveMethod(pjp); // 获取方法上的SentinelResource注解 SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class); if (annotation == null) { // Should not go through here. throw new IllegalStateException("Wrong state for SentinelResource annotation"); } // 获取资源名 String resourceName = getResourceName(annotation.value(), originMethod); EntryType entryType = annotation.entryType(); Entry entry = null; try { entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs()); Object result = pjp.proceed(); return result; } catch (BlockException ex) { // 处理被限制的异常,回调事先配置的异常处理方法 return handleBlockException(pjp, annotation, ex); } catch (Throwable ex) { Tracer.trace(ex); throw ex; } finally { if (entry != null) { entry.exit(); } } } }
上面是整个切面的代码,对所有加了SentinelResource注解的方法进去切入。细节代码在AbstractSentinelAspectSupport中,大家自己去看看。