1. sentinel
分布式系统的流量防卫兵。
以流量为切入点,从流量控制、熔断降级、系统负载保护等多个纬度保护服务的稳定性。
1.1 特征
- 丰富的应用场景:秒杀、消息削峰填谷、集群流量控制、事实熔断下游不可应用等。
- 完备的实时监控:可以在控制台中看到接入应用的单台机器秒级数据。
- 广泛的开源生态:提供开箱即用的与其他开源框架/库的整合,与Spring Cloud、Dubbo、等,只需要接入相应依赖并简单的配置就可以;
- 完善的SPI扩展点:可以通过扩展接口来快速的定制逻辑。
- 作用:
- 防止服务雪崩;
- 服务降级;
- 服务熔断;
- 服务限流。
1.2 安装
- 下载地址:https://github.com/alibaba/Sentinel/releases
- 下载之后,直接使用
java -jar
运行jar包即可,但是要保证8080端口不能被占用。
默认用户名和密码都是sentinel。
1.3 微服务搭建
- 新建moudule:
cloudalibaba-sentinel-service8401
; - 依赖:
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 配置文件:
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
- 启动类:
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelMain8401 {
public static void main(String[] args) {
SpringApplication.run(SentinelMain8401.class,args);
}
}
- controller:
@Slf4j
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
- 启动之后,由于sentinel是懒加载机制,必须执行一次当前微服务的访问:
http://localhost:8401/testA
,才可以看到:
1.4 流量控制
1.4.1 QPS——直接快速失败
QPS:达到设置的每秒请求数,进行限流;直接:api达到限流条件时,直接限流;快速失败:直接抛出异常。
- 对
/testA
新增如下流控规则: - 连续请求两次接口:直接被拦截
1.4.2 线程数——直接失败
线程数:当调用该API的线程数达到阈值时,进行限流;直接:api达到限流条件时,直接限流。
即针对一个请求设置对应数量的线程进行处理,如果当前线程来不及处理请求时才会抛出异常。
1.4.3 关联
关联:当关联的资源达到阈值时,限流自己。
例如,这里如果/testB
超过设置的阈值,就会对/testA
进行访问限制。
1.4.4 预热(Warm up)
预热方式,当系统长期处于低水位的情况,当流量突然激增时,直接把系统拉升到高水位可能瞬间把系统压垮。
通过冷启动,让通过的流量缓慢增加,在一定时间内增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
默认的冷加载因子是3,在设定的预热时长内最多承受阈值10/3的QPS,当超过预热时长(5秒)后可以达到最大阈值。
1.4.4 排队等待
匀速排队,让请求以均匀的速度通过,阈值只能是QPS类型。
主要用于处理间歇性突发的流量。
如果超过当前设置的阈值(1),则等待20000毫秒。
1.4 熔断降级
熔断降级会在调用链路中某个资源出现部稳定时,对资源的调用进行限制,让请求快速失败,避免影响到其它资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口内,对该资源的调用都自动熔断。
- 降级规则:
- RT(秒级):平均响应时间,只有时间窗口内通过的请求大于等于5且超出阈值同时触发,才会触发降级。
- 异常比例(秒级):QPS>= 5超过阈值时,触发降级,时间窗口结束后,关闭降级。
- 异常数(分支级):当异常数超过于阈值时,触发降级,时间窗口结束后,关闭降级。
不存在半开状态,只有开和关。
1.4.1 RT
- 断开方式:
- 修改代码:这里让
/testB
睡2秒。
@GetMapping("/testB")
public String testB() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "------testB";
}
- 配置RT:如果一秒钟进来的请求大于等于5,且都超过了设置的处理时间RT,那么在未来的1秒的窗口时间内,断路器会自动打开,当前微服务不可用。
- 通过压力测试之后,在访问就会发现异常。
1.4.2 异常比例
当请求每秒超过或等于5个,并且每秒的异常总数占通过量的比例阈值,就会熔断,在下面的窗口期中,微服务无法进行调用。
- 断开方式:
- 修改代码:
@GetMapping("/testB")
public String testB() {
int a = 10/0;
return "------testB";
}
- 配置:异常比例∈[0.0,1.0];
1.4.3 异常数
当资源近一分钟异常数目超过阈值后会自动进行熔断。需要注意的是创窗口时间需要大于60s,否则状态结束后可能仍然处于熔断状态。
- 断开方式:
- 配置:
1.5 热点参数限流
很多时候会统计某个热点数据访问频次最高的前几个数据,并进行访问限制。
热点参数限流会统计传入的参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
- 在controller中新建方法:
/**
* 热点key测试代码
* @param p1
* @param p2
* @return
*/
@GetMapping("/hot")
@SentinelResource(value = "hot",blockHandler = "deal_hot") // 不符合配置的热点规则,由deal_hot处理
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2){
return "hotKey" + p1 + p2;
}
/**
* testHotKey 的 blockHandler
* @param p1
* @param p2
* @param e
* @return
*/
public String deal_hot(String p1, String p2, BlockException e){
return "deal_hot -> blockHandler" + p1 + p2;
}
- 在sentinel配置热点规则:
- 访问测试:当携带第一个参数(p1)的QPS超过1,就会执行blockHandler方法。
- 如果不指定fallback方法,会直接在页面抛出异常。
- 参数例外项:指定特殊指时的限流规则与平常限流规则不同。
例如这里,当p1 == 1
时,它的阈值为500
1.6 系统规则
Sentinel系统自适应限流从整体维度对应用入口流量进行控制。
- 系统规则模式:
- Load自适应:仅对Linux/Unix-like生效,进行自适应的系统保护;
- CPU:当前系统CPU使用率超过阈值即触发保护系统(取值[0.0,1.0]);
- RT:单台机器上所有入口流量的平均RT达到阈值时即触发保护系统,单位毫秒;
- 并发线程数:单台机器上所有入口流量的并发线程数达到阈值时即触发保护系统;
- 入口QPS:单台机器上所有入口流量的QPS达到阈值时即触发保护系统。
但是用起来很危险,比如更细粒度的划分。
1.7 @SentinelResource
- 新建一个
RateLimitController
:
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
}
1.7.1 按资源名称配置限流规则
- 新建流控规则:
- 测试:超过阈值执行
blockHandler
的方法
1.7.2 按URL地址配置限流规则
RateLimitController
中新增方法:
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl() {
return new CommonResult(200, "按url限流测试OK", new Payment(2020L, "serial002"));
}
- 配置流控规则:
- 访问测试:由于未指定
blockHandler
,就会执行系统默认的。
1.7.3 自定义限流处理逻辑
上方处理方式,仍然存在代码耦合和代码膨胀问题,且没有全局统一处理方式。
- 创建
CustomerBlockHandler
用于自定义处理限流逻辑
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(4444, "按客戶自定义,global handlerException----1");
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(4444, "按客戶自定义,global handlerException----2");
}
}
- 在
RateLimitController
新增方法:
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客戶自定义", new Payment(2020L, "serial003"));
}
- 配置流控规则:
- 测试:超过阈值执行
CustomerBlockHandler.handlerException2()
1.8 整合Ribbon
让消费者84通过ribbon实现负载均衡调用9003和9004微服务。
1.8.1 新建服务提供者微服务
- 新建
cloudalibaba-provider-payment9004
和cloudalibaba-provider-payment9005
- 依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 配置:记得修改9005端口
server:
port: 9004
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
- 启动类:记得修改9005类名
@SpringBootApplication
@EnableDiscoveryClient
public class NacosMain9004 {
public static void main(String[] args) {
SpringApplication.run(NacosMain9004.class,args);
}
}
- 业务类:
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort: " + serverPort, payment);
return result;
}
}
- 访问测试:
1.8.2 新建服务消费者微服务
- 新建module:
cloudalibaba-consumer-nacos-order84
- 依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 配置文件:
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
- 主启动类:
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class NacosOrderMain84 {
public static void main(String[] args) {
SpringApplication.run(NacosOrderMain84.class, args);
}
}
- 配置RestTmplate:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
- 业务类:
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback") //没有配置
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
}
- 访问:可以实现轮询
1.8.3 未配置fallback
- 如下代码:没有指定配置fallback,
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback") //没有配置
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
- 访问
http://localhost:84/consumer/fallback/4
:可以发现没有任何处理。
1.8.4 只配置fallback
- 如下代码,指定fallback为
handlerFallback()
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback只负责业务异常
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底异常handlerFallback,exception内容 " + e.getMessage(), payment);
}
- 重启84端口后,访问
http://localhost:84/consumer/fallback/4
,出现自定义的fallback。
1.8.5 只配置blackHandler
- 代码如下:
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(445, "blockHandler-sentinel限流,无此流水: blockException " + blockException.getMessage(), payment);
}
}
- 重启84端口后,访问
http://localhost:84/consumer/fallback/4
,可以看出blockHandler
sentinel控制台配置违规。
1.8.5 配置blackHandler和fallback
- 代码如下:
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底异常handlerFallback,exception内容 " + e.getMessage(), payment);
}
//本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(445, "blockHandler-sentinel限流,无此流水: blockException " + blockException.getMessage(), payment);
}
- 重启后,访问
http://localhost:84/consumer/fallback/4
测试: - 配置流控规则:
- 超出阈值,再访问
http://localhost:84/consumer/fallback/1
测试: - 超出阈值再访问
http://localhost:84/consumer/fallback/4
,测试: - 可以看出,被限流降级后会由
blockHandler
来处理。
1.8.6 异常忽略属性
- 代码如下:
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",
fallback = "handlerFallback",
blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底异常handlerFallback,exception内容 " + e.getMessage(), payment);
}
//本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(445, "blockHandler-sentinel限流,无此流水: blockException " + blockException.getMessage(), payment);
}
- 也就是说如果当前方法抛出
IllegalArgumentException
异常,就不会由fallback来处理,但如果限流时仍然会走blockHandler
。
1.9 整合OpenFeign
- 新建接口:
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
- fallback实现类:
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444, "服务降级返回,---PaymentFallbackService", new Payment(id, "errorSerial"));
}
}
- 新增controller:
@Slf4j
@RestController
public class MyFeignController {
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
}
- 启动84,访问:
http://localhost:84/consumer/paymentSQL/4
,可以访问成功。 - 模拟服务提供者宕机,看84会不会自动关闭调用:测试成功。
1.10 sentinel持久化
由于默认的流控规则等,都是临时的,重启之后就消失。需要将配置的规则存入持久化容器中。
- 将配置持久化到nacos中,只要nacos中的配置不删除,针对当前微服务配置的sentinel中的规则都不会删除。
- 这里以8401为例:
- 新增依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- 修改配置文件:
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: 49.232.141.194:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
datasource:
ds1: # 将当前微服务的sentinel配置存入到nacos持久化
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
- 在nacos中新增配置:
- json内容:
[
{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
- 启动8401,在sentinel中可以看到配置的规则。