先看下如下图,两个服务之间的调用 A服务调用另外一个B服务。
图片
在这个图当中有个接口A需要调用另外一个服务的接口B。这里看似没有什么问题。
例如,本身A服务接口执行逻辑需要5ms执行完后再调用B服务接口的,调用B接口执行完成需要4s,比如下面的下定单扣库存的流程:
图片
这里整个接口调用链执行完成需要实际T=4s+5ms;
当有大量的请求调用时我们的所有线程都会被阻塞T的时间。本身Tomcat线程池的线程数量是有限的,这就会造成很多的客户端只能等待,尤其是越是后来的请求等待的时间会越长,同时由于这一个接口把所有的tomcat线程全部占用完,导致了其他的所有服务不可用全部处于等待状态,从而会拖垮整个tomcat,而这种现象我们称诶雪崩效应。
对于服务之间的调用我们也应该能够设置一个超时时间,不能让其一直等待下去。当超过了给定的时间后我们也能够给予用户一个响应,通过这种方式来避免这种级联的故障。如上所述,当整个A服务不可用的时候 这时候的B服务也就不可用了,这种现象被称为雪崩效应。
雪崩效应常见场景
- 硬件故障:如服务器宕机,机房断电,光纤被挖断等。
- 流量激增:如异常流量,重试加大流量等。
- 缓存穿透:一般发生在应用重启,所有缓存失效时,以及短时间内大量缓存失效时。大量的缓存不命中,使请求直击后端服务,造成服务提供者超负荷运行,引起服务不可用。
- 程序BUG:如程序逻辑导致内存泄漏,JVM长时间FullGC等。
- 同步等待:服务间采用同步调用模式,同步等待造成的资源耗尽。
雪崩效应应对策略
针对造成雪崩效应的不同场景,可以使用不同的应对策略,没有一种通用所有场景的策略,参考如下:
- 硬件故障:多机房容灾、异地多活等。
- 流量激增:服务自动扩容、流量控制(限流、关闭重试)等。
- 缓存穿透:缓存预加载、缓存异步加载等。
- 程序BUG:修改程序bug、及时释放资源等。
- 同步等待:资源隔离、MQ解耦、不可用服务调用快速失败等。资源隔离通常指不同服务调用采用不同的线程池;不可用服务调用快速失败一般通过熔断器模式结合超时机制实现。
在程序中我们能通过Hystrix来实现资源的隔离,保护我们的服务,不至于导致整个tomcat服务不可用。
Hystrix是Netflix开源的一款针对分布式系统的延迟和容错库,目的是用来隔离分布式服务故障。它提供线程和信号量隔离,以减少不同服务之间资源竞争带来的相互影响;提供优雅降级机制;提供熔断机制使得服务可以快速失败,而不是一直阻塞等待服务响应,并能从中快速恢复。Hystrix通过这些机制来阻止级联失败并保证系统弹性、可用。通过下图来理解:
图片
也就是在调用B服务接口的时候我们放到另外的一个线程中去执行,防止出现上面说的问题。
接下来我们来通过程序代码来看看如何使用hystrix。
这里我们以调用订单服务为例
pom.xml加入依赖
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
2.2.1.RELEASE
方式1:
通过继承HystrixCommand
public class OrdersCommand extends HystrixCommand {
private RestTemplate restTemplate ;
private Long id ;
public OrdersCommand(RestTemplate restTemplate, Long id) {
super(buildSetter()) ;
this.restTemplate = restTemplate ;
this.id = id ;
}
private static Setter buildSetter() {
com.netflix.hystrix.HystrixThreadPoolProperties.Setter threadPoolProp = com.netflix.hystrix.HystrixThreadPoolProperties.Setter() ;
threadPoolProp.withCoreSize(5)
.withKeepAliveTimeMinutes(5)
.withMaxQueueSize(Integer.MAX_VALUE)
.withQueueSizeRejectionThreshold(1000) ;
com.netflix.hystrix.HystrixCommandProperties.Setter commandProp = com.netflix.hystrix.HystrixCommandProperties.Setter() ;
commandProp.withCircuitBreakerEnabled(true)
.withExecutionTimeoutInMilliseconds(6000)
.withRequestCacheEnabled(true)
.withExecutionIsolationStrategy(ExecutionIsolationStrategy.THREAD);
return Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("orders"))
.andCommandKey(HystrixCommandKey.Factory.asKey("getOrder"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("order-pool"))
.andThreadPoolPropertiesDefaults(threadPoolProp)
.andCommandPropertiesDefaults(commandProp) ;
}
@Override
protected Orders run() throws Exception {
return restTemplate.getForObject("http://localhost:9810/orders/queryOrder/{1}", Orders.class, id);
}
@Override
protected Orders getFallback() {
return new Orders() ;
}
@Override
protected String getCacheKey() {
return "order-" + this.id ;
}
}
这里我们实现了父类的getFallback方法
该方法为当服务调用失败或者超时会被调用。
这里通过buildSetter方法来构建hystrix相关的配置。说明:
threadPoolProp.withCoreSize(5)
.withKeepAliveTimeMinutes(5)
.withMaxQueueSize(Integer.MAX_VALUE)
.withQueueSizeRejectionThreshold(1000) ;
以上是对线程池的配置。
其它设置:
RequestVolumeThreshold
HystrixCommandProperties.Setter().withCircuitBreakerRequestVolumeThreshold(int)表示在滑动窗口中,至少有多少个请求,才可能触发断路。Hystrix 经过断路器的流量超过了一定的阈值,才有可能触发断路。比如说,要求在 10s 内经过断路器的流量必须达到 20 个,而实际经过断路器的流量才 10 个,那么根本不会去判断要不要断路。
ErrorThresholdPercentage
HystrixCommandProperties.Setter().withCircuitBreakerErrorThresholdPercentage(int)表示异常比例达到多少,才会触发断路,默认值是 50(%)。如果断路器统计到的异常调用的占比超过了一定的阈值,比如说在 10s 内,经过断路器的流量达到了 30 个,同时其中异常访问的数量也达到了一定的比例,比如 60% 的请求都是异常(报错 / 超时 / reject),就会开启断路。
SleepWindowInMilliseconds
HystrixCommandProperties.Setter().withCircuitBreakerSleepWindowInMilliseconds(int)断路开启,也就是由 close 转换到 open 状态(close -> open)。那么之后在 SleepWindowInMilliseconds 时间内,所有经过该断路器的请求全部都会被断路,不调用后端服务,直接走 fallback 降级机制。而在该参数时间过后,断路器会变为 half-open 半开闭状态,尝试让一条请求经过断路器,看能不能正常调用。如果调用成功了,那么就自动恢复,断路器转为 close 状态。
EnabledHystrixCommandProperties.Setter().withCircuitBreakerEnabled(boolean)控制是否允许断路器工作,包括跟踪依赖服务调用的健康状况,以及对异常情况过多时是否允许触发断路。默认值是 true。
ForceOpen
HystrixCommandProperties.Setter().withCircuitBreakerForceOpen(boolean)如果设置为 true 的话,直接强迫打开断路器,相当于是手动断路了,手动降级,默认值是 false。
ForceClosedHystrixCommandProperties.Setter().withCircuitBreakerForceClosed(boolean)
如果设置为 true,直接强迫关闭断路器,相当于手动停止断路了,手动升级,默认值是 false。
参考:
图片
com.netflix.hystrix.HystrixCommandProperties.Setter commandProp = com.netflix.hystrix.HystrixCommandProperties.Setter() ;
commandProp.withCircuitBreakerEnabled(true)
.withExecutionTimeoutInMilliseconds(6000)
.withRequestCacheEnabled(true)
.withExecutionIsolationStrategy(ExecutionIsolationStrategy.THREAD);
以上是对命令属性的配置。
withCircuitBreakerEnabled:控制是否允许断路器工作,包括跟踪依赖服务调用的健康状况,以及对异常情况过多时是否允许触发断路。默认值是 true。
withExecutionTimeoutInMilliseconds:执行超时时间的设置。如果一个 command 运行时间超过了设定的时长,那么就被认为是 timeout,然后 Hystrix command 标识为 timeout,同时执行 fallback 降级逻辑。
withExecutionIsolationStrategy:执行隔离的策略,这里设置为线程。还可以设置为基于信号量的
ExecutionIsolationStrategy.SEMAPHORE:一般如果并发量比较大的情况下我们用信号量,性能要好。如果并发量大你还用线程池,那么你该创建多少的线程呢?而过多的线程带来了更多线程的切换而影响性能。
return Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("orders"))
.andCommandKey(HystrixCommandKey.Factory.asKey("getOrder"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("order-pool"))
.andThreadPoolPropertiesDefaults(threadPoolProp)
.andCommandPropertiesDefaults(commandProp) ;
withGroupKey:服务分组;比如这里调用订单系统就是一个服务分组。模块;
andCommandKey:服务标识;比如这里订单系统有一个获取订单信息服务。子模块;
andThreadPoolKey:线程池名称;
andThreadPoolPropertiesDefaults:线程池配置;
andCommandPropertiesDefaults:命令属性配置;
示例:
@GetMapping("/custom/{id}")
public Object custom(@PathVariable Long id) {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext() ;
try {
OrdersCommand command = new OrdersCommand(restTemplate, id) ;
System.out.println(Thread.currentThread().getName() + ": " + System.currentTimeMillis()) ;
Orders res = command.execute() ;
} finally {
ctx.shutdown() ;
}
return null ;
}
注意:HystrixRequestContext ctx =HystrixRequestContext.initializeContext() ;这行代码必须调用。
方式2:
通过注解的方式。
@Service
public class RemoteHystrixService {
@Resource
private RestTemplate restTemplate ;
/**
*
* groupKey: 服务分组;比如这里调用订单系统就是一个服务分组。模块
* commandKey: 服务标识;比如这里订单系统有一个获取订单信息服务。子模块
* threadPoolKey: 线程池名称;
* threadPoolProperties:线程池配置
*
* @author 爷爷
* @param id
* @return Orders
*/
@HystrixCommand(fallbackMethod = "defaultOrder",
groupKey = "orders",
commandKey = "getOrder",
threadPoolKey = "order-pool",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "10"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "5"),
@HystrixProperty(name = "maxQueueSize", value = "1000000"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "1000")
},
commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "6000")
}
)
public Orders getOrder(Long id) {
System.out.println(Thread.currentThread() + ", start") ;
Orders res = restTemplate.getForObject("http://localhost:9810/orders/queryOrder/{1}", Orders.class, id);
System.out.println(Thread.currentThread() + ", end") ;
return res ;
}
public Orders defaultOrder(Long id) {
return new Orders() ;
}
}
这里具体注解属性的说明与方式1中 一一对应。