服务瘫痪的问题
微服务架构下的服务之间会频繁进行调用,由于网路或者其他因素,服务不能保证100%可用,倘若一个服务出了问题,就有可能引发整个系统的瘫痪,这是开发者不希望看到的情况。我们把微服务架构的应用比作是一家餐厅,餐厅里面分为前台接待员,点菜员,厨师,端菜员,收银员,这5个岗位好比是5个微服务,他们之间正常的调用才能服务好客户,如果其中一个岗位出了问题导致整个餐厅无法正常工作,那这个餐厅就太脆弱了。
下面通过代码来模拟一个微服务出现问题引发整个系统瘫痪的情况。修改之前pay模块中的Controller,在pay方法中利用线程睡眠模拟网络延迟,然后再添加一个退款的方法:
package com.monkey1024.controller;
import com.monkey1024.service.PayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/*
支付服务
*/
@RestController
public class PayController {
@Autowired
private PayService payService;
@GetMapping("pay")
public String pay() {
//模拟网络延迟的问题
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//支付操作
payService.pay();
return "支付成功";
}
@GetMapping("refund")
public String refund() {
return "退款成功";
}
}
修改pay模块中的配置文件,将tomcat可以处理的线程设置为1
#tomcat端口号
server.port=8084
#最大线程数
server.tomcat.threads.max=1
#给微服务命名
spring.application.name=pay-service
#注册中心nacos的地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
buy模块中再添加一个cancel方法来调用上面的退款服务,下面的payUrl的值是从配置文件中获取的,即将微服务的地址放到配置文件中,通过代码来读取,这样做到了配置和代码的分离。
package com.monkey1024.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/*
购买
*/
@RestController
public class BuyController {
@Autowired
private RestTemplate restTemplate;
//从配置文件中读取调用服务的地址
@Value("${service-url.pay-service}")
private String payUrl;
@GetMapping("buy")
public String buy() {
//通过微服务实例对象获取其地址和端口号进行调用
String msg = restTemplate.getForObject(payUrl+"/pay", String.class);
return "购买成功:" + msg;
}
/*
取消交易
*/
@GetMapping("cancel")
public String cancel() {
String msg = restTemplate.getForObject(payUrl + "/refund", String.class);
return msg;
}
}
buy模块的配置文件:
#tomcat端口号
server.port=8086
#给微服务命名
spring.application.name=buy-service
#注册中心nacos的地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#要调用微服务的名字,通过controller中的payUrl获取
service-url.pay-service:http://pay-service
将上面两个模块启动之后,打开第一个浏览器访问buy,然后迅速的打开第二个浏览器访问cancel,此时可以看到cancel是无法正常访问的。因为我们设置了tomcat处理的线程是1,并且在buy调用的服务中让线程睡眠了,这期间系统是无法处理其他请求的,倘若此时有大量请求涌入,系统就瘫痪了。
服务雪崩效应
在造船领域中有一个名词叫做水密隔舱,他解决的就是类似雪崩的问题:
微服务架构下,多个微服务之间会进行调用,倘若一个微服务出现了问题,故障会进行传播,此时就导致整个系统出现灾难性的后果,这个就是服务的雪崩效应。
如下图所示,A服务调用了B服务,B服务调用了C服务,C服务出现了问题。
此时B服务会不断的调用C服务,这期间会有新的请求A继续调用B服务,导致调用大量的积压,产生大量的调用等待和重试调用,这样会慢慢的耗尽B服务的资源,导致B出现问题。
道理同上,随着新的请求的到底,A服务的资源也就慢慢耗尽了,这样整个系统就瘫痪了。
服务雪崩出现的原因有很多种,比如流量激增,线程的等待,程序的bug,硬件故障等,开发者无法完全杜绝雪崩源头的发生,只有做好容错,当一个服务出现问题之后不会引发其他服务的故障。
常见的容错方案有下面几个:
- 限流顾名思义就是限制客户端调用的流量,比如设置qps为100,超出之后直接拒绝请求。
- 熔断当下游服务出现问题不可调用之后,就不再继续调用了,切断对下游服务的调用,相当于是牺牲局部保全整体。日常生活中电源的保险丝使用的就是这个原理。服务熔断一般有三种状态:
- 熔断关闭状态(Closed)
服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制 - 熔断开启状态(Open)
后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法 - 半熔断状态(Half-Open)
尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
- 熔断关闭状态(Closed)
- 降级当下游服务不可调用之后,直接返给用户一个友好的提示信息或者调用其他备选服务。好比做事的时候列出了A计划和B计划,如果A计划失败的话就执行B计划。
- 隔离好比是疫情期间,需要对患者进行隔离,这样就会切断病毒的传播路径,减少感染人数。常见的隔离方式有:线程池隔离和信号量隔离。
常见的容错组件
- Hystrix
Hystrix是由Netflix开源的一个延迟和容错库,目前已经停止开发了。 - Resilience4J
Resilience4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,是Hystrix官方推荐的替代产品。 - Sentinel
Sentinel 是阿里巴巴开源的一款组件,本身在阿里内部已经被大规模采用,非常稳定。
三者的对比
sentinel | Hystrix | resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口 | 滑动窗口 | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持预热模式、匀速器模式、预热排队模式 | 不支持 | 简单的 Rate Limiter模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 | 无,可对接其它监控系统 |