Alibaba Sentinel 是一款高性能且轻量级的流量控制、熔断降级解决方案。是面向分布式服务架构的高可用流量控制组件。
Sentinel 官网:https://sentinelguard.io/zh-cn/
Github:https://github.com/alibaba/Sentinel
Sentinel 是什么
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来保障微服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 主要特征
Sentinel 开源生态
Sentinel 目前已经针对 Servlet、Dubbo、Spring Boot/Spring Cloud、gRPC 等进行了适配,用户只需引入相应依赖并进行简单配置即可非常方便地享受 Sentinel 的高可用流量防护能力。Sentinel 还为 Service Mesh 提供了集群流量防护的能力。未来 Sentinel 还会对更多常用框架进行适配。
Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
Sentinel 的历史
- 2012 年,Sentinel 诞生,主要功能为入口流量控制。
- 2013-2017 年,Sentinel 在阿里巴巴集团内部迅速发展,成为基础技术模块,覆盖了所有的核心场景。Sentinel 也因此积累了大量的流量归整场景以及生产实践。
- 2018 年,Sentinel 开源,并持续演进。
- 2019 年,Sentinel 朝着多语言扩展的方向不断探索,推出 C++ 原生版本,同时针对 Service Mesh 场景也推出了 Envoy 集群流量控制支持,以解决 Service Mesh 架构下多语言限流的问题。
- 2020 年,推出 Sentinel Go 版本,继续朝着云原生方向演进。
Sentinel 核心
Sentinel 的使用可以分为两个部分:
- 核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 7 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持(见 主流框架适配)。
- 控制台(Dashboard):控制台主要负责管理推送规则、监控、集群限流分配管理、机器发现等。
Sentinel 控制台
Sentinel 提供一个轻量级的开源控制台,它提供机器发现以及健康情况管理、监控(单机和集群),规则管理和推送的功能。
官网文档:https://github.com/alibaba/Sentinel/wiki/控制台
获取控制台
您可以从 release 页面 下载最新版本的控制台 jar 包。
您也可以从最新版本的源码自行构建 Sentinel 控制台:
- 下载 控制台 工程
- 使用以下命令将代码打包成一个 fat jar:
mvn clean package
启动控制台
启动命令如下,本文使用的是目前最新 1.7.2 版本:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.2.jar
注意:启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。
其中 -Dserver.port=8080
用于指定 Sentinel 控制台端口为 8080
。
从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel
。可以参考 鉴权模块文档 配置用户名和密码。
注:若您的应用为 Spring Boot 或 Spring Cloud 应用,您可以通过 Spring 配置文件来指定配置,详情请参考 Spring Cloud Alibaba Sentinel 文档。
为了方便启动,可以编写一个启动脚本 run.bat
:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.2.jar
pause
访问
访问:http://localhost:8080/
输入默认用户名和密码 sentinel
点击登录。至此控制台就安装完成了。
环境准备
sentinel-demo
聚合工程。SpringBoot 2.3.0.RELEASE
、Spring Cloud Hoxton.SR4
。
Nacos 注册中心
-
product-service
:商品服务,提供了/product/{id}
接口 -
order-service-rest
:订单服务,基于Ribbon
通过RestTemplate
调用商品服务 -
order-server-feign
:订单服务,基于Feign
通过声明式服务调用商品服务
客户端接入控制台
控制台启动后,客户端需要按照以下步骤接入到控制台:
- 添加依赖
- 定义资源
- 定义规则
先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。
由于我们的项目是 Spring Cloud 项目,所以可以借助官方文档来进行学习。
Spring 官网文档:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
Github 文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
添加依赖
父工程需要添加如下依赖:
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.1.0.RELEASE
pom
import
子工程需要添加如下依赖:
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
配置文件
客户端需要启动 Transport 模块来与 Sentinel 控制台进行通信。
order-service-rest
的 application.yml
spring:
cloud:
# 配置 Sentinel
sentinel:
transport:
port: 8719
dashboard: localhost:8080
这里的 spring.cloud.sentinel.transport.port
端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。
初始化客户端
确保客户端有访问量,Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包。
简单的理解就是:访问一次客户端,Sentinel 即可完成客户端初始化操作,并持续向控制台发送心跳包。
访问
多次访问:http://localhost:9090/order/1 然后查看控制台实时监控
结果如下:
定义资源
资源 是 Sentinel 中的核心概念之一。我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。最常用的资源是我们代码中的 Java 方法。Sentinel 提供了 @SentinelResource
注解用于定义资源,并提供了 AspectJ 的扩展用于自动定义资源、处理 BlockException
等。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
官网文档:https://github.com/alibaba/Sentinel/wiki/如何使用#定义资源
注解支持
官网文档:https://github.com/alibaba/Sentinel/wiki/注解支持
OrderServiceImpl.java
package com.example.service.impl;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.pojo.Order;
import com.example.service.OrderService;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private ProductService productService;
/**
* 根据主键和订单编号查询订单
*
* @param id
* @param orderNo
* @return
*/
@Override
@SentinelResource(value = "selectOrderByIdAndOrderNo",
blockHandler = "selectOrderByIdAndOrderNoBlockHandler",
fallback = "selectOrderByIdAndOrderNoFallback")
public Order selectOrderByIdAndOrderNo(Integer id, String orderNo) {
return new Order(id, orderNo, "中国", 2666D,
Arrays.asList(productService.selectProductById(1)));
}
// 服务流量控制处理,参数最后多一个 BlockException,其余与原函数一致。
public Order selectOrderByIdAndOrderNoBlockHandler(Integer id, String orderNo,
BlockException ex) {
// Do some log here.
ex.printStackTrace();
return new Order(id, "服务流量控制处理-托底数据", "中国", 2666D,
Arrays.asList(productService.selectProductById(1)));
}
// 服务熔断降级处理,函数签名与原函数一致或加一个 Throwable 类型的参数
public Order selectOrderByIdAndOrderNoFallback(Integer id, String orderNo,
Throwable throwable) {
System.out.println("order-service 服务的 selectOrderById 方法出现异常,异常信息如下:"
+ throwable);
return new Order(id, "服务熔断降级处理-托底数据", "中国", 2666D,
Arrays.asList(productService.selectProductById(1)));
}
}
ProductServiceImpl.java
package com.example.service.impl;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* 商品管理
*/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private RestTemplate restTemplate;
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@SentinelResource(value = "selectProductById",
blockHandler = "selectProductByIdBlockHandler", fallback = "selectProductByIdFallback")
@Override
public Product selectProductById(Integer id) {
return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
}
// 服务流量控制处理,参数最后多一个 BlockException,其余与原函数一致。
public Product selectProductByIdBlockHandler(Integer id, BlockException ex) {
// Do some log here.
ex.printStackTrace();
return new Product(id, "服务流量控制处理-托底数据", 1, 2666D);
}
// 服务熔断降级处理,函数签名与原函数一致或加一个 Throwable 类型的参数
public Product selectProductByIdFallback(Integer id, Throwable throwable) {
System.out.println("product-service 服务的 selectProductById 方法出现异常,异常信息如下:"
+ throwable);
return new Product(id, "服务熔断降级处理-托底数据", 1, 2666D);
}
}
注意:注解方式埋点不支持 private 方法。
@SentinelResource
用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource
注解包含以下属性:
-
value
:资源名称,必需项(不能为空) -
entryType
:entry 类型,可选项(默认为EntryType.OUT
) -
blockHandler
/blockHandlerClass
:blockHandler
对应处理BlockException
的函数名称,可选项。blockHandler 函数访问范围需要是public
,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为BlockException
。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定blockHandlerClass
为对应的类的Class
对象,注意对应的函数必需为 static 函数,否则无法解析。 -
fallback
:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore
里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个
Throwable
类型的参数用于接收对应的异常。 - fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定
fallbackClass
为对应的类的Class
对象,注意对应的函数必需为 static 函数,否则无法解析。
-
defaultFallback
(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore
里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个
Throwable
类型的参数用于接收对应的异常。 - defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定
fallbackClass
为对应的类的Class
对象,注意对应的函数必需为 static 函数,否则无法解析。
-
exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
注:1.6.0 之前的版本 fallback 函数只针对降级异常(
DegradeException
)进行处理,不能针对业务异常进行处理。
特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑。若未配置 blockHandler
、fallback
和 defaultFallback
,则被限流降级时会将 BlockException
直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException
)。
从 1.4.0 版本开始,注解方式定义资源支持自动统计业务异常,无需手动调用 Tracer.trace(ex)
来记录业务异常。Sentinel 1.4.0 以前的版本需要自行调用 Tracer.trace(ex)
来记录业务异常。
定义规则
Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel 也提供相关 API,供您来定制自己的规则策略。
Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、热点参数规则、系统保护规则和来源访问控制规则。
官网文档:https://github.com/alibaba/Sentinel/wiki/如何使用#规则的种类
流量控制规则
添加流量控制规则
选择 簇点链路
找到定义好的资源 selectProductById
并点击对应的规则按钮进行设置。
比如我们设置一个流量控制规则,定义资源访问的 QPS 为 1(每秒能处理查询数目)。
测试
快速刷新页面多次访问:http://localhost:9090/order/idAndOrderNo?id=1&orderNo=order-001 结果如下:
熔断降级规则
模拟服务出错
修改 order-service-rest
项目中的核心代码,模拟服务出错。
package com.example.service.impl;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* 商品管理
*/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private RestTemplate restTemplate;
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@SentinelResource(value = "selectProductById",
blockHandler = "selectProductByIdBlockHandler", fallback = "selectProductByIdFallback")
@Override
public Product selectProductById(Integer id, String productName) {
// 模拟查询主键为 1 的商品信息会导致异常
if (1 == id)
throw new RuntimeException("查询主键为 1 的商品信息导致异常");
return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
}
// 服务流量控制处理,参数最后多一个 BlockException,其余与原函数一致。
public Product selectProductByIdBlockHandler(Integer id, BlockException ex) {
// Do some log here.
ex.printStackTrace();
return new Product(id, "服务流量控制处理-托底数据", 1, 2666D);
}
// 服务熔断降级处理,函数签名与原函数一致或加一个 Throwable 类型的参数
public Product selectProductByIdFallback(Integer id, Throwable throwable) {
System.out.println("product-service 服务的 selectProductById 方法出现异常,异常信息如下:"
+ throwable);
return new Product(id, "服务熔断降级处理-托底数据", 1, 2666D);
}
}
添加熔断降级规则
熔断降级规则支持相应时间、异常比例、异常数三种方式。
测试
访问:http://localhost:9090/order/idAndOrderNo?id=1&orderNo=order-001 结果如下:
热点参数规则
热点参数规则是一种更细粒度的流控规则,它允许将规则具体到参数上。比如 selectOrderByIdAndOrderNo
方法有两个参数,我们对第一个参数进行限流,对第二个参数不限流。
添加热点参数规则
选择 簇点链路
找到定义好的资源 selectOrderByIdAndOrderNo
并点击对应的规则按钮进行设置。
设置热点参数规则,定义对资源的第一个参数的 QPS 为 1(每秒能处理查询数目)。
测试
分别用两个参数访问,会发现只对第一个参数限流了。
快速刷新页面多次访问:http://localhost:9090/order/idAndOrderNo?id=1 被限流。
快速刷新页面多次访问:http://localhost:9090/order/idAndOrderNo?orderNo=order-001 正常访问。
授权规则
很多时候,我们需要根据调用来源来判断该次请求是否被允许,这时候可以使用 Sentinel 的来源访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过。
Sentinel 提供了 RequestOriginParser
接口来处理来源。一旦 Sentinel 保护的接口资源被访问,Sentinel 就会调用 RequestOriginParser
的实现类去解析访问来源。
自定义来源处理规则
package com.example.sentinel;
import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义来源处理规则
*/
@Component
public class MyRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
return request.getParameter("userName");
}
}
新增授权规则
下图配置的意思是资源 selectOrderByIdAndOrderNo
只有 userName=zhangsan
的用户无法访问(黑名单)
测试
快速刷新页面多次访问:http://localhost:9090/order/idAndOrderNo?id=1&userName=zhangsan 被限流。
快速刷新页面多次访问:http://localhost:9090/order/idAndOrderNo?id=1&userName=lisi 正常访问。
系统保护规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 LOAD、RT、线程数、入口 QPS 和 CPU 使用率五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量(进入应用的流量)生效。
- Load(仅对 Linux/Unix-like 机器生效):当系统 load 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5。
- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
- CPU使用率:当单台机器上所有入口流量的 CPU 使用率达到阈值即触发系统保护。
动态规则扩展
官网文档:
- https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel#动态数据源支持
- https://github.com/alibaba/Sentinel/wiki/动态规则扩展#示例
SentinelProperties
内部提供了 TreeMap
类型的 datasource
属性用于配置数据源信息。支持:
- 文件配置规则
- Nacos 配置规则
- ZooKeeper 配置规则
- Apollo 配置规则
- Redis 配置规则
文件配置规则
Sentinel 支持通过本地文件加载规则配置,使用方式如下(限流规则作为演示):
spring:
cloud:
# 配置 Sentinel
sentinel:
datasource:
ds1:
file:
file: classpath:flowRule.json
data-type: json
rule-type: flow
flowRule.json 对应 com.alibaba.csp.sentinel.slots.block.RuleConstant
各属性。
[
{
"resource": "selectProductList",
"count": 1,
"grade": 1,
"limitApp": "default",
"strategy": 0,
"controlBehavior": 0
}
]
重要属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,资源名是限流规则的作用对象 | |
count | 限流阈值 | |
grade | 限流阈值类型,QPS 模式(1)或并发线程数模式(0) | QPS 模式 |
limitApp | 流控针对的调用来源 | default ,代表不区分调用来源 |
strategy | 调用关系限流策略:直接、链路、关联 | 根据资源本身(直接) |
controlBehavior | 流控效果(直接拒绝 / 排队等待 / 慢启动模式),不支持按调用关系限流 | 直接拒绝 |
clusterMode | 是否集群限流 | 否 |
访问客户端以后,刷新控制台,查看流控规则如下:
RestTemplate 支持
Spring Cloud Alibaba Sentinel 支持对 RestTemplate 调用的服务进行服务保护。需要在构造 RestTemplate Bean 时添加 @SentinelRestTemplate
注解。
启动类
OrderServiceRestApplication.java
package com.example;
import com.alibaba.cloud.sentinel.annotation.SentinelRestTemplate;
import com.example.exception.ExceptionUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class OrderServiceRestApplication {
@Bean
@LoadBalanced
@SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class,
fallback = "fallback", fallbackClass = ExceptionUtil.class)
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(OrderServiceRestApplication.class, args);
}
}
服务熔断处理类
ExceptionUtil.java 必须使用静态方法。
package com.example.exception;
import com.alibaba.cloud.sentinel.rest.SentinelClientHttpResponse;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.example.pojo.Product;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpResponse;
public class ExceptionUtil {
// 服务流量控制处理
public static ClientHttpResponse handleException(HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution,
BlockException exception) {
exception.printStackTrace();
return new SentinelClientHttpResponse(
JSON.toJSONString(new Product(1, "服务流量控制处理-托底数据", 1, 2666D)));
}
// 服务熔断降级处理
public static ClientHttpResponse fallback(HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution,
BlockException exception) {
exception.printStackTrace();
return new SentinelClientHttpResponse(
JSON.toJSONString(new Product(1, "服务熔断降级处理-托底数据", 1, 2666D)));
}
}
访问
控制台设置流量控制规则,定义资源访问的 QPS 为 1(每秒能处理查询数目)。
快速刷新页面多次访问:http://localhost:9090/order/1 结果如下:
OpenFeign 支持
添加依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
org.springframework.cloud
spring-cloud-starter-openfeign
开启 Sentinel
server:
port: 9091 # 端口
spring:
application:
name: order-service-feign # 应用名称
cloud:
# 配置 Nacos 注册中心
nacos:
discovery:
enabled: true # 如果不想使用 Nacos 进行服务注册和发现,设置为 false 即可
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
# 配置 Sentinel
sentinel:
transport:
port: 8719
dashboard: localhost:8080
# feign 开启 sentinel 支持
feign:
sentinel:
enabled: true
熔断降级
ProductServiceFallback.java
package com.example.fallback;
import com.example.pojo.Product;
import com.example.service.ProductService;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 服务熔断降级处理可以捕获异常
*/
@Slf4j
@Component
public class ProductServiceFallbackFactory implements FallbackFactory {
@Override
public ProductService create(Throwable throwable) {
return new ProductService() {
@Override
public Product selectProductById(Integer id) {
// 获取日志,在需要捕获异常的方法中进行处理
log.error("product-service 服务的 selectProductById 方法出现异常,异常信息如下:"
+ throwable);
return new Product(id, "托底数据", 1, 2666D);
}
};
}
}
消费服务
ProductService.java
package com.example.service;
import com.example.fallback.ProductServiceFallbackFactory;
import com.example.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// 声明需要调用的服务
@FeignClient(value = "product-service", fallbackFactory = ProductServiceFallbackFactory.class)
public interface ProductService {
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@GetMapping("/product/{id}")
Product selectProductById(@PathVariable("id") Integer id);
}
OrderServiceImpl.java
package com.example.service.impl;
import com.example.pojo.Order;
import com.example.service.OrderService;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private ProductService productService;
/**
* 根据主键查询订单
*
* @param id
* @return
*/
@Override
public Order selectOrderById(Integer id) {
return new Order(id, "order-001", "中国", 2666D,
Arrays.asList(productService.selectProductById(1)));
}
}
控制层
package com.example.controller;
import com.example.pojo.Order;
import com.example.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 根据主键查询订单
*
* @param id
* @return
*/
@GetMapping("/{id}")
public Order selectOrderById(@PathVariable("id") Integer id) {
return orderService.selectOrderById(id);
}
}
启动类
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
// 开启 FeignClients 注解
@EnableFeignClients
// 开启 @EnableDiscoveryClient 注解,当前版本默认会开启该注解
//@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceFeignApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceFeignApplication.class, args);
}
}
测试
控制台信息如下:
添加流量控制规则,定义资源访问的 QPS 为 1(每秒能处理查询数目)。
快速刷新页面多次访问:http://localhost:9091/order/1 结果如下:
或者关闭服务提供者,访问:http://localhost:9091/order/1 结果如下:
Gateway 支持
Sentinel 支持对 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 进行限流。
官网文档:
- https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
- https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81#spring-cloud-gateway
创建项目
创建 gateway-server-sentinel
项目。
添加依赖
单独使用添加 sentinel-spring-cloud-gateway-adapter
依赖即可。
若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway
依赖来让 spring-cloud-alibaba-sentinel-gateway
模块里的 Spring Cloud Gateway 自动化配置类生效。
同时请将 spring.cloud.sentinel.filter.enabled
配置项置为 false(若在网关流控控制台上看到了 URL 资源,就是此配置项没有置为 false)。
gateway-demo
com.example
1.0-SNAPSHOT
4.0.0
gateway-server-sentinel
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.csp
sentinel-spring-cloud-gateway-adapter
配置文件
server:
port: 9001 # 端口
spring:
application:
name: gateway-server-sentinel # 应用名称
cloud:
sentinel:
filter:
enabled: false
gateway:
discovery:
locator:
# 是否与服务发现组件进行结合,通过 serviceId 转发到具体服务实例。
enabled: true # 是否开启基于服务发现的路由规则
lower-case-service-id: true # 是否将服务名称转小写
# 路由规则
routes:
- id: order-service # 路由 ID,唯一
uri: lb://order-service # 目标 URI,lb:// 根据服务名称从注册中心获取服务请求地址
predicates: # 断言(判断条件)
# 匹配对应 URI 的请求,将匹配到的请求追加在目标 URI 之后
- Path=/order/**
限流规则配置类
使用时只需注入对应的 SentinelGatewayFilter
实例以及 SentinelGatewayBlockExceptionHandler
实例即可。
GatewayConfiguration.java
package com.example.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 限流规则配置类
*/
@Configuration
public class GatewayConfiguration {
private final List viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
/**
* 构造器
*
* @param viewResolversProvider
* @param serverCodecConfigurer
*/
public GatewayConfiguration(ObjectProvider> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 限流异常处理器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 限流过滤器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* Spring 容器初始化的时候执行该方法
*/
@PostConstruct
public void doInit() {
// 加载网关限流规则
initGatewayRules();
}
/**
* 网关限流规则
*/
private void initGatewayRules() {
Set rules = new HashSet<>();
/*
resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
count:限流阈值
intervalSec:统计时间窗口,单位是秒,默认是 1 秒
*/
rules.add(new GatewayFlowRule("order-service")
.setCount(3) // 限流阈值
.setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
// 加载网关限流规则
GatewayRuleManager.loadRules(rules);
}
}
启动类
GatewayServerSentinelApplication.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解
//@EnableEurekaClient
@SpringBootApplication
public class GatewayServerSentinelApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerSentinelApplication.class, args);
}
}
访问
多次访问:http://localhost:9001/order/1 结果如下:
接口 BlockRequestHandler
的默认实现为 DefaultBlockRequestHandler
,当触发限流时会返回默认的错误信息:Blocked by Sentinel: FlowException
。我们可以通过 GatewayCallbackManager
定制异常提示信息。
自定义异常提示
GatewayCallbackManager
的 setBlockHandler
注册函数用于实现自定义的逻辑,处理被限流的请求。
package com.example.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* 限流规则配置类
*/
@Configuration
public class GatewayConfiguration {
private final List viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
/**
* 构造器
*
* @param viewResolversProvider
* @param serverCodecConfigurer
*/
public GatewayConfiguration(ObjectProvider> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 限流异常处理器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 限流过滤器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* Spring 容器初始化的时候执行该方法
*/
@PostConstruct
public void doInit() {
// 加载网关限流规则
initGatewayRules();
// 加载自定义限流异常处理器
initBlockHandler();
}
/**
* 网关限流规则
*/
private void initGatewayRules() {
Set rules = new HashSet<>();
/*
resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
count:限流阈值
intervalSec:统计时间窗口,单位是秒,默认是 1 秒
*/
rules.add(new GatewayFlowRule("order-service")
.setCount(3) // 限流阈值
.setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
// 加载网关限流规则
GatewayRuleManager.loadRules(rules);
}
/**
* 自定义限流异常处理器
*/
private void initBlockHandler() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map result = new HashMap<>();
result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
result.put("route", "order-service");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
}
};
// 加载自定义限流异常处理器
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
访问
多次访问:http://localhost:9001/order/1 结果如下:
分组限流
package com.example.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* 限流规则配置类
*/
@Configuration
public class GatewayConfiguration {
private final List viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
/**
* 构造器
*
* @param viewResolversProvider
* @param serverCodecConfigurer
*/
public GatewayConfiguration(ObjectProvider> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 限流异常处理器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 限流过滤器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* Spring 容器初始化的时候执行该方法
*/
@PostConstruct
public void doInit() {
// 加载网关限流规则
initGatewayRules();
// 加载自定义限流异常处理器
initBlockHandler();
}
/**
* 网关限流规则
*/
private void initGatewayRules() {
Set rules = new HashSet<>();
/*
resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
count:限流阈值
intervalSec:统计时间窗口,单位是秒,默认是 1 秒
*/
// rules.add(new GatewayFlowRule("order-service")
// .setCount(3) // 限流阈值
// .setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
// --------------------限流分组----------start----------
rules.add(new GatewayFlowRule("product-api")
.setCount(3) // 限流阈值
.setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
rules.add(new GatewayFlowRule("order-api")
.setCount(5) // 限流阈值
.setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
// --------------------限流分组-----------end-----------
// 加载网关限流规则
GatewayRuleManager.loadRules(rules);
// 加载限流分组
initCustomizedApis();
}
/**
* 自定义限流异常处理器
*/
private void initBlockHandler() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map result = new HashMap<>();
result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
result.put("route", "order-service");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
}
};
// 加载自定义限流异常处理器
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
/**
* 限流分组
*/
private void initCustomizedApis() {
Set definitions = new HashSet<>();
// product-api 组
ApiDefinition api1 = new ApiDefinition("product-api")
.setPredicateItems(new HashSet() {{
// 匹配 /product-service/product 以及其子路径的所有请求
add(new ApiPathPredicateItem().setPattern("/product-service/product/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
// order-api 组
ApiDefinition api2 = new ApiDefinition("order-api")
.setPredicateItems(new HashSet() {{
// 只匹配 /order-service/order/index
add(new ApiPathPredicateItem().setPattern("/order-service/order/index"));
}});
definitions.add(api1);
definitions.add(api2);
// 加载限流分组
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}
访问
访问:http://localhost:9001/product-service/product/1 触发限流
访问:http://localhost:9001/order-service/order/index 触发限流
访问:http://localhost:9001/order-service/order/1 不会触发限流
至此 Sentinel 服务哨兵知识点就讲解结束了。
本文采用 知识共享「署名-非商业性使用-禁止演绎 4.0 国际」许可协议
。
大家可以通过 分类
查看更多关于 Spring Cloud
的文章。
您的点赞
和转发
是对我最大的支持。
关注公众号 哈喽沃德先生
「文档 + 视频」每篇文章都配有专门视频讲解,学习更轻松噢 ~