在基础篇中咱们学习了如何为项目整合Sentinel,并搭建了Sentinel的可视化控制台,介绍及演示了各类Sentinel所支持的规则配置方式。本文则对Sentinel进行更进一步的介绍。java
首先咱们来了解控制台是如何获取到微服务的监控信息的:node
微服务集成Sentinel须要添加
spring-cloud-starter-alibaba-sentinel
依赖,该依赖中包含了sentinel-transport-simple-http模块。集成了该模块后,微服务就会经过配置文件中所配置的链接地址,将自身注册到Sentinel控制台上,并经过心跳机制告知存活状态,由此可知Sentinel是实现了一套服务发现机制的。git
以下图:github
经过该机制,从Sentinel控制台的机器列表中就能够查看到Sentinel客户端(即微服务)的通讯地址及端口号:web
如此一来,Sentinel控制台就能够实现与微服务通讯了,当须要获取微服务的监控信息时,Sentinel控制台会定时调用微服务所暴露出来的监控API,这样就能够实现实时获取微服务的监控信息。spring
另一个问题就是使用控制台配置规则时,控制台是如何将规则发送到各个微服务的呢?同理,想要将配置的规则推送给微服务,只须要调用微服务上接收推送规则的API便可。apache
咱们能够经过访问http://{微服务注册的ip地址}:8720/api
接口查看微服务暴露给Sentinel控制台调用的API,以下:api
相关源码:app
com.alibaba.csp.sentinel.transport.heartbeat.SimpleHttpHeartbeatSender
com.alibaba.csp.sentinel.command.CommandHandler
的实现类本小节简单介绍一下在代码中如何使用Sentinel API,Sentinel主要有如下三个API:dom
BlockException
异常)示例代码以下:
@GetMapping("/test-sentinel-api") public String testSentinelAPI(@RequestParam(required = false) String a) { String resourceName = "test-sentinel-api"; // 这里不使用try-with-resources是由于Tracer.trace会统计不上异常 Entry entry = null; try { // 定义一个sentinel保护的资源,名称为test-sentinel-api entry = SphU.entry(resourceName); // 标识对test-sentinel-api调用来源为test-origin(用于流控规则中“针对来源”的配置) ContextUtil.enter(resourceName, "test-origin"); // 模拟执行被保护的业务逻辑耗时 Thread.sleep(100); return a; } catch (BlockException e) { // 若是被保护的资源被限流或者降级了,就会抛出BlockException log.warn("资源被限流或降级了", e); return "资源被限流或降级了"; } catch (InterruptedException e) { // 对业务异常进行统计 Tracer.trace(e); return "发生InterruptedException"; } finally { if (entry != null) { entry.exit(); } ContextUtil.exit(); } }
对几个可能有疑惑的点说明一下:
test-sentinel-api
的调用来源均为test-origin
。例如使用postman或其余请求方式调用了该资源,其来源都会被标识为test-origin
BlockException
及其子类进行统计,其余异常不在统计范围,因此须要使用Tracer.trace
手动统计。1.3.1 版本开始支持自动统计,将在下一小节进行介绍相关官方文档:
通过上一小节的代码示例,能够看到这些Sentinel API的使用方式并非很优雅,有点相似于使用I/O流API的感受,显得代码比较臃肿。好在Sentinel在1.3.1 版本开始支持@SentinelResource
注解,该注解可让咱们避免去写这种臃肿不美观的代码。但即使如此,也仍是有必要去学习Sentinel API的使用方式,由于其底层仍是得经过这些API来实现。
学习一个注解除了须要知道它能干什么以外,还得了解其支持的属性做用,下表总结了@SentinelResource
注解的属性:
属性 | 做用 | 是否必须 |
---|---|---|
value | 资源名称 | 是 |
entryType | entry类型,标记流量的方向,取值IN/OUT,默认是OUT | 否 |
blockHandler | 处理BlockException的函数名称 | 否 |
blockHandlerClass | 存放blockHandler的类。对应的处理函数必须static修饰,不然没法解析,其余要求:同blockHandler | 否 |
fallback | 用于在抛出异常的时候提供fallback处理逻辑。fallback函数能够针对全部类型的异常(除了exceptionsToIgnore 里面排除掉的异常类型)进行处理 |
否 |
fallbackClass【1.6支持】 | 存放fallback的类。对应的处理函数必须static修饰,不然没法解析,其余要求:同fallback | 否 |
defaultFallback【1.6支持】 | 用于通用的 fallback 逻辑。默认fallback函数能够针对全部类型的异常(除了exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准 |
否 |
exceptionsToIgnore【1.6支持】 | 指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出 | 否 |
exceptionsToTrace | 须要trace的异常 | Throwable |
blockHandler,处理BlockException函数的要求:
public
BlockException
类型的参数blockHandlerClass
,并指定blockHandlerClass里面的方法fallback函数要求:
Throwable
类型的参数fallbackClass
,并指定fallbackClass里面的方法defaultFallback函数要求:
Throwable
类型的参数fallbackClass
,并指定 fallbackClass
里面的方法如今咱们已经对@SentinelResource
注解有了一个比较全面的了解,接下来使用@SentinelResource
注解重构以前的代码,直观地了解下该注解带来了哪些便利,重构后的代码以下:
@GetMapping("/test-sentinel-resource") @SentinelResource( value = "test-sentinel-resource", blockHandler = "blockHandlerFunc", fallback = "fallbackFunc" ) public String testSentinelResource(@RequestParam(required = false) String a) throws InterruptedException { // 模拟执行被保护的业务逻辑耗时 Thread.sleep(100); return a; } /** * 处理BlockException的函数(处理限流) */ public String blockHandlerFunc(String a, BlockException e) { // 若是被保护的资源被限流或者降级了,就会抛出BlockException log.warn("资源被限流或降级了.", e); return "资源被限流或降级了"; } /** * 1.6 以前处理降级 * 1.6 开始能够针对全部类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理 */ public String fallbackFunc(String a) { return "发生异常了"; }
注:@SentinelResource
注解目前不支持标识调用来源
Tips:
1.6.0 以前的版本
fallback
函数只针对降级异常(DegradeException
)进行处理,不能针对业务异常进行处理若
blockHandler
和fallback
都进行了配置,则被限流降级而抛出BlockException
时只会进入blockHandler
处理逻辑。若未配置blockHandler
、fallback
和defaultFallback
,则被限流降级时会将BlockException
直接抛出从 1.3.1 版本开始,注解方式定义资源支持自动统计业务异常,无需手动调用
Tracer.trace(ex)
来记录业务异常。Sentinel 1.3.1 之前的版本须要自行调用Tracer.trace(ex)
来记录业务异常
@SentinelResource
注解相关源码:
com.alibaba.csp.sentinel.annotation.aspectj.AbstractSentinelAspectSupport
com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect
相关官方文档:
若是有了解过Hystrix的话,应该就会知道Hystrix除了能够对当前服务的接口进行容错,还能够对服务提供者(被调用方)的接口进行容错。到目前为止,咱们只介绍了在Sentinel控制台对当前服务的接口添加相关规则进行容错,但尚未介绍如何对服务提供者的接口进行容错。
实际上有了前面的铺垫,如今想要实现对服务提供者的接口进行容错就很简单了,咱们都知道在Spring Cloud体系中能够经过RestTemplate或Feign实现微服务之间的通讯。因此只须要在RestTemplate或Feign上作文章就能够了,本小节先以RestTemplate为例,介绍如何整合Sentinel实现对服务提供者的接口进行容错。
很简单,只须要用到一个注解,在配置RestTemplate的方法上添加@SentinelRestTemplate
注解便可,代码以下:
package com.zj.node.contentcenter.configuration; import org.springframework.cloud.alibaba.sentinel.annotation.SentinelRestTemplate; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class BeanConfig { @Bean @LoadBalanced @SentinelRestTemplate public RestTemplate restTemplate() { return new RestTemplate(); } }
注:@SentinelRestTemplate
注解包含blockHandler、blockHandlerClass、fallback、fallbackClass属性,这些属性的使用方式与@SentinelResource
注解一致,因此咱们能够利用这些属性,在触发限流、降级时定制本身的异常处理逻辑
而后咱们再来写段测试代码,用于调用服务提供者的接口,代码以下:
package com.zj.node.contentcenter.controller.content; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @Slf4j @RestController @RequiredArgsConstructor public class TestController { private final RestTemplate restTemplate; @GetMapping("/test-rest-template-sentinel/{userId}") public UserDTO test(@PathVariable("userId") Integer userId) { // 调用user-center服务的接口(此时user-center即为服务提供者) return restTemplate.getForObject( "http://user-center/users/{userId}", UserDTO.class, userId); } }
编写完以上代码重启项目并能够正常访问该测试接口后,此时在Sentinel控制台的簇点链路中,就能够看到服务提供者(user-center)的接口已经注册到这里来了,如今只须要对其添加相关规则就能够实现容错:
若咱们在开发期间,不但愿Sentinel对服务提供者的接口进行容错,能够经过如下配置进行开关:
# 用于开启或关闭@SentinelRestTemplate注解 resttemplate: sentinel: enabled: true
Sentinel实现与RestTemplate整合的相关源码:
org.springframework.cloud.alibaba.sentinel.custom.SentinelBeanPostProcessor
上一小节介绍RestTemplate整合Sentinel时已经作了相关铺垫,这里就不废话了直接上例子。首先在配置文件中添加以下配置:
feign: sentinel: # 开启Sentinel对Feign的支持 enabled: true
定义一个FeignClient接口:
package com.zj.node.contentcenter.feignclient; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "user-center") public interface UserCenterFeignClient { @GetMapping("/users/{id}") UserDTO findById(@PathVariable Integer id); }
一样的来写段测试代码,用于调用服务提供者的接口,代码以下:
package com.zj.node.contentcenter.controller.content; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import com.zj.node.contentcenter.feignclient.UserCenterFeignClient; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor public class TestFeignController { private final UserCenterFeignClient feignClient; @GetMapping("/test-feign/{id}") public UserDTO test(@PathVariable Integer id) { // 调用user-center服务的接口(此时user-center即为服务提供者) return feignClient.findById(id); } }
编写完以上代码重启项目并能够正常访问该测试接口后,此时在Sentinel控制台的簇点链路中,就能够看到服务提供者(user-center)的接口已经注册到这里来了,行为与RestTemplate整合Sentinel是同样的:
默认当限流、降级发生时,Sentinel的处理是直接抛出异常。若是须要自定义限流、降级发生时的异常处理逻辑,而不是直接抛出异常该如何作?@FeignClient
注解中有一个fallback属性,用于指定当远程调用失败时使用哪一个类去处理。因此在这个例子中,咱们首先须要定义一个类,并实现UserCenterFeignClient接口,代码以下:
package com.zj.node.contentcenter.feignclient.fallback; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import com.zj.node.contentcenter.feignclient.UserCenterFeignClient; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Component public class UserCenterFeignClientFallback implements UserCenterFeignClient { @Override public UserDTO findById(Integer id) { // 自定义限流、降级发生时的处理逻辑 log.warn("远程调用被限流/降级了"); return UserDTO.builder(). wxNickname("Default"). build(); } }
而后在UserCenterFeignClient接口的@FeignClient
注解上指定fallback属性,以下:
@FeignClient(name = "user-center", fallback = UserCenterFeignClientFallback.class) public interface UserCenterFeignClient { ...
接下来作一个简单的测试,看看当远程调用失败时是否调用了fallback属性所指定实现类里的方法。为服务提供者的接口添加一条流控规则,以下图:
使用postman频繁发生请求,当QPS超过1时,返回结果以下:
能够看到,返回了代码中定义的默认值。由此可证当限流、降级或其余缘由致使远程调用失败时,就会调用UserCenterFeignClientFallback类里所实现的方法。
可是又有另一个问题,这种方式没法获取到异常对象,而且控制台不会输出任何相关的异常信息,若业务须要打印异常日志或针对异常进行相关处理的话该怎么办呢?此时就得用到@FeignClient
注解中的另外一个属性:fallbackFactory,一样须要定义一个类,只不过实现的接口不同。代码以下:
package com.zj.node.contentcenter.feignclient.fallbackfactory; import com.zj.node.contentcenter.domain.dto.user.UserDTO; import com.zj.node.contentcenter.feignclient.UserCenterFeignClient; import feign.hystrix.FallbackFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Component public class UserCenterFeignClientFallbackFactory implements FallbackFactory<UserCenterFeignClient> { @Override public UserCenterFeignClient create(Throwable cause) { return new UserCenterFeignClient() { @Override public UserDTO findById(Integer id) { // 自定义限流、降级发生时的处理逻辑 log.warn("远程调用被限流/降级了", cause); return UserDTO.builder(). wxNickname("Default"). build(); } }; } }
在UserCenterFeignClient接口的@FeignClient
注解上指定fallbackFactory属性,以下:
@FeignClient(name = "user-center", fallbackFactory = UserCenterFeignClientFallbackFactory.class) public interface UserCenterFeignClient { ...
须要注意的是,fallback与fallbackFactory只能二选一,不能同时使用。
重复以前的测试,此时控制台就能够输出相关异常信息了:
Sentinel实现与Feign整合的相关源码:
org.springframework.cloud.alibaba.sentinel.feign.SentinelFeign
Sentinel默认在当前服务触发限流或降级时仅返回简单的异常信息,以下:
而且限流和降级返回的异常信息是同样的,致使没法根据异常信息区分是触发了限流仍是降级。
因此咱们须要对错误信息进行相应优化,以即可以细致区分触发的是什么规则。Sentinel提供了一个UrlBlockHandler接口,实现该接口便可自定义异常处理逻辑。具体以下示例:
package com.zj.node.contentcenter.sentinel; import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlBlockHandler; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; import com.alibaba.csp.sentinel.slots.block.flow.FlowException; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException; import com.alibaba.csp.sentinel.slots.system.SystemBlockException; import lombok.Builder; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 自定义流控异常处理 * * @author 01 * @date 2019-08-02 **/ @Slf4j @Component public class MyUrlBlockHandler implements UrlBlockHandler { @Override public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException e) throws IOException { MyResponse errorResponse = null; // 不一样的异常返回不一样的提示语 if (e instanceof FlowException) { errorResponse = MyResponse.builder() .status(100).msg("接口限流了") .build(); } else if (e instanceof DegradeException) { errorResponse = MyResponse.builder() .status(101).msg("服务降级了") .build(); } else if (e instanceof ParamFlowException) { errorResponse = MyResponse.builder() .status(102).msg("热点参数限流了") .build(); } else if (e instanceof SystemBlockException) { errorResponse = MyResponse.builder() .status(103).msg("触发系统保护规则") .build(); } else if (e instanceof AuthorityException) { errorResponse = MyResponse.builder() .status(104).msg("受权规则不经过") .build(); } response.setStatus(500); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); new ObjectMapper().writeValue(response.getWriter(), errorResponse); } } /** * 简单的响应结构体 */ @Data @Builder class MyResponse { private Integer status; private String msg; }
此时再触发流控规则就能够响应代码中自定义的提示信息了:
当配置流控规则或受权规则时,若须要针对调用来源进行限流,得先实现来源的区分,Sentinel提供了RequestOriginParser
接口来处理来源。只要Sentinel保护的接口资源被访问,Sentinel就会调用RequestOriginParser
的实现类去解析访问来源。
写代码:首先,服务消费者须要具有有一个来源标识,这里假定为服务消费者在调用接口的时候都会传递一个origin的header参数标识来源。具体以下示例:
package com.zj.node.contentcenter.sentinel; import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; import com.alibaba.nacos.client.utils.StringUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * 实现区分来源 * * @author 01 * @date 2019-08-02 **/ @Slf4j @Component public class MyRequestOriginParser implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest request) { // 从header中获取名为 origin 的参数并返回 String origin = request.getHeader("origin"); if (StringUtils.isBlank(origin)) { // 若是获取不到,则抛异常 String err = "origin param must not be blank!"; log.error("parse origin failed: {}", err); throw new IllegalArgumentException(err); } return origin; } }
编写完以上代码并重启项目后,此时header中不包含origin参数就会报错了:
了解过RESTful URL的都知道这类URL路径能够动态变化,而Sentinel默认是没法识别这种变化的,因此每一个路径都会被当成一个资源,以下图:
这显然是有问题的,好在Sentinel提供了UrlCleaner接口解决这个问题。实现该接口可让咱们对来源url进行编辑并返回,这样就能够将RESTful URL里动态的路径转换为占位符之类的字符串。具体实现代码以下:
package com.zj.node.contentcenter.sentinel; import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; import org.springframework.stereotype.Component; import java.util.Arrays; /** * RESTful URL支持 * * @author 01 * @date 2019-08-02 **/ @Slf4j @Component public class MyUrlCleaner implements UrlCleaner { @Override public String clean(String originUrl) { String[] split = originUrl.split("/"); // 将数字转换为特定的占位标识符 return Arrays.stream(split) .map(s -> NumberUtils.isNumber(s) ? "{number}" : s) .reduce((a, b) -> a + "/" + b) .orElse(""); } }
此时该RESTful接口就不会像以前那样一个数字就注册一个资源了: