Zer0e's Blog

【架构之路10】sentinel限流上手

字数统计: 2.9k阅读时长: 11 min
2024/07/25 Share

前言

这篇讲讲sentinel限流。

之前我们内部系统其实没有限流这个概念,我个人尝试过在网关层面直接做请求限制,没有在服务层尝试过。

正文

sentinel是阿里的产品,目的是为了限制服务的流量来达到保护应用的目的。

sentinel功能

流量控制

流量控制有以下几个角度:

  • 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
  • 运行指标,例如 QPS、线程池、系统负载等;
  • 控制的效果,例如直接限流、冷启动、排队等。

熔断降级

当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。

Hystrix通过线程池的方式,来对依赖(在sentinel的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。

Sentinel则是:

  • 通过并发线程数进行限制

和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。

  • 通过响应时间对资源进行降级

除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。

系统负载保护

Sentinel 同时提供系统维度的自适应保护能力。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。

针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。

简单来说就是可以根据系统的某些指标,如CPU使用情况,RT,QPS等数据限制请求。

使用

sentinel的使用也是十分简单,sentinel提供dashboard服务供限流服务接入,我们可以通过dashboard快速下发规则给应用,实现界面化管理。

下载dashboard,启动

1
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.8.jar

这里采用springCloudAlibaba快速接入。

pom文件依赖如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.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>
</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.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

使用start.aliyun.com构建的springboot程序会自动生成demo文件,这里我们手动写下。

流控

先写配置,用于在触发sentinel流控时显示的返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class SentinelConfig {

@Bean
public BlockExceptionHandler sentinelBlockExceptionHandler() {
return (request, response, e) -> {
response.setStatus(429);

PrintWriter out = response.getWriter();
out.print("Oops, blocked by Sentinel: " + e.getClass().getSimpleName());
out.flush();
out.close();
};
}
}

定义服务

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class UserService {
@SentinelResource(value = "UserService#getUserNameById", defaultFallback = "getUserFallback")
public String getUserNameById(Integer id) {
return "default user";
}


public String getUserFallback() {
return "get user fall back";
}
}

这里我们讲讲@SentinelResource注解。

注意这个注解也采用aop,所以private方法不支持。

这个注解用于定义资源,并提供可选的异常处理和 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 逻辑中,而是会原样抛出。

若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 BlockException 直接抛出

其实核心参数就两个,value和defaultFallback,一个用于定义资源,另一个配置默认的失败方法。

注意这里getUserFallBack的方法参数为空或者接收一个Throwable参数。

接下来写controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/user")
public class UserController {

@Resource
private UserService userService;

@GetMapping("/get")
public String getUserById(@RequestParam Integer id) {
return userService.getUserNameById(id);
}

}

很简单的一个接口,我们调用看看。

1
2
http://127.0.0.1:8081/user/get?id=1
default user

打开dashboard,账号密码都是sentinel。

1
http://127.0.0.1:8080

在左侧可以发现我们的应用,并且在实时监控中可以看到我们调用的接口和对应的资源。

1
2
3
sentinel_spring_web_context
/user/get
UserService#getUserNameById

我们可以对/user/get做限流,也可以对UserService#getUserNameById限流,因为他们都属于sentinel的资源。

这里我们对/user/get限流,qps单机阈值限制为1。新增完成后,多次访问接口,会出现Oops, blocked by Sentinel: FlowException的错误提示。

对UserService#getUserNameById做qps为1的限流,可以发现限流请求默认返回了get user fall back。

熔断降级

在上面服务的基础上增加一个服务方法和接口。

1
2
3
4
5
6
7
8
@SentinelResource(value = "UserService#getUsers", defaultFallback = "getUserFallback")
public String getUsers() {
try {
Thread.sleep(1000);
}catch (Exception ignored) {
}
return "all users";
}
1
2
3
4
5
@GetMapping("/get-all")
public String getAllUser() {
return userService.getUsers();
}

在这个接口上我们等待了一秒延迟。

我们在控制台对这个资源进行降级。为了能更直观看出来,最大RT填为500,最小请求数为1,比例阈值0.5(这个无所谓),熔断时间10s。

保存之后我们请求接口,第一次请求成功,然后由于请求时间大于500ms,直接触发了熔断降级,此时再次请求就会执行默认的fallback方法。

集群限流

这个好理解,就是一组服务的最大请求量。配置起来也简单。

先copy一份idea的启动文件,然后加上vmoptions:-Dserver.port=8082再启动一个服务,在dashboard上就能看见应用为2/2.

选择集群流控,添加token-server,这个服务的作用是用于接收其他客户端的请求,判断请求是否通过。

然后将另一个服务变为token-client,填写最大qps即可完成集群流控。

当然我们在配置普通流控规则时,也可以勾选是否集群,选择均摊模式,填写阈值即可应用流控规则。原理也是请求前向token-server获取令牌实现集群限流。

总结

今天试用了一下sentinel,很多年前在我还在大学的时候就初步使用过。

今天遇到的坑是defaultFallback不生效,我在源码中调试了很久,包括源码是如何寻找到方法,最后调试才发现name.equals(method.getName())判断不通过,原来我在SentinelResource定义的是getUserFallback,而编写的方法是getUserFallBack,大小写错了!崩溃!

其他的话问题不是很大,还有一个就是规则的持久化了,这个的话得配合nacos做。官方demo和网上的案例我也看了,难度较小,只需要知道流控的规则json如何编写,然后引入对应的包监听nacos数据即可。

原文作者:Zer0e

原文链接:https://re0.top/2024/07/25/devops10/

发表日期:July 25th 2024, 9:30:00 pm

更新日期:July 26th 2024, 1:07:30 pm

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. sentinel功能
      1. 2.1.1. 流量控制
      2. 2.1.2. 熔断降级
      3. 2.1.3. 系统负载保护
    2. 2.2. 使用
      1. 2.2.1. 流控
      2. 2.2.2. 熔断降级
      3. 2.2.3. 集群限流
  3. 3. 总结