Spring Cloud Alibaba之服务容错组件 - Sentinel [代码篇]
在基础篇中我们学习了如何为项目整合Sentinel,并搭建了Sentinel的可视化控制台,介绍及演示了各种Sentinel所支持的规则配置方式。本文则对Sentinel进行更进一步的介绍。
首先我们来了解控制台是如何获取到微服务的监控信息的:
微服务集成Sentinel需要添加
spring-cloud-starter-alibaba-sentinel
依赖,该依赖中包含了sentinel-transport-simple-http模块。集成了该模块后,微服务就会通过配置文件中所配置的连接地址,将自身注册到Sentinel控制台上,并通过心跳机制告知存活状态,由此可知Sentinel是实现了一套服务发现机制的。
如下图:
通过该机制,从Sentinel控制台的机器列表中就可以查看到Sentinel客户端(即微服务)的通信地址及端口号:
如此一来,Sentinel控制台就可以实现与微服务通信了,当需要获取微服务的监控信息时,Sentinel控制台会定时调用微服务所暴露出来的监控API,这样就可以实现实时获取微服务的监控信息。
另外一个问题就是使用控制台配置规则时,控制台是如何将规则发送到各个微服务的呢?同理,想要将配置的规则推送给微服务,只需要调用微服务上接收推送规则的API即可。
我们可以通过访问http://{微服务注册的ip地址}:8720/api
接口查看微服务暴露给Sentinel控制台调用的API,如下:
相关源码:
注册/心跳机制:com.alibaba.csp.sentinel.transport.heartbeat.SimpleHttpHeartbeatSender
通信API:com.alibaba.csp.sentinel.command.CommandHandler
的实现类Sentinel API的使用
本小节简单介绍一下在代码中如何使用Sentinel API,Sentinel主要有以下三个API:
SphU:添加需要让sentinel监控、保护的资源Tracer:对业务异常进行统计(非BlockException
异常)ContextUtil:上下文工具类,通常用于标识调用来源示例代码如下:
@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(); }}
对几个可能有疑惑的点说明一下:
资源名:可任意填写,只要是唯一的即可,通常使用接口名ContextUtil.enter:在该例子中,用于标识对test-sentinel-api
的调用来源均为test-origin
。例如使用postman或其他请求方式调用了该资源,其来源都会被标识为test-origin
Tracer.trace:降级规则中可以针对异常比例或异常数的阈值进行降级,而Sentinel只会对BlockException
及其子类进行统计,其他异常不在统计范围,所以需要使用Tracer.trace
手动统计。1.3.1 版本开始支持自动统计,将在下一小节进行介绍相关官方文档:
如何使用#其它-api@SentinelResource注解
经过上一小节的代码示例,可以看到这些Sentinel API的使用方式并不是很优雅,有点类似于使用I/O流API的感觉,显得代码比较臃肿。好在Sentinel在1.3.1 版本开始支持@SentinelResource
注解,该注解可以让我们避免去写这种臃肿不美观的代码。但即便如此,也还是有必要去学习Sentinel API的使用方式,因为其底层还是得通过这些API来实现。
学习一个注解除了需要知道它能干什么之外,还得了解其支持的属性作用,下表总结了@SentinelResource
注解的属性:
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函数要求:
返回类型与原方法一致参数类型需要和原方法相匹配,Sentinel 1.6开始,也可在方法最后加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
相关官方文档:
注解支持官方代码示例RestTemplate整合Sentinel
如果有了解过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;@Configurationpublic 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@RequiredArgsConstructorpublic 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
Feign整合Sentinel
上一小节介绍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@RequiredArgsConstructorpublic 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@Componentpublic 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@Componentpublic 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默认在当前服务触发限流或降级时仅返回简单的异常信息,如下:
并且限流和降级返回的异常信息是一样的,导致无法根据异常信息区分是触发了限流还是降级。
所以我们需要对错误信息进行相应优化,以便可以细致区分触发的是什么规则。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@Componentpublic 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@Builderclass 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@Componentpublic 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支持
了解过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@Componentpublic 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接口就不会像之前那样一个数字就注册一个资源了:
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。