微服务架构 基础(九)
持续更新…
Sentinel实现服务熔断、服务降级和服务限流
Sentinel:分布式系统的流量防卫兵
Sentinel 是面向分布式服务架构的轻量级流量控制产品,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助您保护服务的稳定性。
Sentinel具有以下特征
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel主要特性
Sentinel开源生态
Sentinel分为两部分
- 核心库(Java客户端)不依赖任何框架/库,能够运行所有Java运行时环境,同时对Dubbo/Spring Cloud等框架也有较好的支持。
- 控制台(Dashboard)基于Spring Boot开发,打包后可以直接运行,不需要额外的Tomcat等Web应用容器
搭建Sentinel控制台
下载完成后,直接通过java -jar sentinel-dashboard-1.8.1.jar编译命令启动即可(默认端口为8080),进入管理界面后,其初始登录账号和密码均为sentinel
sentinel基本使用
新建服务提供者子模块(sentinel-service-8002)
添加主要依赖:
<dependencies>
<!-- sentinel依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- nacos依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
添加application.yml文件:
server:
port: 8002
spring:
application:
name: eentinel-service
cloud:
nacos:
discovery:
server-addr: 192.168.50.248:1111 # Nacos集群入口
sentinel:
transport:
# 配置Sentinel dashboard属性
dashboard: localhost:8080 # sentinel控制面板的地址
# port: 8888 # 如果端口8080被占用,则从该端口递增扫描查找未杯占用的端口
management:
endpoints:
web:
exposure:
include: '*' # 暴露监控端点
主启动类:
package cn.wu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelServiceApplication8002 {
public static void main(String[] args) {
SpringApplication.run(SentinelServiceApplication8002.class,args);
}
}
控制层:
package cn.wu.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test/a")
public String testA(){
return "TestA方法… ";
}
@GetMapping("/test/b")
public String testB(){
return "TestB方法… ";
}
}
测试结果(由于sentinel加载是懒加载机制,因此需要事先访问服务提供者的端口才能看到结果… ):
流控规则
基本配置选项:
参数简述
- 资源名:唯一名称,默认请求路径
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认为default
- 阈值类型/单机阈值:
- QPS(每秒的请求数量):当调用的API达到阈值时,进行限流
- 线程数:当调用该API的线程数达到阈值时,进行限流
- 是否集群:Sentinel集群模式是否开启
- 流控模式:
- 直接:API达到限流条件时,直接限流
- 关联:但关联的资源达到阈值时,进行自我限流
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)
- 流控效果:
- 快速失败:直接失败,抛异常
- Warm Up:根据coldFactor(冷加载因子,默认为3)的值,经过预热时长公式阈值除以coldFactor ,才会达到设置的QPS阈值
- 排队等待:匀速排队,让请求匀速的速度通过,阈值类型为必须设置为QPS,否则无效
阈值类型为QPS,其余默认的配置
对/test/a添加流量规则
此时在一秒内超过一次的请求访问http://localhost:8002/test/a的会出现以下结果:
阈值类型为线程数,流控模式为直接
以下单机阈值为1的含义为:处理请求方法可以执行的当前线程个数最多为1
利用JMeter进行简单的压力测试(读者如果不了解JMeter可以查看这儿… ):
流控模式为关联,阈值类型为QPS,流控效果为快速失败
当与A关联的资源B达到阈值后,就限流A本身,以下说明只要API /test/b达到单机阈值(但并不是根据API /test/b本身是否会限流),API /test/a就会限流自身
利用JMeter进行简单的压力测试:
测试结果为:
流控模式为直接,阈值类型为QPS,流控效果为Warm Up
以下配置参数说明:当API /test/aQPS超过初始单机阈值:12(单机阈值)/3(冷加载因子)=4,会进入预热过程,此时会从该初始阈值开始,经过5秒时间慢慢恢到最终的单机阈值上限12。如果最大的QPS依然大于单机阈值,则抛出异常
简单起见,读者可以直接手动模拟(有点考验手速,首先快速点击使得QPS超过初始阈值4,结果会显示异常,当预热时长达到5秒后,由于笔者手速实在达不到10次/S以上,因此,一段时间都是正常访问的… )
流控模式为直接,阈值类型为QPS,流控效果为排队等待
以下含义为:客户端API /test/a的QPS一旦超过单机阈值1,就会排队等待,等待的时长为10000ms
多次点击API的结果为(已经出现了加载符号… ):
由于排队的因素,此时JMeter中可以看到请求成功和失败泾渭分明:
流控模式为链路,阈值类型为QPS,流控效果为快速失败
以下含义为:当前入口API /test/b路径访问API /test/a的最大QPS不能超过单机阈值,否则会当前入口API会被限流
降级规则
基本配置选项:
- 慢调用比例:在一个统计时长内,如果持续进入请求个数大于最小最小请求数,并且它们的RT都超过了最大RT这个阈值,则进入熔断状态,时间段长度为熔断时长,该时间段内的请求将进行服务降级。(最大RT超出4900ms都会算作4900ms)
- 异常比例:一个统计时长内请求数超过最小请求数并且一个统计时长内失败率达到比例阈值则进入熔断状态,持续时间为熔断时长
- 异常数:一个统计时长内请求数超过最小请求数并且一个统计时长内失败的请求个数超过异常数时,进入熔断状态,持续时间为熔断时长
慢调用比例
添加控制层API:
@GetMapping("/test/c")
public String testC(){
try {
// 该线程休眠2秒时间
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "TestC 方法";
}
设置线程参数配置为一秒完成50个线程:
设置降级规则配置为:
由于最大的响应时间小于请求处理时间,因此以上访问都会失败…
异常比例
添加控制层异常:
@GetMapping("/test/d")
public String testD(){
int i = 10/0;
return "TestD 方法";
}
配置降级规则:
连续点击结果:
异常数
结果为:
热点限流(重要)
何为热点?
热点即为经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的Top K 数据,并对其进行限制。
添加控制层代码:
这里建议读者阅读完Hystrix内容,能更好地理解代码…
@GetMapping("/test/e")
// 这里的value是要和资源名匹配,降级方法名称为testEHandler
@SentinelResource(value = "testE",blockHandler = "testEHandler")
public String testE(@RequestParam(value = "id",required = false) String id,
@RequestParam(value = "name",required = false) String name){
return "TestE 方法"+"Id为:"+id+",Name为:"+name;
}
public String testEHandler(String id, String name, BlockException blockException){
return "testEHandler 方法"+"Id为:"+id+",Name为:"+name;
}
设置热点规则配置(这里为第一个参数,即id进行限流):
连续多次访问函数方法中携带第一个参数(即只要匹配到id)的请求,服务将将进入到降级:
参数例外项
如果期望参数的值为特殊值时进行特例限流,作进一步细化
对第二个参数(即匹配到name)的值为’张三’进行特例限流
多次快速访问URLhttp://localhost:8002/test/e?id=1&name=张三结果为:
系统自适应限流
Sentinel系统自适应限流从整体维度读应用入口流量进行控制,结合应用的Load、CPU使用率、总体平均RT、入口QPS和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
- Load 自适应(仅对 Linux/Unix-like 机器生效): 系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU 核数 * 2.5。
- CPU使用率 : 当系统 CPU 使用率超过阈值即触发系统保护,取值范围为0到1,比较灵敏。
- 平均 RT: 当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 入口 QPS: 当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
- 并发线程数: 当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
测试(设置全局入口QPS最大为1… ),此时短时间内连续多次访问微服务的任意URL:
Sentinel注解配置
@SentinelResource注解参数
- fallback:运行异常
- blockHandler:配置违规
- blockHandlerClass:降级处理类
- blockHandler:降级处理类中的静态方法
前面存在的问题总结
- 系统默认的方法,不能体现实际灵活的业务需求
- 控制层代码存在耦合
- 每个业务方法都添加相应的服务降级方法,代码变得冗杂
- 全局降级处理方法未能体现
- 不能持久化存储配置信息
修改控制层代码:
package cn.wu.controller;
import cn.wu.handler.TestHandler;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@SentinelResource(value = "test", // 资源名
blockHandlerClass = TestHandler.class, // 降级处理类
blockHandler = "testHandler") // 降级处理方法
@GetMapping("/test")
public String test(){
return "正常执行… ";
}
}
添加降级处理类:
package cn.wu.handler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
public class TestHandler {
// 这里需要静态方法
public static String testHandler(BlockException blockException){
return "服务繁忙,请稍后再试!";
}
}
添加流控规则:
连续多次访问,测试结果为:
初步解决耦合的问题…
Sentinel整合Ribbon
利用前面基础八工程的两个服务提供者子模块(nacos-service-8001/nacos-service-9001)以及服务消费者(nacos-main-80)进行扩展学习…
只有运行异常处理类
服务消费者添加主要依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
服务消费者修改application.yml:
server:
port: 80
spring:
application:
name: nacos-consumer
cloud:
nacos:
discovery:
server-addr: 192.168.50.248:1111 # Nacos集群地址
sentinel:
transport:
dashboard: localhost:8080 # sentinel控制面板URL
# 自定义服务提供者地址属性
nacos-provider-addr: http://nacos-service-provider
management:
endpoints:
web:
exposure:
include: '*' # 暴露监控点
服务消费者控制层:
package cn.wu.controller;
import cn.wu.handler.TestHandler;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class ConsumerController {
// 配置与代码做到分离
@Value("${nacos-provider-addr}")
private String serverURL ;
private RestTemplate restTemplate;
@Autowired
@Qualifier("restTemplateBean")
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@GetMapping("/test/{id}")
@SentinelResource(value = "test", // 资源名
fallbackClass = TestHandler.class,
fallback = "testHandler")
public String test(@PathVariable("id") String id){
if(Integer.parseInt(id) < 0){
throw new IllegalArgumentException("参数非法异常… ");
}
return restTemplate.getForObject(serverURL+"/provide/"+id,String.class);
}
}
服务消费者的降级处理类:
package cn.wu.handler;
import org.springframework.web.bind.annotation.PathVariable;
public class TestHandler {
public static String testHandler(@PathVariable("id") String id, Throwable throwable){
return "Id为: "+id+"是非法参数,请重新输入!";
}
}
修改服务提供者控制层:
package cn.wu.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProviderController {
@Value("${server.port}")
private String port;
@GetMapping("/provide/{id}")
public String provide(@PathVariable("id") String id){
return "当前端口为: "+port+",当前Id为: "+id;
}
}
结果为:
既有运行异常处理类,又有Sentinel违规处理类
修改服务消费者控制层:
package cn.wu.controller;
import cn.wu.fallback.TestFallBack;
import cn.wu.handler.TestHandler;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class ConsumerController {
// 配置与代码做到分离
@Value("${nacos-provider-addr}")
private String serverURL ;
private RestTemplate restTemplate;
@Autowired
@Qualifier("restTemplateBean")
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@GetMapping("/test/{id}")
@SentinelResource(value = "test", // 资源名
fallbackClass = TestFallBack.class, // 运行异常处理类
fallback = "testFallBack", // 运行异常处理方法
blockHandlerClass = TestHandler.class, // Sentinel违规处理类
blockHandler = "testHandler",// Sentinel违规处理方法
// exceptionsToIgnore = {IllegalArgumentException.class} // 异常忽略
)
public String test(@PathVariable("id") String id){
if(Integer.parseInt(id) < 0){
throw new IllegalArgumentException("参数非法异常… ");
}
return restTemplate.getForObject(serverURL+"/provide/"+id,String.class);
}
}
修改运行异常处理类:
package cn.wu.fallback;
import org.springframework.web.bind.annotation.PathVariable;
public class TestFallBack {
public static String testFallBack(@PathVariable("id") String id, Throwable throwable){
return "Id非法,请重新设置!";
}
}
添加Sentinel违规处理类:
package cn.wu.handler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.web.bind.annotation.PathVariable;
public class TestHandler {
public static String testHandler(@PathVariable("id") String id, BlockException blockException){
return "服务繁忙,请重新尝试!";
}
}
新增降级规则:
访问相应的URL,如果不连续多次发送请求,结果如下:
多次连续发送以上请求,结果如下(进入熔断状态…):
继续添加限流规则:
此时,多次发送请求正常的URL,也会进入到Sentinel违规处理类中,同时,多次发送异常请求,服务降级也会重叠…
规则持久化
一旦重启Sentinel应用,Sentinel中的规则将会刷新重置,而生产环境需要配置规则进行持久化
添加持久化依赖:
<!-- 将Sentinel规则持久化到nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改application.yml配置:
server:
port: 80
spring:
application:
name: nacos-consumer
cloud:
nacos:
discovery:
server-addr: 192.168.50.248:1111 # Nacos集群地址
sentinel:
transport:
dashboard: localhost:8080 # sentinel控制面板URL
datasource:
datasource1: # 持久化数据源1名称
nacos:
server-addr: 192.168.50.248:1111 # Nacos集群地址
dataId: ${spring.application.name} # 持久化配置文件DataId,保持一致
gourpId: DEFAULT_GROUP # 配置组
data-type: json # 配置类型
rule-type: flow
# 自定义服务提供者地址属性
nacos-provider-addr: http://nacos-service-provider
management:
endpoints:
web:
exposure:
include: '*' # 暴露监控点
打开Nacos管理界面,新建配置文件(这里的Data Id要和application.yml配置信息保持一致… ):
[
{
"resource": "test",
"limitiApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
- resource: 资源名称
- limitApp: 来源应用
- grade: 阈值类型,0表示线程数,1表示QPS
- count: 单机阈值
- strategy: 流控模式,0表示直接,1表示关联,2表示链路
- controlBehavior: 流控效果,0表示快速失败,1表示Warm Up,2表示排队等待
- clusterMode:Sentinel是否开启集群
通过push模式,此时只要添加规则,无论Sentinel是否发生重启,规则配置信息都会保存,也就是完成了持久化…
流控规则Json格式:
resource:资源名称
limitApp:来源应用
grade:阀值类型,0:线程数,1:QPS
count:单机阀值
strategy:流控模式,0:直接,1:关联,2:链路
controlBehavior:流控效果,0:快速失败,1:warmUp,2:排队等待
clusterMode:是否集群
降级规则Json格式:
resource:资源名称
limitApp:来源应用
grade:降级策略,0:RT,1:异常比列,2:异常数
count:阈值
timeWindow:熔断时间
minRequestAmount:最小请求数
statIntervalMs:统计时长