1.问题的产生
在分布式环境下,不可避免的会出现一些依赖服务失效的问题(dubbo,netty,socket,http,thrift,hession,rmi,webservice…)。
image.png
依赖服务bug、抛出异常、网络阻塞、服务延时、服务不可用、服务无响应、超时、线程池占满等情况都可能导致RPC出错,单一的依赖服务失效可能导致整个客户端不可用。
服务都正常的情况:
App Container是我们平时用的容器tomcat,jetty,或者是一个j2se程序。
当Dependency I不可用时, App Container线程池的线程会等待Dependency I的响应而被阻塞(tomcat默认线程池200;dubbo默认用netty通信,netty的worker线程池)。
当并发量增加时,单一依赖服务超时,就有可能堵塞整个app Container的线程池。
image.png
当整个App Container线程池被阻塞占满时,导致整个服务也不可用,其它的依赖服务也不可用。
如果其它服务(比如Dependency H)也依赖了Dependency I,这个也不用,问题会被逐渐放大,出现级联错误。整个系统就服务雪崩了。
image.png
问题产生的原因:
因为服务提供者不可用,导致服务调用者(线程资源耗尽)也不可用,并逐渐放大。
- 服务提供者不可用(程序bug、硬件故障、缓存穿透、用户大量请求)
- 重试加大流量(用户重试、代码逻辑重试、rpc retry)
- 服务调用者不可用(同步调用,产生大量等待线程占用系统资源。线程资源耗尽)
2.解决方案
2.1 常用解决方案:
- 针对硬件故障,多机房容灾。
- 针对缓存穿透,改进缓存模式,缓存预加载,改为异步刷新
- 针对服务调用者流量激增,采用服务自动扩容,应对突发流量
- 针对重试,关闭或减少重试,直接failfast
- 针对服务提供者不可用,使用资源隔离,熔断器机制。
2.2 资源隔离:
bulkhead如果一个船舱破了进水,只损失一个船舱,其它船舱不受影响。
image.png2.3 常用隔离策略:
- 线程池隔离
信号量隔离
image.png
通过将对依赖服务的访问执行放到单独的线程,将其与调用线程(例如 Tomcat 线程池中的线程)隔离开来,调用线程能空出来去做其他的工作而不至于被依赖服务的访问阻塞过长时间。
使用独立的,每个依赖服务对应一个线程池的方式,来隔离这些依赖服务,这样,某个依赖服务的高延迟只会拖慢这个依赖服务对应的线程池。
尽管线程池能提供隔离性,但你仍然需要对你的依赖服务客户端代码增加超时逻辑,并且/或者处理线程中断异常,以使这些代码不会无故地阻塞或者拖慢 Hystrix 线程池。
使用线程池的主要弊端是会增加系统 CPU 的负载,每个命令的执行,都包含了 CPU 任务的排队,调度,上下文切换。 相对于其带来的好处,其带来的负载的一点点升高对系统的影响是微乎其微的。
根据实际的业务场景设置最佳值,如果设置过小,就难以应对突发流量,如果设置的过高,则起不到隔离的目的。
image.png
用信号量去限制并发请求量,没有超时机制,而且会一直等在调用端,不能异步。
不像线程池自带缓冲队列,无法应对突发情况,当达设定的并发后,就会执行失败。因此信号量更适用于非网络请求的场景中。(仅访问内存数据的请求,一般能达到耗时在 1ms 以内,且能达到 5000rps,这样的请求对应的信号量可以设置为 1 或者 2。默认值为 10)
3.实现方案
3.1 实现方案:
- 开源框架
Hystrix、Resilience4j、Sentinel- 自研
基于历史(最近一段时间,5-10分钟)接口的调用记录,从不同维度(异常比例、响应时长),判断是否允许本次调用或调用fallback/降级服务3.1.1 开源框架:
hystrix
基于命令模式包装调用者,逻辑在run方法里
public class CommandHelloWorld extends HystrixCommand<String> {
private final String name;
public CommandHelloWorld(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() {
return "Hello " + name + "!";
}
}
执行方式
String s = new CommandHelloWorld("Bob").execute();
Future<String> s = new CommandHelloWorld("Bob").queue();
Observable<String> s = new CommandHelloWorld("Bob").observe();
resilience4j
基于Java8 and functional programming
// Simulates a Backend Service
public interface BackendService {
String doSomething();
}
// Create a CircuitBreaker (use default configuration)
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName");
// Decorate your call to BackendService.doSomething() with a CircuitBreaker
Supplier<String> decoratedSupplier = CircuitBreaker
.decorateSupplier(circuitBreaker, backendService::doSomething);
这两种框架都是调用端方式熔断,开放平台做为中转,既为调用端又为服务提供方,所以这种方案不适用。
3.1.2 自研:
3.1.2.1 服务调用在开放平台中的流程如下:
- 调用端发起请求
- 经过 Inbound 阶段
- 发送请求去 service
- service 处理
- 经过 outbound 阶段
返回响应结果给调用端
image.png3.1.2.2 熔断器
以调用次数为例:正常状态下,电路处于关闭状态,如果调用持续出错或者超时,电路被打开进入熔断状态,后续一段时间内的所有调用都会被拒绝,一段时间以后,保护器会尝试进入半熔断状态,允许少量请求进来尝试,如果调用仍然失败,则回到熔断状态,如果调用成功,则回到电路闭合状态。
image.png
两种特殊状态:
Forece_Open,强制熔断器打开
Force_Closed,强制熔断器关闭3.1.2.3 调用流程
image.png3.1.2.4 熔断策略
rollingWindow(默认5分钟) 记录滑动时间窗口内的数据
requestVolumeThreshold(默认20) 使熔断器有效的最小请求数量
sleepWindow(默认5秒钟) 熔断时间,熔断器 OPEN 状态的时长
- Latency Strategy 平均响应时长
当平均响应时长超过avgRTThreshold(默认5秒)阈值后,并且后续连续的 latencyCount次(默认5)请求的响应时长都超过 avgRTThreshold后
熔断器状态变为 OPEN,在sleepWindow时间范围内,对后续的请求都熔断- Exception Ratio Strategy 异常比例
当请求的异常比达到ratioThreshold阈值(默认90%)后
熔断器状态变为 OPEN,在sleepWindow时间范围内,对后续的请求都熔断
异常比例值的范围为 [0.0-1.0],即 0%-100%- Exception Count Strategy 异常个数
当近 recentTime(默认1分钟)的请求异常数超过 failureThreshold(默认60)阈值后
熔断器状态变为 OPEN,在sleepWindow时间范围内,对后续的请求都熔断
网友评论