导航
- 引言
- 一、Sentinel的两个基本概念
- 二、流控规则
- 2.1 基本选项
- 2.2 高级选项
- 三、熔断(降级)规则
- 四、热点规则
- 五、授权规则(了解)
- 六、系统规则(了解)
- 七、自定义异常返回
- 八、@SentinelResource
- 九、Sentinel 规则持久化(待补充)
- 十、Feign 整合 Sentinel 实现容错
引言
Sentinel 官方文档:https://sentinelguard.io/zh-cn/docs/introduction.html
本文承接《Spring Cloud Alibaba —— Sentinel 入门》,继续深入使用 Sentinel。
本文总结自B站,黑马程序员视频《深入学习Java微服务开发(SpringCloud) 》(P21 - P32)。将介绍 Sentinel 的概念和功能、基本配置、流控模式、熔断降级规则、自定义规则、规则持久化,以及 Feign 整合Sentinel 实现容错降级逻辑等课题。
一、Sentinel的两个基本概念
- 资源
Sentinel 中最基本的概念。资源可以指代任何需要保护的内容,可以是服务、方法,甚至可以是一段代码。例如,/order/message1 ,这个接口受到了 Sentinel 的流控保护,因此它就是一个资源。 - 规则
规则就是用来定义如何进行资源保护的。主要分为三种类型:流量控制规则、熔断降级规则、系统保护规则。
学习 Sentinel 很大程度就是在学习这些规则如何配置。
服务容错的三个核心思想:不被上游压垮、不被下游拖垮、保证外界环境(cpu、内存)良好。(这也是服务雪崩的三个主要成因,见《微服务架构 —— 服务雪崩与容错方案》)
流量控制规则:针对上游请求,由于网络传输是不稳定的,这种不稳定可能导致某时刻涌入大量的请求,但系统的处理能力是有限的,Sentinel 作为调配器,可以根据需要把随机的请求调整成合适的形状。所谓合适的形状,例如漏斗,只允许通过 n 个请求,那么后面的请求就会等待前面的请求通过后才能通过。
熔断降级:当检测到调用链中出现不稳定的情况,如请求时间长或异常比例升高等。就可以针对频繁发生故障的节点的调用进行限制,避免产生级联故障。(两种手段:限制并发线程数、通过响应时间对资源降级)
系统负载保护:提供了系统纬度的自适应能力。当系统负载高时,如果还持续让请求进入,可能导致系统崩溃。在集群环境下,可以分散请求到其他机器上,如果其他机器也处于高负载状态,Sentinel 会采取必要的保护机制,让系统入口的流量和系统的负载达到一个平衡,保证系统在能力范围内处理最多请求。
Sentinel 与 Hystrix 比较
两者的原则都是一致的,都是在发现服务发生故障后,快速失败,避免产生级联故障。
区别是,Sentinel 采用并发线程数和响应时间来进行服务熔断降级,而Hystrix采用线程池隔离的方式,线程池隔离的缺点是增加了线程切换的成本。
二、流控规则
2.1 基本选项
登录 Sentinel 控制台,在簇点链路中找到需要做流控的接口,这里就以 /order/message1 接口为例:
打开流控规则设置会话框:
其中,针对来源 为 default 代表不限制来源,在微服务中,如果服务 A 和服务 B 都调用 order 服务这个接口,那么可以通过针对服务A或服务B,分别进行流控限制,如果是 default 就不做来源的区分。
QPS 不多解释,当每秒请求数达到阈值,自然就被限流。这里主要演示下 并发线程数。
并发线程数代表统一时间处理请求的线程数量,如果通过一个客户端浏览器发送请求进行测试,是无法模拟的,因为http一次请求和响应只对应一个线程,手动刷新的方式只能模拟一条线程的情况,这时可以使用 jMeter 工具来进行并发线程数的测试(jMeter 参考 《jMeter 模拟 web 高并发请求》)。
首先,设置 Sentinel 流控规则为 并发线程数 = 2:
然后启动 jMeter,并添加线程组:
并添加 http 取样器,请求接口就是 http://localhost:8091/order/message1 ,并点击发送:
因为我们设置了流控规则为两个并发线程数,jMeter 的线程数也是2 个,因此就会占用这个线程名额,当我们另一边手动用浏览器请求接口的时候,就会被流控规则限制:
2.2 高级选项
流控模式
-
直接(默认):接口达到限流条件时,开启限流。
-
关联:当关联资源达到限流条件时,开启限流(适合做应用让步)。
例如,关联一个 /order/message2 接口(该资源要确实存在,此处具体代码略),并设置关联限流模式:
它表示这个服务节点(单机)的请求每秒不得超过3个,如果关联资源 /order/message2 的请求超过3,那么就限流掉 /order/message1 的请求,让资源名指定的资源对关联资源做出让步。 -
链路:当从某个接口过来的资源达到限流条件时,开启限流。该方式可以对 服务内部的 service 层方法进行限流,例如两个controller入口都调用了某 service 方法,我们给该方法标记上@SentinelResource注解,配合链路流控模式,就可以针对某个controller进入的请求进行限流操作。它的功能有点类似于“针对来源”的配置,但区别是针对来源是针对上级服务的,而链路流控是针对上级接口的,也就是说它的粒度更细。(具体实现有一些版本问题)
流控效果
流控效果非常简单,意思就是限流掉的请求以什么方式来处理。分为三种:快速失败、预热、排队等待。
- 快速失败(默认):直接失败,抛出异常。不做任何额外的处理,是最简单的效果。
- Warm Up:预热,它从开始阈值到最大阈值会有一个缓冲阶段。一开始的阈值是最大阈值的 1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。
- 排队等待:单机阈值为每秒通过数量,其他请求排队等待,可设置超时时长,单位ms。当请求超过超时时间还未处理,就会被丢弃。
三、熔断(降级)规则
降级规则就是设置当满足什么条件的时候,对微服务进行熔断降级。
四、热点规则
Sentinel 可以对接口参数进行限流,以热点规则来配置。
@GetMapping("/message1")
@SentinelResource("params-api")
public String message1(String name, String age) {return "message1";
}
找到资源,设置热点规则:
配置好后,点击新增,那么当请求参数含有 name 时,就会受到该热点规则的限制,因为热点规则设置的参数索引是 0,即name:
五、授权规则(了解)
根据请求来源判断是否放行。
流控应用:
标识具体来源,Sentinel 提供 RequestOriginParser 接口处理来源。只要 Sentinel 保护的接口资源被访问,Sentinel 就会调用 RequestOriginParser 实现类去解析访问来源。
授权类型:
- 若配置白名单,则只有请求来源在白名单内时才通过;
- 若配置黑名单,则请求来源在黑名单内时不通过,其余请求可以通过。
对于流控应用,需要自定义请求来源解析器。
@Component
public class ServiceNameParser implements RequestOriginParser {/*** 解析请求来源,自定义规则*/@Overridepublic String parseOrigin(HttpServletRequest httpServletRequest) {String serviceName = httpServletRequest.getParameter("serviceName");return serviceName;}
}
然后配置授权规则:
像上图,当Sentinel通过自定义的 parseOrigin() 方法取到的值为 “pc” 时,就放行请求,如果是其他请求就不予通过。若配置的是黑名单,则表示当 parseOrigin() 方法返回的值为 “pc” 时,不予通过,其他请求正常放行。
六、系统规则(了解)
系统保护规则是从应用级别的入口流量进行控制的,从单台机器的总体 load 、RT、入口qps、cpu使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用纬度的,而不是资源纬度的,并且仅对入口流量(进入应用的流量)生效。
- Load(仅对类 Unix 系统生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时,才触发系统保护。系统容量由系统的maxQps * minRt 计算得出。设定参考值一般是 cpu cores * 2.5.
- RT:当单台机器上所有入口流量的平均 RT 达到阈值时触发系统保护,单位毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值时触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值时触发系统保护。
- CPU 使用率:当单台机器上所有入口流量的 CPU 使用达到阈值时触发系统保护。
七、自定义异常返回
默认情况,所有的流控规则返回的错误信息都是一样的,客户端无法通过流控的异常返回执行特定的逻辑。为了解决这个问题,我们可以自定义异常返回信息,帮助请求方区分不同的限流异常。
Sentinel 提供了一个 UrlBlockHandler 接口,只需要实现该接口就可以轻松定义异常返回。其中 BlockException 就是所有限流异常的根接口。
import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlBlockHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.fastjson.JSON;/*** 自定义限流返回信息*/
@Component
public class SentinelFlowLimitConfig implements UrlBlockHandler {@Overridepublic void blocked(HttpServletRequest req, HttpServletResponse resp, BlockException e) throws IOException {// 避免中文乱码问题resp.setContentType("application/json;charset=utf-8");DataResp dataResp = null;if (e instanceof FlowException) {dataResp = new DataResp(-1, "限流异常!");}resp.getWriter().write(JSON.toJSONString(dataResp));}
}@Data
@NoArgsConstructor
@AllArgsConstructor
class DataResp {private Integer code;private String msg;
}
除了 FlowException 之外,还有其他几种规则的限流异常,可以具体情况具体处理:
八、@SentinelResource
定义流控资源的注解,一般标记在方法上,常见于 controller 接口上。
除了可以指定资源名称外,@SentinelResource 还可以指定 blockHandler 和 fallback 两个属性,
@GetMapping("/message1")@SentinelResource(value = "params-api",blockHandler = "message1Block",fallback = "message1Fallback")public String message1(String name, String age) {return "message1";}
blockHandler 指定一个方法名,用于处理当流控规则触发时对应异常需要处理的逻辑;
fallback 指定一个方法名,用于处理资源发生任何异常时需要处理的逻辑,相当于针对该接口特定的异常捕获逻辑。
如果 blockHandler 已经拦截了流控规则产生的异常,就不会再执行 fallback 定义的异常逻辑。有点类似:
try {// 资源逻辑
} catch(BlockException e) {// blockHandler 需要处理的逻辑
} catch(Throwable e) {// fallback 需要处理的逻辑
}
blockHandler 指定的方法要求必须和资源方法具有相同的返回值类型,以及参数列表,但允许参数列表最后一个参数加入一个 BlockException:
public String message1Block(String name, String age, BlockException e) {// 流控触发后需要执行的逻辑
}
同样,fallback 指定的方法也要求必须和资源方法具有相同的返回值类型,以及参数列表,但允许参数列表最后一个参数加入一个 Throwable :
public String message1Fallback(String name, String age, Throwable e) {// 更通用的异常处理逻辑
}
另外,还可以定义具体的处理类来专门针对对应资源维护具体的接口降级逻辑,具体参数是 blockHandlerClass、fallbackClass。
九、Sentinel 规则持久化(待补充)
官方文档:动态规则扩展
Sentinel 采用 c/s 架构,Dashboard 为 s,微服务节点为 c。
Sentinel Dashboard 可以为每个 Sentinel 微服务客户端设置各种限流监控规则,但这些规则默认是存储在客户端内存中,当客户端应用重启后,配置就会消失,极不稳定,所以,将规则持久化的需求是必要的。
本地文件数据源会定时轮询文件的变更,读取规则。这样既可以本地直接修改文件来更新规则,也可以通过 Dashboard 来推送规则:
十、Feign 整合 Sentinel 实现容错
- 引入相关依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 开启 feign 对 sentinel 支持
启动类添加 @EnableFeignClients 注解,yaml 配置添加如下配置:
feign:sentinel:enabled: true
- 编写 FeignClient 的降级处理类
@Slf4j
@Service
public class ProductFeignFallback implements ProductFeignClient {@Overridepublic Product findByPid(Integer pid) {log.error("查询商品信息失败...");Product product = new Product();product.setPid(-100);return product;}
}
- 指定 FeignClient 的降级处理类
@FeignClient(value = "service-product",fallback = ProductFeignFallback.class
)
public interface ProductFeignClient {@GetMapping("/product/{pid}")Product findByPid(@PathVariable("pid") Integer pid);
}
- 降级逻辑处理
当 findByPid 方法返回 -100时,就代表走了降级实现,我们可以返回下单失败消息:
@GetMapping("/prod/{pid}")public Order order(@PathVariable("pid") Integer pid) {log.info("接收到{}号商品的下单请求,准备调用商品微服务", pid);// 调用商品微服务,查询商品信息Product product = productService.findByPid(pid);// 下单(即创建订单并保存)Order order = new Order();if (product.getPid() == -100) {order.setUsername("下单失败!");return order;}// ...其他的费降级逻辑...}
测试:正常调用 order --> product 微服务调用成功,可以返回皮大衣以及订单id:
当关闭 product 微服务之后,就出现我们的降级响应:
这样就实现了降级逻辑的处理,和前面 @SentinelResource 有类似的效果。
不过,这种 fallback 还是不理想。我们无法取得 Feign 调用失败的真正异常,最好采用 fallbackFactory 来实现:
@Slf4j
@Service
public class ProductFallbackFactory implements FallbackFactory<ProductFeignClient> {@Overridepublic ProductFeignClient create(Throwable throwable) {return new ProductFeignClient() {@Overridepublic Product findByPid(Integer pid) {log.error("查询商品信息失败...");log.error("{}", throwable);Product product = new Product();product.setPid(-100);return product;}};}
}
当然,FeignClient 接口不要既指定了 fallback 又指定了 fallbackFactory,留一个即可:
@FeignClient(value = "service-product",
// fallback = ProductFeignFallback.class,fallbackFactory = ProductFallbackFactory.class
)
public interface ProductFeignClient {@GetMapping("/product/{pid}")Product findByPid(@PathVariable("pid") Integer pid);
}