二, 服务雪崩效应以及服务容错
由于tomcat等服务器支持的最大线程数有限,如果一个服务出现了问题,调用这个服务的服务器就会出现线程阻塞的情况,此时如果有大量请求进入,就会导致服务瘫痪,这个服务器上的其他接口也不能调用,继而导致其他服务也会阻塞,瘫痪。
这种传播就是服务雪崩效应。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
服务容错:常见的容错思路有隔离、超时、限流、熔断、降级这几种。
降级:类似于try/catch,在服务接口级别的try/catch
容错组件有:Hystrix,Resilience4J,Sentinel。
- Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。
- Resilience4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推荐的替代产品。不仅如此,Resilience4J还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。
- Sentinel是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
常见的容错思路有隔离、超时、限流、熔断、降级这几种,下面分别介绍一下。
- 隔离: 能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离.
- 超时: 在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
- 限流: 限流就是限制系统的输入和输出流星已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
- 熔断: 在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
- 熔断关闭状态(Closed):服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制。
- 熔断开启状态(Open)后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法。
- 半熔断状态.(Half-Open)尝试恢复服务调用,允许有限的流星调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
- 降级: 降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案。
从流量控制、熔断降级、系统负载保护展开使用sentinel
Sentinel具有以下特征:**
- 丰富的应用场景: Sentinel 承接了阿里巴巴近10年的双十—大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控: Sentinel提供了实时的监控功能。通过控制台可以看到接入应用的单台机器秒级数据,甚至500台以下规模的集群的汇总运行情况。
- 广泛的开源生态: Sentinel提供开箱即用的与其它开源框架/库的整合模块,例如与Spring Cloud、Dubbo、gRPC的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
- 完善的SPI扩展点: Sentinel提供简单易用、完善的SPI扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel分为两个部分:
- 核心库(Java客户端)不依赖任何框架/库,能够运行于所有Java运行时环境,同时对Dubbo / Spring Cloud等框架也有较好的支持。
- 控制台(Dashboard)基于Spring Boot开发,打包后可以直接运行,不需要额外的Tomcat 等应用容器。
Sentinel预设五种规则
- 流控规则
- 资源名 /order/message1
- 阈值类型QPS: 每秒查询率
- 阈值类型 线程数: 最大并发线程
- 单机阈值 2 : 每秒允许两次查询
- 降级规则
- 降级策略:RT:一秒内的请求平均响应时间,时间窗口:等待的时间;
- 异常比例:一秒内的请求异常的数量/请求总数量,时间窗口:等待时间;
- 异常数:一分钟产生的异常个数,时间窗口:等待时间(需要大于60s)
- 热点规则 :带参数的流控规则
- 系统规则 :服务器维度的流控。
- 授权规则 :判断请求头/参数,确定是否为白名单/黑名单,下面是一个配置类,,请求发出的时候的该类就可以从url上获取request域的源标识。
4.1 在shop-order的pom中导入依赖(容错在上游服务上,调用者)
<!--sentinel 服务熔断和降级-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
4.2 Sentinel控制台需要启动jar包 sentinel-dashboard-1.7.0.jar
打开jar包所在的文件夹,cmd ,输入命令
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
port端口号,ashboard.server 定义好的sentinel控制台服务的地址 ,Dproject.name项目名称
4.3 访问localhost:8080 进入控制台 ( 默认用户名密码是sentinel/sentinel )
注:需要微服务访问一次,才能刷新加载出微服务的资源信息。
4.4 修改配置(每个集成了Sentinel的微服务模块都要加):
spring:
cloud:
sentinel:
transport:
port: 9999 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:8080 # 指定控制台服务的地址
4.5 此时访问http://localhost:8091/order/message1 会在Sentinel控制台的实时监控里看到图表
4.6 在Sentinel控制台的流控规则里新增流控规则 ----------------------限流
- 资源名 /order/message1
- 阈值类型QPS: 每秒查询率
- 单机阈值 2 : 每秒允许两次查询
再次/order/message1,每秒访问次数达到上限就会显示 Blocked by Sentinel (flow limiting)
----------------------
降级,流控,熔断的区别:
流控超过频率就把请求完全屏蔽等频率降下来就继续提供服务,
降级超过频率采用备用服务或者丢弃部分资源,等待一定时间以后继续提供服务。
熔断直接停用服务,等一段时间后再去尝试是否可以调用服务。
流控规则之高级配置-链路 通过在service中设置流控属性值
流控规则
- 资源名 /order/message1
- 阈值类型QPS: 每秒查询率
- 阈值类型 线程数: 最大并发线程
- 单机阈值 2 : 每秒允许两次查询
应用并不多,了解就好
4.7 在shop-order的service包中创建一个OrderSentinelServiceImpl.java
@Service
@Slf4j
public class OrderSentinelServiceImpl {
//定义一个资源
//定义当资源内部发生异常的时候的处理逻辑
@SentinelResource(
value = "message" //指定message为资源名
)
public String message() {
return "message";
}
}
4.8 通过在controller包中OrderControllerSentinel
@RestController
@Slf4j
public class OrderControllerSentinel {
@Autowired
private OrderSentinelServiceImpl orderSentinelService;
@RequestMapping("/order/message1")
public String message1() {
orderSentinelService.message(); //调用
return "message1";
}
@RequestMapping("/order/message2")
public String message2() {
orderSentinelService.message();
return "message2";
}
}
4.9 在高级设置里 流控模式=>链路,阈值=>1,需要改cloudAlibaba版本
<alibaba.version>2.1.1.RELEASE</alibaba.version>
将不用的nacos-config依赖注释掉
<!-- <dependency>-->
<!-- <groupId>com.alibaba.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>-->
<!-- </dependency>-->
4.10 在配置文件里添加配置,并编写配置类FilterContextConfig
filter: # 配置文件中关闭sentinel的CommonFilter实例化
enabled: false
// 测试流控高级设置-链路
@Configuration
public class FilterContextConfig {
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口资源关闭聚合
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
}
4.11 再在sentinel控制台中的簇点链路=>选一个资源流控=>高级=>设置阈值1=>入口资源任意,再次访问massage1或2
- 页面报500错误
- idea后台报错com.alibaba.csp.sentinel.slots.block.flow.FlowException: null
流控效果:
快速失败:当一个资源被流控时,再次访问该资源时直接出异常界面。(直接抛异常)
Warm up:预热,逐渐放开频率限制,阈值逐渐增长。
排队等待:处理不过来的请求会放到队列中等待被处理。
---------------------------------------------
降级规则
- 降级策略:RT:一秒内的请求平均响应时间,时间窗口:等待的时间;
- 异常比例:一秒内的请求异常的数量/请求总数量,时间窗口:等待时间;
- 异常数:一分钟产生的异常个数,时间窗口:等待时间(需要大于60s)
4.12 在sentinel控制台中的簇点链路=>选一个资源降级> -------------------RT
RT:1
时间窗口:5
注意Sentinel默认统计的RT上限是4900 ms,超出此阈值的都会算作4900 ms,
若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx来配置。
4.13 访问该资源http://localhost:8091/order/message1,报以下错降级成功
报错: Blocked by Sentinel (flow limiting)
4.14 在sentinel控制台中的簇点链路=>选一个资源降级> -----------------异常比例
异常比例:一秒内的请求异常的数量/请求总数量,时间窗口:等待时间;
需要修改方法添加测试代码
public class OrderControllerSentinel {
int i = 0;
//测试降级策略的异常比例
@RequestMapping("/order/message1")
public String message1() {
//---------------------------
//测试降级的异常比例
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message1";
}
}
4.15 在sentinel控制台中的簇点链路=>选一个资源降级>
降级策略 :异常比例
异常比例 :0.25
时间窗口 :5 #降级时间为5S
4.16 访问该资源http://localhost:8091/order/message1,报以下错降级成功,
报错: Blocked by Sentinel (flow limiting)
4.17 在sentinel控制台中的簇点链路=>选一个资源降级> -----------------异常数
异常数:一分钟产生的异常个数,时间窗口:等待时间(需要大于60s)
降级策略 :异常数
异常数 : 3 #分钟为单位
时间窗口 :70 #要大于60秒
---------------------------------------------
4.18 在sentinel控制台中的簇点链路=>选一个资源=>热点规则
热点规则:带参数的流控规则
4.19 Sentinel控制台需要启动jar包 sentinel-dashboard-1.7.0.jar
打开jar包所在的文件夹,cmd ,输入命令
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
port端口号,ashboard.server 定义好的sentinel控制台服务的地址 ,Dproject.name项目名称
4.20 添加测试代码
@RequestMapping("/order/message3")
@SentinelResource("message3")
public String message3(String name, Integer age) {
return "message3" + name + age;
}
4.21 热点规则的参数
资源名 :固定
限流模式 :QPS
参数索引 :0 //对第几个参数限流,索引下标从0开始
单机阈值 :3
统计窗口时长 :1
是否集群 :否
-------------高级选项(用处不大)
参数类型 : int/double/long/string
参数值 : 15
限流阈值 : 10000 //可以比单机阈值大很多
---------------------------------------------
4.22 授权规则 —处理接口来源
授权规则 :判断请求头/参数,确定是否为白名单/黑名单,下面是一个配置类,请求发出的时候的该类就可以从url上获取request域的源标识。
- 若配置白名单,则只有请求来源位于白名单内时才可通过;
- 若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。
授权规则参数
流控应用 : 其实这个位置要填写的是来源标识,Sentinel提供了RequestOriginParser接口来处理来源。只要Sentinel保护的接口资源被访问,Sentinel就会调用RequestOriginParser的实现类去解析访问来源。
授权类型 : 白名单、黑名单 (通过和不通过)
4.23 配置授权规则代码
@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
//定义区分来源: 本质作用是通过request域获取到来源标识
//app pc
//然后 交给流控应用 位置进行匹配
@Override
public String parseOrigin(HttpServletRequest request) {
String serviceName = request.getParameter("serviceName");
if (StringUtils.isEmpty(serviceName)){
throw new RuntimeException("serviceName is not empty");
}
return serviceName;
}
}
4.24 在sentinel控制台中的簇点链路=>选一个资源=>热点规则
流控应用 :pc
授权类型 :白名单
4.25 访问 localhost:8091/order/message1?serviceName=app 以及 serviceName=pc
---------------------------------------------
4.26 系统规则 (很少用,属于运维层次了)
前四种是针对资源级别的,系统规则是针对应用级别的(目的是不被外界影响)
- 系统规则 :服务器维度的流控。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量(进入应用的流量)生效。参数:
- Load(仅对Linux/Unix-like机器生效)︰当系统load1(一分钟内的平均负载),超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的maxQps * minRt 计算得出。设定参考值一般是CPU cores * 2.5。. RT:当单台机器上所有入口流量的平均RT达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口QPS:当单台机器上所有入口流量的QPS达到阈值即触发系统保护。
- CPU使用率:当单台机器上所有入口流量的CPU使用率达到阈值即触发系统保护。
---------------------------------------------
测试 自定义返回异常页面 config包里写配置类ExceptionHandlerPage.java
//自定义异常返回页面
@Component
public class ExceptionHandlerPage implements UrlBlockHandler {
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException e) throws IOException {
response.setContentType("application/json;charset=utf-8");
ResponseData responseData = null;
//BlockException 异常接口,包含Sentinel的五个异常
// FlowException 限流异常
// DegradeException 降级异常
// ParamFlowException 参数限流异常
// AuthorityException 授权异常
// SystemBlockException 系统负载异常
if (e instanceof FlowException) {
responseData = new ResponseData(-1, "接口被限流了...");
} else if (e instanceof DegradeException) {
responseData = new ResponseData(-2, "接口被降级了...");
}
response.getWriter().write(JSON.toJSONString(responseData));
}
}
@Data
@AllArgsConstructor//全参构造
@NoArgsConstructor
//无参构造
class ResponseData {
private int code;
private String message;
}
在sentinel控制台中的簇点链路=>选一个资源=>设置流控或者降级规则。发现报错已经是预设的样子了
-------------------------------------------------------
@SentinelResource有value属性,定义资源名,有blockHandler,后面跟函数名,处理sentinel的异常。fallback后面跟函数名。处理所有异常。
在之前的 OrderSentinelServiceImpl中有设置
@Service
@Slf4j
public class OrderSentinelServiceImpl {
// int i = 0;
//SentinelResource 定义一个资源
//定义当资源内部发生异常的时候的处理逻辑
//blockHandler 定义当资源内部发生了BlockException应该进入的方法[捕获的是Sentinel定义的异常]
//fallback 定义当资源内部发生了Throwable应该进入的方法
@SentinelResource(
value = "message",
blockHandlerClass = OrderSentinelServiceImplBlockHandler.class,
blockHandler = "blockHandler",
fallbackClass = OrderSentinelServiceImplFallback.class,
fallback = "fallback"
)
public String message(String name) {
return "message";
}
}
进入Sentinel中有设置流控看报错显示情况
---------------------------------------------
Sentinel规则持久化 ,持久化到本地文件
1 config包下添加一个固定的配置类
// Sentinel规则持久化
public class FilePersistence implements InitFunc {
}
2 在resources包下添加 :META-INF.services 文件夹
3 META-INF.services文件夹下新建文件: com.alibaba.csp.sentinel.init.InitFunc
4 在com.alibaba.csp.sentinel.init.InitFunc文件里加入全类路径名: springcloud.config.FilePersistence
---------------------------------------------
Feign整合Sentinel
1 fallBack容错类
1.1 在配置文件中开启Feign对Sentinel的支持
feign: #在配置文件中开启Feign对Sentinel的支持
sentinel:
enabled: true
1.2 新建容错类 需要实现feign所在接口,并实现接口中所有方法,一旦远程调用出错,即进入容错类的同名方,ProductServiceFallback
这是一个容错类
它要求实现Feign所在接口,并实现里面的方法
当feign调用出现问题的时候,就会进入到当前类中同名方法中
@Service
public class ProductServiceFallback implements ProductService {
@Override
public Product findByPid(Integer pid) {
Product product = new Product();
product.setPid(-100);
product.setPname("商品微服务调用出现异常了,已经进入到了容错方法中");
return product;
}
}
!!! fallBack有个缺陷,就是远程调用失败转容错类的时候不会把远程调用的异常信息抛出来。
1.3 开启容错类 fallback = ProductServiceFallback.class
//value用于指定调用nacos下哪个微服务
//fallback 指定当调用出现问题之后,要进入到哪个类中的同名方法之下执行备用逻辑
@FeignClient(
value = "service-product",
fallback = ProductServiceFallback.class
)
public interface ProductService {
//@FeignClient的value + @RequestMapping的value值 其实就是完成的请求地址 "http://service-product/product/" + pid
//指定请求的URI部分
@RequestMapping("/product/{pid}")
Product findByPid(@PathVariable Integer pid);
}
1.4在访问路径上添加测试代码
public class OrderController {
//下单--feign
@RequestMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);
//调用商品微服务,查询商品信息
Product product = productService.findByPid(pid);
//========================================================
//模拟超时 使用容错类的时候的测试
if (product.getPid() == -100) {
Order order = new Order();
order.setOid(-100L);
order.setPname("下单失败");
return order;
}
//========================================================
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
//下单(创建订单)
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(pid);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.createOrder(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
return order;
}
}
1.5启动各项服务,访问 http://localhost:8091/order/prod/1 ,访问正常,再关掉提供者Product服务,就会进入备用逻辑ProductServiceFallback
---------------------------------------------
2 fallBackFactory容错类
添加容错类实现FallbackFactory<>接口范型里面是feign的接口类,需要重写方法create,返回值就是feign的接口类。用匿名内部类,或者Lambda表达式的方式创建新的接口类,并实现
这是容错类,他要求我们要是实现一个FallbackFactory<要为哪个接口产生容错类>
@Slf4j
@Service
public class ProductServiceFallbackFactory implements FallbackFactory<ProductService> {
//Throwable 这就是feign在调用过程中产生异常
@Override
public ProductService create(Throwable throwable) {
return new ProductService() {
@Override
public Product findByPid(Integer pid) {
log.error("{}",throwable);
Product product = new Product();
product.setPid(-100);
product.setPname("商品微服务调用出现异常了,已经进入到了容错方法中");
return product;
}
};
}
}
开启容错类 fallback = ProductServiceFallbackFactory.class
//value用于指定调用nacos下哪个微服务
//fallback 指定当调用出现问题之后,要进入到哪个类中的同名方法之下执行备用逻辑
//fallback和fallbackFactory都是做容错的,二选一
@FeignClient(
value = "service-product",
// fallback = ProductServiceFallback.class ,
fallbackFactory = ProductServiceFallbackFactory.class
)
public interface ProductService {
//@FeignClient的value + @RequestMapping的value值 其实就是完成的请求地址 "http://service-product/product/" + pid
//指定请求的URI部分
@RequestMapping("/product/{pid}")
Product findByPid(@PathVariable Integer pid);
}