重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
在基础篇中我们学习了如何为项目整合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
com.alibaba.csp.sentinel.command.CommandHandler
的实现类本小节简单介绍一下在代码中如何使用Sentinel API,Sentinel主要有以下三个API:
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 {
@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接口就不会像之前那样一个数字就注册一个资源了: