在微服务架构中,我们将系统拆分成了一个个的服务单元,各单元应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,线程资源无法释放,最终导致自身服务的瘫痪,进一步甚至出现故障的蔓延最终导致整个系统的瘫痪。如果这样的架构存在如此严重的隐患,那么相较传统架构就更加的不稳定。为了解决这样的问题,因此产生了断路器等一系列的服务保护机制。
针对上述问题,在Spring Cloud Hystrix
中实现了线程隔离、断路器等一系列的服务保护功能。它也是基于Netflix
的开源框架 Hystrix
实现的,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix
具备了服务降级、服务熔断、线程隔离、请求缓存、请求合并以及服务监控等强大功能。
定义服务降级
fallback
是Hystrix
命令执行失败时使用的后备方法,用来实现服务的降级处理逻辑。在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
的访问修饰符没有特定的要求,定义为private
、protected
、public
均可。
在上面的例子中,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
中除了使用线程池之外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销要远比线程池的开销小得多,但是它不能设置超时和实现异步访问。所以,只有在依赖服务是足够可靠的情况下才使用信号量。在HystrixCommand
和HystrixObservableCommand
中2
处支持信号量的使用:
- 命令执行:如果隔离策略参数
execution.isolation.strategy
设置为SEMAPHORE
,Hystrix
会使用信号量替代线程池来控制依赖服务的并发控制。 - 降级逻辑:当
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
请求缓存的使用非常简单,我们只需要在实现HystrixCommand
或HystrixObservableCommand
时,通过重载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
为更新User
的id
值的缓存内容进行清理。而在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
,而createCommand
和mapResponseToRequests
是请求合并器的两个核心。
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 Config
和Spring Cloud Bus
实现的动态刷新配置功能配合下,可以实现对“全局默认值”的覆盖以及在运行期对“全局默认值”的动态调整。 - 实例默认值:通过代码为实例定义的默认值。通过代码的方式为实例设置属性值来覆盖默认的全局配置。
- 实例配置属性:通过配置文件来为指定的实例进行属性配置,以覆盖前面的三个默认值。它也可用
Spring Cloud Config
和Spring 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
配置:与HystrixCommand
和HystrixObservableCommand
执行中捕获的指标信息有关,具体属性如下:
属性 | 说明 |
---|---|
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仪表盘
断路器是根据一段时间窗内的请求情况来判断并操作断路器的打开和关闭状态的。而这些请求情况的指标信息都是HystrixCommand
和HystrixObservableCommand
实例在执行过程中记录的重要度量信息,它们除了在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
加入eureka
和turbine
的相关配置,具体如下:
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 Stream
的URL
中通过cluster
参数来指定turbine.combine-host-port
参数设置为true
,可以让同一主机上的服务通过主机名与端口号的组合来进行区分,默认情况下会以host
来区分不同的服务,这会使得在本地调试的时候,本机上的不同服务聚合成一个服务来统计
在完成了上面的内容构建之后,我们来体验一下Turbine
对集群的监控能力。分别启动eureka-server
、eureka-client
、eureka-consumer-ribbon-hystrix
、turbine
以及hystrix-dashboard
。访问Hystrix Dashboard
,并开启对http://localhost:8989/turbine.stream
的监控,这时候,我们将看到针对服务eureka-consumer-ribbon-hystrix
的聚合监控数据。
而此时的架构如下图所示:
通过消息代理收集聚合
Spring Cloud
在封装Turbine
的时候,还实现了基于消息代理的收集实现。所以,我们可以将所有需要收集的监控信息都输出到消息代理中,然后Turbine
服务再从消息代理中异步的获取这些监控信息,最后将这些监控信息聚合并输出到Hystrix Dashboard
中。通过引入消息代理,我们的Turbine
和Hystrix 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-stream
和spring-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
的监控,我们可以获得如之前实现的同样效果,只是这里我们的监控信息收集时是通过了消息代理异步实现的。