Hystrix
服务可用性
复杂分布式体系结构中的服务具有多个依赖关系,每个依赖关系在某些时候都将不可避免地失败。如果主服务未能与这些外部故障隔离,则他们可能会收。
例如,对于依赖于 30 个服务的服务 A,其中每个服务的正常运行时间为 99.99%,服务 A 的最终可用性如下:
99.99^30 = 99.7%正常运行时间
十亿次请求中的 0.3%= 3,000,000 请求失败次数
每月平均 2 小时的服务宕机时间,即使所有依赖项都具有出色 99.99%的正常运行时间。
现实情况通常更糟。即使所有依赖项都表现良好,如果您没有为整个系统设计弹性,那么即使 0.01%停机时间对数十种服务中的每项服务的总体影响也相当于每月停机时间可能达到数小时。
分布式体系结构中服务之间的依赖问题
- 由业务原因在某一时刻,某服务并发请求骤增,导致该服务的延迟,响应过慢。
- 复杂分布式体系结构中的应用程序之间往往存在多层级联赖的关系,当 Provider 服务在某一时刻出现故障,如果 Consumer 服务如果没有与这些故障服务隔离,会导致 Consumer 服务也会出现请求堆积、资源占用、宕机不可用等问题,此时会引发系统服务间的雪崩效应,直到整个系统不可用。
综上所述,需要一种机制来处理服务延迟和服务故障引起雪崩问题,并保护整个系统处于可用稳定的状态。
Hystrix 设计目的
- 通过客户端库对延迟和故障进行保护和控制。
- 在一个复杂的分布式系统中终止级联故障。
- 快速失败和迅速恢复。
- 在合理的情况下回退和优雅的降级。
- 开启近乎于实时的监控、警告和操作控制。
传送门
入门案例
引入 Hystrix 的 Maven 依赖
<!--Hystrix依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
启动类添加注解@EnableCircuitBreaker
@SpringCloudApplication
- @SpringBootApplication //SpringBoot 注解
- @EnableDiscoveryClient //注册服务中心 Eureka 注解
- @EnableCircuitBreaker //断路器注解
@EnableFeignClients
@SpringCloudApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
最外层 API 使用,好比异常处理(网络异常,参数或者内部调用问题)
@RestController
@RequestMapping("api/v1/order")
public class OrderController {
@Autowired
private ProductOrderService productOrderService;
@RequestMapping("save")
@HystrixCommand(fallbackMethod = "saveOrderFail")
public Object save(@RequestParam("user_id")int userId, @RequestParam("product_id") int productId){
Map<String, Object> data = new HashMap<>();
data.put("code",0);
data.put("data",productOrderService.save(userId,productId));
return data;
}
//兜底数据
private Object saveOrderFail(int userId, int productId){
Map<String, Object> msg = new HashMap<>();
msg.put("code",-1);
msg.put("msg","抢购人数太多,稍后重试!"); //
return msg;
}
}
正常访问
http://localhost:8781/api/v1/order/save?product_id=4&user_id=2
{
"code": 0,
"data": {
"id": 0,
"productName": "\"电话 data from port=8771\"",
"tradeNo": "54354d6f-146f-49d1-acd1-b36b0e620888",
"price": 64345,
"createTime": "2019-07-01T05:38:37.650+0000",
"userId": 2,
"userName": null
}
}
服务降级处理
http://localhost:8781/api/v1/order/save?product_id=4&user_id=2
{
"msg": "抢购人数太多,稍后重试!",
"code": -1
}
Feign 中使用断路器
Feign 集成了 Hysrix,需要引入以下依赖:
<!--feign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--hystrix依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
旧版本默认支持,新版本默认关闭,配置 feign 开启 hystrix 功能
feign:
hystrix:
enabled: true
FeignClient 接口类中使用断路器
自定义异常类处理,并继承对应的 FeignClient 接口类
/**
* 针对商品服务:错误降级处理
*/
@Component
public class ProductClientFallback implements ProductClient {
@Override
public String findById(int id) {
System.out.println("feign client 调用product-service findbyid 异常");
return null;
}
}
FeignClient 接口类中使用 fallback 属性指定异常处理类
/**
* 商品服务客户端
*/
@FeignClient(name = "product-service", fallback = ProductClientFallback.class)
public interface ProductClient {
@GetMapping("/api/v1/product/find")
String findById(@RequestParam(value = "id") int id);
}
模拟故障 发现 order_service 项目的控制台打印如下信息:
//访问 http://localhost:8781/api/v1/order/save?product_id=4&user_id=2
feign client 调用product-service findbyid 异常
熔断、降级之异常报警通知
熔断有效的避免了分布式系统中雪崩效应的问题,而服务降级最大程度上以友好的方式保证了核心服务可用性。然后生成环境中我们需要及时准确的定位到故障的服务,因此在故障发生的第一时间需要有完善的通知机制用于第一时间发现问题并定位问题。
为了避免发生故障时,在同一时间有多个服务同时调用降级处理中的逻辑(通常是调用第三方短信服务接口)造成的资源浪费,这里引入了缓存标识位的概念,缓存可以使用 Redis。
引入 Redis 缓存依赖
<!--springboot整合redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置 Redis 信息
spring:
redis:
database: 0
host: 192.168.31.210
port: 6379
timeout: 2000
创建 Redis 服务
#拉取镜像
docker pull redis
#创建容器 windows查看端口的命令: netstat -aon|findstr "6379"
docker run -id --name=redis -p 6379:6379 redis
#使用redis-cli连接测试
C:\Users\RyzenPlatform>d:
D:\>cd D:\Development\Redis
D:\Development\Redis>redis-cli.exe -h 192.168.31.210
192.168.31.210:6379> set name abc
OK
192.168.31.210:6379> get name
"abc"
在 fallback 中添加短信报警机制
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import net.xdclass.order_service.service.ProductOrderService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("api/v1/order")
public class OrderController {
@Autowired
private ProductOrderService productOrderService;
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("save")
@HystrixCommand(fallbackMethod = "saveOrderFail")
public Object save(@RequestParam("user_id")int userId, @RequestParam("product_id") int productId, HttpServletRequest request){
Map<String, Object> data = new HashMap<>();
data.put("code",0);
data.put("data",productOrderService.save(userId,productId));
return data;
}
//兜底数据
private Object saveOrderFail(int userId, int productId, HttpServletRequest request){
//监控报警标识位key
String saveOrderKey = "save-order";
//从redis查询监控报警标识位key
String sendValue = redisTemplate.opsForValue().get(saveOrderKey);
//从HttpServletRequest中获取请求这者的IP 和 本机服务IP
final String rip = request.getRemoteAddr();
final String lip = request.getLocalAddr();
//因为第三方短信接口响应速度比较慢,所以不能使用同步,同步会增加最终响应时间。
//因此这里使用异步任务,因为报警服务使用率并不高,所以这里没有采用以线程池的方式创建异步任务
new Thread(()->{
if(StringUtils.isBlank(sendValue)){
//发送一个http请求,调用短信服务。 TODO+
System.out.println("紧急短信,用户下单失败,请立刻查找原因!请求者ip地址是="+rip+",本机ip"+lip+
",应用名称及URI:order-service:api/v1/order/save,依赖应用名称及URI:product-service:/api/v1/product/find");
//设置标识位
redisTemplate.opsForValue().set(saveOrderKey,"save-order-fail",20, TimeUnit.SECONDS);
}else {//redis不为空
System.out.println("已经发送过短信,20秒内不重复发送");
}
}).start();
Map<String, Object> msg = new HashMap<>();
msg.put("code",-1);
msg.put("msg","抢购人数太多,稍后重试!"); //
return msg;
}
}
测试
http://192.168.31.230:8781/api/v1/order/save?product_id=4&user_id=2
{
"msg": "抢购人数太多,稍后重试!",
"code": -1
}
查看控制台
feign client 调用product-service findbyid 异常
紧急短信,用户下单失败,请立刻查找原因!请求者ip地址是=192.168.31.154,本机ip192.168.31.230,应用名称及URI:order-service:api/v1/order/save,依赖应用名称及URI:product-service:/api/v1/product/find
feign client 调用product-service findbyid 异常
已经发送过短信,20秒内不重复发送
feign client 调用product-service findbyid 异常
紧急短信,用户下单失败,请立刻查找原因!请求者ip地址是=192.168.31.154,本机ip192.168.31.230,应用名称及URI:order-service:api/v1/order/save,依赖应用名称及URI:product-service:/api/v1/product/find
降级超时策略调整
隔离策略
- THREAD 线程池隔离 (默认)
- SEMAPHORE 信号量 : 适用于接口并发量高的情况,如每秒数千次调用的情况,导致的线程开销过高,通常只适用于非网络调用,执行速度快
超时时间
- execution.isolation.thread.timeoutInMilliseconds (默认 1000 毫秒)
超时策略调整原则
最终超时时间是 hystrix 和 Feign 集成 robbin 的超时时间,默认取两者之中的最小值,Hystrix 的默认超时时间是 1 秒。在实际生产环境中,1 秒有些过短,通过会设置 5~10 秒左右,一般情况下 Ribbon 的时间应短于 Hystrix 超时时间。
Feign 集成 robbin 的超时时间配置
feign:
hystrix:
enabled: true
client:
config:
default:
connectTimeout: 2000 #请求建立连接超时
readTimeout: 2000 #请求处理的超时
Hystrix 超时时间配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 4000