在前文中,介绍了 Sentinel 的流控模式和流控效果,然而限流只是一种预防措施,虽然可以尽量避免因为并发问题而引起的服务故障,但服务仍然可能因其他因素而发生故障。为了将这些故障控制在一定范围内,以避免雪崩效应的发生,我们需要依赖线程隔离(舱壁模式)和熔断降级机制。
无论是线程隔离还是熔断降级,它们都是为了保护客户端(调用方)免受服务故障的影响。
在微服务之间的调用通常依赖于 Open Feign,因此我们首先需要将Feign与 Sentinel进行有效整合。
本文将探讨 Feign 如何与 Sentinel 整合,以及 Sentinel 的隔离、熔断降级规则以及授权规则等关键概念。
在 Spring Cloud 中,微服务之间的调用通常依赖于 Feign 来实现。要在微服务架构中保护客户端,需要将 Feign 和 Sentinel 整合在一起。以下是将 Feign 与 Sentinel 整合的步骤,以一个名为 cloud-demo
的微服务案例为例:
1. 修改 order-service
的 application.yml
文件,启用 Feign 对 Sentinel 的支持:
feign:
sentinel:
enabled: true # 启用 Feign 对 Sentinel 的支持
通过这个配置,我们告诉 Feign 在进行远程调用时要与 Sentinel 一起工作,以确保客户端受到适当的保护。
2. 编写调用失败后的降级逻辑:
当远程调用失败时,可以实现降级逻辑。有两种方式可供选择:
在接下来的部分,我们将深入研究如何使用 FallbackFactory
来处理远程调用的异常,并为客户端提供更好的降级体验。
步骤一:在 feign-api
模块中创建一个UserClientFallbackFactory
类,实现FallbackFactory
接口,并重写 create
方法:
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
@Override
public User findById(Long id) {
log.error("查询用户失败!", throwable);
return new User();
}
};
}
}
这段代码将在 order-service
调用 user-service
失败后自动调用,在控制台会输出错误日志,并返回一个空的 User
对象。
步骤二:在 config
中将 UserClientFallbackFactory
类注册为一个 Bean:
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
步骤三:在 feign
的 UserClient
接口的@FeignClient
注解中指定 fallbackFactory
为 UserClientFallbackFactory
:
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
通过这些步骤,就可以使用 FallbackFactory
处理远程服务调用失败,捕获异常信息,并提供更好的降级体验。
在 Sentinel 中,隔离有两种主要实现方法,即信号量隔离和线程隔离。其中默认采用的是信号量隔离:
信号量隔离:
信号量隔离是一种资源隔离方式,它通过设置每个资源(或接口)的许可证数量来控制并发访问。当并发请求到达时,如果资源的许可证数量已经用尽,新的请求将被阻塞,以保护资源不被过度访问。信号量隔离适用于需要控制并发访问的场景,例如数据库连接、外部API调用等。
优点:
缺点:
场景:适用于高频调用和高扇出的场景,其中资源隔离较为轻量。
线程隔离:
线程隔离是一种资源隔离方式,它将不同的资源请求隔离到不同的线程池中执行,以确保它们不会相互影响。每个线程池负责执行特定资源的请求,如果一个请求由于某种原因导致线程阻塞或异常,不会影响其他资源的请求。线程隔离适用于需要独立线程执行的场景,例如耗时操作、阻塞调用等。
优点:
缺点:
场景:线程隔离适用于低扇出场景,其中资源隔离更为重要,或需要支持异步调用和主动超时的情况。
选择隔离的实现方法取决于具体需求和应用场景。根据不同的场景和资源调用特点,可以灵活选择信号量隔离或线程隔离,以保障系统的稳定性和性能。
回顾在使用 Sentinel 控制台设置限流规则的时候,发现有两种阈值类型:
下面是一个示例,演示如何在 Sentinel 的控制台中实现线程隔离的设置。
1. 给 UserClient
的查询用户接口设置流控规则,线程数不能超过 2。
2. 然后利用 JMeter 测试。
启动 JMeter:
可以发现最终 10 个线程的请求只通过了其中两个。
查看 Sentinel 控制台的实时监控:
熔断降级原理:
熔断降级是解决雪崩问题的重要手段,其原理是由断路器统计服务调用的异常比例和慢请求比例,如果超出阈值则会熔断该服务。具体原理如下:
熔断降级流程:
熔断降级的流程如下图所示:
流程说明如下:
Closed
状态,允许所有请求访问服务。Open
状态,拦截所有请求,快速失败。Half-Open
状态,允许部分请求访问服务,用于测试服务是否已经恢复正常。Half-Open
状态下的请求成功,则断路器进入 Closed
状态,允许所有请求访问服务。Half-Open
状态下的请求仍然失败,断路器继续保持 Open
状态,直到下一次尝试进入 Half-Open
状态。熔断降级通过这种状态机实现,可以帮助服务在异常情况下避免雪崩效应,提高系统的可用性和稳定性。
断路器熔断策略有三种:慢调用、异常比例、异常数。
慢调用就是当业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
例如,通过 Sentinel 控制台设置慢调用降级策略:
说明:
当 RT 超过 500ms 的调用就是慢调用,统计最近 10000ms 内的请求,如果请求量超过 5 次,并且慢调用比例不低于 0.5,则触发熔断,熔断时长为 5 秒。然后进入 Half-open
状态,放行一次请求做测试。
现在有一个需求:就是给 UserClient
的查询用户接口设置降级规则,慢调用的 RT 阈值为 50ms,统计时间为 1 秒,最小请求数量为 5,失败阈值比例为 0.4,熔断时长为 5:
为了触发慢调用规则,我们需要修改UserService
中的业务,增加业务耗时:
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id ) throws InterruptedException {
if(id == 1){
// 休眠,触发慢调用熔断策略
Thread.sleep(60);
}
return userService.queryById(id);
}
重启 user-service
服务后,我们可以通过快速刷新浏览器,来触发这个熔断机制:
当触发了容器之后,服务其他 ID 的接口,也会被熔断:
异常比例或异常数都是统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
统计最近 1000ms 内的请求,如果请求量超过 10 次,并且异常比例不低于 0.5,则触发熔断,熔断时长为5秒。然后进入Half-open
状态,放行一次请求做测试。
下面以异常比例为例:
例如现在有一个需求:就是给 UserClient
的查询用户接口设置降级规则,统计时间为 1 秒,最小请求数量为 5,失败阈值比例为 0.4,熔断时长为 5s:
为了触发异常统计,同样需要修改UserService
中的业务,抛出异常:
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) throws InterruptedException {
if(id == 1){
// 休眠,触发慢调用熔断策略
Thread.sleep(60);
} else if (id == 2) {
throw new RuntimeException("演示触发异常比例熔断!");
}
return userService.queryById(id);
}
此时,访问 ID 为 2 的用户就会抛出异常。
重启 user-service
服务后,我们可以通过快速刷新浏览器,来触发这个熔断机制:
授权规则用于对调用方的来源进行控制,通常分为白名单和黑名单两种方式:
在 Sentinel 控制台中,可以配置授权规则,如下所示:
现在,我们可以从浏览器和网关两个路径去服务 order-service
服务:
如果现在需要限定只允许从网关来的请求访问 order-service
服务,那么流控应用中就填写网关的名称。
下面将演示如何通过 Sentinel 的授权规则来限制对 order-service
服务的请求只能来自网关 gateway
。
Sentinel 是通过 RequestOriginParser
这个接口的 parseOrigin
来获取请求的来源的:
public interface RequestOriginParser {
// 从请求request对象中获取origin,获取方式自定义
String parseOrigin(HttpServletRequest request);
}
RequestOriginParser
是 Sentinel 提供的接口,用于解析请求的来源(origin)。这接口定义了一个方法 parseOrigin
,我们需要实现这个方法,以自定义方式从请求对象中获取请求的来源信息。
因此,我们尝试从request
中获取一个名为origin
的请求头,作为origin
的值,作为判断请求是否来源于网关的依据。实现的步骤如下:
gateway
服务的 application.yml
文件中,利用网关的过滤器给所有的请求都添加一个名为 gateway
的 origin
请求头:spring:
cloud:
gateway:
default-filters: # 默认过滤器,会对所有的路由请求都生效
- AddRequestHeader=origin, gateway # Sentinel 授权规则,只有从网关服务的才合法,通过添加请求头标识
gateway
:order-service
中实现一个 HeadOriginParser
类,实现 RequestOriginParser
接口,用来获取请求源:@Component
public class HeadOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
// 1. 获取请求头
String origin = httpServletRequest.getHeader("origin");
// 2. 非空判断
if (StringUtils.isEmpty(origin)) {
return "blank";
}
return origin;
}
}
如果不存在origin
这个字段的请求头,直接返回"blank",否则直接返回 origin
的值,然后交给 Sentinel 进行判断请求源。
order-service
和 gateway
服务,进行演示:此时,如果我们之间通过浏览器访问 order-service
的接口,发现就被禁止访问了:
如果通过 gateway
网关,就能够成功访问了:
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方,但是通过上面所有的例子,我们发现都只返回了一种结果,那就是“Blocked by Sentinel (flow limiting)”。如果我们想要知道微服务调用失败的具体原因,就需要对异常进行自定义处理:
如果要实现对 Sentinel 的异常实现自定义,那么就需要实现 BlockExceptionHandler
接口,它是 Sentinel 提供的一个接口,用于自定义处理调用失败时的异常情况。
而关于 BlockException
这个类,包含很多个子类,分别对应不同的场景:
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
我们可以通过这些子类来判断在调用微服务失败的时候,具体出现了哪种情况,然后通过自定义异常进行结果的返回,下面是一个实现自定义异常的代码示例:
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
在上述示例中,创建了一个名为 CustomBlockExceptionHandler
的自定义异常处理类,实现了 BlockExceptionHandler
接口。在重写的 handle
方法中,根据不同的 BlockException
类型来确定异常消息和状态码,然后将这些信息返回给调用方。
例如,在上面配置了授权规则的情况下,直接通过浏览器访问 order-service
服务:
此时,就能够清楚的知道微服务调用失败的原因了。