微服务架构中,系统拆分成了多个服务单元,各单元的应用间通过服务注册与订阅的方式互相依赖.由于每个单元都在不同的进程中运行,依赖通过进程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最终导致自身服务的瘫痪.
一句话就是,服务提供方出现问题,服务调用方就有可能瘫痪
快速入门
- 引入Spring Cloud Hystrix依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
- 在客户端工程的主类上添加
@EnableCircuitBreaker
注解 - 改造服务消费方式,在业务方法上添加
@HystrixCommand(fallbackMethod = "helloFallback",commandKey = "helloKey")
@Service
public class HelloService {
private final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(String.valueOf(HelloService.class));
@Autowired
RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "helloFallback",commandKey = "helloKey")
public String helloService(){
long start = System.currentTimeMillis();
String result = restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
long end = System.currentTimeMillis();
logger.info("SpendTime = " + (end - start));
return result;
}
public String helloFallback(){
return "error";
}
}
两种情况,第一种是断开具体的服务实例,第二种是服务阻塞
第一种是直接断开服务,第二种是在服务提供方处直接sleep一段时间
原理分析
Hystrix的环节
1.创建HystrixCommand或HystrixObservableCommand对象
命令模式:将来自客户端的请求封装成一个对象,从而使用不同的请求对客户端进行参数化
2.命令执行:
HystrixCommand
实现了两个方法:
- execute():同步执行,从依赖服务返回一个单一的结果对象,或是在发生错误的时候抛出异常
- queue():异步执行,直接返回一个Future对象,其中包含了服务执行结果时要返回的单一结果对象
HystrixObservableCommand
实现了两个方法: - observe():返回Observable对象,它代表了操作的多个结果,它是一个Hot Observable
- toObserve():返回Observable对象,代表了操作的多个结果,返回的是一个Cold Observable
Hot Observable
:不论"事件源"是否有"订阅者",都会在创建后对事件进行发布,Hot Observable的每一个"订阅者"有可能是从"事件源"的中途开始的,只能看到整个操作的局部过程
Cold Observable
:在没有"订阅者"的时候不会发布事件,直到有"订阅者"才会发布事件,可以保证从一开始就看到真个操作的全部过程
3.结果是否被缓存
4.断路器是否打开
5.线程池/请求队列/信号量是否占满
6.HystrixObservableCommand.construct()
或HystrixCommand.run()
7.计算断路器的健康度
Hystrix会将"成功",“失败”,“拒绝”,“超时"等信息报告给断路器,而断路器会维护一组计数器来统计这些数据.断路器会根据这些数据进行判断是否熔断或者再次熔断
8.fallback处理
fallback处理也就是"服务降级”,能够引起服务降级处理的情况有:
1)第4步,当前命令处于"熔断/断路"状态,断路器是打开的时候
2)第5步,当前命令的线程池.请求队列或者信号量被占满的时候
3)第6步抛出异常的时候
9.返回成功的响应
断路器原理
断路器HystrixCircuitBreaker
的三个方法:
- allowRequest():每个Hystrix命令的请求都通过它来判断是否执行
- isOpen():返回当前断路器是否打开
- markSuccess():用来闭合断路器
依赖隔离
Hystrix使用"舱壁模式"实现线程池的隔离,会为每一个依赖服务创建一个独立的线程池
对依赖服务的线程池的隔离,可以有如下优势:
- 应用自身得到完全保护,不会受到不可控的依赖服务影响.
- 有效降低接入新服务的风险,新服务有问题也不会影响其他服务
- 清理恢复的范围小,能够很快恢复
- 依赖的服务出现错误,线程池快速反应并通过实时的动态属性刷新来处理
- 依赖服务实现机制改变,可通过实时动态刷新依赖服务的阈值进行调整
- 专有线程池提供内置并发实现,异步访问依赖服务
使用详解
创建请求命令
1.通过继承的方式,继承HystrixCommand
- 同步执行:
User u = new UserCommand(restTemplate,1L).execute();
- 异步执行
Future<User> futureUser = new UserCommand(restTemplate,1L).queue();
异步执行可以通过futureUser的get方法获取结果
2.通过注解@HystrixCommand
,优雅实现Hystrix命令
通过observableExecutionMode参数来控制是observe()还是toObservable()执行方式
@HystrixCommand(observableExecutionMode = ObservableExecution-Mode.EAGER)
:
EAGER表示使用observe()执行方式
@HystrixCommand(observableExecutionMode = ObservableExecution-Mode.LAZY)
:表示使用toObservable()执行方式
定义服务降级
1.重载HystrixCommand中的getFallback()方法来实现服务降级
2.通过注解实现服务降级,使用@HystrixCommand中的fallbackMethod参数来指定具体的服务降级实现方法
不用实现服务降级的场景:
1)执行写操作的命令
2)执行批处理或离线计算的命令
异常处理
1.异常传播
通过设置@HystrixCommand
注解的ignoreException
参数
@HystrixCommand(ignoreExceptions = {BadRequestException.class})
public User getUserById(Long id){
return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id);
}
当getUserById方法抛出类型为BadRequestException
的异常时,Hystrix会将异常包装在HystrixBadRequestException
中抛出,不会触发后续的fallback逻辑
2.异常获取
1)传统方式继承,getFallback()方法通过Throwable getExecutionException()
方法获取具体的异常
2)注解配置方式实现异常获取,在fallback实现方法的参数增加Throwable e
@HystrixCommand(fallbackMethod="fallback1")
User getUserById(String id){
throw new RuntimeException("getUserById command failed");
}
User fallback1(String id,Throwable e){
assert "getUserById command failed".equals(e.getMessage());
}
命令名称\分组以及线程池划分
在继承HystrixCommand的类的构造器中使用Setter
静态类来设置
public UserCommand(){
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"));
默认情况下,Hystrix会让相同组名的命令使用同一个线程池,所以GroupKey是每个Setter必需的参数,但是CommandKey不是必需的
Hystrix还提供了HystrixThreadPoolKey
来对线程池进行设置,实现更加细粒度的线程池划分
public UserCommand(){
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("CommandGroupKey"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CommandKey"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
多个不同的命令可能从业务逻辑上来看属于同一个组,但往往从实现本身上需要跟其他命令进行隔离,所以尽量通过HystrixThreadPoolKey
的方式来指定线程池的划分
@HystrixCommand(commandKey="getUserById",groupKey="UserGroup",threadPoolKey="getUserByIdThread")
public User getUserById(Long id){
return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id);
}
请求缓存
分布式环境下,依赖服务会引起一部分性能损失
高并发环境下,http相比于其他高性能的通信协议在速度上处于劣势,容易成为系统瓶颈
1.开启请求缓存功能
public class UserCommand extends HystrixCommand<User>{
private RestTemplate restTemplate;
private Long id;
public UserCommand(RestTemplate restTemplate,Long id){ super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserGroup")));
}
@Override
protected User run() throws Exception {
return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id);
}
@Override
protected String getCacheKey(){
return String.valueOf(id);
}
}
开启请求缓存具备几个好处:
- 减少重复的请求数,降低依赖服务的并发度
- 在同一用户的请求的上下文中,相同依赖服务的返回数据时钟保持一致
- 请求缓存在run()和construct()执行之前生效,可以减少不必要的线程开销
2.清理失效缓存功能
读操作:
protected String getCacheKey(){
//根据id置入缓存
return String.valueOf(id);
}
public static void flushCache(Long id){
//刷新缓存,根据id进行清理
HystrixRequestCache.getInstance(GETTER_KEY,HystrixConcurrencyStrategyDefault .getInstance()).clear(String.valueOf(id));
}
写操作:
protected User run(){
//写操作
User r = restTemplate.postForObject("http://USER-SERVICE/users",user,User.class);
//刷新缓存,清理缓存中失效的User
UserGetCommand.flushCache(user.getId());
return r;
}
可以看到,id作为很重要的索引存在,读写操作都是通过id来进行处理的
3.工作原理
主要有两个步骤:尝试获取请求缓存以及将请求结果加入缓存
- 尝试获取请求缓存:
开启请求缓存,并重写getCacheKey(),返回一个非null的缓存key,则可以根据key去查询缓存 - 将请求结果加入缓存
根据key值去缓存map中查找是否有对应值,若有则将缓存命中结果获取,若无则将当前缓存值缓存起来,并将其转换成Observable返回给调用者
4.使用注解实现请求缓存
注解 | 描述 | 属性 |
---|---|---|
@CacheResult | 该注解用来标记请求命令返回的结果应该被缓存,必须与@HystrixCommand 注解结合使用 | cacheKeyMethod |
@CacheRemove | 该注解用来让请求命令的缓存失效,失效的缓存根据定义的Key决定 | commandKey,cacheKeyMethod |
@CacheKey | 该注解用来在请求命令的参数上标记,使其作为缓存的Key值,如果没有标注则会使用所有参数.如果同时还使用了@CacheResult 和@CacheRemove 注解的cacheKeyMethod 方法指定缓存key的生成,那么该注解将不会起作用 | value |
注解方式的几个用法:
1)设置请求缓存:
加上@CacheResult
之后,Hystrix会将该结果置入请求缓存中,而key值使用所有的参数,这里就是Long id
@CacheResult
@HystrixCommand
public User getUserById(Long id){
return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id);
}
2)定义缓存Key:
第一种方式: 配置方式如同@HystrixCommand
服务降级fallbackMethod
的使用
@CacheResult(cacheKeyMethod="getUserByIdCacheKey")
@HystrixCommand
public User getUserById(Long id){
return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id);
}
private Long getUserByIdCacheKey(Long id){
return id;
}
第二种方式:通过@CacheKey
注解实现,但是就如上面表格所说,如果已经使用了cacheKeyMethod
的生成函数,则@CacheKey
注解不会生效
@CacheResult
@HystrixCommand
public User getUserById(@CacheKey("id") Long id){
return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id);
}
@CacheKey还可以用参数的内部属性作为key
@CacheResult
@HystrixCommand
public User getUserById(@CacheKey("id") User user){
return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,user.getId());
}
3)缓存清理:
@CacheResult
@HystrixCommand
public User getUserById(@CacheKey("id") Long id){
return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id);
}
@CacheRemove(commandKey="getUserById")
@HystrixCommand
public void update(@CacheKey("id")User user){
return restTemplate.postForObject("http://USER-SERVICE/users",user,User.class);
}
请求合并
在高并发的情况下,通信次数增加,总的通信时间消耗也会变得不理想,同时依赖服务的线程池资源有限,将出现排队等待和响应延迟的情况,通过请求的合并,可以达到减少通信消耗和线程数占用的效果.
第一步:为请求合并的实现准备一个批量请求命令的实现
public class UserBatchCommand extends HystrixCommand<List<User>>{
UserService userService;
List<Long> userIds;
public UserBatchCommand(UserService userService,List<Long> userIds){
super(Setter.withGroupKey(asKey("userServiceCommand")));
this.userIds = userIds;
this.userService = userService;
}
@Override
protected List<User> run() throws Exception{
return userService.findAll(userIds);
]
}
通过调用userService.findAll方法来访问/users?ids={ids}接口以返回User的列表结果
第二步:通过继承HystrixCollapser实现请求合并器
public class UserCollapseCommand extends HystrixCollapser<List<User>,User,Long>{
private UserService userService;
private Long userId;
public UserCollapseCommand(UserService userService,Long userId){
//设置时间延迟窗口
super(Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("userCollapseCommand")).andCollapserPropertiesDefaults(
HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(100)
));
this.userService = userService;
this.userId = userId;
}
@Override
public Long getRequestArgument() {
return userId;
}
@Override
protected HystrixCommand<List<User>> createCommand(Collection<CollapsedRequest<User, Long>> collection) {
//初始化一个list
List<Long> userIds = new ArrayList<>(collection.size());
//将所有请求的id放在这个list中
userIds.addAll(collection.stream().map(CollapsedRequest::getArgument).collect(Collectors.toList()));
//合并成一个批量请求返回
return new UserBatchCommand(userService,userIds);
}
@Override
protected void mapResponseToRequests(List<User> users, Collection<CollapsedRequest<User, Long>> collection) {
int count = 0;
//将响应分发到每个请求上,完成批量结果到单个请求结果的转换
for(CollapsedRequest<User,Long> collapsedRequest : collection){
User user = users.get(count++);
collapsedRequest.setResponse(user);
}
}
}
以下是请求合并器的原理图,在资源有效并且短时间内会产生高并发请求的时候,为避免连接不够用而引起的延迟可以考虑使用请求合并器的方式来处理和优化
1.使用注解实现请求合并器
@Service
public class UserService {
@Autowired
private RestTemplate restTemplate;
@HystrixCollapser(batchMethod = "findAll", collapserProperties = {@HystrixProperty(name="timerDelayInMilliseconds",value="100")})
public User find(Long id){
return null;
}
@HystrixCommand
public List<User> findAll(List<Long> ids){
return restTemplate.getForObject("http://USER-SERVICE/users?ids={1}",List.class, StringUtils.join(ids,","));
}
}
2.请求合并的额外开销
若请求不经过合并器访问的平均耗时为5ms,请求合并器的延迟时间窗为10ms,那么最坏情况下需要15ms,所以请求合并器的延迟时间窗会带来额外开销,所以是否使用请求合并器需要根据服务调用的实际情况来选择,主要考虑两个方面:
- 请求命令本身的延迟,意思是请求合并器的耗时对于请求本身耗时所占比例很小
- 延迟时间窗内的并发量,意思是时间窗内的并发量要高才可以,越高效果越好