Netflix hystrix入门教程

  Hystrix是Netflix针对微服务分布式系统的熔断保护中间件,当我们的客户端连接远程的微服务时,有两种情况需要考虑:首先,如果远程系统当机了我们怎么办?其次,我们如何管理对远程微服务的调用性能,以保证每个微服务以最小延迟最快性能响应?

  Hystrix是一个有关延迟和失败容错的开源库包,用来设计隔离访问远程系统端点或微服务等,防止级联爆炸式的失败,也就是由一个小问题引起接二连三扩大的疯狂的错误爆炸直至整个系统瘫痪,能够让复杂的分布式系统更加灵活具有弹性。

  想象这样一个情况:一个电子商务网站因为在黑色周五发生过载,因为过多访问负载,在线支付延迟半天都没有响应,用户的支付因为太多并发请求导致响应时间过长,在等待很长时间以后最终失败。这样的错误导致退出购物车的用户重新刷新或者再次尝试支付,更加增加了服务器的负载,如同滚雪球一样爆炸,导致更多长时间等待线程和网络拥塞。

  Hystrix的熔断机制可以帮助我们解决这个问题。下面我们使用Hystrix开发一个简单案例,以便了解Hystrix的原理,整个源码见Github.

这个案例是使用简单的BettingService 来模拟如何使用Hystrix访问远程的服务。其代码接口如下:

public interface BettingService {
 
/**
 * 获得今天比赛所有竞赛的名称集合
 * @return List of race course names
*/
 
List<Racecourse> getTodaysRaces();
 
/**
 * 获得在一个指定比赛中所有跑马集合
 * @param race Name of race course
 * @return List of the names of all horses running in the specified race
*/
 
List<Horse> getHorsesInRace(String raceCourseId);
 
/**
 * 获得今天某个指定比赛中指定的跑马的赢率。
 * @param race Name of race course
 * @param horse Name of horse
 * @return Current odds as a string (e.g. "10/1")
*/
 
String getOddsForHorse(String raceCourseId, String horseId);
}

这个微服务暴露两个领域对象,Racescourse和Horse:

Racecourse.java

public class Racecourse {
private String id;
private String name;
 
public Racecourse(String id, String name) {
super();
this.id = id;
this.name = name;
}
 
  public String getId() {
return id;
}
 
  public void setId(String id) {
this.id = id;
}
 
  public String getName() {
return name;
}
 
public void setName(String name) {
this.name = name;
}
 
@Override
 
  public String toString() {
return "Racecourse [id=" + id + ", name=" + name + "]";
}
 
@Override
 
  public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
 
  @Override
 
  public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
 
}

Horse.java

public class Horse {
private String id;
private String name;
public Horse(String id, String name) {
super();
this.id = id;
this.name = name;
}
 
public String getId() {
return id;
}
 
public void setId(String id) {
this.id = id;
}
 
public String getName() {
return name;
}
 
public void setName(String name) {
this.name = name;
}
 
@Override
 
public String toString() {
return "Horse [id=" + id + ", name=" + name + "]";
}
 
@Override
 
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
 
@Override
 
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
 
}

 

Hysterix的功能目标

当一个应用高度耦合其他服务时则是非常危险且容易导致失败,这种失败很容易伤害服务的调用者,最后导致一个接一个的连续爆炸错误,最后失去控制,如何处理这些问题是有关系统性能和效率等关键性问题。

Hysterix主要是使用命令模式构建。在同步调用情况下,每个远程微服务调用被包装在HysterixCommand中,而在异步调用情况下,每个远程服务调用是被包装在一个HysterixObserveableCommand 中。

我们假设有一个简单的出错,比如远程服务变得不可用,或者抛出错误了,HysterixCommand让你在getFallback()方法中定义针对这种出错应该采取何种应对措施,如广播exception或者返回一个默认设置或者指定的错误值。

负责快速失败的熔断器(Circuit Breaker )

熔断机制的原理很简单,如同房间中电力过载保护器,你可以将一个函数调用放入一个熔断器,其负责监控失败,一旦延迟时间超过一个确定的阀值,熔断器将遍历当前更多调用,向它们返回一个错误。

熔断器可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,也就是不让它们再次访问远程服务器了,这是通过HystricCommands的 getFallback() 实现,在过一段配置的时间期间后,它会再次关闭熔断,又开始接受远程调用,然后再次侦测错误,再次熔断,如此反复。

延迟和线程池

当在系统高峰时期,大量对微服务的调用可能会堵塞远程服务器的线程池,如果这个线程池没有和主应用服务器的线程池隔离,就可能有潜在危险导致整个服务器挂机。

Hystrix使用其自己的线程池,这样和主应用服务器线程池隔离,如果调用花费很长时间,会停止调用,不同的命令或命令组能够被配置使用它们各自的线程池,可以隔离不同的服务。

无论如何,使用线程池都会有过载,Netflix有自己的度量方式(比如使用线程隔离每天有100多亿个命令执行)。

面对失败,在客户端可以通过对远程调用的编程实现更高效应对和低延迟的响应时间。Hystrix会提供一些工具帮助你完成。

假设多个用户客户端并发访问系统,如果没有任何调停协调,这会导致很多重复的请求跨网络实时传输。请求折叠Request collapsing在远程调用被请求之前引入一个很小的等待时间,当它被执行时,会看看在同一个时间窗口是否有重复请求执行,这个时间窗口很短如10ms,这里有一个可选Global'上下文,用来折叠压缩所有当前Tomcat线程中任何用户的所有请求,而'User Request' 上下文则是对当前单个Tomcat线程进行折叠。

请求折叠适合某个命令以高频率访问,降低线程和网络负载。对于非高频的调用,我们使用请求缓存Request caching,能够保证不同线程不会进行重复冗余的微服务调用,因为通过缓存保证了唯一性,在微重复请求创建和执行一个新的线程时,hystrix能够返回一个缓存的结果,通过重载HystrixCommand or HystrixObserveableCommand 的 getCacheKey() 方法可以容易实现缓存。

实例

下面我们以BettingService为例子,客户端调用其getTodaysRaces方法或得到当天所有的比赛,最简单方式使用Hystrix是继承HystrixCommand 。

CommandGetTodaysRaces.java

public class CommandGetTodaysRaces extends HystrixCommand<List<Racecourse>> {
private final BettingService service;
private final boolean failSilently;
 
/**
* CommandGetTodaysRaces
*
* @param service
* Remote Broker Service
* @param failSilently
* If <code>true</code> will return an empty list if a remote service exception is thrown, if
* <code>false</code> will throw a BettingServiceException.
*/
 
public CommandGetTodaysRaces(BettingService service, boolean failSilently) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BettingServiceGroup")).andThreadPoolKey(
HystrixThreadPoolKey.Factory.asKey("BettingServicePool")));
this.service = service;
this.failSilently = failSilently;
}
 
public CommandGetTodaysRaces(BettingService service) {
this(service, true);
}
 
@Override
 
protected List<Racecourse> run() {
return service.getTodaysRaces();
}
 
@Override
 
protected List<Racecourse> getFallback() {
 
// can log here, throw exception or return default
if (failSilently) {
return new ArrayList<Racecourse>();
} else {
throw new RemoteServiceException("Unexpected error retrieving todays races");
}
 
}
}

下面我们使用缓存实现服务中GetHorsesInRaceWithCaching,获得某个比赛中所有正在奔跑的马。需要完成getCacheKey() 的实现,返回你这个应用缓存Key值。

public class CommandGetHorsesInRaceWithCaching extends HystrixCommand<List<Horse>> {
 
private final BettingService service;
private final String raceCourseId;
 
/**
* CommandGetHorsesInRaceWithCaching.
* @param service
* Remote Broker Service
* @param raceCourseId
* Id of race course
*/
 
public CommandGetHorsesInRaceWithCaching(BettingService service, String raceCourseId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BettingServiceGroup")).andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("BettingServicePool")));
this.service = service;
this.raceCourseId = raceCourseId;
}
 
@Override
 
protected List<Horse> run() {
return service.getHorsesInRace(raceCourseId);
}
 
@Override
 
protected List<Horse> getFallback() {
 
    // can log here, throw exception or return default
 
return new ArrayList<Horse>();
}
 
@Override
 
protected String getCacheKey() {
return raceCourseId;
}
}

对于BettingService中最后一个方法获得某个比赛某个赛马的赢率,这个使用请求折叠来防止快速重复的请求,使用请求折叠会有几毫秒的延迟,我们并不想让类似大量请求造成远程服务过载。

GetOddsForHorseRequest.java

public class GetOddsForHorseRequest {
private final String raceCourseId;
private final String horseId;
public GetOddsForHorseRequest(String raceCourseId, String horseId){
this.raceCourseId = raceCourseId;
this.horseId = horseId;
}
 
public String getRaceCourseId() {
return raceCourseId;
}
 
public String getHorseId() {
return horseId;
}
}

 

BatchCommandGetOddsForHorse.java

public class BatchCommandGetOddsForHorse extends HystrixCommand<List<String>> {
private final Collection<CollapsedRequest<String, GetOddsForHorseRequest>> requests;
private BettingService service;
public BatchCommandGetOddsForHorse(Collection<CollapsedRequest<String, GetOddsForHorseRequest>> requests) {
super(Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("BettingServiceGroup"))
.andThreadPoolKey(
HystrixThreadPoolKey.Factory.asKey("GetOddsPool")));
this.requests = requests;
}
 
@Override
 
  protected List<String> run() {
ArrayList<String> response = new ArrayList<String>();
for (CollapsedRequest<String, GetOddsForHorseRequest> request : requests) {
GetOddsForHorseRequest callRequest = request.getArgument();
response.add(service.getOddsForHorse(callRequest.getRaceCourseId(), callRequest.getHorseId()));
}
return response;
}
 
public void setService(BettingService service) {
this.service = service;
}
}

CommandCollapserGetOddsForHorse.java

public class CommandCollapserGetOddsForHorse extends HystrixCollapser<List<String>, String, GetOddsForHorseRequest> {
 
private final GetOddsForHorseRequest key;
private BettingService service;
public CommandCollapserGetOddsForHorse(GetOddsForHorseRequest key) {
this.key = key;
}
 
@Override
 
public GetOddsForHorseRequest getRequestArgument() {
return key;
}
 
@Override
 
protected HystrixCommand<List<String>> createCommand(final Collection<CollapsedRequest<String, GetOddsForHorseRequest>> requests) {
BatchCommandGetOddsForHorse command = new BatchCommandGetOddsForHorse(requests);
command.setService(service);
return command;
}
 
@Override
 
protected void mapResponseToRequests(List<String> batchResponse, Collection<CollapsedRequest<String, GetOddsForHorseRequest>> requests) {
int count = 0;
for (CollapsedRequest<String, GetOddsForHorseRequest> request : requests) {
request.setResponse(batchResponse.get(count++));
}
}
 
public void setService(BettingService service) {
this.service = service;
}
}

本文试图通过简单代码演示Hystrix的几个主要功能使用,可以认为是Hystrix的简单入门。

微服务架构