服务雪崩就是:一个服务不可用,导致一系列服务不可用,而这种后果往往无法预料。
造成雪崩原因可以归结为以下三个:
1,服务提供者不可用(硬件故障,程序bug,缓存击穿,用户大量请求)
2,重试加大流量(用户重试,代码逻辑重试)
3,服务调用者不可用(同步等待造成的资源耗尽)
解决方案有如下5个,其中隔离包括两种:
1,降级:超时降级,资源不足时(线程或信号量)降级,降级后可以配合降级接口放回托底数据。实现一个fallback方法,当请求后端服务出现异常的时候,可以使用fallback方法返回的值;
2,隔离(线程池隔离和信号量隔离):限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用;
3,熔断:当失败率(如因网络故障或超时 造成的失败率高)达到阈值自动触发降级,熔断器触发的快速失败会进行快速恢复;
4,缓存:提供了请求缓存;
5,请求合并:提供了请求合并。
下面的雪崩解决方案是基于ribbon的,即在发送请求的时候,需要自己手工构造远程调用。
场景:比如一个订单系统请求一个库存系统,一个请求发过去,由于各种原因,网络超时,在规定的时间内没有返回响应结果,这个时候更多的请求过来了,不断地请求库存服务,不断的创建线程,由于没有返回,也就没有资源的释放。时间一长,必将耗尽系统资源,导致系统崩溃。本来你的订单系统好好的,但是请求了一个有问题的库存系统,导致订单系统也崩溃了,然后订单系统又不断调用其他的依赖系统,然后导致其他依赖系统也崩溃了,造成服务雪崩。这个时候hystrix可以实现快速失败。
如果hystrix在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作而导致系统资源被耗尽。这时hystrix进行fallback操作来服务降级。
Fallback相当于是降级操作。对于查询操作,我们可以实现一个fallback方法,当请求后端服务出现异常的时候,可以使用fallback方法返回的值。fallback方法的返回值一般是默认的值或者是从缓存中得到的值,通知后面的请求服务暂时不可用了。
下面是一个实例:
(1)在e-book-consumer模块下新建子模块为e-book-consumer-hystrix,在新的子模块下新建maven项目为e-book-consumer-hystrix-ribbon-fallback。
(2)拷贝eureka-consumer项目的文件和代码到该项目。
(3)修改pom文件,加入如下依赖:
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-hystrix
(4)修改配置文件的端口和应用名
(5)改造ProductService.java。
package com.twf.e.book.consumer.hystrix.ribbon.fallback.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import com.twf.e.book.consumer.hystrix.ribbon.fallback.domain.Product;
@Service
public class ProductService {
@Autowired
private LoadBalancerClient loadBalancerClient; // ribbon的负载均衡客户端
// @HystrixCommand这个注解指定了fallbackMethod为fallback。
@HystrixCommand(fallbackMethod="fallback",commandProperties={
@HystrixProperty(name=HystrixPropertiesManager.FALLBACK_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,value="15")
})
public List listProduct() {
ServiceInstance si = loadBalancerClient.choose("e-book-product");
StringBuffer sb = new StringBuffer("");
sb.append("http://");
sb.append(si.getHost());
sb.append(":");
sb.append(si.getPort());
sb.append("/product/list");
System.out.println(sb.toString());
RestTemplate restTemplate = new RestTemplate();
ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>(){};
ResponseEntity> resp = restTemplate.exchange(sb.toString(), HttpMethod.GET, null, typeRef);
List plist = resp.getBody();
return plist;
}
// 当调用远程服务出现异常的时候,会调用这个方法
public List fallback() {
List list = new ArrayList();
list.add(new Product(-1,"fallback"));
return list;
}
}
(6)启动注册中心,启动e-book-product-core和这个项目。
(7)访问http://localhost:8100/list,得到如下结果:
若关闭e-book-product-core,再访问http://localhost:8100/list,则得到如下结果:
(8)源码点这里。
比如一个请求过来请求我userId=1的数据,你后面的请求也过来请求同样的数据,这时我不会继续走原来的那条请求链路了,而是把第一次请求缓存过了,把第一次的请求结果返回给后面的请求,这样在一定程度上减少了请求的次数,降低网络资源的压力,减少雪崩发生的概率。
1,首先在本地安装redis,进入https://github.com/MicrosoftArchive/redis/releases下载安装包:
然后安装。安装成功之后,进入安装目录,做如下操作,若能顺利执行,说明redis安装成功:
2,在e-book-consumer-hystrix模块下新建模块e-book-consumer-hystrix-ribbon-cache,拷贝e-book-consumer-hystrix-ribbon-fallback的文件及代码到这个项目。
3,修改pom.xml文件:
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-hystrix
org.springframework.boot
spring-boot-starter-data-redis
4,修改配置文件:
spring.application.name=e-book-consumer-hystrix
server.port=8101
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8762/eureka/
#redis数据索引(默认为0)
spring.redis.database=1
#redis服务器地址
spring.redis.host=127.0.0.1
#redis服务器连接端口
spring.redis.port=6379
#redis服务器连接密码
spring.redis.password=
#连接池最大连接数(负值表示没有限制)
spring.redis.pool.max-active=100
#连接池最大阻塞等待时间(负值表示没有限制)
spring.redis.pool.max-wait=3000
#连接池最大空闲连接数
spring.redis.pool.max-idle=200
#连接池最小空闲连接数
spring.redis.pool.min-idle=50
5,改造启动类
@EnableCaching // 开启缓存
@EnableCircuitBreaker // 开启服务降级断路器
@EnableEurekaClient
@SpringBootApplication
public class ConsumerHystrixRibbonCacheApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerHystrixRibbonCacheApplication.class, args);
}
}
6,改造service类,添加红框的代码。
7,改造controller类。
8,启动注册中心,然后运行这个类。
9,浏览器分别访问http://localhost:8101/get?id=2和http://localhost:8101/del?id=2,
同时控制台也可以看到添加的缓存:
10,源码点这里。
请求合并就是将单个请求合并成一个请求,去调用服务提供者,从而降低服务提供者负载的,一种应对高并发的解决方法。
在传统的线程池中,有多少个请求就启动多少个线程,比如上图有6个请求就会启动6条线程,这6条线程再去请求provider。如果一下子来了1000个请求,那么线程池中的队列就会被撑爆。这样怎么办呢?我们就来做一个请求合并。
看下面这张图:
现在将6个请求合并成一个请求,再由线程池执行,这样就可以解决线程池被撑爆的局面。
原理:通过请求合并器设置延迟时间,将延迟时间内的多个请求的请求参数取出来,封装成一个参数list,作为batchMethod指定的方法(比如该方法名称为batchProduct)的参数,然后调用这个batchProduct()方法。返回的对象list再通过一个方法(mapResponseToRequests方法),按照请求的次序将结果对象对应的装到Request对应的Response中返回结果。
下面是一个实战例子:
(1)在e-book-consumer-hystrix模块下新建一个maven项目,命名为e-book-consumer-hystrix-ribbon-batch。拷贝e-book-consumer-hystrix-ribbon-fallback的文件及代码到这个项目。
(2)修改配置文件,修改端口号。
(3)改造service文件,如下:
package com.twf.e.book.consumer.hystrix.ribbon.batch.service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import org.springframework.stereotype.Service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.twf.e.book.consumer.hystrix.ribbon.batch.domain.Product;
@Service
public class ProductService {
// 利用hystrix合并请求
@HystrixCollapser(batchMethod = "batchProduct", scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL, collapserProperties = {
// 请求时间间隔在20ms之内的请求会被合并为一个请求
@HystrixProperty(name = "timerDelayInMilliseconds", value = "20"),
// 设置触发批处理执行之前,在批处理中允许的最大请求数
@HystrixProperty(name = "maxRequestsInBatch", value = "200") })
public Future getProduct(Integer id) {
System.out.println("---------" + id + "----------");
return null;
}
@HystrixCommand
public List batchProduct(List ids) {
for (Integer id : ids) {
System.out.println(id);
}
List list = new ArrayList();
list.add(new Product(1, "登山包"));
list.add(new Product(2, "登山杖"));
list.add(new Product(3, "冲锋衣"));
list.add(new Product(4, "帐篷"));
list.add(new Product(5, "睡袋"));
list.add(new Product(6, "登山鞋"));
return list;
}
}
(4)改造controller文件。
package com.twf.e.book.consumer.hystrix.ribbon.batch.controller;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.twf.e.book.consumer.hystrix.ribbon.batch.domain.Product;
import com.twf.e.book.consumer.hystrix.ribbon.batch.service.ProductService;
@RestController
public class ProductController {
@Autowired
private ProductService productService;
@RequestMapping(value="list",method=RequestMethod.GET)
public void getProduct() throws InterruptedException, ExecutionException {
Future p1 = productService.getProduct(1);
Future p2 = productService.getProduct(2);
Future p3 = productService.getProduct(3);
System.out.println(p1.get().toString());
System.out.println(p2.get().toString());
System.out.println(p3.get().toString());
}
}
(5)启动注册中心,并运行该项目的启动类。
(6)浏览器访问http://localhost:8102/list,后台打印如下:
(7)下面是请求合并参数介绍:
(8)源码点这里。
熔断机制相当于电路的跳闸功能。
例如:我们可以配置熔断策略为当请求错误比例在10s内>50%时,该服务将进入熔断状态,后续请求都会进入fallback。
下面是熔断原理图:
例子:
(1)在e-book-consumer-hystrix模块下新建maven项目,命名为e-book-consumer-hystrix-ribbon-breaker,拷贝e-book-consumer-hystrix-ribbon-fallback项目的文件及代码到该项目。
(2)修改pom文件。
(3)修改配置文件。
(4)修改service文件。
package com.twf.e.book.consumer.hystrix.ribbon.breaker.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import com.twf.e.book.consumer.hystrix.ribbon.breaker.domain.Product;
@Service
public class ProductService {
@Autowired
private LoadBalancerClient loadBalancerClient; // ribbon的负载均衡客户端
@HystrixCommand(fallbackMethod="fallback",
commandProperties={
// 默认20个;10秒内请求数大于20个时就启动熔断器,当请求符合条件时将触发getFallback()。
@HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,value="10"),
// 请求错误率大于50%时就熔断,然后for循环发起请求,当请求符合熔断条件时将触发getFallback()。
@HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE,value="50"),
// 默认5秒;熔断多少秒后去尝试请求。
@HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,value="5000")
})
public List listProduct(int n) {
System.out.println(n);
if (n == 1) {
throw new RuntimeException();
}
ServiceInstance si = loadBalancerClient.choose("e-book-product");
StringBuffer sb = new StringBuffer("");
sb.append("http://");
sb.append(si.getHost());
sb.append(":");
sb.append(si.getPort());
sb.append("/product/list");
System.out.println(sb.toString());
RestTemplate restTemplate = new RestTemplate();
ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>(){};
ResponseEntity> resp = restTemplate.exchange(sb.toString(), HttpMethod.GET, null, typeRef);
List plist = resp.getBody();
return plist;
}
// 当调用远程服务出现异常的时候,会调用这个方法
public List fallback(int n) {
List list = new ArrayList();
list.add(new Product(-1,"fallback"));
return list;
}
}
(5)修改controller文件。
package com.twf.e.book.consumer.hystrix.ribbon.breaker.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.twf.e.book.consumer.hystrix.ribbon.breaker.domain.Product;
import com.twf.e.book.consumer.hystrix.ribbon.breaker.service.ProductService;
@RestController
public class ProductController {
@Autowired
private ProductService productService;
@RequestMapping(value="list",method=RequestMethod.GET)
public ListlistProduct(@RequestParam("n") Integer n) {
List list = productService.listProduct(n);
return list;
}
}
(6)启动注册中心,启动provider和这个项目。
(7)浏览器访问http://localhost:8103/list?n=1,观察后台控制台的变化。
(8)熔断参数介绍。
(9)源码点这里。
场景:consumer端有一个线程池,里面有两个接口,短时间内接口A有10万次请求,接口B有10次请求,由于接口A的大并发量导致整个线程池瘫痪,也就会导致B接口的不可用。最后整个consumer端不可用。
解决的方法:进行线程池隔离。
给A接口单独设置一个线程池,给B接口也设置一个线程池,这样当A接口瘫痪之后,不会影响B接口的运行。
通过每次都开启一个单独线程运行。它的隔离是通过线程池,即每个隔离粒度都是个线程池,互相不干扰。
线程池隔离方式,等于多了一层的保护措施,可以通过hytrix直接设置超时,超时后直接返回。
例子:
(1)在e-book-consumer-hystrix模块下新建maven项目,命名为e-book-consumer-hystrix-ribbon-threadpool,拷贝e-book-consumer-hystrix-ribbon-fallback的文件和代码到该项目。
(2)修改pom文件。
(3)修改配置文件。
(4)修改service文件。
package com.twf.e.book.consumer.hystrix.ribbon.threadpool.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.twf.e.book.consumer.hystrix.ribbon.threadpool.domain.Product;
@Service
public class ProductService {
@Autowired
private LoadBalancerClient loadBalancerClient; // ribbon的负载均衡客户端
@HystrixCommand(groupKey="e-book-product",commandKey="listProduct",
threadPoolKey="e-book-product",
threadPoolProperties={
@HystrixProperty(name="coreSize",value="30"), // 线程池大小
@HystrixProperty(name="maxQueueSize",value="100"), // 最大队列长度
@HystrixProperty(name="keepAliveTimeMinutes",value="2"), // 线程存活时间
@HystrixProperty(name="queueSizeRejectionThreshold",value="15") // 拒绝请求
},
fallbackMethod = "fallback")
public List listProduct() {
ServiceInstance si = loadBalancerClient.choose("e-book-product");
StringBuffer sb = new StringBuffer("");
sb.append("http://");
sb.append(si.getHost());
sb.append(":");
sb.append(si.getPort());
sb.append("/product/list");
System.out.println(sb.toString());
RestTemplate restTemplate = new RestTemplate();
ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>(){};
ResponseEntity> resp = restTemplate.exchange(sb.toString(), HttpMethod.GET, null, typeRef);
List plist = resp.getBody();
return plist;
}
// 当调用远程服务出现异常的时候,会调用这个方法
public List fallback() {
List list = new ArrayList();
list.add(new Product(-1,"fallback"));
return list;
}
}
(5)其他的不需要变化了,启动项目,访问http://localhost:8104/list。
(6)线程池隔离参数。
(7)源码点这里。
信号量的使用示意图如下图所示,当n个并发请求去调用一个目标服务接口时,都要获取一个信号量才能真正去调用目标服务接口,但信号量有限,默认是10个,可以使用maxConcurrentRequests参数配置,如果并发请求数多于信号量个数,就有线程需要进入队列排队,但排队队列也有上限,默认是 5,如果排队队列也满,则必定有请求线程会走fallback流程,从而达到限流和防止雪崩的目的。
最重要的是,信号量的调用是同步的,也就是说,每次调用都得阻塞调用方的线程,直到结果返回。这样就导致了无法对访问做超时(只能依靠调用协议超时,无法主动释放)。
例子:
(1)拷贝上一节的项目,重命名为e-book-consumer-hystrix-ribbon-semaphore。
(2)修改service。
package com.twf.e.book.consumer.hystrix.ribbon.semaphore.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import com.twf.e.book.consumer.hystrix.ribbon.semaphore.domain.Product;
@Service
public class ProductService {
@Autowired
private LoadBalancerClient loadBalancerClient; // ribbon的负载均衡客户端
@HystrixCommand(fallbackMethod="fallback",
commandProperties={
@HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,value="SEMAPHORE"), // 信号量隔离
@HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,value="100") // 信号量最大并发度
})
public List listProduct() {
ServiceInstance si = loadBalancerClient.choose("e-book-product");
StringBuffer sb = new StringBuffer("");
sb.append("http://");
sb.append(si.getHost());
sb.append(":");
sb.append(si.getPort());
sb.append("/product/list");
System.out.println(sb.toString());
RestTemplate restTemplate = new RestTemplate();
ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>(){};
ResponseEntity> resp = restTemplate.exchange(sb.toString(), HttpMethod.GET, null, typeRef);
List plist = resp.getBody();
return plist;
}
// 当调用远程服务出现异常的时候,会调用这个方法
public List fallback() {
List list = new ArrayList();
list.add(new Product(-1,"fallback"));
return list;
}
}
(3)启动方式同上。
(4)信号量隔离参数说明。
(5)源码点这里。
1,什么情况下,用线程池隔离?
请求并发量大,并且耗时长(请求耗时长一般是计算量大,或读数据库):采用线程池隔离策略,这样的话,可以保证大量的容器(tomcat)线程可用,不会由于服务原因,一直处于阻塞或等待状态,快速失败返回。
2,什么情况下,用信号量隔离?
请求并发量大,并且耗时短(请求耗时短可能是计算量小,或读缓存):采用信号量隔离策略,因为这类服务的返回通常会非常的快,不会占用容器线程太长时间,而且也减少了线程切换的一些开销,提高了缓存服务的效率。