上一期教程讲解了 Sentinel 的限流规则: Sentinel限流规则,这一期主要讲述 Sentinel 的 隔离、降级和授权规则
虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了
不管是线程隔离还是熔断降级,都是对客户端(调用方) 的保护
Feign整合Sentinel
在 SpringCloud 中,微服务调用一般都是通过 Feign 来实现的,因此做客户端保护必须整合Feign和Sentinel
Feign 的使用在之前的教程中有详细讲解过:Eureka结合Feign
实现的步骤如下:
1.在调用方的 application.yml 文件中,开启 Feign 对 Sentinel 的支持
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
2.为 FeignClient 编写失败后的降级逻辑
这里有两种方式:
① FallbackClass
,这种方式无法对远程调用的异常做处理
② FallbackFactory
,这种方式可以对远程调用的异常做处理
这里我们选择 FallbackFactory
定义一个类 UserClientFallbackFactory
(名字可以任意),然后实现接口 FallbackFactory<UserClient>
,这里泛型要和对应的 Feign 客户端对应
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public String user() {
cause.printStackTrace();
// 异常时返回空值
return "";
}
};
}
}
3.在 Feign 客户端的 @FeignClient
注解中,添加属性 fallbackFactory
,值就是刚才定义的类的 class 对象
@FeignClient(name = "service2", url = "http://127.0.0.1:8082", fallbackFactory = UserClientFallbackFactory.class)
这里为了方便,就直接定义了对应服务的 url,如果结合了注册中心,这里只需要填写对应的服务名即可
4.启动测试
访问对应服务端点后,进入 Sentinel 控制台,可以看到如下界面,表示 Feign 已经成功整合了 Sentinel
然后添加限流规则,将 QPS 设为 1,测试异常情况
然后快速刷新浏览器,可以发现限流后,返回了空值,即触发了 Feign 客户端的失败降级逻辑
线程隔离
线程隔离有两种方式实现:
1.线程池隔离
这种方式就是将服务消费者的资源划分成一个又一个独立的线程池,假设某一服务提供者挂掉了,也只有一个线程池的资源会被耗尽,不会导致所有的资源被耗尽
优点:
① 支持主动超时(可以通过线程池来控制线程,比如发现某个线程耗时较长,就可以直接终止这个线程)
② 支持异步调用(自己的线程继续接收消息,把调用交给线程池里的线程)
缺点: 线程的额外开销比较大(需要开启的线程较多,同时CPU还需要进行线程的上下文切换)
场景: 低扇出(扇出指的是原本的请求分散后的请求数),扇出越高,需要开启的线程就越多
2.信号量隔离(Sentinel 默认采用)
信号量隔离指的是不会为每个业务创建一个线程池,而是会统计当前业务已经使用的线程数,然后进行限制。当达到这个限制时,就会阻止对这个业务的请求,即限制每个业务能使用的线程数量
优点: 轻量级,无额外开销
缺点:
① 不支持主动超时
② 不支持异步调用
场景: 高频调用、高扇出
下面是两种方式的对比图:
在 Sentinel 控制台中,可以选择阈值类型,共有两种:
① QPS:就是每秒的请求数
② 线程数:是该资源能使用的 tomcat 线程数的最大值。也就是通过限制线程数量,实现舱壁模式,对应信号量隔离
熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务,即拦截访问该服务的一切请求。而当服务恢复时,断路器会放行访问该服务的请求
断路器是由内部的状态机实现的:
整个的流程是:正常情况下,断路器关闭,处于 Closed
状态。如果断路器统计的慢调用或异常比例达到失败阈值,会开启断路器,进入 Open
状态,此时的请求会快速失败。直到熔断时间结束,会进入 Half-Open
状态(断路器半开启状态),此时会尝试放行一次请求,如果此次请求成功,则关闭断路器,进入 Closed
状态;如果请求失败,则重新打开断路器,回到 Open
状态,并重复刚才的过程
断路器熔断策略有三种:慢调用、异常比例、异常数
1.慢调用
业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过指定请求数,并且慢调用比例大于设定的阈值,则触发熔断
例如,在 Sentinel 控制台中选择新增降级规则,并填写如下信息
这个表单的信息表示,响应时长(RT) 超过 500ms 的调用是慢调用,统计最近 10000ms 内的请求,如果请求量不少于 10 次,并且慢调用比例不低于 0.5,则触发熔断,熔断时长为 5 秒。然后进入 half-open 状态,放行一次请求做测试
2.异常比例或异常数
统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常(指的是程序抛出的异常)的比例达到设定的比例阈值(或超过指定异常数),则触发熔断
例如,设置如下的降级规则
这两种设置方式都是一样的,区别在于一个是设置异常比例,一个是设置异常数,但表达的意思都是,统计最近 1000ms 内的请求,如果请求量不少于 10 次,并且异常比例不低于 0.4(或异常数不少于2),则触发熔断,熔断时长为 5 秒。然后进入 half-open 状态,放行一次请求做测试
授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式
白名单:来源(origin)在白名单内的调用者允许访问
黑名单:来源(origin)在黑名单内的调用者不允许访问
在控制台中可以选择新增授权规则,此时就可以选择黑名单或白名单两种方式
Sentinel 是通过 RequestOriginParser
接口中的 parseOrigin()
方法来获取请求的来源的
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
但是,默认情况下,这个方法的返回值永远是 “default”。也就是说,Sentinel 默认无法区分所有的请求。因此,我们需要自己实现这个接口,并且编写业务逻辑
案例:
限定只允许从网关发来的请求来访问 service1
实现步骤如下:
1.编写类,实现 RequestOriginParser
接口并编写 parseOrigin()
方法,从 HTTP 请求中获取名为 origin
的请求头并返回
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String origin = httpServletRequest.getHeader("origin");
if(StringUtils.isEmpty(origin)) {
origin = "blank";
}
System.out.println(origin);
return origin;
}
}
2.在 gateway 网关服务中,利用过滤器添加 origin
请求头,值为 gateway
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
gateway:
routes: # 网关路由配置
- id: service1
uri: http://localhost:8081 # 路由的目标地址
predicates: # 路由断言,判断请求是否符合路由规则的条件
- Path=/service1/** # 路径断言
default-filters:
- AddRequestHeader=origin,gateway # 添加名为origin的请求头,值为gateway
3.在控制台中配置授权规则
4.测试
首先直接跨过网关访问 service1,如下图所示,可以发现访问失败
然后,通过网关来访问 service1,如下图所示,可以发现访问成功
自定义异常结果
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。如果要自定义异常时的返回结果,需要实现 BlockExceptionHandler
接口
public interface BlockExceptionHandler {
/**
* 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
*/
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
;}
可以看到,handle()
方法中有一个类型为 BlockException
的参数,这个参数表示了具体的异常,而 BlockException
包含很多个子类,分别对应不同的场景,如下所示
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
实现自定义异常返回结果的具体方式是,定义一个类,实现 BlockExceptionHandler
接口,然后编写业务逻辑,如下所示:
@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 + "}");
}
}
然后进行测试。先在控制台创建流控规则,将 QPS 设为1,如下
然后快速刷新浏览器,结果如下所示,可以看出异常结果和代码所展现的一致
其他的测试也是类似的,但是发现热点规则无法触发该逻辑,用 debug 设置断点发现在异常时没有进入到对应的代码里
因此建议对于热点规则的异常,直接使用全局异常处理即可,如下所示:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ParamFlowException.class)
public void handleParamFlowException(HttpServletRequest request, HttpServletResponse response, Exception e)
throws IOException {
response.setContentType("application/json;Charset=utf-8");
response.setStatus(429);
response.getWriter().println("{\"msg\": " + "请求被热点参数限流" + ", \"status\": " + 429 + "}");
}
}
测试后发现异常结果可以正常显示