pull/1/head
595208882@qq.com 3 years ago
parent 8f625a6b4b
commit fb7b0197ee

@ -3744,6 +3744,428 @@ Netflix Eureka 是由 Netflix 开源的一款基于 REST 的服务发现组件
### Eurka工作流程
Eureka的工作流程如下
1. **Eureka Server 启动成功,等待服务端注册**。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息
2. Eureka Client 启动时根据配置的 Eureka Server 地址**去注册中心注册服务**
3. Eureka Client 会**每30s向 Eureka Server 发送一次心跳请求**,证明客户端服务正常
4. 当Eureka Server **90s内没有收到Eureka Client的心跳注册中心则认为该节点失效**,会注销该实例
5. **单位时间内Eureka Server统计到有大量的Eureka Client没有上送心跳则认为可能为网络异常**,进入自我保护机制,不再剔除没有上送心跳的客户端
6. **当Eureka Client心跳请求恢复正常之后Eureka Server自动退出自我保护模式**
7. **Eureka Client定时全量或者增量从注册中心获取服务注册表并且将获取到的信息缓存到本地**
8. 服务调用时Eureka Client会先从本地缓存找调取的服务。若获取不到先从注册中心刷新注册表再同步到本地缓存
9. Eureka Client获取到目标服务器信息发起服务调用
10. Eureka Client程序关闭时向Eureka Server发送取消请求Eureka Server将实例从注册表中删除
### Eureka Server
Eureka Server注册中心服务端主要对外提供了三个功能
- **服务注册**
服务提供者启动时,会通过 Eureka Client 向 Eureka Server 注册信息Eureka Server 会存储该服务的信息Eureka Server 内部有二层缓存机制来维护整个注册表。
- **提供注册表**
服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表。
- **同步状态**
Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态。
**自我保护机制**
默认情况下,如果 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用网络通信往往会面临着各种问题比如微服务状态正常网络分区故障导致此实例被注销。固定时间内大量实例被注销可能会严重威胁整个微服务架构的可用性。为了解决这个问题Eureka 开发了自我保护机制那么什么是自我保护机制呢Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%Eureka Server 即会进入自我保护机制。Eureka Server 触发自我保护机制后,页面会出现提示:
![EurekaServer触发自我保护机制](images/Middleware/EurekaServer触发自我保护机制.png)
Eureka Server进入自我保护机制会出现以下几种情况
- **Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务**
- **Eureka仍然能够接受新服务的注册和查询请求但是不会被同步到其它节点上(即保证当前节点依然可用)**
- **当网络稳定时,当前实例新的注册信息会被同步到其它节点中**
Eureka自我保护机制是为了防止误杀服务而提供的一个机制
- **当个别客户端出现心跳失联时,则认为是客户端的问题,剔除掉客户端**
- **当Eureka捕获到大量的心跳失败时则认为可能是网络问题进入自我保护机制**
- **当客户端心跳恢复时Eureka会自动退出自我保护机制**
如果在保护期内刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,即会调用失败。对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。通过在 Eureka Server 配置如下参数,开启或者关闭保护机制,生产环境建议打开:
```properties
eureka.server.enable-self-preservation=true
```
### Eureka Client
Eureka Client注册中心客户端是一个 Java 客户端,用于简化与 Eureka Server 的交互。Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。
- **Register —— 服务注册**
服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL主页等。
- **Renew —— 服务续约**
Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。服务续约的两个重要属性:
```properties
# 服务续约任务的调用间隔时间默认为30秒
eureka.instance.lease-renewal-interval-in-seconds=30
# 服务失效的时间默认为90秒。
eureka.instance.lease-expiration-duration-in-seconds=90
```
- **Eviction —— 服务剔除**
当Eureka Client和Eureka Server不再有心跳时Eureka Server会将该服务实例从服务注册列表中删除即服务剔除。
- **Cancel —— 服务下线**
Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:
```java
DiscoveryManager.getInstance().shutdownComponent();
```
- **GetRegisty —— 获取注册列表信息**
Eureka Client 从服务器获取注册表信息并将其缓存在本地。客户端会使用该信息查找其他服务从而进行远程调用。该注册列表信息定期每30秒钟更新一次。每次返回注册列表信息可能与 Eureka Client 的缓存信息不同Eureka Client 自动处理。
如果由于某种原因导致注册列表信息不能及时匹配Eureka Client 则会重新获取整个注册表信息。 Eureka Server 缓存注册列表信息整个注册表以及每个应用程序的信息进行了压缩压缩内容和没有压缩的内容完全相同。Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯。在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息。获取服务是服务消费者的基础,所以必有两个重要参数需要注意:
```properties
# 启用服务消费者从注册中心拉取服务列表的功能
eureka.client.fetch-registry=true
# 设置服务消费者从注册中心拉取服务列表的间隔
eureka.client.registry-fetch-interval-seconds=30
```
- **Remote Call —— 远程调用**
当Eureka Client从注册中心获取到服务提供者信息后就可以通过Http请求调用对应的服务服务提供者有多个时Eureka Client客户端会通过Ribbon自动进行负载均衡。
### Eureka缓存机制
**Eureka Server数据存储**
Eureka Server的数据存储层是双层的 ConcurrentHashMap
```java
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
```
- **key**:为服务名,即 `spring.application.name`,也就是客户端实例注册的应用名
- **subKey**:为 `instanceId`也就是服务的唯一实例ID
- **Lease< InstanceInfo >**Lease对象存储着这个实例的所有注册信息包括 ip 、端口、属性等
**Eureka Server缓存机制**
Eureka Server为了提供响应效率提供了两层的缓存结构将Eureka Client所需要的注册信息直接存储在缓存结构中。
- **第一层缓存readOnlyCacheMap本质上是 ConcurrentHashMap**
依赖定时从 readWriteCacheMap 同步数据,默认时间为 30 秒。
readOnlyCacheMap 是一个 CurrentHashMap 只读缓存,这个主要是为了供客户端获取注册信息时使用,其缓存更新,依赖于定时器的更新,通过和 readWriteCacheMap 的值做对比,如果数据不一致,则以 readWriteCacheMap 的数据为准。
- **第二层缓存readWriteCacheMap本质上是 Guava 缓存**
readWriteCacheMapreadWriteCacheMap 的数据主要同步于存储层。当获取缓存时判断缓存中是否没有数据,如果不存在此数据,则通过 CacheLoader 的 load 方法去加载加载成功之后将数据放入缓存同时返回数据。readWriteCacheMap缓存过期时间默认为180秒当服务下线、过期、注册、状态变更都会来清除此缓存中的数据。
Eureka Client 获取全量或者增量的数据时,会先从一级缓存中获取;如果一级缓存中不存在,再从二级缓存中获取;如果二级缓存也不存在,这时候先将存储层的数据同步到缓存中,再从缓存中获取。通过 Eureka Server 的二层缓存机制,可以非常有效地提升 Eureka Server 的响应时间,通过数据存储层和缓存层的数据切割,根据使用场景来提供不同的数据支持。
**其它缓存设计**
除过 Eureka Server 端存在缓存外Eureka Client 也同样存在着缓存机制Eureka Client 启动时会全量拉取服务列表,启动后每隔 30 秒从 Eureka Server 量获取服务列表信息并保持在本地缓存中。Eureka Client 增量拉取失败,或者增量拉取之后对比 hashcode 发现不一致,就会执行全量拉取,这样避免了网络某时段分片带来的问题,同样会更新到本地缓存。同时对于服务调用,如果涉及到 ribbon 负载均衡,那么 ribbon 对于这个实例列表也有自己的缓存,这个缓存定时(默认30秒)从 Eureka Client 的缓存更新。这么多的缓存机制可能就会造成一些问题,一个服务启动后可能最长需要 90s 才能被其它服务感知到:
1. 首先Eureka Server 维护每 30s 更新的响应缓存
2. Eureka Client 对已经获取到的注册信息也做了 30s 缓存
3. 负载均衡组件 Ribbon 也有 30s 缓存
这三个缓存加起来,就有可能导致服务注册最长延迟 90s ,这个需要我们在特殊业务场景中注意其产生的影响。
### 常用配置
Eureka Server常用配置
```properties
#服务端开启自我保护模式,前面章节有介绍
eureka.server.enable-self-preservation=true
#扫描失效服务的间隔时间单位毫秒默认是60*1000即60秒
eureka.server.eviction-interval-timer-in-ms= 60000
#间隔多长时间,清除过期的 delta 数据
eureka.server.delta-retention-timer-interval-in-ms=0
#请求频率限制器
eureka.server.rate-limiter-burst-size=10
#是否开启请求频率限制器
eureka.server.rate-limiter-enabled=false
#请求频率的平均值
eureka.server.rate-limiter-full-fetch-average-rate=100
#是否对标准的client进行频率请求限制。如果是false则只对非标准client进行限制
eureka.server.rate-limiter-throttle-standard-clients=false
#注册服务、拉去服务列表数据的请求频率的平均值
eureka.server.rate-limiter-registry-fetch-average-rate=500
#设置信任的client list
eureka.server.rate-limiter-privileged-clients=
#在设置的时间范围类期望与client续约的百分比。
eureka.server.renewal-percent-threshold=0.85
#多长时间更新续约的阈值
eureka.server.renewal-threshold-update-interval-ms=0
#对于缓存的注册数据,多长时间过期
eureka.server.response-cache-auto-expiration-in-seconds=180
#多长时间更新一次缓存中的服务注册数据
eureka.server.response-cache-update-interval-ms=0
#缓存增量数据的时间,以便在检索的时候不丢失信息
eureka.server.retention-time-in-m-s-in-delta-queue=0
#当时间戳不一致的时候,是否进行同步
eureka.server.sync-when-timestamp-differs=true
#是否采用只读缓存策略,只读策略对于缓存的数据不会过期。
eureka.server.use-read-only-response-cache=true
################server node 与 node 之间关联的配置#####################33
#发送复制数据是否在request中总是压缩
eureka.server.enable-replicated-request-compression=false
#指示群集节点之间的复制是否应批处理以提高网络效率。
eureka.server.batch-replication=false
#允许备份到备份池的最大复制事件数量。而这个备份池负责除状态更新的其他事件。可以根据内存大小,超时和复制流量,来设置此值得大小
eureka.server.max-elements-in-peer-replication-pool=10000
#允许备份到状态备份池的最大复制事件数量
eureka.server.max-elements-in-status-replication-pool=10000
#多个服务中心相互同步信息线程的最大空闲时间
eureka.server.max-idle-thread-age-in-minutes-for-peer-replication=15
#状态同步线程的最大空闲时间
eureka.server.max-idle-thread-in-minutes-age-for-status-replication=15
#服务注册中心各个instance相互复制数据的最大线程数量
eureka.server.max-threads-for-peer-replication=20
#服务注册中心各个instance相互复制状态数据的最大线程数量
eureka.server.max-threads-for-status-replication=1
#instance之间复制数据的通信时长
eureka.server.max-time-for-replication=30000
#正常的对等服务instance最小数量。-1表示服务中心为单节点。
eureka.server.min-available-instances-for-peer-replication=-1
#instance之间相互复制开启的最小线程数量
eureka.server.min-threads-for-peer-replication=5
#instance之间用于状态复制,开启的最小线程数量
eureka.server.min-threads-for-status-replication=1
#instance之间复制数据时可以重试的次数
eureka.server.number-of-replication-retries=5
#eureka节点间间隔多长时间更新一次数据。默认10分钟。
eureka.server.peer-eureka-nodes-update-interval-ms=600000
#eureka服务状态的相互更新的时间间隔。
eureka.server.peer-eureka-status-refresh-time-interval-ms=0
#eureka对等节点间连接超时时间
eureka.server.peer-node-connect-timeout-ms=200
#eureka对等节点连接后的空闲时间
eureka.server.peer-node-connection-idle-timeout-seconds=30
#节点间的读数据连接超时时间
eureka.server.peer-node-read-timeout-ms=200
#eureka server 节点间连接的总共最大数量
eureka.server.peer-node-total-connections=1000
#eureka server 节点间连接的单机最大数量
eureka.server.peer-node-total-connections-per-host=10
#在服务节点启动时eureka尝试获取注册信息的次数
eureka.server.registry-sync-retries=
#在服务节点启动时eureka多次尝试获取注册信息的间隔时间
eureka.server.registry-sync-retry-wait-ms=
#当eureka server启动的时候不能从对等节点获取instance注册信息的情况应等待多长时间。
eureka.server.wait-time-in-ms-when-sync-empty=0
```
Eureka Client 常用配置:
```properties
#该客户端是否可用
eureka.client.enabled=true
#实例是否在eureka服务器上注册自己的信息以供其他服务发现默认为true
eureka.client.register-with-eureka=false
#此客户端是否获取eureka服务器注册表上的注册信息默认为true
eureka.client.fetch-registry=false
#是否过滤掉非UP的实例。默认为true
eureka.client.filter-only-up-instances=true
#与Eureka注册服务中心的通信zone和url地址
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
#client连接Eureka服务端后的空闲等待时间默认为30 秒
eureka.client.eureka-connection-idle-timeout-seconds=30
#client连接eureka服务端的连接超时时间默认为5秒
eureka.client.eureka-server-connect-timeout-seconds=5
#client对服务端的读超时时长
eureka.client.eureka-server-read-timeout-seconds=8
#client连接all eureka服务端的总连接数默认200
eureka.client.eureka-server-total-connections=200
#client连接eureka服务端的单机连接数量默认50
eureka.client.eureka-server-total-connections-per-host=50
#执行程序指数回退刷新的相关属性是重试延迟的最大倍数值默认为10
eureka.client.cache-refresh-executor-exponential-back-off-bound=10
#执行程序缓存刷新线程池的大小默认为5
eureka.client.cache-refresh-executor-thread-pool-size=2
#心跳执行程序回退相关的属性是重试延迟的最大倍数值默认为10
eureka.client.heartbeat-executor-exponential-back-off-bound=10
#心跳执行程序线程池的大小,默认为5
eureka.client.heartbeat-executor-thread-pool-size=5
# 询问Eureka服务url信息变化的频率s默认为300秒
eureka.client.eureka-service-url-poll-interval-seconds=300
#最初复制实例信息到eureka服务器所需的时间s默认为40秒
eureka.client.initial-instance-info-replication-interval-seconds=40
#间隔多长时间再次复制实例信息到eureka服务器默认为30秒
eureka.client.instance-info-replication-interval-seconds=30
#从eureka服务器注册表中获取注册信息的时间间隔s默认为30秒
eureka.client.registry-fetch-interval-seconds=30
# 获取实例所在的地区。默认为us-east-1
eureka.client.region=us-east-1
#实例是否使用同一zone里的eureka服务器默认为true理想状态下eureka客户端与服务端是在同一zone下
eureka.client.prefer-same-zone-eureka=true
# 获取实例所在的地区下可用性的区域列表用逗号隔开。AWS
eureka.client.availability-zones.china=defaultZone,defaultZone1,defaultZone2
#eureka服务注册表信息里的以逗号隔开的地区名单如果不这样返回这些地区名单则客户端启动将会出错。默认为null
eureka.client.fetch-remote-regions-registry=
#服务器是否能够重定向客户端请求到备份服务器。 如果设置为false服务器将直接处理请求如果设置为true它可能发送HTTP重定向到客户端。默认为false
eureka.client.allow-redirects=false
#客户端数据接收
eureka.client.client-data-accept=
#增量信息是否可以提供给客户端看默认为false
eureka.client.disable-delta=false
#eureka服务器序列化/反序列化的信息中获取“_”符号的的替换字符串。默认为“__“
eureka.client.escape-char-replacement=__
#eureka服务器序列化/反序列化的信息中获取“$”符号的替换字符串。默认为“_-”
eureka.client.dollar-replacement="_-"
#当服务端支持压缩的情况下是否支持从服务端获取的信息进行压缩。默认为true
eureka.client.g-zip-content=true
#是否记录eureka服务器和客户端之间在注册表的信息方面的差异默认为false
eureka.client.log-delta-diff=false
# 如果设置为true,客户端的状态更新将会点播更新到远程服务器上默认为true
eureka.client.on-demand-update-status-change=true
#此客户端只对一个单一的VIP注册表的信息感兴趣。默认为null
eureka.client.registry-refresh-single-vip-address=
#client是否在初始化阶段强行注册到服务中心默认为false
eureka.client.should-enforce-registration-at-init=false
#client在shutdown的时候是否显示的注销服务从服务中心默认为true
eureka.client.should-unregister-on-shutdown=true
```
Eureka Instance 常用配置:
```properties
#服务注册中心实例的主机名
eureka.instance.hostname=localhost
#注册在Eureka服务中的应用组名
eureka.instance.app-group-name=
#注册在的Eureka服务中的应用名称
eureka.instance.appname=
#该实例注册到服务中心的唯一ID
eureka.instance.instance-id=
#该实例的IP地址
eureka.instance.ip-address=
#该实例相较于hostname是否优先使用IP
eureka.instance.prefer-ip-address=false
```
### Eureka集群原理
![Eureka集群原理](images/Middleware/Eureka集群原理.png)
- **Eureka Server集群相互之间通过 Replicate(复制) 来同步数据**
相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其它节点。
- **如果某台Eureka Server宕机Eureka Client的请求会自动切换到新的Eureka Server节点**
当宕机的服务器重新恢复后Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 所知的所有节点中。
- **Eureka Server的同步遵循一个原则只要有一条边将节点连接就可以进行信息传播与同步**
如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。
- **Eureka Server集群之间的状态是采用异步方式同步的**
所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。
**Eureka 分区**
Eureka 提供了 Region 和 Zone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS:
- **region**:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region
- **zone**:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone
上图中的 us-east-1c、us-east-1d、us-east-1e 就代表了不同的 Zone。Zone 内的 Eureka Client 优先和 Zone 内的 Eureka Server 进行心跳同步,同样调用端优先在 Zone 内的 Eureka Server 获取服务列表,当 Zone 内的 Eureka Server 挂掉之后,才会从别的 Zone 中获取信息。
**Eurka 保证 AP**
Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。
### Eureka一致性协议
Eureka 和 Zookeeper 的最大区别: Eureka 是 AP 模型Zookeeper 是 CP 模型。在出现脑裂等场景时Eureka 可用性是每一位也就是说出现脑裂时每个分区仍可以独立提供服务是去中心化的。那Eureka是如何实现最终一致性的呢
#### 消息广播
1. Eureka Server 管理了全部的服务器列表PeerEurekaNodes
2. 当 Eureka Server 收到客户端的注册、下线、心跳请求时,通过 PeerEurekaNode 向其余的服务器进行消息广播,如果广播失败则重试,直到任务过期后取消任务,此时这两台服务器之间数据会出现短暂的不一致。
**注意:** 虽然消息广播失败,但只要收到客户端的心跳,仍会向所有的服务器(包括失联的服务器)广播心跳任务。
3. 如果网络恢复正常,收到了其它服务器广播的心跳任务,此时可能有三种情况:
1. 一是脑裂很快恢复,一切正常;
2. 二是该实例已经自动过期,则重新进行注册;
3. 三是数据冲突,出现不一致的情况,则需要发起同步请求,其实也就是重新注册一次,同时踢除老的实例。
总之,通过集群之间的消息广播可以实现数据的最终一致性。
**服务注册**
1. Spring Cloud Eureka 在应用启动时,会在 EurekaAutoServiceRegistration 这个类初始化的时候,主动去 Eureka Server 端注册。
2. Eureka 在启动完成之后会启动一个 40 秒执行一次的定时任务,该任务会去监测自身的 IP 信息以及自身的配置信息是否发生改变,如果发生改变,则会重新发起注册。
3. 续约返回 404 状态码时,会去重新注册
**主动下线**
Eureka 会在 spring 容器销毁的时候执行 shutDown 方法 ,该方法首先会将自身的状态改为 DOWN接着发送cancle 命令至 Eureka Server 请求下掉自己的服务。
**心跳续约与自动下线**
健康检测,一般都是 TTL(Time To Live) 机制。eg: 客户端每 5s 发送心跳,服务端 15s 没收到心跳包,更新实例状态为不健康, 30s 未收到心跳包,从服务列表中删除。**Eureka Server 默认每 30s 发送心跳包90s 未收心跳则删除。这个清理过期实例的线程,每 60s 执行一次。**
#### 崩溃恢复
**重启**
Spring Cloud Eureka 启动时,在初始化 EurekaServerBootstrap#initEurekaServerContext 时会调用 PeerAwareInstanceRegistryImpl#syncUp 从其它 Eureka 中同步数据。
**脑裂**
- 发生脑裂后:和 Eureka Server 同区的服务可以正常访问,而不同区的服务则自动过期。
- 脑裂恢复后:接收其它 Eureka Sever 发送过来的心跳请求,此时有三种情况:一是脑裂很快恢复,一切正常;二是该实例已经自动过期,则重新进行注册;三是数据冲突,出现不一致的情况,则需要发起同步请求。
## Zuul
Zuul 是由 Netflix 孵化的一个致力于“网关 “解决方案的开源组件。
@ -3767,6 +4189,157 @@ Zuul 是由 Netflix 孵化的一个致力于“网关 “解决方案的开源
## Feign
`Feign` 使用时分成两步:一是生成 Feign 的动态代理;二是 Feign 执行。
![Feign整体设计](images/Middleware/Feign整体设计.png)
**总结:**
- 前两步是生成动态对象:将 Method 方法的注解解析成 MethodMetadata并最终生成 Feign 动态代理对象
- 后几步是调用过程根据解析的MethodMetadata对象将Method方法的参数转换成Request最后调用Client发送请求
### Feign动态代理
![Feign动态代理](images/Middleware/Feign动态代理.png)
`Feign` 的默认实现是 ReflectiveFeign通过 Feign.Builder 构建。**总结**
1. Contract 统一将方法解析 MethodMetadata(*),这样就可以通过实现不同的 Contract 适配各种 REST 声明式规范。
2. buildTemplate 实际上将 Method 方法的参数转换成 Request。
3. 将 metadata 和 buildTemplate 封装成 MethodHandler。
### 基于Feign实现负载均衡
基于 Feign 的负载均衡(整合 Ribbon。想要进行负载均衡那就要对 Client 进行包装,实现负载均衡。 相关代码见`RibbonClient`和`LBClient`。
```java
// RibbonClient 主要逻辑
private final Client delegate;
private final LBClientFactory lbClientFactory;
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
// 封装 RibbonRequest包含 Client、Request、uri
LBClient.RibbonRequest ribbonRequest =
new LBClient.RibbonRequest(delegate, request, uriWithoutHost);
// executeWithLoadBalancer 实现负载均衡
return lbClient(clientName).executeWithLoadBalancer(
ribbonRequest,
new FeignOptionsClientConfig(options)).toResponse();
} catch (ClientException e) {
propagateFirstIOException(e);
throw new RuntimeException(e);
}
}
```
**总结:** 实际上是把 Client 对象包装了一下,通过 executeWithLoadBalancer 进行负载均衡,这是 Ribbon 提供了 API。更多关于 Ribbon 的负载均衡就不在这进一步说明了。
```java
public final class LBClient extends AbstractLoadBalancerAwareClient
<LBClient.RibbonRequest, LBClient.RibbonResponse> {
// executeWithLoadBalancer 方法通过负载均衡算法后,最终调用 execute
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
throws IOException, ClientException {
Request.Options options;
if (configOverride != null) {
options = new Request.Options(
configOverride.get(CommonClientConfigKey.ConnectTimeout, connectTimeout),
configOverride.get(CommonClientConfigKey.ReadTimeout, readTimeout),
configOverride.get(CommonClientConfigKey.FollowRedirects, followRedirects));
} else {
options = new Request.Options(connectTimeout, readTimeout);
}
// http 请求
Response response = request.client().execute(request.toRequest(), options);
if (retryableStatusCodes.contains(response.status())) {
response.close();
throw new ClientException(ClientException.ErrorType.SERVER_THROTTLED);
}
return new RibbonResponse(request.getUri(), response);
}
}
```
### 基于Feign实现熔断
基于 Feign 的熔断与限流(整合 Hystrix。想要进行限流那就要在方法执行前进行拦截也就是重写InvocationHandlerFactory在方法执行前进行熔断与限流。相关代码见 `HystrixFeign`,其实也就是实现了 HystrixInvocationHandler。
```java
// HystrixInvocationHandler 主要逻辑
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
HystrixCommand<Object> hystrixCommand =
new HystrixCommand<Object>(setterMethodMap.get(method)) {
@Override
protected Object run() throws Exception {
return HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
}
@Override
protected Object getFallback() {
};
}
...
return hystrixCommand.execute();
}
```
### Feign参数编码整体流程
![Feign参数编码整体流程](images/Middleware/Feign参数编码整体流程.png)**总结:**
- 前两步是`Feign`代理生成阶段解析方法参数及注解元信息。后三步是调用阶段将方法参数编码成Http请求的数据格式
- Contract 接口将 UserService 中每个接口中的方法及其注解解析为 MethodMetadata然后使用 RequestTemplate# request 编码为一个 Request
- RequestTemplate#request 编码为一个 Request 后就可以调用 Client#execute 发送 Http 请求
- Client 的具体实现有 HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty 等。本文关注前三步:即 Feign 方法元信息解析及参数编码过程。
### Feign整体装配流程
![Feign整体装配流程](images/Middleware/Feign整体装配流程.png)
**总结:** OpenFeign 装配有两个入口:
1. @EnableAutoConfiguration 自动装配spring.factories
```properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration
```
- FeignAutoConfiguration自动装配 FeignContext 和 Targeter以及 Client 配置。
- `FeignContext` 是每个 FeignClient 的装配上下文,默认的配置是 FeignClientsConfiguration
- `Targeter` 有两种实现:一是 DefaultTargeter直接调用 Feign.Builder **二是 HystrixTargeter调用 HystrixFeign.Builder开启熔断。**
- `Client` :自动装配 ApacheHttpClientOkHttpClient装配条件不满足默认是 Client.Default。但这些 Client 都没有实现负载均衡。
- FeignRibbonClientAutoConfiguration实现负载均衡负载均衡是在 Client 这一层实现的。
- `HttpClientFeignLoadBalancedConfiguration` ApacheHttpClient 实现负载均衡
- `OkHttpFeignLoadBalancedConfiguration` OkHttpClient实现负载均衡
- `DefaultFeignLoadBalancedConfiguration` Client.Default实现负载均衡
2. @EnableFeignClients 自动扫描
@EnableFeignClients 注入 FeignClientsRegistrarFeignClientsRegistrar 开启自动扫描,将包下 @FeignClient 标注的接口包装成 FeignClientFactoryBean 对象,最终通过 Feign.Builder 生成该接口的代理对象。而 Feign.Builder 的默认配置是 FeignClientsConfiguration是在 FeignAutoConfiguration 自动注入的。
**注意:** 熔断与限流是 FeignAutoConfiguration 通过注入 HystrixTargeter 完成的而负载均衡是FeignRibbonClientAutoConfiguration 注入的。
Feign是是一个声明式的Web Service客户端。
![Feign介绍](images/Middleware/Feign介绍.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Loading…
Cancel
Save