Sentinel
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来保障微服务的稳定性。
1 Sentinel 基本概念
资源 (Resource):
- 资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码;
- 只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,服务名称作为资源名来标示资源。
规则 (Rule):围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
2 Sentinel核心功能
2.1 流量控制
任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求进行整型,如下图所示
流量控制主要有如下三个角度:
- 限流指标,例如QPS,并发线程数等指标
- 限流策略,例如直接限流(根据自己的指标来限流,关联限流(根据有关联关系的其他资源来限流),调用链路限流(根据指定路调用链路涌入的的流量来限流)
- 限流效果,触发限流以后的请求该如何处理, 例如快速失败(fast fail),慢启动(warm up), 排队等待
2.2 熔断降级
除了流量控制以外,对调用链路中的不稳定资源进行处理, 保护整个调用链路, 也是 Sentinel 的作用之一。
2.2.1 熔断
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。
在服务内调用其它服务时, 比如基于 RestTemplate 进行调用时, 是同步式的调用, 发起调用的服务在被调用的服务返回前, 不能去做别的事情;
如果下游服务出现故障, 迟迟无法返回, 那么上游服务就被阻塞在调用下游服务的位置
随着新的请求不断到来, 将有越来越多的上游服务线程被阻塞在调用故障服务的位置, 进而使上游服务没有线程可用, 最终使上游服务也发生故障,
以此类推, 导致整个调用链路中的服务都不可用, 造成雪崩;
预防雪崩问题有以下几个方法:
-
超时时间: 设定超时时间, 请求超过一定时间没有响应, 就立即返回一个错误状态, 不会无休止等待;
缺点是仍然会有请求到达故障, 并且数量没有限制, 服务器的线程被阻塞直到超时时间才被释放;
-
线程隔离: 遵循仓壁模式, 对当前服务的不同API, 设置其所能占用的并发线程数量的上限, 这样, 即使当前 API 内调用了某个故障的下游服务, 也不会耗尽当前服务的所有线程资源;
sentinel中, 配置限流规则时, 设置最大线程数, 就有线程隔离的作用;
缺点是仍然有一定数量的请求可以到达故障服务;
-
熔断机制: 根据响应时间, 错误响应比例等依据, 对故障业务进行熔断, 拦截访问该业务的请求; 对这些请求, 也不能置之不理, 比如我可以对这些请求进行降级处理;
2.2.2 降级
服务调用无法正常完成时, 例如被限流, 超时, 或者出现异常, 或者服务被熔断等后, 最好是我不能把请求扔在那不管, 对到来的请求, 可以执行一个替代的简化的处理逻辑, 这就是降级;
2.3 熔断和降级辨析
目的上来说, 熔断和降级, 都是为了提高系统的可用性, 都是为了防止系统崩溃;
熔断的侧重点是防止服务雪崩, 阻止对故障服务的调用, 防止因为调用故障服务导致当前服务资源耗尽,保护上游服务的稳定性。
降级侧重点是在服务出现问题时或者 QPS 过大时取消问题服务或者边缘服务的完整业务, 转而提供基本的服务。比如服务被熔断时, 就可以使用降级; 比如电商平台大促期间, 就可以对边缘业务进行降级处理;
熔断的时候, 可以配合降级机制, 阻止对故障服务调用的同时, 提供保底的基本的服务; 但降级一般不会触发熔断机制;
3 Sentinel的使用
Sentinel 可以分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。
使用 Dashboard 只需要下载 jar 包Releases · alibaba/Sentinel (github.com), 并通过命令行启动; 网页访问时, 默认用户名和密码都是 sentinel
bash
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.3.jar
使用核心库, 需要导包
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
在服务中进行配置
yaml
spring:
cloud:
sentinel:
transport:
# 指定操作面板的地址
dashboard: localhost:8080
3.1 定义资源
定义资源的方式有多种,其中比较常用是注解方式定义,框架自适配定义。
-
框架自适配
为了减少开发的复杂程度,Sentinel 对大部分的主流框架,例如 Web Servlet、Dubbo、Spring Cloud等都做了适配
例如使用SpringMVC时, 一个请求的应用内路径就会被自动识别为资源;
-
注解方式定义资源
Sentinel 支持通过
@SentinelResource
注解定义资源并配置blockHandler
和fallback
函数来进行降级处理。示例:java// 使用注解声明资源时, 一定要指定blockHandler, 并blockHandler一定要是public的 // 否则触发限流时, 直接服务器内部错误500; 而自动识别的资源, 会自动输出 // Blocked by Sentinel (flow limiting) @SentinelResource(blockHandler = "blockHandlerForGetUser") public User getUserById(String id) { //...... } // blockHandler 函数,原方法调用被限流/降级/系统保护的时候调用 public User blockHandlerForGetUser(String id, BlockException ex) { return new User("admin"); }
3.2 定义规则
Sentinel 的所有规则都可以在内存中动态地查询及修改,修改之后立即生效; 规则可以分为流控规则, 熔断规则, 热点规则, 授权规则等;
推荐使用 Dashboard 去定义规则;
4 流控规则
顾名思义, 用于流量控制的规则; 同一个资源可以对应多条限流规则。Sentinel 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。
阈值类型
主要有两种统计类型,一种是统计线程数,另外一种则是统计 QPS; 其中线程数、QPS 值,都是 Sentinel 实时统计获取的。
- 线程数限流用于保护服务器线程数不被耗尽, 可以防止雪崩。
- 基于QPS的限流,直接限制同时处理请求的数量。当 QPS 超过某个阈值的时候,则采取措施进行流量控制;
流控模式
直接限流:
即最直接的方式,根据被访问资源本身的流量,决定是否要限流;
关联流量限流
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。
比如不同的 Sentinel Resource 对数据库同一个字段进行写操作,如果放任这两个 Sentinel Resource 争抢数据库资源,则争抢本身带来的开销会降低整体的吞吐量。
可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说
read_db
和write_db
这两个资源分别代表数据库读写;- 我们可以给
read_db
设置限流规则来达到写优先的目的:设置限流策略为关联流量限流, 同时设置关联资源为write_db
。 - 这样当写库操作过于频繁时,读数据的请求会被限流。
链路限流:
在高版本 Sentinel中, 已经不支持链路限流;
sentinel中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root
的虚拟节点,调用链的入口都是这个虚节点的子节点。
machine-root
/ \
/ \
Entrance1 Entrance2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
上图中来自入口 Entrance1
和 Entrance2
的请求都调用到了资源 NodeA
,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 限流策略为 链路限流,同时设置 访问入口为 Entrance1
来表示只有从入口 Entrance1
的调用才会记录到 NodeA
的限流统计当中,而对来自 Entrance2
的调用漠不关心。
流控效果
只有在阈值类型
为QPS的时候, 才能设置流控效果;
快速失败
该方式是默认的流量控制方式,当QPS超过阈值后,新的请求就会被立即拒绝,拒绝方式为直接抛出 FlowException
, FlowException
是 BlockException
的子类;
慢启动
系统启动时, 不能上来就把流量拉到系统稳定运行时能承受的最大QPS, 多种原因:
-
这时候各级缓存还没有产生, 所以启动时的平均响应时间要大于稳定后的平均响应时间, 所以启动的时候能承受的 QPS 要更小;
-
刚启动时, 所有请求到来需要先建立TCP连接, 比较耗时, 稳定运行后只有部分请求 ( 新的客户端发来的请求 ) 需要建立连接;
通过慢启动,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。
如果将流控效果设置为慢启动, 则实际进行流控时, 实际的阈值是逐渐提高的, 一开始是设定值的 1/3, 最后才变为设定值;
流量达到阈值后新的请求还是快速失败的逻辑处理;
排队等待方式
根据设置的QPS计算出两个请求处理的最小间隔时间, 例如 QPS = 10, 则 间隔时间 = 100ms;
当请求到达时,如果当前请求速率未超过设置QPS,请求立即通过。
如果当前请求速率超过阈值,请求将进入等待队列,每经过间隔时间处理队列中的一个请求。
当队列非空时, 新到来的请求肯定超过了流量阈值(因为就是按阈值的速度在处理队列中的请求), 进入队列;
如果请求入队前, 根据队列大小计算预期等待时间, 如果超过设定的最大等待时间,请求将直接被拒绝。
热点规则
热点参数限流会统计资源方法传入的参数,并根据配置进行限流,热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
可以对某个位置上的参数进行控制
对 id 参数进行控制, 不论 id 的值是多少, 只要是携带 id 的请求, 就会参与限流;
java
@GetMapping("testSen")
@SentinelResource(value = "testSen", blockHandler = "testSenBlockHandler")
String testSen(@RequestParam(value = "id", required = false) Integer id){
return "test Sen SUCCESS";
}
对固定位置上的参数的某个固定值进行限流, 也叫参数例外项
例如 一秒内, 最多有 2 个 id 值为 2022001 的请求通过;
5 熔断规则
熔断时, 新请求来到直接抛 DegradeException
限流时, 抛FlowException
, 二者都继承自BlockException
熔断策略
Sentinel中, 有三种熔断触发策略,分别是慢调用比例 (SLOW_REQUEST_RATIO
),异常比例 (ERROR_RATIO
),异常数 (ERROR_COUNT
)触发;
慢调用比例 (SLOW_REQUEST_RATIO
):
-
选择以慢调用比例作为阈值,需要设置最大 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。需要设置统计时长, 比例阈值, 最小请求数, 熔断时长;
-
当统计时长内请求数目大于设置的最小请求数,并且慢调用的比例大于最大 RT,则接下来的熔断时长内请求会自动被熔断。
-
经过熔断时长后熔断器会进入半开状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
异常比例 (ERROR_RATIO
):
-
当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
-
经过熔断时长后熔断器会进入半开状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
-
异常比率的阈值范围是 0 ~ 1, 代表百分比。
异常数 (ERROR_COUNT
):
-
当统计时长内总请求数目大于最小请求数, 并且异常请求的数目超过阈值之后, 会自动进行熔断。
-
经过熔断时长后熔断器会进入半开状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
熔断效果
熔断时, 如果指定了降级逻辑 fallback, 就会执行降级的逻辑;
如果没有指定降级逻辑, 请求来到时就直接抛出降级异常, 将被 BlockHandler 处理, 如果有自定义的, 就用自定义的, 没有就用公用的 BlockHandler, 显示Blocked by Sentinel (flow limiting)
java
public class TestService {
// 对应的 `handleException` 函数需要位于 `ExceptionUtil` 类中,
// 并且必须为 public static 函数.
@SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
public void test() {
System.out.println("Test");
}
// fallback方法位于同类中, 必须为public
@SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
public String hello(long s) {
return String.format("Hello at %d", s);
}
// Fallback 函数,必须为public, 函数签名与原函数一致或加一个 Throwable 类型的参数.
public String helloFallback(long s) {
return String.format("Halooooo %d", s);
}
// Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
public String exceptionHandler(long s, BlockException ex) {
// Do some log here.
ex.printStackTrace();
return "Oops, error occurred at " + s;
}
}
value
资源名称,必需项(不能为空)
blockHandler / blockHandlerClass:
blockHandler
指定的函数, 将处理当前资源的 BlockException;
blockHandler
函数访问范围需要是 public
,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException
。
blockHandler
函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass
为对应的类的 Class
对象,注意这时, 对应的函数必需为 static
函数,否则无法解析。
blockHandler 和 fallback 本质上, 都是一种降级逻辑, 早期 fallback 函数只针对降级异常(DegradeException
)进行处理, 而这个异常只在发生熔断的时候抛出; 相当于 fallback 专门针对熔断提供降级服务;
后来, fallback 支持所有类型的异常, 这时, 它和 BlockHandler 的区别在于, BlockHanlder 只对限流, 熔断提供降级服务, 而 fallback 还可以对业务异常提供降级服务
fallback / fallbackClass:
fallback 函数名称,可选项,用于提供降级处理逻辑。
在被熔断, 或者被限流, 或者出现业务异常的时候, 都会交由 fallback 提供降级服务;
fallback 函数可以针对所有类型的异常, 包括BlockException
进行处理。
fallback 函数签名和位置要求:
-
返回值类型必须与原函数返回值类型一致;
-
方法参数列表需要和原函数一致,或者可以额外多一个
Throwable
类型的参数用于接收对应的异常。 -
fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定
fallbackClass
为对应的类的Class
对象,注意对应的函数必需为static
函数,否则无法解析。 -
exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
注:1.6.0 之前的版本 fallback 函数只针对降级异常(
DegradeException
)进行处理;
如果只配置了 fallback, 那么限流, 熔断, 抛出业务异常时, 均进入 fallback 进行处理;
如果 blockHandler 和 fallback同时配置, 发生熔断和限流的时候, 都会由 fallback 处理;
如果两个都没配置, 发生限流和熔断时, 由 Sentinel 提供的默认BlockHandler 处理; 显示Blocked by Sentinel (flow limiting)
, 熔断的时候也显示 flow limiting;
与OpenFeign整合
Sentinel 适配了 Feign 组件。如果想使用,需要引入 spring-cloud-starter-alibaba-sentinel
和 spring-cloud-starter-openfeign
的依赖;
并且在使用 FeignClient 的服务的配置文件中设置 feign.sentinel.enabled = true
与Feign整合后, 可以指定降级类或者降级类工厂, BlockException 和 其它业务异常, 都会在降级类中处理;
方式一:
编写 OpenFeignClient 接口, 提供一个实现类, 实现类中重写方法, 该方法实现降级后的策略, 向容器注册该实现类;
在 @FeignClient 注解中, 指定 降级实现类;
java
@FeignClient(name = "order-service", fallback = OrderServiceFallback.class)
public interface OrderFeignClient {
@GetMapping("order/getOrder")
String getOrder(@RequestParam("order_id") Integer id);
}
@Configuration
class FeignConfiguration {
@Bean
public OrderServiceFallback orderServiceFallback() {
return new OrderServiceFallback();
}
}
class OrderServiceFallback implements OrderFeignClient {
@Override
public String getOrder(@RequestParam("order_id") Integer id) {
return "order-service fallback";
}
}
需要注意的是:
FeignClient 对应的接口的资源名为:method:protocol://requesturl
,例如GET:http://account-service/account/test
方式二
java
@FeignClient(value = "stock-service", fallbackFactory = StockFallbackFactory.class)
public interface StockClient {
@GetMapping("/stock/test")
public Stock test(){
//......
}
}
public class StockFallbackFactory implements FallbackFactory<StockClient> {
@Override
public StockClient create(Throwable throwable) {
return new StockClient() {
@Override
public Stock test() {
// 可以获取异常信息, 可以处理异常; 降级时抛出的异常类型是DegradeException
throwable.printStackTrace();
return new Stock();
}
}
};
}
@Configuration
public FallbackConfig{
@Bean
public StockFallbackFactory stockClientFallbackFactory(){
return new StockFallbackFactory();
}
}
6 Gateway规则
本质是做成了 Gateway 应用的热点规则;
配置Gateway
Sentinel 从 1.6.0 版本开始,提供了 Spring Cloud Gateway 的适配模块,可提供两种资源维度的限流:
- route 维度:在 Spring 配置路由条目时,资源名为 routeId
- 自定义 API 维度,用户可以用 Sentinel 提供的 API 来定义一些 API 分组
网关应用引入依赖
xml
<!--加入nacos的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!--添加Sentinel的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<!-- sentinel整合gateway-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!-- sentinel-nacos持久化 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
编写bootstrap.yaml
yaml
server:
port: 7770
spring:
application:
name: gateway-server
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yaml
其余配置
yml
spring:
main:
allow-bean-definition-overriding: true
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: duolai-order
uri: lb://duolai-order
predicates:
- Path=/shopping/order/**,/shopping/cancelOrder
- id: duolai-user
uri: lb://duolai-user
predicates:
- Path=/user/**, /shopping/addresses
# sentinel配置
sentinel:
transport:
dashboard: 127.0.0.1:8080
port: 9719
clientIp: 127.0.0.1
datasource:
# 从nacos获取路由条目对应的流控规则
gateway-flow-rules:
nacos:
server-addr: 127.0.0.1:8848
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GATEWAY_GROUP
data-type: json
rule-type: gw-flow
# 从nacos获取api分组流控规则
gateway-api-rules:
nacos:
server-addr: 127.0.0.1:8848
dataId: ${spring.application.name}-api-groups
groupId: SENTINEL_GATEWAY_GROUP
data-type: json
rule-type: gw-api-group
然后就是在nacos对应dataId的配置文件中,添加流控规则即可,具体流控规则,可以在dashboard中添加并获取
获取网关流控规则
http
GET http://ip:端口/gateway/getRules
获取api分组
http
GET http://ip:端口/gateway/getApiDefinitions
Sentinel配置
路由ID模式
API分组模式, 对匹配到同一个API分组的请求, 进行流量控制;
7 自定义BlockExceptionHandler
自定义BlockExceptinHandler, 只对SpringMVC默认的路径资源有效; 对 @SentinelResource 注解定义的资源无效;
抛出的 ExceptionHandler 类型异常将由自定义的BlockExceptionHandler处理;
java
@Component
public class MyBlockHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.getWriter().println(e.toString());
}
}
8 规则持久化
Sentinel还做了和Nacos的适配,我们只需要把我们的规则配置存储在Nacos中,就可以实现Sentinel规则的持久化保存;
之后 Sentinel 会自动从 Nacos 读取规则配置,并且当 Nacos 中配置内容发生变更的时候,Sentinel 也会实时感知到规则的变化,从而让规则生效。
但是 Sentinel 中修改添加新的规则, 不会自动推送到 Nacos, 如果想实现, 需要修改 Sentinel dashboard 的源码;
为了实现Sentinel和Nacos的整合,我们首先需要在项目中导入依赖
xml
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
并且在配置文件中添加如下依赖
yaml
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080
port: 8719
datasource:
# ds1 是第一个数据源的名字,我们可以定义多个数据源
ds1:
nacos:
# nacos地址
server-addr: localhost:8848
# 配置的 dataId
dataId: sentinel-rules
# 配置的 group
groupId: SENTINEL_GROUP
# 配置的 namespace
namespace: 41a9d584-4dbd-480b-aadd-d53c1700ecb4
# 配置的数据类型
data-type: json
# 规则类型
rule-type: flow #flow、degrade、param-flow、gw-flow
然后,我们在nacos中去配置好,对应的规则即可。
那么如何通过json字符串去定义规则呢?我们可以在 Dashboard 中配置规则, 然后通过给定的API直接获取规则的 JSON 格式;
http
GET http://ip:port/getRules?type=xxx
- ip是你进行限流的那个服务所在的ip地址
- port是你的服务与dashboard通信的端口号
- type指查询的规则类型,这里可以取flow,degrade等,分别代表流控规则和降级规则。
其实请求地址 Sentinel 已经自动生成