学习笔记(Sentinel)
分布式流量防护
在分布式系统中,服务之间的相互调用会生成分布式流量。如何通过组件进行流量防护,并有效控制流量,是分布式系统的技术挑战之一
什么是服务雪崩
假设我有一个微服务系统,这个系统内包含了 ABCD 四个微服务,这四个服务都是以集群模式构建的,微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况就是服务雪崩
安装软件
下载地址:https://github.com/alibaba/Sentinel/releases
直接下载
wget https://github.com/alibaba/Sentinel/releases/download/1.8.7/sentinel-dashboard-1.8.7.jar
注意事项:Sentinel 控制台目前仅支持单机部署。Sentinel 控制台项目提供 Sentinel 功能全集示例,不作为开箱即用的生产环境控制台,若希望在生产环境使用请根据文档自行进行定制和改造
// 前台运行
java -Dserver.port=3000 -Dcsp.sentinel.dashboard.server=111.180.198.78:3000 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.7.jar
// 后台运行
nohup java -server -Xms64m -Xmx256m -Dserver.port=3000 -Dcsp.sentinel.dashboard.server=111.180.198.78:3000 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.7.jar >> /root/temp/Sentinel/sentinel.log 2>&1 &
在浏览器输入 111.180.198.78:3000
即可,登录用户名密码都是 sentinel
搭建项目
第一步:导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>sentinel-basic</artifactId>
<version>1.0.0</version>
<name>sentinel-basic</name>
<description>sentinel-basic</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2021.0.6.0</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>sentinel</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
第二步:修改配置文件
server:
port: 8080
spring:
application:
name: sentinel-basic
cloud:
sentinel:
transport:
# Sentinel 控制台地址
dashboard: 192.168.0.104:8858
第三步:配置主启动类
package org.example;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Slf4j
@SpringBootApplication
public class SentinelBasicApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelBasicApplication.class, args);
log.info("********************* 应用启动成功 *********************");
}
}
第四步:创建测试控制器
package org.example.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SentinelController {
@RequestMapping("/sentinel")
public Map<String, String> sentinelController(){
Map<String, String> map = new HashMap<>();
map.put("sentinel", "sentinel success");
return map;
}
@RequestMapping("/relation")
public Map<String, String> relationController(){
Map<String, String> map = new HashMap<>();
map.put("relation", "relation success");
return map;
}
}
第五步:测试即可,Sentinel1 是懒加载的,只有访问了控制台里才有路径信息
流量控制概述
监控应用流量的 QPS
(每秒能够处理的请求数) 或并发线程数,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性
流控控制规则
- 直接:直接作用于当前资源,如果访问压力大于某个阈值,后续请求将被直接拦下来
- 关联:统计与当前资源相关的另一个资源,另一个资源触发阈值时,对当前资源限流
- 链路:当指定链路上的访问量⼤于某个阈值时,对当前资源进⾏限流,这⾥的指定链路是细化到 API 级别的限流维度
- 快速失败:默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException
- Warm Up:即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压爆
- 排队等待:匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法 。这种方式主要用于处理间隔性突发的流量
流控模式(直接模式)
概述:当 QPS 超过某个阈值的时候,则采取措施进行流量控制
如图所示:当每秒的请求数超过 3 时就会触发流控规则,使用 ApiFox 进行压力测试,失败返回:Blocked by Sentinel (flow limiting)
流控模式(关联模式)
概述:当关联的资源达到阈值时,就限流自己
/sentinel
资源关联了 /relation
资源,当 /relation
资源的 QPS
达到 3
后,/sentinel
就会被限流
流控模式(链路模式)
概述:链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细
比如说,项目中的两个资源都用到了同一个 Service
下的服务,我们希望走 /sentinel
资源的超过阈值就限流,/relation
资源不进行限流
添加的相关代码,需要使用 @SentinelResource()
注解来标记资源,必须要配置文件里添加 web-context-unify: false
package org.example.service;
public interface LinkService {
String linkService();
}
package org.example.service.impl;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.service.LinkService;
import org.springframework.stereotype.Service;
@Service
public class LinkServiceImpl implements LinkService {
@SentinelResource(value = "goods")
@Override
public String linkService() {
return "goods";
}
}
package org.example.controller;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class SentinelController {
@Autowired
private LinkService linkService;
@RequestMapping("/sentinel")
public Map<String, String> sentinelController(){
linkService.linkService();
Map<String, String> map = new HashMap<>();
map.put("sentinel", "sentinel success");
return map;
}
@RequestMapping("/relation")
public Map<String, String> relationController(){
linkService.linkService();
Map<String, String> map = new HashMap<>();
map.put("relation", "relation success");
return map;
}
}
server:
port: 8080
spring:
application:
name: sentinel-basic
cloud:
sentinel:
transport:
# Sentinel 控制台地址
dashboard: 192.168.0.104:8858
# sentnel api端口号 默认 8719
port: 8719
# 关闭context整合
web-context-unify: false
测试结果
流控控制效果
流控效果(预热)
warm up
也叫预热模式,是应对服务冷启动的一种方案,下图所表示的含义是,当请求量大量到来时,在5秒的时间内,慢慢的把 QPS 提高到 10
添加控制器,然后使用 ApiFox 测试200个请求即可
package org.example.controller;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class SentinelController {
......
@RequestMapping("/warmup")
public Map<String, String> warmupController(){
Map<String, String> map = new HashMap<>();
map.put("warmup", "warmup success");
return map;
}
}
热点参数限流
热点即经常访问的数据,给 /hot
这个资源添加热点参数限流,规则如下:
- 默认的热点参数规则是每1秒请求量不超过2
- 给102这个参数设置例外:每1秒请求量不超过4
- 给103这个参数设置例外:每1秒请求量不超过10
添加控制器代码,必须用 @SentinelResource("hot")
标记资源才可以
package org.example.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class SentinelController {
......
@SentinelResource("hot")
@RequestMapping("/hot")
public Map<String, String> hotController(Long id) {
Map<String, String> map = new HashMap<>();
map.put("hot", "success" + id);
return map;
}
}
对于特定的资源,点击热点进行设置,这里的参数索引指的是对控制器的第几个参数进行限流,然后去热点规则里点击编辑进行热点数据的设置,最后自己测试就可以了
线程隔离
限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了
线程隔离就是允许的并发访问的线程数
熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求
断路器控制熔断和放行是通过状态机来完成的
状态机包括三个状态
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。 请求成功:则切换到closed状态 请求失败:则切换到open状态
熔断降级策略
-
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断
-
异常比例,异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断
慢调用
下图表示:超过 50ms 的请求都会被认为是慢请求,1 秒内 5 个请求数的异常比例达到百分之40,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了。过 5 秒钟由断路器又打开状态变为半开状态放一部分请求进来
相关控制器代码
package org.example.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class SentinelController {
......
@RequestMapping("/slow")
public Map<String, String> slowController() throws InterruptedException {
Thread.sleep(100);
Map<String, String> map = new HashMap<>();
map.put("slow", "success");
return map;
}
}
异常比例
当资源每秒报错异常总数占通过量的比值超过阈值之后,资源进入降级状态。异常比率的阈值范围是 [0.0,1.0]
在 1 秒内的 5 次请求中,只要异常比例超过 0.4,也就是有 2 次以上的异常,就会触发熔断
异常数
异常数:当资源近 1 分钟的异常数目超过阈值之后会进行熔断
在 1 秒内超过有 5 个异常就进入熔断
授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式
- 白名单:来源(origin)在白名单内的调用者允许访问
- 黑名单:来源(origin)在黑名单内的调用者不允许访问
比如我们有过一个需求:我们允许请求从 Higress 网关到服务访问,不允许浏览器访问服务,那么白名单中就要填写网关的来源名称(origin)
Sentinel 是通过 RequestOriginParser 这个接口的 parseOrigin 来获取请求的来源的,定义接口和实现类
package org.example.service;
import javax.servlet.http.HttpServletRequest;
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
package org.example.service.impl;
import org.example.service.RequestOriginParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 1.获取请求头
String origin = request.getHeader("origin");
// 2.非空判断
if (StringUtils.isEmpty(origin)) {
origin = "blank";
}
return origin;
}
}
然后给网关添加请求头:既然获取请求 origin 的方式是从 reques-header 中获取 origin 值,我们必须让所有从网关路由到微服务的请求都带上 origin 头(在路由设置就可以了)
然后去相应的资源配置授权规则,点击授权,然后就可以进行测试了
系统自适应限流
Sentinel 的系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性
系统规则支持以下模式(在系统规则里新增系统规则即可):
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
- CPU usage:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护
@SentinelResource
服务降级功能,但是只是限制后,返回不可控的结果肯定是不行的,我们还要保证调用者在调用那些被限制的服务时候,不管是不是被限制,都要让他们拿到一个合理的结果,而不是扔回去一个异常就完事了
Sentinel 提供了这样的功能,让我们可以另外定义一个方法来代替被限制或异常服务返回数据,这就是 fallback 和 blockHandler
- fallback:针对Java本身出现的异常进行处理的对应属性
- blockHandler:针对违反Sentinel控制台配置规则时触发 BlockException 异常时对应处理的属性
只配置fallback
放在一起,注意方法的返回值必须一致才行,当出现异常时就返回下面的东西
package org.example.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class SentinelController {
......
@SentinelResource(value = "testFallback", fallback = "testFallback")
@RequestMapping("/fallback")
public Map<String, String> fallbackController(String id) {
if (id.equals("1")) {
throw new RuntimeException("出异常了");
}
Map<String, String> map = new HashMap<>();
map.put("exception", "success");
return map;
}
// 降级
public Map<String, String> testFallback(String id, Throwable e) {
Map<String, String> map = new HashMap<>();
map.put("exception", "服务繁忙请稍后在试");
return map;
}
}
放在外面,现在 fallback 包下创建类,然后再去控制器修改
package org.example.fallback;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class TestFallback {
// 降级
public static Map<String, String> testFallback(String id, Throwable e) {
Map<String, String> map = new HashMap<>();
map.put("exception", "服务繁忙请稍后在试");
return map;
}
}
package org.example.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.fallback.TestFallback;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class SentinelController {
......
@SentinelResource(value = "testFallback", fallbackClass = TestFallback.class, fallback = "testFallback")
@RequestMapping("/fallback")
public Map<String, String> fallbackController(String id) {
if (id.equals("1")) {
throw new RuntimeException("出异常了");
}
Map<String, String> map = new HashMap<>();
map.put("exception", "success");
return map;
}
}
只配置blockHandler
超出流量限制的部分是否会进入到 blockHandler 的方法,要注意是超出流量限制的请求调用,会进入 blockHandler 方法
package org.example.handler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class TestBlockHander {
// 降级
public static Map<String, String> testBlockHander(String id, BlockException e) {
Map<String, String> map = new HashMap<>();
map.put("exception", "被限流了");
return map;
}
}
package org.example.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.example.fallback.TestFallback;
import org.example.handler.TestBlockHander;
import org.example.service.LinkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class SentinelController {
......
@SentinelResource(value = "testBlockHander", blockHandlerClass = TestBlockHander.class, blockHandler = "testBlockHander")
@RequestMapping("/blockhander")
public Map<String, String> blockhanderController(String id) {
if (id.equals("1")) {
System.out.println(10 / 0);
}
Map<String, String> map = new HashMap<>();
map.put("exception", "success");
return map;
}
}
都配置
都加上就可以了