1. 介绍
分布式系统中一个微服务依赖于很多的其他服务,那么服务就会不可避免的失败。原因是:A服务依赖于B,C,D 等很多的服务,当B服务不可用的时候,会一直阻塞或者异常,更不会去调用 C 服务和 D 服务。同事假设有其他的服务也依赖于 B 服务,也会碰到同样的问题,这就有可能导致雪崩效应。
如下案例:一个用户通过 web 容器访问应用,他要先后调用 A,H,I,P四个模块,一切都很正常是吧。
由于某些原因,导致 I 服务不可用,与此同时我们没有快速处理,会导致该用户一致处于阻塞状态。
当其他用户做同样的请求,也会面临着同样的问题,tomcat 支持的最大并发数是有限的,资源都是有限的,将整个服务拖垮都是有可能的。
所以,Sentinel 是一个分布式系统的延迟和容错的开源库,在分布式系统中,许多依赖会不可避免的调用失败,例如超时,异常等,Sentinel 能保证在一个依赖出现问题的情况下爱,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
断路器本身是一种开关装置,当某个服务单元发生故障后,通过断路器的故障监控(类似于保险丝),想调用者返回符合预期的,可处理的备选响应而不是长时间的等待或者抛出无法处理的异常,这样就保证了服务调用的线程不会长时间,不必要的占用,从而避免故障在分布式系统中的蔓延,乃至雪崩。
Sentinel 在网络依赖服务出现高延迟或者失败时,为系统提供保护和控制;可以进行快速失败,缩短延迟等待时间;提供失败回退(Fallback)和相对优雅的服务降级机制;提供有效的服务容错监控,报警和运维控制手段。
下载地址: Releases · alibaba/Sentinel · GitHub
2. 使用
1. 依赖
在 springcloud-alibaba-microservice-consumer 和 springcloud-alibaba-microservice-provider 工程的 pom.xml 文件导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
在存放snetinel下载的jar包的位置输入cmd 运行 java -jar sentinel-dashboard-1.7.2.jar --server.port=8081 命令启动
2. 配置
在 springcloudalibaba-micro-service-consumer 和 springcloudalibaba-micro-service-provider 工程的 application.yml 中添加 sentinel 配置
server:
port: 8080
spring:
application:
name: micro-service-consumer #服务消费方
cloud:
nacos:
discovery:
enabled: true
server-addr: 127.0.0.1:8848
sentinel:
transport:
port: 8719 #推送数据端口
dashboard: 127.0.0.1:8081 #启动端口
web-context-unify: false #false表示针对调用同一接口的不同url进行链路限制
启动服务提供方以及消费方并调用服务(多调用几次),然后查看 sentinel 的控制面板
3. sentinel 的控制面板
在浏览器输入 localhost:8081,用户名和密码都是 sentinel ,然后访问 sentinel 的控制面板
接下来对 sentinel 的控制面板一一讲解。
实时监控:
继承控制台后,当有请求时,实时监控页面会显示当前服务各个接口的访问信息,以图表的形式展示给用户,包括访问时间,通过 QPS,拒绝 QPS,响应时间(ms)等信息。
簇点链路
列表:用于展示服务所有接口,包含通过 QPS,拒绝 QPS,线程数,平均RT,分钟通过,分钟拒绝等信息,端口为服务与 sentinel 控制台交互的端口,服务本地会起一个改端口占用的 HttpServer,该 Server 会与 Sentinel 控制台做交互。
比如:Sentinel 控制台添加一个限流规则,会把规则数据 push 给这个 HttpServer 接收,HttpServer 再将规则注册到 Sentinel 中。
操作:可以点击,然后对当前资源添加流控,降级,热点,授权等想关操作
流控规则
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
资源名:唯一名称,默认是请求路径,可自定义
针对来源:流控针对的调用来源,若为 default 则不区分调用来源
阈值类型:
QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流 线程数:当调用该接口的线程数达到阈值的时候,进行限流
单机阈值:QPS或线程数的阈值数,达到阈值则限流
是否集群:是否开启集群流控,不选择则默认为单机模式
流控模式:
直接(默认):接口达到限流条件时,开启限流 关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步] 链路:当从某个接口过来的资源达到限流条件时,开启限流
流控效果:
快速失败:该方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。 Warm Up(冷启动):该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。 排队等待:这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
降级规则
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一
资源名: 要实现降级的资源。
慢调用比例:
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断
调用:一个请求发送到服务器,服务器给与响应,一个响应就是一个调用
慢调用:当调用的时间(响应的实际时间)>设置的RT的时,这个调用叫做慢调用
慢调用比例:在所以调用中,慢调用占有实际的比例,= 慢调用次数 / 调用次数 。
RT:响应时间,指系统对请求作出响应的时间。
比例阈值:RT模式下慢速请求比率的阈值。默认1.0d,自己设定, 慢调用次数 / 调用次数=比例阈值
熔断时长:断路器打开时的恢复超时(以秒为单位)。超时后,断路器将转换为半开状态以尝试一些请求,单位秒,图中表示触发熔断后,接下来的10秒内请求会自动被熔断,经过10S后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
最小请求数:可以触发熔断中断的最小请求数(在有效的统计时间范围内)。默认值为5,设置的调用最小请求数
统计时长: (单位为 ms),如 60*1000 代表分钟级(1.8.0 引入),默认1000
异常比例:
当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
异常数:
当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
代码实现
1.使用@SentinelResource注解完成对流控以及降级规则的实现
2.在springcloudalibaba-micro-consumer-8080工程中的FeignUserController中添加对应的方法进行测试
/**
1. 如果没有用blockHandler,fallback无论是违反了什么规则,都走fallback
2. blockHandler和 fallback 如果都配置了,优先走blockHandler,可以区别限流、降级、热点等。
3. blockHandler是将服务降级的方法与目标方法在同一个类中。如果不同的类中,有些方法需要使用相同的服务降级, 就可以使用blockHandlerClass来定义个一类,然后通过blockHandler来指定对应的降级的方法。方法必须是静态。
4. fallback可以处理非 sentinel 的异常。
*/
//blockHandler对应方法的名称,该方法返回值要和调用的方法一致,并且在同一类中
//@SentinelResource(value = "yangl-findUsers",blockHandler = "findUsersBlockHandler")
@SentinelResource(value = "yangl-findUsers",blockHandler = "findUsersBlockHandler",blockHandlerClass = MyBlockHandler.class)
//@SentinelResource(value = "yangl-findUsers",fallback = "findUsersfallback",fallbackClass = Myfallback.class)
@RequestMapping("findUsers")
public JsonResult findUsers(){
//int i = 1/0;
return userService.findAll();
}
// public JsonResult findUsersBlockHandler(BlockException e){
//
// JsonResult jsonResult = JsonResult.error();
//
// if (e instanceof FlowException){
// jsonResult.setMsg("流控异常");
// }else if (e instanceof DegradeException){
// jsonResult.setMsg("降级异常");
// }else{
// jsonResult.setMsg("其他sentinel异常");
// }
// return jsonResult;
// }
3.除了在本类中编写blockHandler对应的方法外,也可以在自己单独创建的类中编写
package com.qf.sentinel;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.qf.utils.JsonResult;
public class MyBlockHandler {
public static JsonResult findUsersBlockHandler(BlockException e){
JsonResult jsonResult = JsonResult.error();
if (e instanceof FlowException){
jsonResult.setMsg("流控异常");
}else if (e instanceof DegradeException){
jsonResult.setMsg("降级异常");
}else if (e instanceof ParamFlowException){
jsonResult.setMsg("热点异常");
}else{
jsonResult.setMsg("其他sentinel异常");
}
return jsonResult;
}
}
4.除了在本类中编写fallback对应的方法外,也可以在自己单独创建的类中编写
package com.qf.sentinel;
import com.qf.utils.JsonResult;
public class Myfallback {
public static JsonResult findUsersfallback(Throwable throwable){
JsonResult jsonResult = JsonResult.error();
if(throwable instanceof FlowException){
jsonResult.setData("流控异常");
}else if(throwable instanceof DegradeException){
jsonResult.setData("降级异常");
}else{
jsonResult.setData(throwable.getMessage());
}
return jsonResult;
}
}
5.新增流控或降级规则进行测试,在@SentinelResource的value对应的值的那一栏点击流控或降级进行配置
6.访问:http://localhost:8080/feign/findUsers 测试即可
热点规则
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
代码实现:
1.在UserFeignController中添加该方法
//该方法只是用于测试热点规则
@SentinelResource(value = "yangl-getById",blockHandler = "getByIdBlockHandler",blockHandlerClass = MyBlockHandler.class)
@GetMapping("getById")
public JsonResult getById(@RequestParam("uid") Integer uid){
return userService.findById(uid);
}
2.在MyBlockHandler中添加对应的方法(返回值,参数列表都一致)
package com.qf.sentinel;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.qf.utils.JsonResult;
public class MyBlockHandler {
public static JsonResult findUsersBlockHandler(BlockException e){
JsonResult jsonResult = JsonResult.error();
if (e instanceof FlowException){
jsonResult.setMsg("流控异常");
}else if (e instanceof DegradeException){
jsonResult.setMsg("降级异常");
}else if (e instanceof ParamFlowException){
jsonResult.setMsg("热点异常");
}else{
jsonResult.setMsg("sentinel异常");
}
return jsonResult;
}
//处理的方法要和调用方法的返回值以及参数一致
public static JsonResult getByIdBlockHandler(Integer uid,BlockException e){
JsonResult jsonResult = JsonResult.error();
if (e instanceof ParamFlowException){
jsonResult.setMsg("热点异常");
}else if( e instanceof AuthorityException){
jsonResult.setMsg("来源不明");
}
return jsonResult;
}
}
3.添加热点规则,设置单机阈值(表示该方法访问限制),设置对应的索引(0表示对访问方法的第一个参数的限制),点击新增
4.添加成功后,点击编辑热点规则,此时可以显示高级选项,可以对索引设置值以及限流阈值进行测试
5.访问:http://localhost:8080/feign/getById?uid=其他值 和 http://localhost:8080/feign/getById?uid=123 进行测试
授权规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
代码实现:
1.创建SentinelOriginParser类实现指定接口,指定要传递的origin参数(参数名任意)
package com.qf.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.util.StringUtil;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class SentinelOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String origin = httpServletRequest.getParameter("origin");
if(StringUtil.isBlank(origin)) {
throw new IllegalArgumentException("origin parameter is must not be empty");
}
//授权规则配置的就是该参数的值
return origin;
}
}
2.添加授权规则,white是origin的值,黑白名单表示是否允许访问
3.访问:localhost:8080/feign/getById?uid=1001&origin=white 进行测试
feign和sentinel的整合
7.1 配置application.yml
在springcloudalibaba-micro-consumer-8080工程中的application.yml中添加 feign 的配置
server:
port: 8080
spring:
application:
name: micro-service-consumer #服务消费方
cloud:
nacos:
discovery:
enabled: true
server-addr: 127.0.0.1:8848
sentinel:
transport:
port: 8719
dashboard: 127.0.0.1:8081
feign:
sentinel:
enabled: true #feign整合sentinel
7.2 配置服务降级后的处理
在 UserService 上的@FeignClient注解上添加 fallback 属性
package com.qf.service;
import com.qf.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Service
//服务提供方的名称(也可以写在方法上)
//配置fallback,当方法熔断降级后调用指定类中对应的方法
@FeignClient(value = "micro-service-provider",fallback = UserServiceHandler.class)
public interface UserService {
@RequestMapping("/user-provider/findAll")
public JsonResult findAll();
//模拟数据库操作
//查询单个
@GetMapping("/user-provider/findById")
public JsonResult findById(@RequestParam("id") Integer id);
//删除单个
@DeleteMapping("/user-provider/deleteById/{id}")
public JsonResult deleteById(@PathVariable("id") Integer id);
//添加
@PostMapping("/user-provider/addUser")
public JsonResult addUser(@RequestBody User user);
//修改
@PutMapping("/user-provider/updateUser")
public JsonResult updateUser(@RequestParam Integer id,@RequestParam String username,@RequestParam String password);
}
7.3 编写降级对应的类
编写 UserServiceHandler类,我们这里先以findById方法为例
package com.qf.service;
import com.qf.pojo.User;
import com.qf.utils.JsonResult;
import org.springframework.stereotype.Component;
//和UserService中的方法一一对应
@Component
public class UserServiceHandler implements UserService{
@Override
public JsonResult getById(Integer id) {
JsonResult jsonResult = JsonResult.error();
jsonResult.setData("sentinel异常");
return jsonResult;
}
@Override
public JsonResult findAll() {
return JsonResult.error();
}
@Override
public JsonResult findById(Integer id) {
return JsonResult.error();
}
@Override
public JsonResult deleteById(Integer id) {
return JsonResult.error();
}
@Override
public JsonResult addUser(User user) {
return JsonResult.error();
}
@Override
public JsonResult updateUser(Integer id, String username, String password) {
return JsonResult.error();
}
}
7.4 配置限流或者降级
4.在sentinel面板上找到对应的方法,配置限流或者降级
5.启动访问:http://localhost:8080/feign/findById?uid=1001&origin=white 测试即可