Loading... ## 引入规划 Sentinel同样是有注解和客户端API两种形式,但基于AOP方式的注解方法仍是无法解决SOA静态方法的问题。 本次打算采用Spring Boot的方式进行接入,用yml上通用规则配置+每个资源的具体配置,再使用Sentinel提供的控制台的同时,采用ETCD作为数据源,通过Push模式将规则中心统一推送。 <!--more--> ## 代码演示 ### 一、引入控制台 (1)方式一:官方下载 `sentinel-dashboard-1.8.1.jar`,通过命令行启动。 ```cmd --server.port:自定义服务器端口。默认为 8080 端口。 --auth.username 和 --auth.password:自定义账号和密码。默认为「sentinel / sentinel」。 --logging.file:自定义日志文件。默认为 ${user.home}/logs/csp/sentinel-dashboard.log。 ``` 本次启动命令:`java -jar sentinel-dashboard-1.8.1.jar --server.port=7777` 后续项目中需要指定控制台地址。 (2)方式二:直接下载源码 `sentinel-dashboard`,然后在IDEA中打包启动。 ```vm cd sentinel-dashboard mvn clean package java -Dserver.port=7777 \ -Dcsp.sentinel.dashboard.server=localhost:7777 \ -jar target/sentinel-dashboard.jar ``` ### 二、引入依赖 ```xml <!-- Sentinel 核心库 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-core</artifactId> <version>1.8.1</version> </dependency> <!-- Sentinel 接入控制台 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-transport-simple-http</artifactId> <version>1.8.1</version> </dependency> <!-- Sentinel 对 SpringMVC 的支持 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-webmvc-adapter</artifactId> <version>1.8.1</version> </dependency> <!-- Sentinel 对 Spring AOP 的拓展 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-annotation-aspectj</artifactId> <version>1.8.1</version> </dependency> ``` ### 三、项目启动与控制台接入 **一、添加Sentinel拦截器,根据访问的URL/Method创建资源** ```java @Configuration public class SpringMvcConfiguration implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 添加Sentinel拦截器 // addSentinelWebTotalInterceptor(registry); addSentinelWebInterceptor(registry); } private void addSentinelWebInterceptor(InterceptorRegistry registry) { // <1.1> 创建 SentinelWebMvcConfig 对象 SentinelWebMvcConfig config = new SentinelWebMvcConfig(); // <1.2> 是否包含请求方法。即基于 URL 创建的资源,是否包含 Method。 config.setHttpMethodSpecify(true); // <1.3> 设置 BlockException 处理器。对达到流量控制阈值后的请求做处理。 // config.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); // <2> 添加 SentinelWebInterceptor 拦截器 registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**"); } private void addSentinelWebTotalInterceptor(InterceptorRegistry registry) { // 针对全局URL进行流量控制,将所有URL合计流量,全局统一控制。 // <1> 创建 SentinelWebMvcTotalConfig 对象 SentinelWebMvcTotalConfig config = new SentinelWebMvcTotalConfig(); // <2> 添加 SentinelWebTotalInterceptor 拦截器 registry.addInterceptor(new SentinelWebTotalInterceptor(config)).addPathPatterns("/**"); } } ``` 顺带一提,扫描是基于SpringMVC,所以同样无法对静态方法进行服务保障。 **二、添加Sentinel通用异常处理** ```java @Controller @RequestMapping(value = "/error") @EnableConfigurationProperties({ ServerProperties.class }) public class ExceptionController implements ErrorController { private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionController.class); /** * 针对Sentinel设置 BlockException 注解形式的通用处理器 * @param blockException * @return */ @ResponseBody @ExceptionHandler(value = BlockException.class) public String blockExceptionHandler(BlockException blockException) { // 可以根据异常类型做不同处理 return "请求过于频繁"; } } ``` **三、添加启动配置项,指定接入控制台(可跳过)** **`sentinel.properties`:** ```xml csp.sentinel.dashboard.server=127.0.0.1:7777 ``` 其他配置项参考:https://github.com/alibaba/Sentinel/wiki/%E5%90%AF%E5%8A%A8%E9%85%8D%E7%BD%AE%E9%A1%B9 **四、设置项目名,供Sentinel控制台区分和读取(可跳过)** ```java @ServletComponentScan @SpringBootApplication(scanBasePackages = "cn.taqu") @TqRpcScan(basePackages = "cn.taqu") @EnableTqmqListener @EnableSoaParamAdapt @EnableSoa //@EnableScheduling @EnableReqRateLimit public class DemoApplication { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { System.setProperty("project.name", "Sentinel"); SpringApplication.run(DemoApplication.class, args); } } ``` 引入RestTemplates()只是为后续测试使用。 **附**:步骤**三、四**也可以直接写在JVM启动参数上(**由于J2的SpringBoot版本过低,只能走这种方式**): `-Dproject.name=xxx -Dcsp.sentinel.dashboard.server=consoleIp:port` **五、简单Demo演示** ```java @RestController @RequestMapping("/demo") @Log4j2 public class DemoController { @Autowired private RestTemplate restTemplate; @GetMapping("/echo") public String echo() { return "echo"; } @GetMapping("/test") public String test() { return "test"; } @GetMapping("/sleep") public String sleep() throws InterruptedException { Thread.sleep(200L); return "sleep"; } } ``` **调用方法:** `http://127.0.0.1:8080/demo/echo` **控制台结果:** ```java INFO: Sentinel log output type is: file INFO: Sentinel log charset is: utf-8 INFO: Sentinel log base directory is: C:\Users\Administrator\logs\csp\ INFO: Sentinel log name use pid is: false ``` (由于客户端是懒加载的,所以第一次请求打进来才会执行,可通过配置改变) **前端结果:** ```json { "code":"0", "success":true, "msg":"", "data":"echo", "extra":"" } ``` **控制台结果:** ![](https://blog-picture01.oss-cn-shenzhen.aliyuncs.com/img/20210716113009.png) (`Sentinel`就是Application中配置的project.name) ### 四、流量控制 **流量控制(flow control)**,其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。 Sentinel在限流时会抛出 `BlockException`的子类 `FlowException`。 ![](https://blog-picture01.oss-cn-shenzhen.aliyuncs.com/img/20210716113020.png) * **资源名(resource)**:限流规则的作用对象 * **针对来源(limitApp)**:流控针对的调用来源,若为 `default` 则不区分调用来源。 可以通过 `ContextUtil.enter(resourceName, origin)` 方法中的 `origin` 参数标明调用方身份。当参数为 `{some_origin_name}`时表示只有来自这个调用者的请求才会进行流量控制。`other`则表示除 {some_origin_name} 以外的其余调用方的流量进行流量控制。 * **阈值类型(grade)**:QPS/并发数 并发数控制用于保护业务线程池不被慢调用耗尽。Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。<br>推荐使用QPS。 * **单机阈值(count)** * **是否集群** * **流控模式(strategy)**:调用关系限流策略,包括直接(STRATEGY_DIRECT)、关联(STRATEGY_RELATE)和链路(STRATEGY_CHAIN)。 直接就是limitApp中指定的origin;关联是具有竞争或依赖关系的资源,如配置写库来达到优写的目的;链路指同资源下被指定的入口将被监控限流,而且其它入口则直接放行。 * **流控效果(controlBehavior)**:由QPS限流触发,直接拒绝、Warm Up、匀速排队 直接拒绝是在打到QPS阈值时直接抛出FlowException异常,适用于测压后确定出系统处理能力的准水位。<br>Warm UP为预热/冷启动,通过令牌桶结合Guava算法,当流量激增时会根据设置的预热时长在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。<>br排队等待是由Leaky Bucket 算法结合虚拟队列等待机制实现的。阈值为每秒通过请求数,排队超时时间单位为毫秒。适用于MQ中以固定速率处理消息,不适用与QPS>1000的场景。 **注意:**控制台中自动按方法构造的资源不能更改其资源名,否则会因为匹配不到而失效。 ### 五、熔断降级 Sentinel的熔断策略包括:**慢调用比例**(默认最大RT=4900ms)、**异常比例**、**异常数**。 当【统计时长】周期内,发起的请求数大于【最小请求数】时,会根据【最大RT/比例阈值/异常数】进行熔断,窗口期为【熔断时长】。 ![](https://blog-picture01.oss-cn-shenzhen.aliyuncs.com/img/20210716113030.png) 除此之外,Sentinel还具备了**热点参数限流**、**系统自适应限流**、**黑/白名单**等。 ![](https://blog-picture01.oss-cn-shenzhen.aliyuncs.com/img/20210716113048.png) 系统自适应限流参数包括:Load(仅对 Linux/Unix-like 机器生效)、RT、线程数、入口QPS、CPU使用率 **注意:** fallback 和 blockHandler 的差异点在于, blockHandler 只能处理 BlockException 异常,fallback 能够处理所有异常。 如果都配置的情况下,BlockException 异常分配给 blockHandler 处理,其它异常分配给 fallback 处理。 ### 六、注解支持 由于Sentinel和Hystrix的注解差不多,都是通过AOP方式代理,所以没有办法直接对静态方法进行熔断降级处理。 ```java /** * 描述:Sentinel注解支持 * * @author CaiTianXin * @date 2021/7/5 17:05 */ @RestController @RequestMapping("/user") public class UserController { @RequestMapping("/getId") @SentinelResource(value = "getId_resource", blockHandler = "getIdBlockHandler", fallback = "getIdFallback") public String getId(@RequestParam String id) throws InterruptedException { if (id.isEmpty()){ throw new IllegalArgumentException("id 参数不允许为空"); } return "get " + sayId(id); } // BlockHandler 处理函数,参数最后多一个 BlockException,其余与原函数一致. public String getIdBlockHandler(String id, BlockException ex) { return "getIdBlock:" + ex.getClass().getSimpleName(); } // Fallback 处理函数,函数签名与原函数一致或加一个 Throwable 类型的参数. public String getIdFallback(String id, Throwable throwable) { return "getIdFallback:" + throwable.getMessage(); } public static String sayId(String id) throws InterruptedException { Thread.sleep(200L); // int a = 1 / 0; return "say:" + id; } @RequestMapping("/getError") public String getError(@RequestParam String error){ return sayError(error); } /** * 由于该方法是静态方法无法被AOP代理,不能被监听,也就没法熔断降级 */ @SentinelResource(value = "sayError_resource", blockHandler = "sayErrorBlockHandler", fallback = "sayErrorFallback") public static String sayError(String error) { if (error.isEmpty()){ throw new IllegalArgumentException("error 参数不允许为空"); } return "error:" + error; } public String sayErrorBlockHandler(String error, BlockException ex) { return "sayErrorBlock:" + ex.getClass().getSimpleName(); } public String sayErrorFallback(String error, Throwable throwable) { return "sayErrorFallback:" + throwable.getMessage(); } } ``` ![](https://api-gw.admin.internal.taqu.cn/uploads/202107/wiki/attach_1691952ecb4be223.png) **客户端API自定义资源的规则优先级大于URL生成的资源规则** ### 七、规则管理及推送 | 推送模式 | 说明 | 优点 | 缺点 | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------ | | 原始模式 | API 将规则推送至客户端并直接更新到内存中,扩展写数据源(WritableDataSource) | 简单,无任何依赖 | 不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境 | | Pull模式 | 扩展写数据源(WritableDataSource), 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等 | 简单,无任何依赖;规则持久化 | 不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。 | | Push模式 | 扩展读数据源(ReadableDataSource),规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。 | 规则持久化;一致性;快速 | 引入第三方依赖 | 本次采用Push模式,以ETCD作为数据源,修改ETCD配置时可以同步变更资源规则。 ### 八、客户端API形式的SOA熔断Demo ```java /** * soa请求相关类 * * @author qiuyuhua * @date 2020/12/01 */ @Service @Slf4j public class SoaService { /** * 熔断降级规则 Map */ public static Map<String, DegradeRuleVo> degradeRuleMap = Maps.newHashMap(); public static void initDegradeRuleMap(String degradeRuleValue) { try { degradeRuleMap = JsonUtils.stringToObject2(degradeRuleValue, new TypeReference<Map<String, DegradeRuleVo>>() { }); } catch (Exception e) { log.error("etcd获取Sentinel资源配置规则转换失败,sentinelResourceValue={}", degradeRuleValue, e); } } private static void initDegradeRule(DegradeRuleVo degradeRule) { if (degradeRule != null) { List<DegradeRule> rules = new ArrayList<>(); DegradeRule rule = new DegradeRule(); rule.setResource(degradeRule.getResource()); rule.setGrade(degradeRule.getGrade()); rule.setCount(degradeRule.getCount()); rule.setTimeWindow(degradeRule.getTimeWindow()); rule.setMinRequestAmount(degradeRule.getMinRequestAmount()); rule.setStatIntervalMs(degradeRule.getStatIntervalMs()); if (degradeRule.getGrade() == 0){ rule.setSlowRatioThreshold(degradeRule.getSlowRatioThreshold()); } rules.add(rule); DegradeRuleManager.loadRules(rules); } } /** * 测试Sentinel熔断:从bbs获取查看和被查看用户的隐私设置 * * @return true-开启了隐私,false-未开启隐私 */ public static Boolean testDegrade(String uuid) { // 配置熔断规则 try { DegradeRuleVo resourceRule = degradeRuleMap.get("default"); // 默认规则需要添加资源名 resourceRule.setResource("testDegrade"); initDegradeRule(resourceRule); } catch (Exception e) { log.warn("default 熔断规则获取失败", e); } Boolean result = true; if (StringUtils.isBlank(uuid)) { return result; } List<String> uuids = Lists.newArrayList(uuid); Object[] form = { uuids, "hide_distance" }; Entry entry = null; try { // 设置资源名 entry = SphU.entry("testDegrade"); // 业务逻辑 SoaClient soaClient = SoaClientFactory.create(SoaServer.PHP.FORUM_V5); SoaResponse soaResponse = soaClient.call("AccountPrivacy", "errorMethod111", form); if (soaResponse.fail()) { log.error("调用获取用户隐私接口失败,msg={}", JsonUtils.objectToString(soaResponse)); throw new IOException("调用获取用户隐私接口失败,msg=" + JsonUtils.objectToString(soaResponse)); } Map<String, Object> map = JsonUtils.stringToObject(soaResponse.getData(), new TypeReference<Map<String, Object>>() { }); Integer uuidStatus = MapUtils.getInteger(map, uuid); if (Objects.equals(CommonEnableStatus.DISABLE.getStatus(), uuidStatus)) { result = false; } } catch (BlockException e) { // 降级或限流 log.error("调用获取用户隐私接口失败,服务降级!"); // 容错处理 return result; }catch (IOException e){ // 记录业务异常 Tracer.trace(e); return result; } catch (Exception e) { log.error("获取用户隐私信息格式不正确", e); } finally { if (entry != null) { entry.exit(); } } return result; } } ``` Last modification:August 22, 2022 © Allow specification reprint Like 0 喵ฅฅ