Spring Cloud学习笔记22——服务熔断、服务容错保护(Hystrix)

在微服务架构中,我们将系统拆分成了一个个的服务单元,各单元应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,线程资源无法释放,最终导致自身服务的瘫痪,进一步甚至出现故障的蔓延最终导致整个系统的瘫痪。如果这样的架构存在如此严重的隐患,那么相较传统架构就更加的不稳定。为了解决这样的问题,因此产生了断路器等一系列的服务保护机制。

针对上述问题,在Spring Cloud Hystrix中实现了线程隔离、断路器等一系列的服务保护功能。它也是基于Netflix的开源框架 Hystrix实现的,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备了服务降级服务熔断线程隔离请求缓存请求合并以及服务监控等强大功能。

定义服务降级

fallbackHystrix命令执行失败时使用的后备方法,用来实现服务的降级处理逻辑。在HystrixCommand中可以通过重载getFallback()方法来实现服务降级逻辑,Hystrix会在run()执行过程中出现错误、超时、线程池拒绝、断路器熔断等情况时,执行getFallback()方法内的逻辑,比如我们可以用如下方式实现服务降级逻辑:

public class UserCommand extends HystrixCommand<User> {

	private RestTemplate restTemplate;
	private Long id;

	public UserCommand(Setter setter,RestTemplate restTemplate,Long id){
		super(setter);
		this.restTemplate=restTemplate;
		this.id=id;
	}

	@Override
	protected User run(){
		return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id);
	}

	@Override
	protected User getFallback(){
		return new User();
	}

}

若要通过注解实现服务降级只需要使用@HystrixCommand中的fallbackMethod参数来指定具体的服务降级实现方法,如下所示:

public class UserService {

	@Autowired
	private RestTemplate restTemplate;

	@HystrixCommand(fallbackMethod="defaultUser")
	public User getUserById(Long id){
		return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id);
	}

	public User defaultUser(){
		return new User();
	}

}

在使用注解来定义服务降级逻辑时,我们需要将具体的Hystrix命令与fallback实现函数定义在同一个类中,并且fallbackMethod的值必须与实现fallback方法的名字相同。由于必须定义在同一个类中,所以对于fallback的访问修饰符没有特定的要求,定义为privateprotectedpublic均可。

在上面的例子中,defaultUser方法将在getUserById执行时发生错误的情况下被执行。若defaultUser方法实现的并不是一个稳定逻辑,它依然可能会发生异常,那么我们可以为它添加@HystrixCommand注解以生成Hystrix命令,同时使用fallbackMethod来指定服务降级逻辑,比如:

public class UserService {

	@Autowired
	private RestTemplate restTemplate;

	@HystrixCommand(fallbackMethod="defaultUser")
	public User getUserById(Long id){
		return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id);
	}

	@HystrixCommand(fallbackMethod="defaultUserSec")
	public User defaultUser(){
		//此处可能是另外一个网络请求来获取,所以也有可能失败
		return new User("First Fallback");
	}

	public User defaultUserSec(){
		return new User("Second Fallback");
	}

}

在实际使用时,我们需要为大多数执行过程中可能会失败的Hystrix命令实现服务降级逻辑,但是也有一些情况可以不去实现降级逻辑:

  • 执行写操作的命令:当Hystrix命令是用来执行写操作而不是返回一些信息的时候,通常情况下这类操作的返回类型是void或是为空的Observable,实现服务降级的意义不是很大。当写入操作失败的时候,我们通常只需要通知调用者即可。
  • 执行批处理或离线计算的命令:当Hystrix命令是用来执行批处理程序生成一份报告或是进行任何类型的离线计算时,那么通常这些操作只需要将错误传播给调用者,然后让调用者稍后重试而不是发送给调用者一个静默的降级处理响应。

不论Hystrix命令是否实现了服务降级,命令状态和断路器状态都会更新,并且我们可以由此了解到命令执行的失败情况。

异常处理

HystrixCommand实现的run()方法中抛出异常时,除了HystrixBadRequestException之外,其他异常均会被Hystrix认为命令执行失败并触发服务降级的处理逻辑,所以当需要在命令执行中抛出不触发服务降级的异常时来使用它。

而在使用注册配置实现Hystrix命令时,它还支持忽略指定异常类型功能,只需要通过设置@HystrixCommand注解的ignoreExceptions参数,比如:

@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逻辑。

Hystrix命令因为异常(除了HystrixBadRequestException的异常)进入服务降级逻辑之后,往往需要对不同异常做针对性的处理:

  • 在以传统继承方式实现的Hystrix命令中,我们可以用getFallback()方法通过Throwable getExecutionException()方法来获取具体的异常,通过判断来进入不同的处理逻辑。
  • 注解配置方式 也同样可以实现异常的获取。它的实现也非常简单,只需要在fallback实现方法的参数中增加Throwable e对象的定义,这样在方法内部就可以获取触发服务降级的具体异常内容了,比如:
@HystrixCommand(fallbackMethod="fallback1")
User getUserById(String id){
	return new RuntimeException("getUserById command failed");
}

User fallback1(String id,Throwable e){
	assert "getUserById command failed".equals(e.getMessage());
}

命令名称、分组以及线程池划分

以继承方式实现的Hystrix命令使用类名作为默认的命令名称,我们也可以在构造函数中通过Setter静态类来设置,比如:

public UserCommand(){
	super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
	.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName")););
}

Setter的定义中,只有withGroupKey静态函数可以创建Setter的实例,所以GroupKey是每个Setter必需的参数,而CommandKey则是一个可选参数。

通过设置命令组,Hystrix会根据组来组织和统计命令的告警、仪表盘等信息。除了根据组能实现统计之外,Hystrix命令默认的线程划分也是根据命令分组来实现的。默认情况下,Hystrix会让相同组名的命令使用同一个线程池,所以我们需要在创建Hystrix命令时为其指定命令组名来实现默认的线程池划分。

如果Hystrix的线程池分配仅仅依靠命令组来划分,那么它就显得不够灵活了,所以Hystrix还提供了HystrixThreadPoolKey来对线程池进行设置,通过它我们可以实现更细粒度的线程池划分,比如:

public UserCommand(){
	super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
			.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"))
			.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
}

如果在没有特别指定HystrixThreadPoolKey的情况下,依然会使用命令组的方式来划分线程池。通常情况下,尽量通过HystrixThreadPoolKey的方式来指定线程池的划分,而不是通过组名的默认方式实现划分,因为多个不同的命令可能从业务逻辑上来看属于同一个组,但是往往从实现本身上需要跟其他命令进行隔离。

使用@HystrixCommand注解来设置命令名称、分组以及线程池划分的示例如下:

@HystrixCommand(commandKey = "getUserById",groupKey = "UserGroup",threadPoolKey = "getUserByIdThread")
public User getUserById(Long id){
	return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id);
}

依赖隔离

Docker通过“舱壁模式”实现进程的隔离,使得容器与容器之间不会互相影响。而Hystrix则使用该模式实现线程池的隔离,它会为每一个Hystrix命令创建一个独立的线程池,这样就算某个在Hystrix命令包装下的依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的服务。

通过对依赖服务的线程池隔离实现,可以带来如下优势:

  • 应用自身得到完全的保护,不会受不可控的依赖服务影响。即便给依赖服务分配的线程池被填满,也不会影响应用自身的其余部分。
  • 可以有效降低接入新服务的风险。如果新服务接入后运行不稳定或存在问题,完全不会影响到应用其他的请求。
  • 当依赖的服务从失效恢复正常后,它的线程池会被清理并且能够马上恢复健康的服务,相比之下容器级别的清理恢复速度要慢得多。
  • 当依赖的服务出现配置错误的时候,线程池会快速的反应出此问题(通过失败次数、延迟、超时、拒绝等指标的增加情况)。同时,我们可以在不影响应用功能的情况下通过实时的动态属性刷新来处理它。
  • 当依赖的服务因实现机制调整等原因造成其性能出现很大变化的时候,线程池的监控指标信息会反映出这样的变化。同时,我们也可以通过实时动态刷新自身应用对依赖服务的阈值进行调整以适应依赖方的改变。
  • 除了上面通过线程池隔离服务发挥的优点之外,每个专有线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步的访问。

总之,通过对依赖服务实现线程池隔离,让我们的应用更加健壮,不会因为个别依赖服务出现问题而引起非相关服务的异常。同时,也使得我们的应用变得更加灵活,可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。

Netflix在设计Hystrix的时候,认为线程池上的开销相对于隔离所带来的好处是无法比拟的。同时,Netflix也针对线程池的开销做了相关的测试,以证明和打消Hystrix实现对性能影响的顾虑。

99%的情况下,使用线程池隔离的延迟有9ms,对于大多数需求来说这样的消耗是微乎其微的,更何况为系统在稳定性和灵活性上所带来的巨大提升。虽然对于大部分的请求我们可以忽略线程池的额外开销,而对于小部分延迟本身就非常小的请求(可能只需要1ms),那么9ms的延迟开销还是非常昂贵的。实际上Hystrix也为此设计了另外的一个解决方案:信号量。

Hystrix中除了使用线程池之外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销要远比线程池的开销小得多,但是它不能设置超时和实现异步访问。所以,只有在依赖服务是足够可靠的情况下才使用信号量。在HystrixCommandHystrixObservableCommand2处支持信号量的使用:

  • 命令执行:如果隔离策略参数execution.isolation.strategy设置为SEMAPHOREHystrix会使用信号量替代线程池来控制依赖服务的并发控制。
  • 降级逻辑:当Hystrix尝试降级逻辑时候,它会在调用线程中使用信号量。

信号量的默认值为10,我们也可以通过动态刷新配置的方式来控制并发线程的数量。对于信号量大小的估算方法与线程池并发度的估算类似。仅访问内存数据的请求一般耗时在1ms以内,性能可以达到5000rps,这样级别的请求我们可以将信号量设置为1或者2,我们可以按此标准并根据实际请求耗时来设置信号量。

断路器的工作原理

当我们把服务提供者eureka-client中加入了模拟的时间延迟之后,在服务消费端的服务降级逻辑因为Hystrix命令调用依赖服务超时,触发了降级逻辑,但是即使这样,受限于Hystrix超时时间的问题,我们的调用依然很有可能产生堆积。

这个时候断路器就会发挥作用,那么断路器是在什么情况下开始起作用呢?这里涉及到断路器的三个重要参数:快照时间窗请求总数下限错误百分比下限。这些参数的作用分别是:

  • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
  • 请求总数下限:在快照时间窗内,必须满足请求总数下限才有资格根据熔断。默认为20,意味着在10秒内,如果该Hystrix命令的调用此时不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
  • 错误百分比下限:当请求总数在快照时间窗内超过了下限,比如发生了30次调用,如果在这30次调用中,有16次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%下限情况下,这时候就会将断路器打开。

那么当断路器打开之后会发生什么呢?我们先来说说断路器未打开之前,对于之前那个示例的情况就是每个请求都会在当Hystrix超时之后返回fallback,每个请求时间延迟就是近似Hystrix的超时时间,如果设置为5秒,那么每个请求就都要延迟5秒才会返回。当熔断器在10秒内发现请求总数超过20,并且错误百分比超过50%,这个时候熔断器打开。打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就不会等待5秒之后才返回fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。

在断路器打开之后,处理逻辑并没有结束,我们的降级逻辑已经被成了主逻辑,那么原来的主逻辑要如何恢复呢?对于这一问题,Hystrix也为我们实现了自动恢复功能。当断路器打开,对主逻辑进行熔断之后,Hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。

通过上面的一系列机制,Hystrix的断路器实现了对依赖资源故障的端口、对降级策略的自动切换以及对主逻辑的自动恢复机制。这使得我们的微服务在依赖外部服务或资源的时候得到了非常好的保护,同时对于一些具备降级逻辑的业务需求可以实现自动化的切换与恢复,相比于设置开关由监控和运维来进行切换的传统实现方式显得更为智能和高效。

断路器的意义

好处:

  • 系统稳定
  • 减少性能损耗
  • 及时响应
  • 阀值可定制

断路器的功能

  • 异常处理
  • 日志记录
  • 测试失败的操作
  • 手动复位
  • 并发
  • 加速断路
  • 重试失败请求

服务熔断与服务降级

服务熔断:一般是某个服务故障或者异常引起,类似现实世界中的“保险丝”,当某个异常条件被触发,直接熔断整个服务,而不是一直等到此服务超时。

服务降级:整体资源快不够了,忍痛将某些服务先关掉,待渡过难关,再开启回来。所谓降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强。服务降级处理是在客户端完成的,与服务端没有关系。

相似性

  • 目的一致
  • 表现类似
  • 粒度一致

主要区别

  • 触发条件不同
  • 管理目标的层次不同

请求缓存

当系统用户不断增长时,每个微服务需要承受的并发压力也越来越大。在分布式环境下,通常压力来自于对依赖服务的调用,因为请求依赖服务的资源需要通过通信来实现,这样的依赖方式比起进程内的调用方式会引起一部分的性能损失,同时HTTP相比于其他高性能的通信协议在速度上没有任何优势,所以它有些类似于对数据库这样的外部资源进行读写操作,在高并发的情况下可能会成为系统的瓶颈。

在高并发的场景之下,Hystrix中提供了请求缓存的功能,我们可以方便地开启和使用请求缓存来优化系统,达到减轻高并发时的请求线程消耗、降低请求响应时间的效果。

开启请求缓存功能

Hystrix请求缓存的使用非常简单,我们只需要在实现HystrixCommandHystrixObservableCommand时,通过重载getCacheKey()方法来开启请求缓存,比如:

public class UserCommand extends HystrixCommand<User> {

	private RestTemplate restTemplate;
	private Long id;

	public UserCommand(RestTemplate restTemplate,Long id){
		super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserGroup")));
		this.restTemplate=restTemplate;
		this.id=id;
	}

	@Override
	protected User run(){
		return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id);
	}

	@Override
	protected String getCacheKey(){
		return String.valueOf(id);
	}

}

在上面的例子中,我们通过在getCacheKey方法中返回的请求缓存key值(使用了传入的获取User对象的id值),就能让该请求命令具备缓存功能。此时,当不同的外部请求处理逻辑调用了同一个依赖服务时,Hystrix会根据getCacheKey方法返回的值来区分是否是重复的请求,如果它们的cacheKey相同,那么该依赖服务只会在第一个请求到达时被真实地调用一次,另外一个请求则是直接从请求缓存中返回结果,所以通过开启请求缓存可以让我们实现的Hystrix命令具备下面几项好处:

  • 减少重复的请求数,降低依赖服务的并发度。
  • 在同一用户请求的上下文上,相同依赖服务的返回数据始终保持一致。
  • 请求缓存在run()construct()执行之前生效,所以可以有效减少不必要的线程开销。

清理失效缓存功能

使用请求缓存时,如果只是只读操作,那么不需要考虑缓存内容是否正确的问题,但是如果请求命令中还有更新数据的写操作,那么缓存中的数据就需要我们在进行写操作时进行及时处理,以防止读操作的请求命令获取到了失效的数据。

Hystrix中,我们可以通过HystrixRequestCache.clear()方法来进行缓存的清理,具体示例如下:

public class UserGetCommand extends HystrixCommand<User> {

	private static final HystrixCommandKey GETTER_KEY=HystrixCommandKey.Factory.asKey("CommandKey");
	private RestTemplate restTemplate;
	private Long id;

	public UserGetCommand(RestTemplate restTemplate,Long id){
		super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetSetGet"))
			.andCommandKey(GETTER_KEY));
		this.restTemplate=restTemplate;
		this.id=id;
	}

	@Override
	protected User run(){
		return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id);
	}

	@Override
	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));
	}

}
public class UserPostCommand extends HystrixCommand<User> {

	private RestTemplate restTemplate;
	private User user;

	public UserPostCommand(RestTemplate restTemplate,User user){
		super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetSetGet")));
		this.restTemplate=restTemplate;
		this.user=user;
	}

	@Override
	protected User run(){
		//写操作
		User r = restTemplate.postForObject("http://USER-SERVICE/users", user, User.class);
		//刷新缓存,清理缓存中失效的User
		UserGetCommand.flushCache(user.getId());
		return r;
	}

}

该示例中主要有两个请求命令:UserGetCommand用于根据id获取User对象、而UserPostCommand用于更新User对象。当我们对UserGetCommand命令实现了请求缓存之后,那么势必需要为UserPostCommand命令实现缓存的清理,以保证User被更新之后,Hystrix请求缓存中相同缓存Key的结果被移除,这样在下一次获取User的时候不会从缓存中获取到未更新的结果。

在上面UserGetCommand的实现中,增加了一个静态方法flushCache,该方法通过HystrixRequestCache.getInstance(GETTER_KEY, HystrixConcurrencyStrategyDefault.getInstance())方法从默认的Hystrix并发策略中根据GETTER_KEY获取到该命令的请求缓存对象HystrixRequestCache的实例,然后再调用该请求缓存对象实例的clear方法,对Key为更新Userid值的缓存内容进行清理。而在UserPostCommand的实现中,在run方法调用依赖服务之后,增加了对UserGetCommand中静态方法flushCache的调用,以实现对失效缓存的清理。

注意:getCacheKey方法默认返回的是null,如果不重写getCacheKey方法,让它返回一个非null值,那么缓存功能是不会开启的;同时请求命令的缓存开启属性也需要设置为true才能开启(该属性默认为true,所以通常用该属性来控制请求缓存功能的强制关闭)。

使用注解实现请求缓存

Hystrix的请求缓存除了可以通过上面传统的方式实现之外,还可以通过注解的方式进行配置实现。Hystrix只提供了三个专用于请求缓存的请求:

注解描述属性
@CacheResult该注解用来标记请求命令返回的结果应该被缓存,它必须与@HystrixCommand注解结合使用cacheKeyMethod
@CacheRemove该注解用来让请求命令的缓存失效,失效的缓存根据定义的Key决定commandKey,cacheKeyMethod
@CacheKey该注解用来在请求命令的参数上标记,使其作为缓存的Key值,如果没有标注则会使用所有参数。如果同时还使用了@CacheResult和@CacheRemove注解的cacheKeyMethod方法指定缓存Key的生成,那么该注解将不会起作用value

这几个注解的具体使用方法:

  • 设置请求缓存:通过注解为请求命令开启缓存功能非常简单,如下例所示,我们只需添加@CacheResult 注解即可。当该依赖服务被调用并返回User对象时,由于该方法被@CacheResult 注解修改,所以Hystrix会将该结果置入请求缓存中,而它的缓存Key值会使用所有的参数,也就是这里Long类型的id值。
@CacheResult
@HystrixCommand
public User getUserById(Long id){
	return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id);
}
  • 定义缓存Key:当使用注解来定义请求缓存时,若要为请求命令指定具体的缓存Key生成规则,我们可以使用@CacheResult@CacheRemove注解的cacheKeyMethod方法来指定具体的生成函数;也可以通过使用@CacheKey注解在方法参数中指定用于组装缓存Key的元素。

使用cacheKeyMethod方法的示例如下,它通过在请求命令的同一个类中定义一个专门生成Key的方法,并用@CacheResult注解的cacheKeyMethod方法来指定它即可。它的配置方式类似于@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注解实现的方式更加简单,具体示例如下。但是在使用@CacheKey注解的时候需要注意,它的优先级比cacheKeyMethod的优先级低,如果已经使用了cacheKeyMethod指定缓存Key的生成函数,那么@CacheKey注解不会生效。

@CacheResult
@HystrixCommand
public User getUserById(@CacheKey("id") Long id){
	return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id);
}

@CacheKey注解除了可以指定方法参数作为缓存Key之外,它还允许访问参数对象的内部属性作为缓存Key。比如下面的例子,它指定了User对象的id属性作为缓存Key

@CacheResult
@HystrixCommand
public User getUserById(@CacheKey("id") User user){
	return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,user.getId());
}
  • 缓存清理:若内容调用了update操作进行了更新,那么此时请求缓存中的结果与实际结果就会产生不一致(缓存中的结果实际上已经失效了),所以我们需要在update类型的操作上对失效的缓存进行清理。在Hystrix的注解配置中,可以通过@CacheRemove注解来实现失效缓存的清理,比如下面的例子所示:
@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);
}

需要注意的是,@CacheRemove注解的commandKey属性是必须要指定的,它用来指明需要使用请求缓存的请求命令,因为只有通过该属性的配置,Hystrix才能找到正确的请求命令缓存位置。

请求合并

微服务架构中的依赖通常通过远程调用实现,而远程调用中最常见的问题就是通信消耗与连接占用。在高并发的情况之下,因通信次数的增加,总的通信时间消耗将会变得不那么理想。同时,因为依赖服务的线程池资源有限,将出现排队等待与响应延迟的情况。为了优化这两个问题,Hystrix提供了HystrixCollapser来实现请求的合并,以减少通信消耗和线程数的占用。

HystrixCollapser实现了在HystrixCommand之前放置一个合并处理器,将处于一个很短的时间窗(默认10毫秒)内对同一依赖服务的多个请求进行整合并以批量方式发起请求的功能(服务提供方也需要提供相应的批量实现接口)。通过HystrixCollapser的封装,开发者不需要关注线程合并的细节过程,只需关注批量化服务和处理。

实现请求合并

假设当前微服务USER-SERVICE提供了两个获取User的接口:

  • /users/{id}:根据id返回User对象的GET请求接口
  • /users?ids={ids}:根据ids返回User对象列表的GET请求接口,其中ids为以逗号分隔的id集合

而在服务消费端,已经为这两个远程接口通过RestTemplate实现了简单的调用,具体如下所示:

@Service
public class UserServiceImpl implements UserService{

	@Autowired
	private RestTemplate restTemplate;


	@Override
	public User find(Long id) {
		return restTemplate.getForObject("http//user-service/users/{1}",User.class,id);
	}

	@Override
	public List<User> findAll(List<Long> ids) {
		return restTemplate.getForObject("http//user-service/users?ids={1}",List.class, StringUtils.join(ids,","));
	}

}

接着,我们实现将短时间内多个获取单一User对象的请求命令进行合并。

第一步,为请求合并的实现准备一个批量请求命令的实现,具体如下:

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);
	}

}

批量请求命令实际上就是一个简单的HystrixCommand实现,从上面的实现中可以看到它通过调用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>> collapsedRequests) {
		List<Long> userIds = new ArrayList<>(collapsedRequests.size());
		userIds.addAll(collapsedRequests.stream().map(CollapsedRequest::getArgument).collect(Collectors.toList()));
		return new UserBatchCommand(userService,userIds);
	}

	@Override
	protected void mapResponseToRequests(List<User> batchResponse, Collection<CollapsedRequest<User, Long>> collapsedRequests) {
		int count = 0;
		for (CollapsedRequest<User,Long> collapsedRequest:collapsedRequests){
			User user = batchResponse.get(count++);
			collapsedRequest.setResponse(user);
		}
	}

}

在上面的构造函数中,我们为请求合并器设置了时间延迟属性,合并器会在该时间窗内收集获取单个User的请求并在时间窗结束时进行合并组装单个批量请求。getRequestArgument方法返回给定的单个请求参数userId,而createCommandmapResponseToRequests是请求合并器的两个核心。

  • createCommand:该方法的collapsedRequests参数中保存了延迟时间窗中收集到的所有获取单个User的请求。通过获取这些请求的参数来组织上面我们准备的批量请求命令UserBatchCommand实例
  • mapResponseToRequests:在批量请求命令UserBatchCommand实例被触发执行完成之后,该方法开始执行,其中batchResponse中保存了createCommand方法中组织的批量请求命令的返回结果,而collapsedRequests参数则代表了每个被合并的请求。在这里我们通过遍历批量结果batchResponse对象,为collapsedRequests中每个合并前的单个请求设置返回结果,以此完成批量结果到单个请求结果的转换。

在未使用HystrixCollapser请求合并器之前,线程使用情况如下图所示。当服务消费者同时对USER-SERVICE/users/{id}接口发起了5个请求时,会向该依赖服务的独立线程池中申请5个线程来完成各自的请求操作。
在这里插入图片描述
而在使用了HystrixCollapser请求合并器之后,相同情况下的线程占用如下图所示。由于同一时间发生的5个请求处于请求合并器的一个时间窗内,这些发向/users/{id}接口的请求被请求合并器拦截下来,并在合并器中进行组合,然后将这些请求合并成一个请求发向USER-SERVICE的批量接口/users?ids={ids}。在获取到批量请求结果之后,通过请求合并器再将批量结果拆分并分配给每个被合并的请求。
在这里插入图片描述
从图中我们可以看到,通过使用请求合并器有效减少了对线程池中资源的占用。所以在资源有效并且短时间内会产生高并发请求的时候,为避免连接不够用而引起的延迟可以考虑使用请求合并器的方式来处理和优化。

使用注解实现请求合并器

以上面实现的请求合并器为例,还可以通过如下方式实现:

@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/user?ids={1}",List.class, StringUtils.join(ids,","));
	}

}

这里通过@HystrixCommand定义了两个Hystrix命令,一个用于请求/users/{id}接口,一个用于请求/users?ids={ids}接口。而在请求/users/{id}接口的方法上通过@HystrixCollapser注解为其创建了合并请求器,通过batchMethod属性指定了批量请求的实现方法为findAll方法(即请求/users?ids={ids}接口的命令),同时通过collapserProperties属性为合并请求器设置了相关属性,这里使用@HystrixProperty(name="timerDelayInMilliseconds",value = "100")将合并时间窗设置为100毫秒。这样通过@HystrixCollapser注解简单而又优雅地实现了在/users/{id}依赖服务之前设置了一个批量请求合并器。

请求合并的额外开销

使用请求合并带来的额外开销:用于请求合并的延迟时间窗会使得依赖服务的请求延迟增高。

由于请求合并器的延迟时间窗会带来额外开销,所以我们是否使用请求合并器需要根据依赖服务调用的实际情况来选择,主要考虑下面两个方面:

  • 请求命令本身的延迟。如果依赖服务的请求命令本身是一个高延迟的命令,那么可以使用请求合并器,因为延迟时间窗的时间消耗显得微不足道了。
  • 延迟时间窗内的并发量。如果一个时间窗内只有1~2个请求,那么这样的依赖服务不适合使用请求合并器。这种情况不但不能提升系统性能,反而会成为系统瓶颈,因为每个请求都需要多消耗一个时间窗才响应。相反,如果一个时间窗内具有很高的并发量,并且服务提供方也实现了批量处理接口,那么使用请求合并器可以有效减少网络连接数量并极大提升系统吞吐量,此时延迟时间窗所增加的消耗就可以忽略不计了。

Hystrix属性详解

我们可以根据实现HystrixCommand的不同方式将配置方法分为如下两类:

  • 当通过继承的方式实现时,可使用Setter对象来请求命令的属性进行设置,比如下面的例子:
public HystrixCommandInstance(int id){
	super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
		.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
		.withExecutionTimeoutInMilliseconds(500)));
	this.id=id;
}
  • 当通过注解的方法实现时,只需使用@HystrixCommand中的commandProperties属性来设置,比如:
@HystrixCommand(commandKey = "helloKey",
		commandProperties = {
				@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "5000")
		}
)
public User getUserById(Long id){
	return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id);
}

优先级

HystrixPropertiesStrategy实现的各项配置属性都存在下面4个不同优先级别的配置(优先级由低到高):

  • 全局默认值:如果没有设置下面三个级别的属性,那么这个属性就是默认值。由于该属性通过代码定义,所以对于这个级别,我们主要关注它在代码中定义的默认值即可。
  • 全局配置属性:通过在配置文件中定义全局属性值,在应用启动时或在与Spring Cloud ConfigSpring Cloud Bus实现的动态刷新配置功能配合下,可以实现对“全局默认值”的覆盖以及在运行期对“全局默认值”的动态调整。
  • 实例默认值:通过代码为实例定义的默认值。通过代码的方式为实例设置属性值来覆盖默认的全局配置。
  • 实例配置属性:通过配置文件来为指定的实例进行属性配置,以覆盖前面的三个默认值。它也可用Spring Cloud ConfigSpring Cloud Bus实现的动态刷新配置功能实现对具体实例配置的动态调整。

Command属性

Command属性主要用来控制HystrixCommand命令的行为。

它主要有下面5种不同类型的属性配置:

  • execution配置:控制的是HystrixCommand.run()的执行,具体属性如下:
属性说明
execution.isolation.strategy用来设置HystrixCommand.run()执行的隔离策略。它有两个选项:THREAD(通过线程池隔离的策略)、SEMAPHORE(通过信号量隔离的策略)。
execution.isolation.thread.timeoutInMilliseconds用来配置HystrixCommand执行的超时时间,单位为毫秒。当HystrixCommand执行时间超过该配置值之后,Hystrix会将该执行命令标记为TIMEOUT并进入服务降级处理逻辑。
execution.timeout.enabled用来配置HystrixCommand.run()的执行是否启用超时时间。默认为true,如果设置为false,那么execution.isolation.thread.timeoutInMilliseconds属性的配置将不再起作用。
execution.isolation.thread.interruptOnTimeout用来配置当HystrixCommand.run()执行超时的时候是否要将它中断。
execution.isolation.thread.interruptOnCancel用来配置当HystrixCommand.run()执行被取消的时候是否要将它中断。
execution.isolation.semaphore.maxConcurrentRequests当HystrixCommand的隔离策略使用信号量的时候,该属性用来配置信号量的大小(并发请求数)。当最大并发请求数达到该设置值时,后续的请求将会被拒绝。
  • fallback配置:控制HystrixCommand.getFallback()的执行,同时适用于线程池的信号量的隔离策略,具体属性如下:
属性说明
fallback.isolation.semaphore.maxConcurrentRequests用来设置从调用线程中允许HystrixCommand.getFallback()方法执行的最大并发请求数。当达到最大并发请求数时,后续的请求将会被拒绝并抛出异常(因为它已经没有后续的fallback可以被调用了)。
fallback.enabled用来设置服务降级策略是否启用,如果设置为false,那么当请求失败或者拒绝发生时,将不会调用HystrixCommand.getFallback()来执行服务降级逻辑。
  • circuitBreaker配置:控制HystrixCircuitBreaker的行为,具体属性如下:
属性说明
circuitBreaker.enabled用来确定当服务请求命令失败时,是否使用断路器来跟踪其健康指标和熔断请求。
circuitBreaker.requestVolumeThreshold用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为20的时候,如果滚动时间窗(默认10秒)内仅收到了19个请求,即使这19个请求都失败了,断路器也不会打开。
circuitBreaker.sleepWindowInMilliseconds用来设置当断路器打开之后的休眠时间窗。休眠时间窗结束之后,会将断路器置为“半开”状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为“打开”状态,如果成功就设置为“关闭”状态。
circuitBreaker.errorThresholdPercentage用来设置断路器打开的错误百分比条件。例如,默认值为5000的情况下,表示在滚动时间窗中,在请求数量超过circuitBreaker.requestVolumeThreshold阈值的前提下,如果错误请求数的百分比超过50,就把断路器设置为“打开”状态,否则就设置为“关闭”状态。
circuitBreaker.forceOpen如果设置为true,断路器将强制进入“打开”状态,它会拒绝所有请求。该属性优先于circuitBreaker.forceClosed属性。
circuitBreaker.forceClosed如果设置为true,断路器将强制进入“关闭”状态,它会接收所有请求。如果circuitBreaker.forceOpen属性为true,该属性不会生效。
  • metrics配置:与HystrixCommandHystrixObservableCommand执行中捕获的指标信息有关,具体属性如下:
属性说明
metrics.rollingStats.timeInMilliseconds用来设置滚动时间窗的长度,单位为毫秒。该时间用于断路器判断健康度时需要收集信息的持续时间。断路器在收集指标信息的时候会根据设置的时间窗长度拆分成多个“桶”来累计各度量值,每个“桶”记录了一段时间内的采集指标。例如,当采用默认值10000毫秒时,断路器默认将其拆分成10个桶(桶的数量也可以通过metrics.rollingStats.numBuckets参数设置)。每个桶记录1000毫秒内的指标信息。该属性从Hystrix 1.4.12版本开始,只有在应用初始化的时候生效,通过动态刷新配置不会产生效果,这样做是为了避免出现运行期监测数据丢失的情况。
metrics.rollingStats.numBuckets用来设置滚动时间窗统计指标信息时划分“桶”的数量。
metrics.rollingPercentile.enabled用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为false,那么所有的概要统计都将返回-1。
metrics.rollingPercentile.timeInMilliseconds用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
metrics.rollingPercentile.numBuckets用来设置百分位统计滚动窗口中使用“桶”的数量。metrics.rollingPercentile.timeInMilliseconds参数的设置必须能够被metrics.rollingPercentile.numBuckets参数整除,不然将会抛出异常。该属性从Hystrix 1.4.12版本开始,只有在应用初始化的时候生效,通过动态刷新配置不会产生效果,这样做是为了避免出现运行期监测数据丢失的情况。
metrics.rollingPercentile.bucketSize用来设置在执行过程中每个“桶”中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,就从最初的位置开始重写。例如,将该值设置为100,滚动窗口为10秒,若在10秒内一个“桶”发生了500次执行,那么该“桶”中只保留最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。该属性从Hystrix 1.4.12版本开始,只有在应用初始化的时候生效,通过动态刷新配置不会产生效果,这样做是为了避免出现运行期监测数据丢失的情况。
metrics.healthSnapshot.intervalInMilliseconds用来设置采集影响断路器状态的健康快照(请求的成功、错误百分比)的间隔等待时间。
  • requestContext配置:涉及HystrixCommand使用的HystrixRequestContext的设置,具体属性如下:
属性说明
requestCache.enabled用来配置是否开启请求缓存。
requestLog.enabled用来设置HystrixCommand的执行和事件是否打印日志到HystrixRequestLog中。

collapser属性

该属性除了在代码中用set和配置文件配置之外,也可使用注解进行配置。可使用@HystrixCollapser中的collapseProperties属性来设置,比如:

@HystrixCollapser(batchMethod = "batch",collapserProperties = {
	@HystrixProperty(name="timerDelayInMilliseconds",value = "20")
})

下面这些属性用来控制命令合并相关的行为:

  • maxRequestsInBatch:用来设置一次请求合并批处理中允许的最大请求数。
  • timerDelayInMilliseconds:用来设置批处理过程中每个命令延迟的时间,单位为毫秒。
  • requestCache.enabled:用来设置批处理过程中是否开启请求缓存。

threadPool属性

该属性除了在代码中用set和配置文件配置之外,也可使用注解进行配置。可使用@HystrixCommand中的threadPoolProperties属性来设置,比如:

@HystrixCommand(fallbackMethod = "helloFallback",commandKey = "helloKey",
	threadPoolProperties = {
		@HystrixProperty(name = "coreSize",value = "20")
	}
)

下面这些属性用来控制Hystrix命令所属线程池的配置:

  • coreSize:用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量。
  • maxQueueSize:用来设置线程池的最大队列大小。当设置为-1时,线程池将使用SynchronousQueue实现的队列,否则将使用LinkedBlockingQueue实现的队列。该属性只有在初始化的时候才有用,无法用过动态刷新的方式来调整。
  • queueSizeRejectionThreshold:用来为队列设置拒绝阈值。通过该参数,即使队列没有达到最大值也能拒绝请求。该参数主要是对LinkedBlockingQueue队列的补充,因为LinkedBlockingQueue队列不能动态修改它的对象大小,而通过属性就可以调整拒绝请求的队列大小了。当maxQueueSize属性为-1的时候,该属性不会生效。
  • metrics.rollingStats.timeInMilliseconds:用来设置滚动时间窗的长度,单位为毫秒。该滚动时间窗的长度用于线程池的指标度量,它会被分成多个“桶”来统计指标。
  • metrics.rollingStats.numBuckets:用来设置滚动时间窗被划分成“桶”的数量。注意:metrics.rollingStats.timeInMilliseconds参数的设置必须能够被metrics.rollingStats.numBuckets参数整除,不然将会抛出异常。

Hystrix仪表盘

断路器是根据一段时间窗内的请求情况来判断并操作断路器的打开和关闭状态的。而这些请求情况的指标信息都是HystrixCommandHystrixObservableCommand实例在执行过程中记录的重要度量信息,它们除了在Hystrix断路器实现中使用之外,对于系统运维也有非常大的帮助。这些指标信息会以“滚动时间窗”与“桶”结合的方式进行汇总,并在内存中驻留一段时间,以供内部或外部进行查询使用,Hystrix Dashboard(仪表盘)就是这些指标内容的消费者之一。

Spring Cloud除了优雅整合了Hystrix,还完美地整合了它的仪表盘组件Hystrix Dashboard,它主要用来实时监控Hystrix的各项指标信息。通过Hystrix Dashboard反馈的实时信息,可以帮助我们快速发现系统中存在的问题,从而及时地采取应对措施。

Spring Cloud中构建一个Hystrix Dashboard非常简单,只需要下面四步:

  • 创建一个标准的Spring Boot工程,命名为:hystrix-dashboard
  • 编辑pom.xml,具体依赖内容如下:
<parent>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-parent</artifactId>
	<version>Dalston.SR1</version>
	<relativePath />
</parent>
<dependencies>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-hystrix</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-actuator</artifactId>
	</dependency>
</dependencies>
  • 为应用主类加上@EnableHystrixDashboard,启用Hystrix Dashboard功能。
@EnableHystrixDashboard
@SpringCloudApplication
public class HystrixDashboardApplication {
	public static void main(String[] args) {
		SpringApplication.run(HystrixDashboardApplication.class, args);
	}
}
  • 根据实际情况修改application.properties配置文件,比如:选择一个未被占用的端口等,此步非必须。
spring.application.name=hystrix-dashboard
server.port=1301

到这里我们已经完成了基本配置,接下来我们可以启动该应用,并访问:http://localhost:1301/hystrix,我们可以看到如下页面:
在这里插入图片描述
这是Hystrix Dashboard的监控首页,该页面中并没有具体的监控信息。从页面的文字内容中我们可以知道,Hystrix Dashboard共支持三种不同的监控方式,依次为:

  • 默认的集群监控:通过URL http://turbine-hostname:port/turbine.stream开启,实现对默认集群的监控。
  • 指定的集群监控:通过URL http://turbine-hostname:port/turbine.stream?cluster=[clusterName]开启,实现对clusterName集群的监控。
  • 单体应用的监控:通过URL http://hystrix-app:port/hystrix.stream开启,实现对具体某个服务实例的监控。

前两者都对集群的监控,需要整合Turbine才能实现,这部分内容我们后面再做详细介绍。现在我们主要实现对单个服务实例的监控,所以这里我们先来实现单个服务实例的监控。

既然Hystrix Dashboard监控单实例节点需要通过访问实例的/hystrix.stream接口来实现,自然我们需要为服务实例添加这个端点,而添加该功能的步骤也同样简单,只需要下面两步:

  • 在服务实例pom.xml中的dependencies节点中新增spring-boot-starter-actuator监控模块以开启监控相关的端点,并确保已经引入断路器的依赖spring-cloud-starter-hystrix
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • 确保在服务实例的主类中已经使用@EnableCircuitBreaker@EnableHystrix注解,开启了断路器功能。

到这里已经完成了所有的配置,我们可以在Hystrix Dashboard的首页输入http://localhost:2101/hystrix.stream,已启动对“eureka-consumer-ribbon-hystrix”的监控,点击“Monitor Stream”按钮,此时我们可以看到如下页面:
在这里插入图片描述
在对该页面介绍前,我们先看看在首页中我们还没有介绍的两外两个参数:

  • Delay:该参数用来控制服务器上轮询监控信息的延迟时间,默认为2000毫秒,我们可以通过配置该属性来降低客户端的网络和CPU消耗。
  • Title:该参数对应了上图头部标题Hystrix Stream之后的内容,默认会使用具体监控实例的URL,我们可以通过配置该信息来展示更合适的标题。

回到监控页面,我们来详细说说其中各元素的具体含义:

  • 实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,如下图所示,它的健康度从绿色、黄色、橙色、红色递减。该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,我们就可以在大量的实例中快速的发现故障实例和高压力实例。
    在这里插入图片描述
  • 曲线:用来记录2分钟内流量的相对变化,我们可以通过它来观察到流量的上升和下降趋势。
  • 其他一些数量指标如下图所示:
    在这里插入图片描述
    注意:当使用Hystrix Dashboard来监控Spring Cloud Zuul构建的API网关时,Thread Pool信息会一直处于Loading状态。这是由于Zuul默认会使用信号量来实现隔离,只有通过Hystrix配置把隔离机制改为线程池的方式才能够得以展示。

Turbine集群监控

通过Hystrix Dashboard,我们可以方便的查看服务实例的综合情况,比如:服务调用次数、服务调用延迟等。但是仅通过Hystrix Dashboard我们只能实现对服务单个实例的数据展现,在生产环境我们的服务是肯定需要做高可用的,那么对于多实例的情况,我们就需要将这些度量指标数据进行聚合。我们需引入Turbine,通过它来汇集监控信息,并将聚合后的信息提供给Hystrix Dashboard来集中展示和监控。

我们将在上述架构基础上,引入Turbine来对服务的Hystrix数据进行聚合展示。

通过HTTP收集聚合

具体实现步骤如下:

  • 创建一个标准的Spring Boot工程,命名为:turbine
  • 编辑pom.xml,具体依赖内容如下:
<parent>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-parent</artifactId>
	<version>Dalston.SR1</version>
	<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-turbine</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-actuator</artifactId>
	</dependency>
</dependencies>
  • 创建应用主类TurbineApplication,并使用@EnableTurbine注解开启Turbine
@Configuration
@EnableAutoConfiguration
@EnableTurbine
@EnableDiscoveryClient
public class TurbineApplication {

	public static void main(String[] args) {
		SpringApplication.run(TurbineApplication.class, args);
	}

}
  • application.properties加入eurekaturbine的相关配置,具体如下:
spring.application.name=turbine

server.port=8989
management.port=8990

eureka.client.serviceUrl.defaultZone=http://localhost:1001/eureka/

turbine.app-config=eureka-consumer-ribbon-hystrix
turbine.cluster-name-expression="default"
turbine.combine-host-port=true

参数说明:

  • turbine.app-config参数指定了需要收集监控信息的服务名
  • turbine.cluster-name-expression参数指定了集群名称为default,当我们服务数量非常多的时候,可以启动多个Turbine服务来构建不同的聚合集群,而该参数可以用来区分这些不同的聚合集群,同时该参数值可以在Hystrix仪表盘中用来定位不同的聚合集群,只需要在Hystrix StreamURL中通过cluster参数来指定
  • turbine.combine-host-port参数设置为true,可以让同一主机上的服务通过主机名与端口号的组合来进行区分,默认情况下会以host来区分不同的服务,这会使得在本地调试的时候,本机上的不同服务聚合成一个服务来统计

在完成了上面的内容构建之后,我们来体验一下Turbine对集群的监控能力。分别启动eureka-servereureka-clienteureka-consumer-ribbon-hystrixturbine以及hystrix-dashboard。访问Hystrix Dashboard,并开启对http://localhost:8989/turbine.stream的监控,这时候,我们将看到针对服务eureka-consumer-ribbon-hystrix的聚合监控数据。

而此时的架构如下图所示:
在这里插入图片描述

通过消息代理收集聚合

Spring Cloud在封装Turbine的时候,还实现了基于消息代理的收集实现。所以,我们可以将所有需要收集的监控信息都输出到消息代理中,然后Turbine服务再从消息代理中异步的获取这些监控信息,最后将这些监控信息聚合并输出到Hystrix Dashboard中。通过引入消息代理,我们的TurbineHystrix Dashoard实现的监控架构可以改成如下图所示的结构:
在这里插入图片描述
从图中我们可以看到,这里多了一个重要元素:RabbitMQ。下面,我们可以来构建一个新的应用来实现基于消息代理的Turbine聚合服务,具体步骤如下:

  • 创建一个标准的Spring Boot工程,命名为:turbine-amqp
  • 编辑pom.xml,具体依赖内容如下:
<parent>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-parent</artifactId>
	<version>Dalston.SR1</version>
	<relativePath /> <!-- lookup parent from repository -->
</parent>

<dependencies>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-turbine-amqp</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-actuator</artifactId>
	</dependency>
</dependencies>

可以看到这里主要引入了spring-cloud-starter-turbine-amqp依赖,它实际上就是包装了spring-cloud-starter-turbine-streamspring-cloud-starter-stream-rabbit

注意:这里我们需要使用Java 8来运行

  • 在应用主类中使用@EnableTurbineStream注解来启用Turbine Stream的配置。
@Configuration
@EnableAutoConfiguration
@EnableTurbineStream
@EnableDiscoveryClient
public class TurbineApplication {

	public static void main(String[] args) {
		SpringApplication.run(TurbineApplication.class, args);
	}

}
  • 配置application.properties文件:
spring.application.name=turbine-amqp

server.port=8989
management.port=8990

eureka.client.serviceUrl.defaultZone=http://localhost:1001/eureka/

对于Turbine的配置已经完成了,下面我们需要对服务消费者eureka-consumer-ribbon-hystrix做一些修改,使其监控信息能够输出到RabbitMQ上。这个修改也非常简单,只需要在pom.xml中增加对spring-cloud-netflix-hystrix-amqp依赖,具体如下:

<dependencies>
	...
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-netflix-hystrix-amqp</artifactId>
	</dependency>
</dependencies>

在完成了上面的配置之后,我们可以继续之前的所有项目(除turbine以外),并通过Hystrix Dashboard开启对http://localhost:8989/turbine.stream的监控,我们可以获得如之前实现的同样效果,只是这里我们的监控信息收集时是通过了消息代理异步实现的。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud Hystrix是一个开源的熔断器框架,它能够帮助开发者有效地处理服务依赖中的延迟和故障。熔断器的主要目的是在出现故障时提供一种优雅的降级机制,以避免整个系统的崩溃。 熔断和降级是Hystrix中两个重要的概念。 熔断(Circuit Breaker)指的是在服务出现故障或错误率过高时,自动地切换到指定的备用服务或返回事先定义好的错误结果,起到保护系统免受故障传播的影响的作用。当服务不可用或响应时间过长时,熔断器会打开,拒绝后续请求的访问,并尝试通过执行降级逻辑来快速响应客户端。一旦后续请求不再出现故障,熔断器将会进入半开状态,允许少量的请求通过以检测服务是否恢复正常。 降级(Degradation)指的是在系统资源不足或者高访问量时,服务降级会关闭一些不重要的功能,以保证系统核心功能的可用性和稳定性。降级可以通过阻止非必要的调用、减少资源的消耗以及返回默认值或缓存结果来实现。降级需要提前定义好一些备用的逻辑,一旦系统资源紧张,就可以立即启用降级逻辑来保障系统的可用性。 总而言之,熔断和降级都是为了保护系统免受故障的影响。熔断主要是针对服务故障和错误率过高的情况,通过切换到备用服务或返回错误结果来保护系统。降级主要是在系统资源紧张或高访问量的情况下,关闭一些不重要的功能来保证核心功能的可用性和稳定性。两者都是通过提前定义备用逻辑来保障系统的正常运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值