Eureka
参考:
ZK和Eureka的区别:
- ZK是CP:网络出问题时保证一致性,节点不可用。Java实现。
- Eureka是AP:网络出问题时保证可用性,节点可用,但是各节点数据会不一致。
Eureka总体架构
下面是Eureka注册中心部署在多个机房的架构图,这正是他高可用性的优势(Zookeeper千万别这么部署)。
从组件功能看:
- 黄色注册中心集群,分别部署在北京、天津、青岛机房;
- 红色服务提供者,分别部署北京和青岛机房;
- 淡绿色服务消费者,分别部署在北京和天津机房;
从机房分布看:
- 北京机房部署了注册中心、服务提供者和服务消费者;
- 天津机房部署了注册中心和服务消费者;
- 青岛机房部署了注册中心和服务提供者;
组件调用关系
服务提供者
- 启动后,向注册中心发起register请求,注册服务
- 在运行过程中,定时向注册中心发送renew心跳,证明“我还活着”。
- 停止服务提供者,向注册中心发起cancel请求,清空当前服务注册信息。
服务消费者
- 启动后,从注册中心拉取服务注册信息
- 在运行过程中,定时更新服务注册信息。
- 服务消费者发起远程调用:
- 服务消费者(北京)会从服务注册信息中选择同机房的服务提供者(北京),发起远程调用。只有同机房的服务提供者挂了才会选择其他机房的服务提供者(青岛)。
- 服务消费者(天津)因为同机房内没有服务提供者,则会按负载均衡算法选择北京或青岛的服务提供者,发起远程调用。
注册中心
- 启动后,从其他节点拉取服务注册信息。
- 运行过程中,定时运行evict任务,剔除没有按时renew的服务(包括非正常停止和网络故障的服务)。
- 运行过程中,接收到的register、renew、cancel请求,都会同步至其他注册中心节点。
本文将详细说明上图中的registry、register、renew、cancel、getRegistry、evict的内部机制。
数据存储结构
既然是服务注册中心,必然要存储服务的信息,我们知道ZK是将服务信息保存在树形节点上。而下面是Eureka的数据存储结构:
Eureka的数据存储分了两层:数据存储层和缓存层。Eureka Client在拉取服务信息时,先从缓存层获取(相当于Redis),如果获取不到,先把数据存储层的数据加载到缓存中(相当于Mysql),再从缓存中获取。值得注意的是,数据存储层的数据结构是服务信息,而缓存中保存的是经过处理加工过的、可以直接传输到Eureka Client的数据结构。
Eureka这样的数据结构设计是把内部的数据存储结构与对外的数据结构隔离开了,就像是我们平时在进行接口设计一样,对外输出的数据结构和数据库中的数据结构往往都是不一样的。
数据存储层
这里为什么说是存储层而不是持久层?因为rigistry本质上是一个双层的ConcurrentHashMap,存储在内存中的。
第一层的key是spring.application.name,value是第二层ConcurrentHashMap;
第二层ConcurrentHashMap的key是服务的InstanceId,value是Lease对象;
Lease对象包含了服务详情和服务治理相关的属性。
二级缓存层
Eureka实现了二级缓存来保存即将要对外传输的服务信息,数据结构完全相同。
一级缓存:ConcurrentHashMap<Key,Value> readOnlyCacheMap,本质上是HashMap,无过期时间,保存服务信息的对外输出数据结构。
二级缓存:Loading<Key,Value> readWriteCacheMap,本质上是guava的缓存,包含失效机制,保存服务信息的对外输出数据结构。
既然是缓存,那必然要有更新机制,来保证数据的一致性。下面是缓存的更新机制:
更新机制包含删除和加载两个部分,上图黑色箭头表示删除缓存的动作,绿色表示加载或触发加载的动作。
删除二级缓存:
- Eureka Client发送register、renew和cancel请求并更新registry注册表之后,删除二级缓存;
- Eureka Server自身的Evict Task剔除服务后,删除二级缓存;
- 二级缓存本身设置了guava的失效机制,隔一段时间后自己自动失效;
加载二级缓存:
- Eureka Client发送getRegistry请求后,如果二级缓存中没有,就触发guava的load,即从registry中获取原始服务信息后进行处理加工,再加载到二级缓存中。
- Eureka Server更新一级缓存的时候,如果二级缓存没有数据,也会触发guava的load。
更新一级缓存:
- Eureka Server内置了一个TimerTask,定时将二级缓存中的数据同步到一级缓存(这个动作包括了删除和加载)。
关于缓存的实现参考ResponseCacheImpl
以下参考:https://www.jianshu.com/p/22219408b382
为什么这样设计?
这让我想起主从数据库的读写分离,数据库的读写分离是为了分摊主数据库服务器的读写压力。而eureka所设计的缓存级别无疑也是为了读写分离,因为在写的时候,如ConcurrentHashmap会持有桶节点对象的锁,阻塞同一个桶的读写线程。这样设计的话,线程在写的时候,并不会影响读操作,避免了争抢资源所带来的压力。
该缓存结构如何保证最终一致性?
- 从只读map中获取key对应的值。
- 如果只读map没有value值的时候,会从读写缓存里面获取。
- 读写缓存180s后过期,又会从本地注册表中获取到最新的实例信息。
- 只读map中会每30s遍历,将读写map里面的key赋值到只读map中。
客户端注册时如何更新缓存的最新数据?
- 在接受客户端注册的时候,服务端会将读写缓存的key清掉。30s后只读缓存从读写缓存拉取数据的时候,该服务列表获取到的是最新的数据。
- 如果客户端下线,同样地,读写缓存也会被清除掉。30s后只读缓存从读写缓存拉取数据。
- 所以极端情况,最长30s后,客户端才能获取到最新的服务列表。
服务注册机制
服务提供者、服务消费者、以及服务注册中心自己,启动后都会向注册中心注册服务(如果配置了注册)。下图是介绍如何完成服务注册的:
注册中心服务接收到register请求后:
- 保存服务信息,将服务信息保存到registry中;
- 更新队列,将此事件添加到更新队列中,供Eureka Client增量同步服务信息使用。
- 清空二级缓存,即readWriteCacheMap,用于保证数据的一致性。
- 更新阈值,供剔除服务使用。
- 同步服务信息,将此事件同步至其他的Eureka Server节点。
服务续约机制
服务注册后,要定时(默认30S,可自己配置)向注册中心发送续约请求,告诉注册中心“我还活着”。
注册中心收到续约请求后:
- 更新服务对象的最近续约时间,即Lease对象的lastUpdateTimestamp;
- 同步服务信息,将此事件同步至其他的Eureka Server节点。
剔除服务之前会先判断服务是否已经过期,判断服务是否过期的条件之一是续约时间和当前时间的差值是不是大于阈值。
服务注销机制
服务正常停止之前会向注册中心发送注销请求,告诉注册中心“我要下线了”。
注册中心服务接收到cancel请求后:
- 删除服务信息,将服务信息从registry中删除;
- 更新队列,将此事件添加到更新队列中,供Eureka Client增量同步服务信息使用。
- 清空二级缓存,即readWriteCacheMap,用于保证数据的一致性。
- 更新阈值,供剔除服务使用。
- 同步服务信息,将此事件同步至其他的Eureka Server节点。
服务正常停止才会发送Cancel,如果是非正常停止,则不会发送,此服务由Eureka Server主动剔除。
服务剔除机制
Eureka Server提供了服务剔除的机制,用于剔除没有正常下线的服务。
服务的剔除包括三个步骤,首先判断是否满足服务剔除的条件,然后找出过期的服务,最后执行剔除。
判断是否满足服务剔除的条件
有两种情况可以满足服务剔除的条件:
- 关闭了自我保护
- 如果开启了自我保护,需要进一步判断是Eureka Server出了问题,还是Eureka Client出了问题,如果是Eureka Client出了问题则进行剔除。
这里比较核心的条件是自我保护机制,Eureka自我保护机制是为了防止误杀服务而提供的一个机制。Eureka的自我保护机制“谦虚”的认为如果大量服务都续约失败,则认为是自己出问题了(如自己断网了),也就不剔除了;反之,则是Eureka Client的问题,需要进行剔除。而自我保护阈值是区分Eureka Client还是Eureka Server出问题的临界值:如果超出阈值就表示大量服务可用,少量服务不可用,则判定是Eureka Client出了问题。如果未超出阈值就表示大量服务不可用,则判定是Eureka Server出了问题。
条件1中如果关闭了自我保护,则统统认为是Eureka Client的问题,把没按时续约的服务都剔除掉(这里有剔除的最大值限制)。
这里比较难理解的是阈值的计算:
自我保护阈值 = 服务总数 * 每分钟续约数 * 自我保护阈值因子。
每分钟续约数 =(60S/客户端续约间隔)
最后自我保护阈值的计算公式为:
自我保护阈值 = 服务总数 * (60S/客户端续约间隔) * 自我保护阈值因子。
举例:如果有100个服务,续约间隔是30S,自我保护阈值0.85。
自我保护阈值=100 * 60 / 30 * 0.85 = 170。
如果上一分钟的续约数=180>170,则说明大量服务可用,是服务问题,进入剔除流程;
如果上一分钟的续约数=150<170,则说明大量服务不可用,是注册中心自己的问题,进入自我保护模式,不进入剔除流程。
找出过期的服务
遍历所有的服务,判断上次续约时间距离当前时间大于阈值就标记为过期。并将这些过期的服务保存到集合中。
剔除服务
在剔除服务之前先计算剔除的数量,然后遍历过期服务,通过洗牌算法确保每次都公平的选择出要剔除的任务,最后进行剔除。
执行剔除服务后:
- 删除服务信息,从registry中删除服务。
- 更新队列,将当前剔除事件保存到更新队列中。
- 清空二级缓存,保证数据的一致性。
实现过程参考AbstractInstanceRegistry.evict()方法。
服务获取机制
Eureka Client获取服务有两种方式,全量同步和增量同步。获取流程是根据Eureka Server的多层数据结构进行的:
无论是全量同步还是增量同步,都是先从缓存中获取,如果缓存中没有,则先加载到缓存中,再从缓存中获取。(registry只保存数据结构,缓存中保存ready的服务信息。)
- 先从一级缓存中获取
- 先判断是否开启了一级缓存
- 如果开启了则从一级缓存中获取,如果存在则返回,如果没有,则从二级缓存中获取
- 如果未开启,则跳过一级缓存,从二级缓存中获取
- 再从二级缓存中获取
- 如果二级缓存中存在,则直接返回;
- 如果二级缓存中不存在,则先将数据加载到二级缓存中,再从二级缓存中获取。注意加载时需要判断是增量同步还是全量同步,增量同步从recentlyChangedQueue中load,全量同步从registry中load。
服务同步机制
服务同步机制是用来同步Eureka Server节点之间服务信息的。它包括Eureka Server启动时的同步,和运行过程中的同步。
启动时同步
Eureka Server启动后,遍历eurekaClient.getApplications获取服务信息,并将服务信息注册到自己的registry中。
注意这里是两层循环,第一层循环是为了保证已经拉取到服务信息,第二层循环是遍历拉取到的服务信息。
运行过程中同步
当Eureka Server节点有register、renew、cancel请求进来时,会将这个请求封装成TaskHolder放到acceptorQueue队列中,然后经过一系列的处理,放到batchWorkQueue中。
TaskExecutor.BatchWorkerRunnable是个线程池,不断的从batchWorkQueue队列中poll出TaskHolder,然后向其他Eureka Server节点发送同步请求。
这里省略了两个部分:
一个是在acceptorQueue向batchWorkQueue转化时,省略了中间的processingOrder和pendingTasks过程。
另一个是当同步失败时,会将失败的TaskHolder保存到reprocessQueue中,重试处理。
作者:Olla_0632
链接:https://www.jianshu.com/p/22219408b382
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
与ZK对比
对微服务解决方案Dubbo和Spring Cloud的对比非常多,这里对注册中心做个简单对比。
~ | Zookeeper | Eureka |
---|---|---|
设计原则 | CP | AP |
优点 | 数据强一致 | 服务高可用 |
缺点 | 网络分区会影响Leader选举,超过阈值后集群不可用 | 服务节点间的数据可能不一致;Client-Server间的数据可能不一致; |
适用场景 | 单机房集群,对数据一致性要求较高 | 云机房集群,跨越多机房部署;对注册中心服务可用性要求较高 |
集群模式配置
- 两个 Eureka Server 相互注册。
- 然后 Eureka Client 上面同时注册两个Eureka即可,用“,”隔开。
spring:
application:
name: spring-cloud-eureka-server
---
spring:
profiles: u1
eureka:
instance:
hostname: EurekaServerA
client:
service-url:
defaultZone: http://EurekaServerB:9091/eureka
register-with-eureka: true
fetch-registry: true
server:
port: 9090
---
spring:
profiles: u2
eureka:
instance:
hostname: EurekaServerB
client:
service-url:
defaultZone: http://EurekaServerA:9090/eureka
register-with-eureka: true
fetch-registry: true
server:
port: 9091
server:
port: 7070
spring:
application:
name: spring-cloud-order-service-provider
eureka:
client:
service-url:
defaultZone: http://EurekaServerA:9090/eureka,http://EurekaServerB:9091/eureka
fetch-registry: true
register-with-eureka: true
instance:
prefer-ip-address: true # 使用ip注册
#自定义实例显示格式,添加版本号
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
优雅停服
如果使用了优雅停服,则不需要再关闭Eureka Server的服务保护模式。
1、POM依赖:
优雅停服是通过Eureka Client发起的,所以需要在Eureka Client中增加新的依赖,这个依赖是autuator组件,添加下述依赖即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
2、修改全局配置文件:
Eureka Client默认不开启优雅停服功能,需要在全局配置文件中新增如下内容:
# 启用shutdown,优雅停服功能
endpoints.shutdown.enabled=true
# 禁用密码验证
endpoints.shutdown.sensitive=false
3、发起shutdown请求:
必须通过POST请求向Eureka Client发起一个shutdown请求。请求路径为:http://ip:port/shutdown。可以通过任意技术实现,如:HTTPClient、form表单,AJAX等。
建议使用优雅停服方式来关闭Application Service/Application Client服务。
源码剖析
Eureka Server
以下原理图复制图片地址打开新标签放大可以看得更清楚点。
Eureka Client
解决Eureka发现慢
https://segmentfault.com/a/1190000023737598
EurekaServer端配置
eureka:
server:
#Eureka Server会定时(间隔值是eureka.server.eviction-interval-timer-in-ms,默认60s)进行检查,如果发现实例在在一定时间
#(此值由客户端设置的eureka.instance.lease-expiration-duration-in-seconds定义,默认值为90s)内没有收到心跳,则会注销此实例。
#我们这里配置每秒钟去检测一次,驱除失效的实例
eviction-interval-timer-in-ms: 1000
#关闭一级缓存,让客户端直接从二级缓存去读取,省去各缓存之间的同步的时间
use-read-only-response-cache: false
EurekaClient端(应用端)配置
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# EurekaClient每隔多久从EurekaServer拉取一次服务列表,默认30秒,这里修改为2秒钟从注册中心拉取一次
registry-fetch-interval-seconds: 2
#租约期限以及续约期限的配置
instance:
#租约到期,服务失效时间,默认值90秒,服务超过90秒没有发生心跳,EurekaServer会将服务从列表移除
#这里修改为6秒
lease-expiration-duration-in-seconds: 6
#租约续约间隔时间,默认30秒,这里修改为3秒钟
lease-renewal-interval-in-seconds: 3
#这里是Ribbon缓存实例列表的刷新间隔,默认30秒钟,这里修改为每秒钟刷新一次实例信息
ribbon:
ServerListRefreshInterval: 1000
OpenFeign
参考:
Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求。Spring Cloud引入 Feign并且集成了Ribbon实现客户端负载均衡调用。
Feign基本流程
核心就是通过一系列的封装和处理,将以JAVA注解的方式定义的远程调用API接口,最终转换成HTTP的请求形式,然后将HTTP的请求的响应结果,解码成JAVA Bean,放回给调用者。
Feign 默认底层通过JDK 的 java.net.HttpURLConnection
实现了feign.Client
接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient
或者OkHttp3
等基于连接池的高性能Http客户端。
而这个client会委托给org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient进行负载均衡地调用,采用了观察者模式。
Feign源码实现
Gateway
参考:
核心概念
- 路由:网关的基本构建组成,表示一个具体的路由信息载体。它由 ID,目标 URI,谓词集合和过滤器集合定义
- 谓词/断言:Java 8 函数谓词,输入类型是 Spring Framework ServerWebExchange,可以匹配 HTTP 请求中的所有内容,例如请求头或参数
- 过滤器:使用特定工厂构造的 Spring Framework GatewayFilter 实例,可以在发送给下游请求之前或之后修改请求和响应
执行流程
执行流程大体如下:
- Gateway Client 向 Gateway Server 发送请求
- 请求首先会被 HttpWebHandlerAdapter 进行提取组装成网关上下文
- 然后网关的上下文会传递到 DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping 负责路由查找,并根据路由断言判断路由是否可用
- 如果过断言成功,由 FilteringWebHandler 创建过滤器链并调用
- 请求会一次经过 PreFilter -> 微服务 -> PostFilter 的方法,最终返回响应
可实现功能:
Filter:“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。Spring Cloud Gateway内置有很多网关的工厂类。其中可分为两大类:
- GatewayFilter:有一堆内置的过滤器,直接在yml配置即可。也可以自定义过滤器,实现GatewayFilter、 Ordered接口即可。在最后启动类Application中注入路由拦截的规则,
builder.routers.filter(f -> ()...)
- GlobalFilters:全局过滤器。也可以自定义过滤器,实现GlobalFilters、 Ordered接口即可。
- GatewayFilter:有一堆内置的过滤器,直接在yml配置即可。也可以自定义过滤器,实现GatewayFilter、 Ordered接口即可。在最后启动类Application中注入路由拦截的规则,
限流:
- 添加redis的pom依赖
spring-boot-starter-data-redis-reactive
- application.yml 主要是配置redis和gateway整合。其实就是配置好redis,再配置一个Filter。
- 代码中实现
KeyResolver
,重写public Mono<String> resolve(ServerWebExchange exchange) {}
方法,在里面进行限流即可。可以根据请求的信息,如IP、用户等进行限流。
- 添加redis的pom依赖
整合Eureka:直接在yml里加入eureka配置。路由里直接用eureka路由即可。
整合Hystrix:添加Hystrix的pom依赖。直接注入Bean即可。添加filter:
.hystrix(...)
。@Bean public RouteLocator myRoutes(RouteLocatorBuilder builder) { String httpUri = "http://httpbin.org:80"; return builder.routes() .route(p -> p .path("/get") .filters(f -> f.addRequestHeader("Hello", "World")) .uri(httpUri)) .route(p -> p .host("*.hystrix.com") .filters(f -> f .hystrix(config -> config .setName("mycmd") .setFallbackUri("forward:/fallback"))) .uri(httpUri)) .build(); } @RequestMapping("/fallback") public Mono<String> fallback() { return Mono.just("fallback"); }
Hystrix
参考:
- https://www.modb.pro/db/85461
- https://www.cnblogs.com/wangjunwei/p/12833626.html
- https://segmentfault.com/a/1190000023458164
对于基本的容错限流模式,主要有以下几点需要考量:
- 主动超时:在调用依赖时尽快的超时,可以设置比较短的超时时间,比如2s,防止长时间的等待。
- 限流:限制最大并发数。
- 降级:资源不足时进行服务降级
- 熔断:错误数达到阈值时,类似于保险丝熔断。
- 隔离:隔离不同的依赖调用
断路器模式
实现流程为:
- 当断路器的开关为关闭时(对应图中的绿色),每次请求进来都是成功的
- 当后端服务出现问题,请求出现的错误数达到一定的阈值,则会触发断路器为打开状态(对应图中的红色),在断路器为打开状态时,进来的所有请求都会被拒。
- 过了特定的时间后,断路器会进入半打开状态(对应图中的黄色),这时会让一部分请求通过进行尝试
- 如果尝试还是有问题,则继续进入打开状态
- 如果尝试没有问题了,则会进入关闭状态。
Hystrix总体概述
Hystrix是Netflix公司开源的一款容错框架。 它可以完成以下几件事情:
- 资源隔离:包括线程池隔离和信号量隔离,避免某个依赖出现问题会影响到其他依赖。
- 断路器:当请求失败率达到一定的阈值时,会打开断路器开关,直接拒绝后续的请求,并且具有弹性机制,在后端服务恢复后,会自动关闭断路器开关。
- 降级回退:当断路器开关被打开,服务调用超时/异常,或者资源不足(线程、信号量)会进入指定的fallback降级方法。
- 请求结果缓存:hystrix实现了一个内部缓存机制,可以将请求结果进行缓存,那么对于相同的请求则会直接走缓存而不用请求后端服务。
- 请求合并: 可以实现将一段时间内的请求合并,然后只对后端服务发送一次请求。
1.资源隔离
资源隔离的思想参考舱壁隔离模式,在hystrix中提供了两种资源隔离策略:线程池隔离、信号量隔离。
线程池隔离:线程池隔离会为每一个依赖创建一个线程池来处理来自该依赖的请求,不同的依赖线程池相互隔离,就算依赖A出故障,导致线程池资源被耗尽,也不会影响其他依赖的线程池资源。
- 优点:支持排队和超时,支持异步调用。
- 缺点:线程的创建一个调度会造成一定的性能开销。
- 适用场景:适合耗时较长的接口场景,比如接口处理逻辑复杂,且与第三方中间件有交互,因为线程池模式的请求线程与实际转发线程不是同一个,所以可以保证容器有足够的线程来处理新的请求。
信号量隔离模式: 初始化信号量currentCount=0,每进来一个请求需要先将currentCount自增,再判断currentCount的值是否小于系统最大信号量,小于则继续执行,大于则直接返回,拒绝请求。
代码如下:
public boolean tryAcquire() {
int currentCount = this.count.incrementAndGet();
if (currentCount > (Integer)this.numberOfPermits.get()) {
this.count.decrementAndGet();
return false;
} else {
return true;
}
}
- 优点:轻量,只是一个简单的计数器,无额外的开销
- 缺点:不支持任务排队和主动超时;不支持异步调用
- 适用场景:适合能快速响应的接口场景,不适合一些耗时较长的接口场景,因为信号量模式下的请求线程与转发处理线程是同一个,如果接口耗时过长有可能会占满容器的线程数。
隔离方式 | 是否支持超时 | 是否支持熔断 | 隔离原理 | 是否异步调用 | 资源消耗 |
---|---|---|---|---|---|
线程池隔离 | 支持,可直接返回 | 支持,当线程池到达maxSize后,再请求会触发fallback接口进行熔断 | 每个服务单独用线程池,请求线程与转发处理线程不是同一个 | 可以是异步,也可以是同步。看调用的方法 | 大,大量线程的上下文切换,容易造成机器负载高 |
信号量隔离 | 不支持,如果阻塞,只能通过调用协议(如:socket超时才能返回) | 支持,当信号量达到maxConcurrentRequests后。再请求会触发fallback | 通过信号量的计数器,请求线程与转发处理线程是同一个 | 同步调用,不支持异步 | 小,只是个计数器 |
2.断路器
断路器工作原理如下:
Hystrix是基于滚筒式来处理:每一秒会产生一个buckets,每产生一个新的buckets就会移除一个最老的buckets,默认是10秒一个窗口。buckets在内存中就是一种数据结构,每个buckets会记录Metrics的相关数据,比如成功、失败、超时、拒绝。
当一个HystrixCommand进来后,会先通过allowRequest()方法判断是否允许通过该次请求:
- allowRequest()方法会通过isOpen判断断路器是否打开。断路器关闭,则允许通过该次请求;
- 断路器打开,则会判断是否过了睡眠周期。没有过睡眠周期则返回false,拒绝通过该次请求,过了睡眠周期则会尝试放行。
isOpen()方法会按照(failure) / (success+failure)公式计算出失败率,如果失败率大于阈值,则会触发熔断。公式中的成功、失败的数据来源于每10秒中一个窗口的滚筒数据。
对于一个依赖调用,要么调用成功,要么调用失败(包括异常、超时、拒绝),这些调用结果都会记录到buckets中。对于调用成功结果来说,还会判断断路器开关是否打开,如果是打开状态的话,则会关闭断路器并重置相关的计数器。
3.降级回退
降级,通常指事务高峰期,为了保证核心服务正常运行,需要停掉一些不太重要的业务,或者某些服务不可用时,执行备用逻辑从故障服务中快速失败或快速返回,以保障主体业务不受影响。 Hystrix提供的降级主要是为了容错,保证当前服务不受依赖服务故障的影响,从而提高服务的健壮性。
1)哪些情况会进入降级逻辑
- 断路器打开
- 线程池/信号量资源不足
- 执行依赖调用超时
- 执行依赖调用异常
2)降级回退方式
- Fail Fast快速失败:最普通的命令执行方法,命令没有重写降级逻辑。 如果命令执行发生任何类型的故障,它将直接抛出异常。
- Fail Fast无声失败:指在降级方法中通过返回null,空Map,空List或其他类似的响应来完成。
- FallBack:Static:指在降级方法中返回静态默认值。 这不会导致服务以“无声失败”的方式被删除,而是导致默认行为发生。如:应用根据命令执行返回true / false执行相应逻辑,但命令执行失败,则默认为true。
- FallBack:Stubbed:当命令返回一个包含多个字段的复合对象时,适合以Stubbed 的方式回退。
- FallBack:Cache via Network:有时,如果调用依赖服务失败,可以从缓存服务(如redis)中查询旧数据版本。由于又会发起远程调用,所以建议重新封装一个Command,使用不同的ThreadPoolKey,与主线程池进行隔离。
- Primary+Secondary with FallBack:有时系统具有两种行为- 主要和次要,或主要和故障转移。主要和次要逻辑涉及到不同的网络调用和业务逻辑,所以需要将主次逻辑封装在不同的Command中,使用线程池进行隔离。为了实现主从逻辑切换,可以将主次command封装在外观HystrixCommand的run方法中,并结合配置中心设置的开关切换主从逻辑。由于主次逻辑都是经过线程池隔离的HystrixCommand,因此外观HystrixCommand可以使用信号量隔离,而没有必要使用线程池隔离引入不必要的开销。
4.请求结果缓存
实际应用场景很少,不予过多介绍。
5.请求合并
实际应用场景很少,不予过多介绍。
6.整体流程
Hystrix通过隔离服务之间的访问点、停止跨服务的级联故障并提供回退选项来实现熔断。
以下步骤的序号对应上图:
1、2.构造命令、执行命令。分为两类
- HystrixCommand:默认线程池隔离。单次处理。包含两个执行命令的方法:
- execute():阻塞,等待响应。
- queue():返回一个
Future
,通过它获取响应。
- HystrixObservableCommand:默认信号隔离。订阅式。包含两个执行命令的方法:
- observe():订阅
Observable
。(subscribes to theObservable
that represents the response(s) from the dependency and returns anObservable
that replicates that sourceObservable
) - toObservable():返回一个
Observable
,订阅它时会执行命令并响应?(returns anObservable
that, when you subscribe to it, will execute the Hystrix command and emit its responses)
- observe():订阅
3.检查缓存
- 请求缓存。该命令如果启用了缓存,则返回缓存值。这里有个重要的点就是一定要开启Hystrix上下文。他们都要在我们的Hystrix上下文中执行。
- 请求合并。请求合并注意的另外一点,它要求两次请求足够的近才能合并,而这个参数我们是可以设置的。实际应用场景较少。
4.检查断路器是否开启
5.检查信号量是否已满?线程池是否已满?
6.执行业务方法
HystrixCommand.run ()
: returns a single response or throws an exceptionHystrixObservableCommand.construct()
:returns an Observable that emits the response(s) or sends anonError
notification
7.计算电路健康状况
- Hystrix向断路器报告成功、失败、拒绝和超时,断路器维护一组滚动计数器,用于计算统计数据。
- 它使用这些统计数据来确定电路什么时候“跳闸”,在这一点上,它会短路任何后续的请求,直到恢复期结束。在此期间,它会在第一次检查某些健康检查之后再次关闭电路。
8.执行Fallback()
- 超时或失败时都会走Fallback()。
- fallBack本身又有个successful的概念,如果fallback success了就返回。如果失败了就抛出错误。
9.返回成功。根据之前执行的命令,返回对应的响应:
- execute() : 以与.queue()相同的方式获取一个Future,然后在这个Future上调用get()来获取可观察对象发出的单个值。
- queue() : 将可观察对象转换为BlockingObservable,以便将其转换为
Future
,然后返回此Future
。 - observe() :返回 可观察对象,立即订阅,并开始执行命令的流;返回一个可观察对象,当您订阅该对象时,将重播排放和通知。
- toObservable() : 返回可观察值不变;您必须订阅它,才能真正开始执行命令的流程。
Ribbon
暂略。
转载请注明来源