Spring Cloud系列之Eureka
Spring Cloud系列之配置中心Config
Spring Cloud系列之gateway
Spring Cloud系列之Feign
Spring Cloud系列之Hystrix
Spring Cloud系列之链路追踪
在介绍Hystrix之前我们先来了解微服务中的雪崩效应
雪崩效应
以上图为例,当我们的用户微服务发生异常时,响应时间过长或者服务不可用的时候,当用户访问订单服务的时候,程序的执行可能就一直卡在订单服务访问用户微服务,实际上其他微服务都没有任何问题,但是因为用户微服务出了问题,导致订单服务响应时间过长,大量的请求阻塞,线程没有释放,导致服务器资源耗尽,最终导致所有服务都不可用,造成整个系统瘫痪,这就形成了雪崩效应。实际上就是其中一个微服务出了问题,导致所有的都不可用。
就好比一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大
雪崩产生原因
在我们实际开发过程中造成雪崩的原因有很多:
硬件故障
程序报错
缓存击穿 (大量不存在的key,或者我们的缓存同一时间大面积的失效)
用户大量请求(比如秒杀场景)
雪崩效应的解决方案
-
服务限流
大量请求同时请求一个服务时,我们需要做服务限流,满足规则的允许访问,否则走服务熔断降级。
使用场景比如秒杀系统(系统主动提供的高并发访问服务),网站攻击(被动接收大量请求)。 常用的限流解决方案有:
限制时间窗口内的平均速率,常用的算法有:计数器、令牌桶、漏桶 (比较常用合理的是令牌桶算法)
限制并发总数 (比如数据库连接池、线程池)
限制瞬时并发总数(比如nginx限制瞬时并发总数)
限制远程接口调用速率(前端控制)
限制MQ的消费速率等等
-
服务降级
当请求超时,资源不足对的情况下进行服务降级。一般使用hystrix方案,降级后可以配合降级返回托底数据实现一个fallback()方法,当请求后端发生异常的时候,可以使用fallback()方法返回一个缺省值 (这个要根据我们实际开发的时候根据产品要求进行合理的方案展示)
-
服务熔断
当我们的某个服务访问失败率到达一定阈值的时候,进行服务的熔断,快速返回失败结果,一般结合我们的降级方案使用
-
服务隔离
服务资源隔离,我们可以想象下,我们把每个服务想象成每一个独立的房间,当某个房间起火时,只要我们把火灾控制在这个房间,其他房间可以照常使用。在我们实际开发中Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离,从而避免服务雪崩
-
服务扩容
这个就是纯粹的增加机器
-
缓存方案优化
对我们的数据访问,尽量控制访问redis等,避免直接访问数据库,并且对我们的缓存过期时间,进行一个随机的设置,避免同一时间大量的缓存失效,造成直接访问数据库
Hystrix简介
Hystrix 中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力。本文所说的Hystrix是Netflix开源的一款容错框架,同样具有自我保护能力。为了实现容错和自我保护,下面我们看看Hystrix如何设计和实现的
Hystrix解决雪崩问题的手段包括:
-
线程隔离
在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池。 例如调用产品服务的Command放入A线程池, 调用账户服务的Command放入B线程池, 这样做的主要优点是运行环境被隔离开了,这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响,但是带来的代价就是维护多个线程池会对系统带来额外的性能开销,如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话, 可以使用Hystrix的信号模式(Semaphores)来隔离资源。通俗来说:
- Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,加速失败判定时间。
- 用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理
-
服务熔断
当Hystrix Command请求后端服务失败数量超过一定比例(默认50%,当然这个阈值我们可以根据实际情况进行相应的设置, 断路器会切换到开路状态(Open。 这时所有请求会直接失败而不会发送到后端服务, 断路器保持在开路状态一段时间后(默认5秒, 自动切换到半开路状态(HALF-OPEN), 这时会判断下一次请求的返回情况,如果请求成功,断路器切回闭路状态(CLOSED,否则重新切换到开路状态(OPEN)。 Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量,并且断路器有自我检测并恢复的能力。
-
服务降级
在hystrix中,Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应
Hystrix工作流程
文字总结下:
- 当调用出现问题的时候,开启一个时间窗口
- 在这个时间窗口内,统计调用次数是否达到我们设置的最小请求数,如果没有达到则重置统计信息,回到第一步,如果达到了,则统计调用失败次数是否达到我们设置的阈值,如果达到则进行跳闸,没有达到,则重置统计信息回到第一步
- 执行跳闸操作后,开启一个跳闸恢复活动窗口
- 在这个窗口内,放行一个请求,看远程服务是否恢复正常,如果没有则回到第三步,如果恢复了,则重置断路器,放行所有请求
入门
- 引入依赖
<!--hystrix依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 添加配置
# 配置熔断策略:
hystrix:
command:
default:
#断路器配置
circuitBreaker:
errorThresholdPercentage: 50 # 触发熔断错误比例阈值,默认值50%
sleepWindowInMilliseconds: 10000 # 熔断后休眠时长,默认值5秒
requestVolumeThreshold: 10 # 熔断触发最小请求次数,默认值是20
execution:
isolation:
thread:
timeoutInMilliseconds: 3000 # 熔断超时设置,默认为1秒
- 启动类
/**
* @Description:
* @author: dy
*/
//开启feign客户端功能 basePackages指定要扫描的feignClient类所在包路径
@EnableFeignClients(basePackages = {"com.dy.user.client"})
//开启Eureka客户端发现功能
@EnableDiscoveryClient
//开启熔断器功能
@EnableCircuitBreaker
@SpringBootApplication(scanBasePackages = {
//client指定了fallback,这里要把所有hystrix的fallback注入容器中
"com.dy.user.fallback",
//指定了scanBasePackages要把我们本身的bean注入到容器中
"com.dy.userapi"
})
public class UserApiApplication {
public static void main(String[] args) {
SpringApplication.run(UserApiApplication.class, args);
}
}
- client类模拟超时
/**
* @Description:
* @author: dy
*/
@Slf4j
@RestController
@RequestMapping(UserClient.MAPPING)
public class UserResource implements UserClient {
@Autowired
private AppInfo appinfo;
@Value("${server.port}")
private Integer port;
@Override
public String getUserId() {
try {
//模拟超时
Thread.sleep(10000);
}catch (Exception e){
}
log.info("==============请求来了===============");
return "======111111222>>"+appinfo.getAppId()+"====port:"+port;
}
}
- 引用client方法指定降级缺省值执行方法
/**
* @Description:
* @author: dy
*/
@Slf4j
@RestController
public class UserController implements UserApi {
@Autowired
private UserClient userClient;
@Override
public String getUserInfo() {
log.info("==========滴滴========");
return userClient.getUserId();
}
@Override
//指定降级缺省值执行方法
@HystrixCommand(fallbackMethod = "getDefaultUserId")
public String testHystrix() {
return userClient.getUserId();
}
public String getDefaultUserId(){
return "默认值";
}
}
执行结果:
我们也可以模拟hystrix的熔断功能,看到达我们设置的阈值是否服务熔断了,也可以得到相同的执行结果
在我们实际开发过程中一般都是feign+hystrix结合使用的!
Hystrix资源隔离(舱壁模式)
资源隔离的原因
在一个分布式系统中,服务之间都是相互调用的,例如,我们容器(Tomcat)配置的线程个数为 1000,服务 A-服务 R,其中服务 I 的并发量非常的大,需要 500 个线程来执行,此时,服务 I 又挂了,那么这 500 个线程很可能就夯死了,那么剩下的服务,总共可用的线程为 500 个,随着并发量的增大,剩余服务挂掉的风险就会越来越大,最后导致整个系统的所有服务都不可用,直到系统宕机。以上就是服务的雪崩效应。
Hystrix 就是用来做资源隔离的,比如说,当客户端向服务端发送请求时,给服务 I 分配了 10 个线程,只要超过了这个并发量就走降级服务,就算服务 I 挂了,最多也就导致服务 I 不可用,容器的 10 个线程不可用了,但是不会影响系统中的其他服务。
实现方式
Hystrix实现资源隔离的方式有两种:
-
线程池
可以为每个接口设置单独的线程池,当该接口的线程池被占满时,只是该接口收到影响,而不会对其他接口造成影响
如下:
//指定降级缺省值执行方法 @HystrixCommand(fallbackMethod = "getDefaultUserId", //设置线程池的key(唯一标识) threadPoolKey = "licenseByOrgThreadPool", threadPoolProperties = { //设置核心线程数和最大排队size @HystrixProperty(name = "coreSize",value="10"), @HystrixProperty(name="maxQueueSize", value="30") } ) public String sync() { return userClient.getUserId(); }
优点:
- 可以设置coreSize和MaxQueueSize,也就是说,支持排队;
- 可以设置调用超时时间;
- 支持异步调用;
缺点:
- 创建线程池会产生额外的开销,且开销比信号量大;
适用场景:
- 调用其他服务(不受信的服务,不能保证其他服务的服务质量);
- 适合调用的服务比较少的情况(扇出少),如果比较多,则线程池也要加大;
-
信号量
用于隔离本地代码或可快速返回的远程调用可以直接使用信号量隔离,降低线程隔离的上下文切换开销。如 memcached,redis。
实现方式:设置一个信号量值(计数器),当一个接收到一个请求后,信号量减1;当请求处理完后,信号量加1;当信号量减为0的时候,表示不能再接受请求,则会拒绝处理。
优点:
- 轻量,开销可以忽略不计;
缺点:
- 不支持任务排队,只要信号量减为0,立即就会被拒绝;
- 不支持设置超时,在执行过程中,只能等待调用拿到结果或者抛出异常,操作才会继续;
- 不支持异步调用;
适用场景:
- 网关(扇出多);
- 受信服务(被调用服务能够保证不超时);
- 高频高速调用(比如查cache这种操作,基本能保证在很多时间内响应)
Hystrix Dashboard+Turbine
当我们的应用程序使用了hystrix后,每个具体的hystrixCommand命令执行后都会产生一堆的监控数据,比如:成功数,失败数,超时数以及与之关联的线程池信息等。既然有了这些监控数据数据,那么我们应该如何进行查看呢?答案当然是通过hystrix dashboard 来进行查看,但hystrix dashboard只能查看单个应用内的服务信息,这个显然是不够的,因此我们需要一个能够将系统内多个服务的监控数据汇总到hystrix dashboard上,这个时候就应该使用turbine
仪表盘项目需要单独的创建
新建hystrix-dashboard的module
- 引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dy-springcloud</artifactId>
<groupId>com.dy</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.dy.dashboard</groupId>
<artifactId>hystrix-dashboard</artifactId>
<dependencies>
<!--eureka-client依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- hystrix dashboard仪表盘依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!-- turbine依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
</dependencies>
</project>
- 配置类
server:
port: 10006
spring:
application:
name: hystrix-dashboard
#eureka 参数配置
eureka:
# 此实例注册到eureka服务端的唯一的实例ID,其组成为${spring.application.name}:${spring.application.instance_id:${random.value}}
instance:
# 获取实例的ip地址
prefer-ip-address: true
# 与此实例相关联的主机名,是其他实例可以用来进行请求的准确名称
hostname: localhost
# eureka客户需要多长时间发送心跳给eureka服务器,表明它仍然活着,默认为30 秒
lease-renewal-interval-in-seconds: 10
# Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认为90秒
lease-expiration-duration-in-seconds: 30
client:
# 注册自己
register-with-eureka: true
# 不拉取服务
fetch-registry: true
# 配置服务地址
service-url:
# eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址 ,如果是多台eureka server 地址以逗号隔开
defaultZone: http://127.0.0.1:10001/eureka
#turbine配置
turbine:
#appconfig配置需要聚合的服务信息,比如这里我们需要聚合user-api的监控数据
#如果需要聚合多个服务,那么可以用英文逗号进行分隔
app-config: USER-API
cluster-name-expression: "'default'" #集群默认名称
- 启动类
/**
* @Description:
* @author: dy
*/
//开启服务注册发现功能
@EnableDiscoveryClient
//开启仪表盘功能
@EnableHystrixDashboard
//开启turbine聚合功能
@EnableTurbine
@SpringBootApplication
public class DashboardApplication {
public static void main(String[] args) {
SpringApplication.run(DashboardApplication.class,args);
}
}
运行项目打开url:http://localhost:10006/hystrix进入监控首页:
输入我们要监控的服务信息,就可以看到我们的监控详情页面了: