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中可以看到配置的规则。