为什么80%的码农都做不了架构师?>>>
Hystrix
分布式系统中经常会出现某个基础服务不可用造成整个系统不可用的情况,这种现象被称为服务雪崩效应。为了应对服务雪崩,一种常见的做法是手动服务降级。而 Hystrix 的出现,给我们提供了另一种选择。
服务雪崩效应
在微服务架构中, 我们将系统拆分成了很多服务单元, 各单元的应用间通过服务注册 与订阅的方式互相依赖。 由于每个单元都在不同的进程中运行,依赖通过远程调用的方式 执行, 这样就有可能因为网络原因或是依赖服务自身间题出现调用故障或延迟, 而这些问 题会直接导致调用方的对外服务也出现延迟, 若此时调用方的请求不断增加, 最后就会因 等待出现故障的依赖方响应形成任务积压, 最终导致自身服务的瘫痪。 举个例子, 在一个电商网站中, 我们可能会将系统拆分成用户、 订单、 库存、 积分、 评论等一系列服务单元。 用户创建一个订单的时候, 客户端将调用订单服务的创建订单接 口,此时创建订单接口又会向库存服务来请求出货(判断是否有足够库存来出货)。 此时若 库存服务因自身处理逻辑等原因造成响应缓慢, 会直接导致创建订单服务的线程被挂起, 以等待库存申请服务的响应, 在漫长的等待之后用户会因为请求库存失败而得到创建订单 失败的结果。 如果在高并发情况之下,因这些挂起的线程在等待库存服务的响应而未能释 放, 使得后续到来的创建订单请求被阻塞, 最终导致订单服务也不可用。即雪崩效应。
定义
服务雪崩效应是一种因 服务提供者 的不可用导致 服务调用者 的不可用,并将不可用 逐渐放大 的过程。
形成的原因
服务雪崩产生的过程分为以下三个阶段来分析形成的原因:
- 服务提供者不可用
- 重试加大流量
- 服务调用者不可用
服务雪崩的每个阶段都可能由不同的原因造成,比如造成 服务不可用 的原因有:
- 硬件故障
- 程序 Bug
- 缓存击穿
- 用户大量请求
- 等等
而形成 重试加大流量 的原因有:
- 用户重试
- 代码逻辑重试
- 等等
服务调用者不可用 产生的主要原因有:
- 同步等待造成的资源耗尽
- 等等
应对策略
针对造成服务雪崩的不同原因,可以使用不同的应对策略:
- 流量控制
- 改进缓存模式
- 服务自动扩容
- 服务调用者降级服务
- 等等
流量控制 的具体措施包括:
- 网关限流
- 用户交互限流
- 关闭重试
- 等等
因为 Nginx 的高性能,目前一线互联网公司大量采用 Nginx+Lua 的网关进行流量控制,由此而来的 OpenResty 也越来越热门。
用户交互限流的具体措施有: 1. 采用加载动画,提高用户的忍耐等待时间。2. 提交按钮添加强制等待时间机制。
改进缓存模式 的措施包括:
- 缓存预加载
- 同步改为异步刷新
服务调用者降级服务 的措施包括:
- 资源隔离
- 对依赖服务进行分类
- 不可用服务的调用快速失败
资源隔离主要是对调用服务的线程池进行隔离。
我们根据具体业务,将依赖服务分为: 强依赖和若依赖。强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不会导致当前业务的中止。
不可用服务的调用快速失败一般通过 超时机制, 熔断器 和熔断后的 降级方法 来实现。
使用 Hystrix 预防服务雪崩
服务降级(Fallback)
对于查询操作,我们可以实现一个 fallback 方法,当请求后端服务出现异常的时候,可以使用 fallback 方法返回的值。fallback 方法的返回值一般是设置的默认值或者来自缓存。
资源隔离
在 Hystrix 中,主要通过线程池来实现资源隔离。通常在使用的时候我们会根据调用的远程服务划分出多个线程池。例如调用产品服务的 Command 放入 A 线程池,调用账户服务的 Command 放入 B 线程池。这样做的主要优点是运行环境被隔离开了。这样就算调用服务的代码存在 bug 或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响。 通过实现对依赖服务的线程池隔离, 可以带来如下优势:
- 应用自身得到完全保护, 不会受不可控的依赖服务影响。 即便给依赖服务分配的线 程池被填满, 也不会影响应用自身的其余部分。
- 可以有效降低接入新服务的风险。 如果新服务接入后运行不稳定或存在问题, 完全 不会影响应用其他的请求。
- 当依赖的服务从失效恢复正常后, 它的线程池会被清理并且能够马上恢复健康的服 务, 相比之下, 容器级别的清理恢复速度要慢得多。
- 当依赖的服务出现配置错误的时候, 线程池会快速反映出此问题(通过失败次数、 延迟、超时、拒绝等指标的增加情况)。 同时, 我们可以在不影响应用功能的情况下 通过实时的动态属性刷新(后续会通过Spring Cloud Config与Spring Cloud Bus的 联合使用来介绍) 来处理它。
- 当依赖的服务因实现机制调整等原因造成其性能出现很大变化的时候, 线程池的监 控指标信息会反映出这样的变化。 同时, 我们也可以通过实时动态刷新自身应用对 依赖服务的阙值进行调整以适应依赖方的改变。
- 除了上面通过线程池隔离服务发挥的优点之外, 每个专有线程池都提供了内置的并 发实现, 可以利用它为同步的依赖服务构建异步访问。
总之, 通过对依赖服务实现线程池隔离, 可让我们的应用更加健壮, 不会因为个别依 赖服务出现问题而引起非相关服务的异常。 同时, 也使得我们的应用变得更加灵活, 可以 在不停止服务的情况下, 配合动态配置刷新实现性能配置上的调整。
虽然线程池隔离的方案带了如此多的好处,但是很多使用者可能会担心为每一个依赖服务都分配一个线程池是否会过多地增加系统的负载和开销。对于这一点,使用者不用过于担心,因为这些顾虑也是大部分工程师们会考虑到的,Netflix 在设计 Hystrix 的时候,认为线程池上的开销相对于隔离所带来的好处是无法比拟的。同时,Netflix 也针对线程池的开销做了相关的测试,以证明和打消 Hystrix 实现对性能影响的顾虑。
Hystrix 中除了使用线程池之外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销要远比线程池的开销小得多,但是它不能设置超时和实现异步访问。所以,只有在依赖服务是足够可靠的情况下才使用信号量。在 HystrixCommand 和 HystrixObservableCommand 中 2 处支持信号量的使用:
- 命令执行:如果隔离策略参数 execution.isolation.strategy 设置为 SEMAPHORE,Hystrix 会使用信号量替代线程池来控制依赖服务的并发控制。
- 降级逻辑:当 Hystrix 尝试降级逻辑时候,它会在调用线程中使用信号量。
信号量的默认值为 10,我们也可以通过动态刷新配置的方式来控制并发线程的数量。对于信号量大小的估算方法与线程池并发度的估算类似。仅访问内存数据的请求一般耗时在 1ms 以内,性能可以达到 5000rps,这样级别的请求我们可以将信号量设置为 1 或者 2,我们可以按此标准并根据实际请求耗时来设置信号量。
断路器模式
在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),直接切断原来的主逻辑调用。但是,在 Hystrix 中的断路器除了切断主逻辑的功能之外,还有更复杂的逻辑,下面我们来看看它更为深层次的处理逻辑。 断路器开关相互转换的逻辑如下图:
当 Hystrix Command 请求后端服务失败数量超过一定阈值,断路器会切换到开路状态 (Open)。这时所有请求会直接失败而不会发送到后端服务。
这个阈值涉及到三个重要参数:快照时间窗、请求总数下限、错误百分比下限。这个参数的作用分别是: 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 秒。 请求总数下限:在快照时间窗内,必须满足请求总数下限才有资格进行熔断。默认为 20,意味着在 10 秒内,如果该 Hystrix Command 的调用此时不足 20 次,即时所有的请求都超时或其他原因失败,断路器都不会打开。 错误百分比下限:当请求总数在快照时间窗内超过了下限,比如发生了 30 次调用,如果在这 30 次调用中,有 16 次发生了超时异常,也就是超过 50% 的错误百分比,在默认设定 50% 下限情况下,这时候就会将断路器打开。
断路器保持在开路状态一段时间后 (默认 5 秒),自动切换到半开路状态 (HALF-OPEN)。这时会判断下一次请求的返回情况,如果请求成功,断路器切回闭路状态 (CLOSED),否则重新切换到开路状态 (OPEN)。
使用详解
使用 Feign Hystrix
因为熔断只是作用在服务调用这一端,因此我们根据上一篇的示例代码只需要改动 server-businessb-woqu 项目相关代码就可以。
- 配置文件
在原来的 application.yml 配置的基础上加入
feign:
hystrix:
enabled: true
- 创建回调类
/**
* @author orrin
*/
@Component(value = "additionHystrix")
public class AdditionHystrix implements Addition {
private static final Logger LOGGER = LoggerFactory.getLogger(AdditionHystrix.class);
@Override
public Integer add(int x, int y) {
LOGGER.error(" Addition is disabled ");
return 0;
}
}
- 添加 fallback 属性
添加指定 fallback 类,在服务熔断的时候返回 fallback 类中的内容
@FeignClient(serviceId = "business-a-woqu", fallback = AdditionHystrix.class)
public interface Addition {
@GetMapping("/add")
public Integer add(@RequestParam("x") int x, @RequestParam("y") int y);
}
- 测试
启动consul server, server-businessb-woqu,进行访问验证
GET http://192.168.2.102:9002/area?length=1&width=2&heigh=3
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 19 Nov 2018 07:39:24 GMT
0
Response code: 200; Time: 80ms; Content length: 1 bytes
其他特性
Hystrix还有以下使用方式:
- 异常传播
- 命令名称、 分组以及线程池划分
- 请求缓存
- 请求合并 这些属于高阶用法,这里不做详细说明。
Hystrix仪表盘
我们提到断路器是根据一段时间窗内的请求情况来判断并操作断路器的打开和关闭状态的。而这些请求情况的指标信息都是 HystrixCommand 和 HystrixObservableCommand 实例在执行过程中记录的重要度量信息,它们除了 Hystrix 断路器实现中使用之外,对于系统运维也有非常大的帮助。这些指标信息会以 “滚动时间窗” 与 “桶” 结合的方式进行汇总,并在内存中驻留一段时间,以供内部或外部进行查询使用,Hystrix Dashboard 就是这些指标内容的消费者之一。
下面我们基于之前的示例来结合 Hystrix Dashboard 实现 Hystrix 指标数据的可视化面板,这里我们将用到下之前实现的几个应用,包括:
- gateway-woqu:网关
- server-businessa-woqu:服务提供者
- server-businessb-woqu:服务消费者
- consul-server:服务注册中心
创建 Hystrix Dashboard
创建一个标准的 Spring Boot 工程,命名为:hystrix-dashboard-woqu
- POM 配置
common-server-woqu
com.orrin
0.0.1-SNAPSHOT
4.0.0
hystrix-dashboard-woqu
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.cloud
spring-cloud-starter-netflix-hystrix-dashboard
org.springframework.boot
spring-boot-starter-actuator
org.jolokia
jolokia-core
- 启动类
在 Spring Boot 的启动类上面引入注解@EnableHystrixDashboard,启用 Hystrix Dashboard 功能。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
/**
* @author orrin
*/
@EnableHystrixDashboard
@SpringBootApplication
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
- 配置文件 配置文件 application.yml
spring:
application:
name: hystrix-dashboard-woqu
server:
port: 7002
启动应用,然后再浏览器中输入 http://localhost:7002/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 开启 ,实现对具体某个服务实例的监控。(现在这里的 URL 应该为 http://hystrix-app:port/actuator/hystrix.stream,Actuator 2.x 以后 endpoints 全部在/actuator下,可以通过management.endpoints.web.base-path修改)
- Delay:控制服务器上轮询监控信息的延迟时间,默认为 2000 毫秒,可以通过配置该属性来降低客户端的网络和 CPU 消耗。
- Title:该参数可以展示合适的标题。
前两者都对集群的监控,需要整合 Turbine 才能实现。这一部分我们先实现对单体应用的监控,这里的单体应用就用我们之前使用 Feign 和 Hystrix 实现的服务消费者——server-businessb-woqu。
为服务实例添加 endpoint
既然 Hystrix Dashboard 监控单实例节点需要通过访问实例的/actuator/hystrix.stream接口来实现,自然我们需要为服务实例添加这个 endpoint。
- POM 配置
在服务实例pom.xml中的dependencies节点中新增spring-boot-starter-actuator监控模块以开启监控相关的端点,并确保已经引入断路器的依赖spring-cloud-starter-netflix-hystrix
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
- 启动类
为启动类添加@EnableCircuitBreaker或@EnableHystrix注解,开启断路器功能。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.woqu")
@EnableHystrix
@ComponentScan(value = "com.woqu")
public class BusinessBApp {
public static void main(String[] args) {
SpringApplication.run(BusinessBApp.class, args);
}
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 配置文件
在配置文件 application.yml 中添加
management:
endpoints:
web:
exposure:
include: hystrix.stream
- 测试
在 Hystrix-Dashboard 的主界面上输入 server-businessb-woqu 对应的地址 http://localhost:9002/actuator/hystrix.stream 然后点击 Monitor Stream 按钮,进入页面。 这时候访问一下 http://192.168.2.102:9002/area?length=1&width=2&heigh=3,可以看到 Hystrix Dashboard 中出现了类似下面的效果:
界面解读
- 实心圆:它有颜色和大小之分,分别代表实例的监控程度和流量大小。如上图所示,它的健康度从绿色、黄色、橙色、红色递减。通过该实心圆的展示,我们就可以在大量的实例中快速的发现故障实例和高压力实例。
- 曲线:用来记录 2 分钟内流量的相对变化,我们可以通过它来观察到流量的上升和下降趋势。
- 其他一些数量指标如下图所示
Hystrix 监控数据聚合 Turbine
通过 HTTP 收集聚合
创建一个标准的 Spring Boot 工程,命名为:turbine-woqu。
- POM 配置
pom.xml 文件如下:
common-server-woqu
com.orrin
0.0.1-SNAPSHOT
4.0.0
turbine-woqu
org.springframework.cloud
spring-cloud-netflix-turbine
org.springframework.cloud
spring-cloud-starter-consul-discovery
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-web
org.jolokia
jolokia-core
- 启动类
package com.woqu.common.turbine;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.turbine.EnableTurbine;
/**
* @author orrin
*/
@EnableTurbine
@EnableDiscoveryClient
@SpringBootApplication
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
- 配置文件
在 application.yml 加入 Consul 和 Turbine 的相关配置
spring:
application:
name: common-server-turbine
cloud:
consul:
host: woqu.consul
port: 8500
discovery:
instance-id: ${spring.application.name}
instance-group: ${spring.application.name}
register: true
service-name: ${spring.application.name}
applications: common-server-gateway,business-b-woqu,business-a-woqu
turbine:
aggregator:
cluster-config: ${applications}
app-config: ${applications}
#cluster-name-expression: new String("default")
combine-host-port: true
management:
endpoints:
web:
exposure:
include: "*"
exclude: dev
server:
port: 7003
参数说明
- turbine.app-config参数指定了需要收集监控信息的服务名;
- turbine.cluster-name-expression 参数指定了集群名称为 default,当我们服务数量非常多的时候,可以启动多个 Turbine 服务来构建不同的聚合集群,而该参数可以用来区分这些不同的聚合集群,同时该参数值可以在 Hystrix 仪表盘中用来定位不同的聚合集群,只需要在 Hystrix Stream 的 URL 中通过 cluster 参数来指定;
- turbine.combine-host-port参数设置为true,可以让同一主机上的服务通过主机名与端口号的组合来进行区分,默认情况下会以 host 来区分不同的服务,这会使得在本地调试的时候,本机上的不同服务聚合成一个服务来统计。
注意:new String("default")这个一定要用 String 来包一下,否则启动的时候会抛出异常。
- 测试 在完成了上面的内容构建之后,我们来体验一下 Turbine 对集群的监控能力。分别启动:
- consul-server
- gateway-woqu
- hystrix-dashboard-woqu
- turbine-woqu
- server-businessa-woqu
- server-businessb-woqu
在浏览器中访问http://localhost:7002/hystrix,通过/clusters获取可以监控的集群
GET http://127.0.0.1:7003/clusters
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 20 Nov 2018 05:47:18 GMT
[
{
"name": "business-b-woqu",
"link": "http://127.0.0.1:7003/turbine.stream?cluster=business-b-woqu"
},
{
"name": "common-server-gateway",
"link": "http://127.0.0.1:7003/turbine.stream?cluster=common-server-gateway"
},
{
"name": "business-a-woqu",
"link": "http://127.0.0.1:7003/turbine.stream?cluster=business-a-woqu"
}
]
Response code: 200; Time: 12ms; Content length: 304 bytes
在Hystrix Dashboard输入http://127.0.0.1:7003/turbine.stream?cluster=business-b-woqu,请求GET http://192.168.2.102:9002/area?length=1&width=2&heigh=3,即可查看到监控效果
通过MQ收集聚合
Spring Cloud 在封装 Turbine 的时候,还实现了基于消息代理的收集实现。所以,我们可以将所有需要收集的监控信息都输出到消息代理中,然后 Turbine 服务再从消息代理中异步的获取这些监控信息,最后将这些监控信息聚合并输出到 Hystrix Dashboard 中。
这里我们使用RabbitMQ来实现基于消息代理的 Turbine 聚合服务。
- POM
在pom.xml中加入以下依赖
org.springframework.cloud
spring-cloud-starter-netflix-turbine-stream
org.springframework.cloud
spring-cloud-starter-stream-rabbit
- 启动类
@SpringBootApplication
@EnableTurbineStream
@EnableDiscoveryClient
public class TurbineStreamRabbitmqApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineStreamRabbitmqApplication.class, args);
}
@Bean
public ConfigurableCompositeMessageConverter integrationArgumentResolverMessageConverter(CompositeMessageConverterFactory factory) {
return new ConfigurableCompositeMessageConverter(factory.getMessageConverterForAllRegistered().getConverters());
}
}
- 配置文件
spring:
application:
name: common-server-turbine-stream
cloud:
consul:
host: woqu.consul
port: 8500
discovery:
instance-id: ${spring.application.name}
instance-group: ${spring.application.name}
register: true
service-name: ${spring.application.name}
stream:
rabbit:
bindings:
test:
consumer:
prefix: z
bindings:
input:
group: default
rabbitmq:
addresses: rabbitmq.server
port: 5672
username: test
password: password
virtualHost: /test
applications: common-server-gateway,business-b-woqu,business-a-woqu
turbine:
aggregator:
cluster-config: ${applications}
app-config: ${applications}
#cluster-name-expression: new String("default")
combine-host-port: true
stream:
port: 18888 # 这是turbine的端口即暴露监控数据的端口,跟server.port不同
management:
endpoints:
web:
exposure:
include: "*"
exclude: dev
server:
port: 7004
- 服务调用者
在服务调用者server-businessb-woqu的pom.xml 里加入以下依赖
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.cloud
spring-cloud-netflix-hystrix-stream
org.springframework.cloud
spring-cloud-starter-stream-rabbit
再在启动类上加上@EnableHystrix注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.woqu")
@EnableHystrix
@ComponentScan(value = "com.woqu")
public class BusinessBApp {
public static void main(String[] args) {
SpringApplication.run(BusinessBApp.class, args);
}
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
添加配置文件
spring:
rabbitmq:
addresses: rabbitmq.server
port: 5672
username: test
password: password
virtualHost: /test
cloud:
stream:
rabbit:
bindings:
test:
consumer:
prefix: z
bindings:
input:
group: default
- 测试
打开地址http://127.0.0.1:7004/hystrix.stream。在Hystrix Dashboard的文字下方输入turbine服务的地址http://127.0.0.1:18888,点击Monitor Stream按钮,就能进入到监控界面。