一、灾难性雪崩
造成灾难性雪崩效应的原因,可以简单归结为下述三种:
-
服务提供者(Application Service)不可用。如:硬件故障、程序BUG、缓存击穿、并发请求量过大等。
-
重试加大流量。如:用户重试、代码重试逻辑等。
-
服务调用者(Application Client)不可用。如:同步请求阻塞造成的资源耗尽等。
雪崩效应最终的结果就是:服务链条中的某一个服务不可用,导致一系列的服务不可用,最终造成服务逻辑崩溃。这种问题造成的后果,往往是无法预料的。
二、Hystrix简介
在Spring Cloud中解决灾难性雪崩效应就是通过Spring Cloud Netflix Hystrix实现的。
Hystrix [hɪst'rɪks],中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力。本文所说的Hystrix(中文:断路器)是Netflix开源的一款容错框架,同样具有自我保护能力。
通俗解释:Hystrix就是保证在高并发下即使出现问题也可以保证程序继续运行的一系列方案。作用包含两点:容错和限流。
在Spring cloud中处理服务雪崩效应,都是在服务调用方(Application Client)实现,需要依赖hystrix组件。
三、降级
降级是指,当请求超时、资源不足等情况发生时进行服务降级处理,不调用真实服务逻辑,而是使用快速失败(fallback)方式直接返回一个托底数据,保证服务链条的完整,避免服务雪崩。
(1)导入依赖
<!-- 容灾处理依赖。 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
(2)编写启动类
/**
* EnableHystrix - 开启Hystrix功能。netflix hystrix包提供
*/
@SpringBootApplication
@EnableHystrix
public class HystrixRestTemplateApp {
public static void main(String[] args) {
SpringApplication.run(HystrixRestTemplateApp.class, args);
}
}
(3)使用
@Service
public class HystrixRestTemplateServiceImpl implements HystrixRestTemplateService {
@Autowired
private RestTemplate restTemplate;
private final String baseUrl = "http://app-service";
/**
* 增加注解 HystrixCommand
* 注解属性 fallbackMethod - 降级方法的名称
* @return
*/
@Override
@HystrixCommand(fallbackMethod = "downgrade")
public String test() {
String url = baseUrl + "/test";
System.out.println("准备访问远程服务 : /test");
String result = restTemplate.getForObject(url, String.class);
System.out.println("远程服务返回:" + result);
return result;
}
/**
* 降级方法。除方法名称外,其他和具体的服务方法签名一致
* 降级方法的返回结果,就是托底数据
*/
public String downgrade(){
System.out.println("降级方法运行。");
return "服务器忙,请稍后重试";
}
}
四、熔断
当一定时间内,异常请求比例(请求超时、网络故障、服务异常等)达到阀值时,启动熔断器,熔断器一旦启动,则会停止调用具体服务逻辑,通过fallback快速返回托底数据,保证服务链的完整。
熔断有自动恢复机制,如:当熔断器启动后,每隔5秒,尝试将新的请求发送给Application Service,如果服务可正常执行并返回结果,则关闭熔断器,服务恢复。如果仍旧调用失败,则继续返回托底数据,熔断器持续开启状态。
降级是出错了返回托底数据,而熔断是出错后如果开启了熔断将会一定时间不在访问application service。
(1)导入依赖
<!-- 容灾处理依赖。 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
(2)编写启动类
/**
* EnableHystrix - 开启Hystrix功能。netflix hystrix包提供
* EnableCircuitBreaker - 开启熔断器|断路由|断路器。spring circuitbreaker提供
*
* 这两个注解都是让Spring可以识别Hystrix的注解。
*/
@SpringBootApplication
@EnableHystrix
@EnableCircuitBreaker
public class HystrixRestTemplateApp {
public static void main(String[] args) {
SpringApplication.run(HystrixRestTemplateApp.class, args);
}
}
(3)使用
注解属性含义解释
-
CIRCUIT_BREAKER_ENABLED "circuitBreaker.enabled"; 是否开启熔断策略。默认值为true。
-
CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD "circuitBreaker.requestVolumeThreshold"; 单位时间内(默认10s内),请求超时数超出则触发熔断策略。默认值为20次请求数。通俗说明:单位时间内容要判断多少次请求。
-
EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS execution.isolation.thread.timeoutInMilliseconds 设置单位时间,判断circuitBreaker.requestVolumeThreshold的时间单位,默认10秒。单位毫秒。
-
CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS "circuitBreaker.sleepWindowInMilliseconds"; 当熔断策略开启后,延迟多久尝试再次请求远程服务。默认为5秒。单位毫秒。这5秒直接执行fallback方法,不在请求远程application service。
-
CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE "circuitBreaker.errorThresholdPercentage"; 单位时间内,出现错误的请求百分比达到限制,则触发熔断策略。默认为50%。
-
CIRCUIT_BREAKER_FORCE_OPEN "circuitBreaker.forceOpen"; 是否强制开启熔断策略。即所有请求都返回fallback托底数据。默认为false。
-
CIRCUIT_BREAKER_FORCE_CLOSED "circuitBreaker.forceClosed"; 是否强制关闭熔断策略。即所有请求一定调用远程服务。默认为false。
/**
* 测试熔断,强化降级。
* 注解属性 commandProperties - 具体的容灾配置参数。
* 类型是HystrixProperty[], HystrixProperty类型是名值对。
* 名 - 是具体的配置参数名,字符串类型,可以从HystrixPropertiesManager中查看,也可以
* 使用其中的静态常量。
* 值 - 参数值,字符串类型。
*/
@Override
@HystrixCommand(fallbackMethod = "circuitBreakerDowngrade", commandProperties = {
@HystrixProperty(name = HystrixPropertiesManager
.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,
value = "5000"), // 统计周期,默认10秒
@HystrixProperty(name = HystrixPropertiesManager
.CIRCUIT_BREAKER_ENABLED,
value = "true"), // 是否开启熔断,默认true
@HystrixProperty(name = HystrixPropertiesManager
.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,
value = "2"), // 统计周期内,错误几次,开启熔断, 默认20
@HystrixProperty(name = HystrixPropertiesManager
.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE,
value = "50"), // 统计周期内,错误百分比达到多少,开启熔断, 默认50
@HystrixProperty(name = HystrixPropertiesManager
.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,
value = "3000"), // 开启熔断后,多少毫秒不访问远程服务,默认5000毫秒
@HystrixProperty(name = HystrixPropertiesManager
.CIRCUIT_BREAKER_FORCE_OPEN,
value = "false"), // 是否强制开启熔断器, 默认false
@HystrixProperty(name = HystrixPropertiesManager
.CIRCUIT_BREAKER_FORCE_CLOSED,
value = "false") // 是否强制关闭熔断器, 默认false
})
public String circuitBreaker() {
String url = baseUrl + "/test";
System.out.println("准备访问远程服务,地址是:" + url);
String result = restTemplate.getForObject(url, String.class);
System.out.println("远程返回结果是:" + result);
return result;
}
/**
* 熔断降级方法
*/
public String circuitBreakerDowngrade(){
System.out.println("熔断降级触发");
return "网站建设中";
}
五、请求缓存
Hystrix为了降低访问服务的频率,支持将一个请求与返回结果做缓存处理。如果再次请求的URL没有变化,那么Hystrix不会请求服务,而是直接从缓存中将结果返回。这样可以大大降低访问服务的压力。 可以利用spring cache。实现请求缓存。
(1)导入依赖
<!-- 容灾处理依赖。 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency><!-- 边路缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)配置缓存地址
spring:
redis:
host: ip地址
(3)在启动器上添加@EnableCaching注解,开启请求缓存的功能
@SpringBootApplication
@EnableHystrix
@EnableCircuitBreaker
//启动Spring Caching
@EnableCaching
public class HystrixRestTemplateApp {
public static void main(String[] args) {
SpringApplication.run(HystrixRestTemplateApp.class, args);
}
}
(4)使用
/**
* 测试请求缓存。
* 使用Spring Cache技术实现,访问Redis,做缓存处理。
* Spring Cache是spring-context.jar提供的技术。可以访问多种缓存服务器。
* 包括redis。
* 想使用Spring Cache技术,访问缓存服务,需要提供以下依赖:
* 1. spring-context.jar,直接或间接。
* 2. 要访问的缓存服务器客户端依赖。如:访问Redis需要Spring Data Redis依赖
* 使用Spring Cache技术后,查询逻辑的流程是:
* 1. 访问缓存,查看是否有缓存的结果。如果有直接返回,不执行当前方法。
* 2. 如果缓存中没有结果,则执行方法。
* 3. 方法返回结果,会被Spring Cache技术自动保存到缓存服务器中。
* 4. 方法结束,返回给调用者。
* 注解: Cacheable
* 属性:
* cacheNames - 缓存中key的前缀
* key - 缓存中key的后缀。可以使用表达式赋值。字面值用单引号标记。方法参数变量
* 使用#参数名标记,可以使用字符串拼接符号 +
* 完整的缓存key是 前缀 + :: + 后缀
*/
@Override
@HystrixCommand(fallbackMethod = "downgrade")
@Cacheable(key = "'client'",cacheNames = "com:bjsxt")
public String test() {
String url = baseUrl + "/test";
System.out.println("准备访问远程服务 : /test");
String result = restTemplate.getForObject(url, String.class);
System.out.println("远程服务返回:" + result);
return result;
}
/**
* 降级方法。除方法名称外,其他和具体的服务方法签名一致
* 降级方法的返回结果,就是托底数据
*/
public String downgrade(){
System.out.println("降级方法运行。");
return "服务器忙,请稍后重试";
}
六、请求合并
增加请求合并处理后:一段时间范围内的所有请求合并为一个请求。大大的降低了Application Service 负载。
(1)依赖同其他一样
(2)使用
注解属性含义
@HystrixCollapser 进行请求合并
batchMethod:处理请求合并的方法
scope:合并请求的请求作用域。可选值有global和request。
global:代表所有的请求线程都可以等待可合并。
常用 request:代表一个请求线程中的多次远程服务调用可合
timerDelayInMilliseconds:等待时长,默认10毫秒。
maxRequestInBatch:最大请求合并数量。
/**
* 是一个批处理逻辑。是做请求合并处理的方法。
* 注意,Hystrix中,请求合并处理,具体方法,不是单处理方法。
* 当前方法,不会执行。由Hystrix通过代理封装后执行。
* 增加一个额外的批处理方法逻辑。
*
* 注解 HystrixCollapser - 代表当前的方法,是一个要合并的方法。
* 属性:
* batchMethod - 批处理方法名称。
* scope - 有效范围。
* 可选值:
* com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL
* 全局有效。可以合并多个客户端的请求。
* com.netflix.hystrix.HystrixCollapser.Scope.REQUEST 默认值
* 请求内有效,只能合并一个请求中的多次远程调用。必须配合指定的框架才能生效。
* 在当前方法中,会抛出异常。
* collapserProperties - 合并约束,类似HystrixCommand注解中的commandProperties
* 类型是HystrixProperty[]
* @param id
* @return
*/
@Override
@HystrixCollapser(batchMethod = "getUsersByIds",
scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,
collapserProperties = {
@HystrixProperty(name =
HystrixPropertiesManager.MAX_REQUESTS_IN_BATCH,
value = "2"), // 最多合并多少个请求
@HystrixProperty(name =
HystrixPropertiesManager.TIMER_DELAY_IN_MILLISECONDS,
value = "500") // 最多等待多少毫秒
}
)
public Future<User> getUserById(Integer id) {
System.out.println("根据主键查询用户方法,Service实现类型中的Override实现");
return null;
}
/**
* 批处理方法。编写访问远程批处理服务的逻辑。
* 使用注解HystrixCommand修饰。
*
* 问题:
* 使用RestTemplate访问远程服务的时候,如果服务返回结果是集合。
* 集合的泛型是自定义类型。因为SpringMVC的@ResponseBody是把Java对象转换成JSON返回,
* RestTemplate不知道返回的JSON对应的具体Java类型是什么,
* 所以使用最通用的类型,Map转换。
* 远程服务返回的是List<User>,SpringMVC注解ResponseBody处理后,返回的是字符串
* [{"id":1,"name":"姓名1","age":20}, {...}]
* RestTemplate转换上述JSON格式字符串,[]使用List集合处理。
* {"id":1,"name":"姓名1","age":20}是什么类型?使用通用类型Map处理。
* JSON对象是属性名id、name、age是map的key。属性值1、姓名1、20是map的value。
*
* 手工使用Jackson实现转换处理。
*
* 定义要求:
* 1. 访问修饰符是public
* 2. 返回值的类型是远程服务的返回类型。
* 3. 方法命名,随意。不重复即可。
* 4. 参数表,是List集合类型,泛型是要合并的方法参数类型。名称和要合并的方法参数名一致
* 5. 抛出异常,不能抛出范围超过要合并的方法的异常类型。
* @param id
* @return
*/
@HystrixCommand
public List<User> getUsersByIds(List<Integer> id){
ObjectMapper mapper = new ObjectMapper();
String url = baseUrl + "/batch";
System.out.println("准备访问远程服务,地址是:" + url + " , 参数是:" + id);
List<LinkedHashMap> result =
restTemplate.postForObject(url, id, List.class);
List<User> users = new ArrayList<>(result.size());
for(LinkedHashMap userMap : result){
try {
// 把Map转换成JSON格式字符串
String userJson = mapper.writeValueAsString(userMap);
// 把JSON格式字符串转换成User类型对象
User user = mapper.readValue(userJson, User.class);
// 把处理后的User类型对象,保存到返回结果集合中
users.add(user);
}catch (Exception e){
e.printStackTrace();
}
}
System.out.println("查询结果数量是:" + result.size());
System.out.println("查询的用户集合是:" + result);
return users;
}
(4)控制器单元方法
/**
* 根据主键查询用户。
* 具体实现逻辑,调用远程服务eureka-client-app-service批处理查询实现。
* 合并当前的请求。把多次请求参数主键,合并成一个集合参数List<Integer>。
* 一次性访问远程服务,返回的批处理查询结果,拆分后,返回给客户端。
* @param id
* @return
*/
@RequestMapping("/getUserById")
public User getUserById(Integer id){
Future<User> future = service.getUserById(id);
System.out.println("控制器执行 - getUserById()");
try {
return future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 发生了异常。返回null。
return null;
}
七、线程池隔离
当使用线程池隔离。不同接口有着自己独立的线程池,即使某个线程池都被占用,也不影响其他线程。
java.util.concurrent.Semaphore用来控制可同时并发的线程数。通过构造方法指定内部虚拟许可的数量。每次线程执行操作时先通过acquire方法获得许可,执行完毕再通过release方法释放许可。如果无可用许可,那么acquire方法将一直阻塞,直到其它线程释放许可。
(1)依赖同上
(2)使用
注解属性说明
/**
* 测试线程池隔离。 也可以提供降级、熔断等处理逻辑。
* 访问远程服务/getNoParams测试。
* 当前方法,使用独立的线程池。和其他方法隔离。
* 提供注解HystrixCommand
* groupKey - 分组唯一值。默认使用当前类型的类名。代表一个独立接口的唯一命名。
* 一般都使用类型的名称定义。是使用 字母 ,数字 ,_ 组成的字符串。
* commandKey - 命令唯一值。默认使用当前方法名称。代表一个独立接口中的命令唯一命名。
* 一般使用方法名称定义。是使用 字母 ,数字 ,_ 组成的字符串
* threadPoolKey - 隔离的线程池命名中缀。默认使用groupKey的值。
* 定义后,线程池命名是 "hystrix-" + threadPoolKey。
* 池中的线程命名是 "hystrix-" + threadPoolKey + 数字编号(从1开始,自然数递增)
* threadPoolProperties - 定义隔离线程池的配置信息。如:线程池容量,线程存活时间等。
* 类型是HystrixProperty[]
* @return
*/
@Override
@HystrixCommand(groupKey = "MyFirstThread",
commandKey = "thread",
threadPoolKey = "pool-name",
threadPoolProperties = {
@HystrixProperty(name = HystrixPropertiesManager.CORE_SIZE,
value = "3"), // 线程池容量
@HystrixProperty(name = HystrixPropertiesManager.KEEP_ALIVE_TIME_MINUTES,
value = "5"), // 线程空闲时,最大存活时间是多少分钟
@HystrixProperty(name = HystrixPropertiesManager.MAX_QUEUE_SIZE,
value = "5"), // 线程池占满时,最多由多少个请求阻塞等待
@HystrixProperty(name = HystrixPropertiesManager
.QUEUE_SIZE_REJECTION_THRESHOLD,
value = "5") // 当阻塞队列MAX_QUEUE_SIZE占满时,可以由多少个
// 请求同时阻塞等待后续处理。
}
)
public String thread() {
String url = baseUrl + "/test";
System.out.println("当前方法使用的线程名称是:" +
Thread.currentThread().getName());
String result =
restTemplate.getForObject(url, String.class);
System.out.println("远程返回:" + result);
return result;
}
八、信号量隔离
采用信号量隔离技术,每接收一个请求,都是服务自身线程去直接调用依赖服务,信号量就相当于一道关卡,每个线程通过关卡后,信号量数量减1,当为0时不再允许线程通过,而是直接执行fallback逻辑并返回,说白了仅仅做了一个限流。
(1)依赖同上
(2)使用
注解属性说明
/**
* 测试信号量隔离
* 就是定义一个阈值,设定同时处理的请求上限。当处理的请求达到阈值时,
* 后续请求,降级处理。
*
* 使用注解 HystrixCommand 修饰
* commandProperties - 描述隔离方案和信号量隔离阈值
*
* @return
*/
@Override
@HystrixCommand(fallbackMethod = "downgrade",
commandProperties = {
@HystrixProperty(name = HystrixPropertiesManager
.EXECUTION_ISOLATION_STRATEGY,
value = "SEMAPHORE"), // 隔离方案。默认线程池隔离。 THREAD | SEMAPHORE
@HystrixProperty(name = HystrixPropertiesManager
.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,
value = "2"), // 最大信号量
@HystrixProperty(name = HystrixPropertiesManager
.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,
value = "1000") // 超时时间。信号量隔离中无效,线程池隔离中有效。默认1秒
}
)
public String semaphore() {
String url = baseUrl + "/test";
System.out.println("远程地址是:" + url + " , 线程名称是:" +
Thread.currentThread().getName());
String result = restTemplate.getForObject(url, String.class);
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("远程返回:" + result);
return result;
}
九、线程池隔离和信号量隔离分析
(1)区别
线程池隔离 | 信号量隔离 | |
---|---|---|
线程 | 请求线程和调用远程服务的线程不是同一个线程 | 请求线程和调用远程服务的线程是同一个线程 |
开销 | 较大(包括排队、调度、上下文等开销) | 很低(无线程切换) |
异步 | 支持 | 不支持 |
并发支持 | 支持(线程池容量上限) | 支持(信号量阈值上限) |
传递Header | 无法传递Http Header | 可以传递Http Header |
支持超时 | 支持超时 | 不支持超时 |
(2)选择
什么时候选择线程池隔离
<font color='red'>请求并发量大,并且耗时较长</font>(如计算量级大、访问其他服务、访问数据库等),使用线程池隔离可以保证容器线程池(Tomcat线程池)利用率更好,不会因为远程服务调用的原因,导致线程处于等待、阻塞等状态,可以实现快速失败返回。
什么时候选择信号量隔离
<font color='red'>请求并发量大,并且耗时较短</font>(如计算量级小、访问缓存等),使用信号量隔离可以保证快速返回,不会因为线程切换而导致不必要的损耗。且因为这种服务返回快速,并不会长时间占用容器线程(Tomcat线程),提高了服务的整体性能。
十、 Openfeign的容灾处理
当使用OpenFeign调用远程服务超时会出现500错误。可使用Hystrix来实现容灾处理。
在OpenFeign启动器依赖中,默认包含Hystrix核心类库,但不包含Hystrix启动器中的全部资源,所以可实现Hystrix容灾处理方案,但不能实现Hystrix其他扩展处理(如@EnableHystrix注解就不包含在默认资源中)。
(1)导入依赖
openfeign自带hystrix依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2)编写配置文件
在application.yml配置中增加下述配置:默认情况下OpenFeign的hystrix是不开启的,需要手动开启。
feign:
hystrix:
enabled: true
(3)编写启动类
@SpringBootApplication
@EnableHystrix
@EnableFeignClients
@EnableCircuitBreaker
public class HystrixRestTemplateApp {
public static void main(String[] args) {
SpringApplication.run(HystrixRestTemplateApp.class, args);
}
}
(4)编写Feign接口
@FeignClient(name = "app-service", fallback = FeignHystricServiceFallback.class)
public interface FeignHystricService {
@PostMapping("/test")
String test();
}
(5)编写Feign接口的实现类,做为降级方法
@Component
public class FeignHystricServiceFallback implements FeignHystricService {
@Override
public String test() {
System.out.println("执行方法,出现服务降级,返回托底数据");
String result = "因为Provider连接不上了,返回托底数据";
return result;
}
}
(6)OpenFeign中的熔断配置
hystrix: # hystrix 容灾配置
command: # hystrix 命令配置,就是具体的容灾方案
default: # 默认环境,相当于全局范围,后续的配置,参考HystrixPropertiesManager中的常量配置
circuitBreaker: # 熔断配置, 常用。 其他配置不常用。
enabled: true
requestVolumeThreshold: 2
sleepWindowInMilliseconds: 2000
errorThresholdPercentage: 50