注册中心 Eureka 源码解析 —— 网络通信
点击上方“芋道源码”,选择“置顶公众号”
技术文章第一时间送达!
源码精品专栏
摘要: 原创出处 http://www.iocoder.cn/Eureka/transport/ 「芋道源码」欢迎转载,保留摘要,谢谢!
本文主要基于 Eureka 1.8.X 版本
1. 概述
2. EurekaHttpClient
2.1 EurekaJerseyClientImpl
2.2 EurekaJerseyClientBuilder
3. EurekaHttpClient
3.1 EurekaHttpResponse
3.2 TransportClientFactory
4. AbstractJerseyEurekaHttpClient
4.1 JerseyApplicationClient
4.2 JerseyReplicationClient
5. EurekaHttpClientDecorator
5.1 MetricsCollectingEurekaHttpClient
5.2 RedirectingEurekaHttpClient
5.3 RetryableEurekaHttpClient
5.4 SessionedEurekaHttpClient
6. 创建网络通讯客户端
666. 彩蛋
1. 概述
本文主要分享 Eureka 的网络通信部分。在不考虑 Eureka 2.x 的兼容的情况下,Eureka 1.x 主要两部分的网络通信:
Eureka-Client 请求 Eureka-Server 的网络通信
Eureka-Server 集群内,Eureka-Server 请求 其它的Eureka-Server 的网络通信
本文涉及类在 com.netflix.discovery.shared.transport
包下,涉及到主体类的类图如下( 打开大图 ):
粉色部分 —— EurekaJerseyClient ,对基于 Jersey Server 的 Eureka-Server 的 Jersey 客户端封装。
绿色部分 —— EurekaHttpClient ,Eureka-Server HTTP 访问客户端,定义了具体的 Eureka-Server API 调用方法。如果把 DiscoveryClient 类比成 Service ,那么 EurekaHttpClient 可以类比城 Dao 。
综色部分 —— EurekaHttpClient 实现类,真正实现了具体的 Eureka-Server API 调用方法。
红色部分 —— EurekaHttpClient 委托类,提供了会话、重试、重定向、监控指标收集等特性。
黄色部分 —— EurekaHttpClientFactory,用于创建 EurekaHttpClient 。
类图看起来很复杂,整体调用关系如下( 打开大图 ):
OK ,我们逐层解析,嗨起来。
推荐 Spring Cloud 书籍:
请支持正版。下载盗版,等于主动编写低级 BUG 。
程序猿DD —— 《Spring Cloud微服务实战》
周立 —— 《Spring Cloud与Docker微服务架构实战》
两书齐买,京东包邮。
推荐 Spring Cloud 视频:
Java 微服务实践 - Spring Boot
Java 微服务实践 - Spring Cloud
Java 微服务实践 - Spring Boot / Spring Cloud
2. EurekaHttpClient
com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient
,EurekaHttpClient 接口。接口代码如下:
public interface EurekaJerseyClient {
ApacheHttpClient4 getClient();
void destroyResources();
}
com.sun.jersey.client.apache4.ApacheHttpClient4
,基于 Apache HttpClient4 实现的 Jersey Client 。
2.1 EurekaJerseyClientImpl
com.netflix.discovery.shared.transport.jersey.EurekaJerseyClientImpl
,EurekaHttpClient 实现类。实现代码如下:
// 超过微信字数上限
com.netflix.discovery.shared.transport.jersey.ApacheHttpClientConnectionCleaner
,Apache HttpClient 空闲连接清理器,负责周期性关闭处于half-close
状态的空闲连接。点击 链接 查看带中文注释的 ApacheHttpClientConnectionCleaner。推荐阅读:《HttpClient容易忽视的细节——连接关闭》 。
2.2 EurekaJerseyClientBuilder
EurekaJerseyClientBuilder ,EurekaJerseyClientImpl 内部类,用于创建 EurekaJerseyClientImpl 。
调用 #build()
方法,创建 EurekaJerseyClientImpl ,实现代码如下:
// EurekaJerseyClientBuilder.java
public EurekaJerseyClient build() {
MyDefaultApacheHttpClient4Config config = new MyDefaultApacheHttpClient4Config();
try {
return new EurekaJerseyClientImpl(connectionTimeout, readTimeout, connectionIdleTimeout, config);
} catch (Throwable e) {
throw new RuntimeException("Cannot create Jersey client ", e);
}
}
MyDefaultApacheHttpClient4Config ,继承自
com.sun.jersey.client.apache4.config.DefaultApacheHttpClient4Config
,实现自定义配置。点击 链接 查看带中文注释的 MyDefaultApacheHttpClient4Config。例如 :自定义的请求、响应的编解码器 `com.netflix.discovery.provider.DiscoveryJerseyProvider` 。
禁用重定向,使用 RedirectingEurekaHttpClient 实现该特性。
自定义 UserAgent 。
自定义 Http Proxy 。
SSL 功能的增强。ApacheHttpClient4 使用的是 Apache HttpClient 4.1.1 版本,`com.netflix.discovery.shared.transport.jersey.SSLSocketFactoryAdapter` 将 Apache HttpClient 4.3.4 对 SSL 功能的增强适配到老版本 API 。点击 链接 查看带中文注释的 SSLSocketFactoryAdapter。
3. EurekaHttpClient
com.netflix.discovery.shared.transport.EurekaHttpClient
,Eureka-Server HTTP 访问客户端,定义了具体的 Eureka-Server API 调用方法 。点击 链接 查看带中文注释的 EurekaHttpClient。
3.1 EurekaHttpResponse
com.netflix.discovery.shared.transport.EurekaHttpResponse
,请求响应对象,实现代码如下:
public class EurekaHttpResponse<T> {
/**
* 返回状态码
*/
private final int statusCode;
/**
* 返回对象( Entity )
*/
private final T entity;
/**
* 返回 header
*/
private final Map<String, String> headers;
/**
* 重定向地址
*/
private final URI location;
// ... 省略 setting / getting 和 Builder
}
3.2 TransportClientFactory
com.netflix.discovery.shared.transport.TransportClientFactory
,创建 EurekaHttpClient 的工厂接口。接口代码如下:
public interface TransportClientFactory {
/**
* 创建 EurekaHttpClient
*
* @param serviceUrl Eureka-Server 地址
* @return EurekaHttpClient
*/
EurekaHttpClient newClient(EurekaEndpoint serviceUrl);
/**
* 关闭工厂
*/
void shutdown();
}
大多数 EurekaHttpClient 实现类都有其对应的工厂实现类。
4. AbstractJerseyEurekaHttpClient
com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient
,实现 EurekaHttpClient 的抽象类,真正实现了具体的 Eureka-Server API 调用方法。实现代码如下:
// 超过微信字数上限
jerseyClient
属性,Jersey Client ,使用上文的EurekaHttpClient#getClient(…)
方法,获取 ApacheHttpClient4 。serviceUrl
属性,请求的 Eureka-Server 地址。#register()
方法,实现向 Eureka-Server 注册应用实例。其他方法代码类似。x
第 22 至 26 行 :设置请求地址。
第 28 行 :调用
#addExtraHeaders(...)
方法,设置请求头( header )。该方法是抽象方法,提供子类实现自定义的请求头。代码如下:protected abstract void addExtraHeaders(Builder webResource);
第 29 至 34 行 :请求 Eureka-Server 。
第 35 至 36 行 :解析响应结果,创建 EurekaHttpResponse 。
4.1 JerseyApplicationClient
com.netflix.discovery.shared.transport.jersey.JerseyApplicationClient
,实现 Eureka-Client 请求 Eureka-Server 的网络通信。点击 链接 查看带中文注释的 JerseyApplicationClient。
4.1.1 JerseyEurekaHttpClientFactory
com.netflix.discovery.shared.transport.jersey.JerseyEurekaHttpClientFactory
,创建 JerseyApplicationClient 的工厂类。实现代码如下:
// 超过微信字数上限
4.1.2 JerseyEurekaHttpClientFactoryBuilder
JerseyEurekaHttpClientFactoryBuilder ,JerseyEurekaHttpClientFactory 内部类,用于创建 JerseyEurekaHttpClientFactory 。点击 链接 查看带中文注释的 JerseyEurekaHttpClientFactory。
调用 JerseyEurekaHttpClientFactory#create(...)
方法,创建 JerseyEurekaHttpClientFactory ,实现代码如下:
// 超过微信字数上限
4.2 JerseyReplicationClient
com.netflix.eureka.transport.JerseyReplicationClient
,Eureka-Server 集群内,Eureka-Server 请求 其它的Eureka-Server 的网络通信。
实现
AbstractJerseyEurekaHttpClient#addExtraHeaders()
方法,添加自定义头x-netflix-discovery-replication=true
,代码如下:
protected void addExtraHeaders(Builder webResource) {
webResource.header(PeerEurekaNode.HEADER_REPLICATION, "true");
}重写了
#sendHeartBeat(…)
方法,在 《Eureka 源码解析 —— Eureka-Server 集群同步》 有详细解析。实现
com.netflix.eureka.cluster.HttpReplicationClient
接口,实现了#submitBatchUpdates(…)
方法,在 《Eureka 源码解析 —— Eureka-Server 集群同步》 有详细解析。
4.2.1 没有工厂
JerseyReplicationClient 没有专属的工厂。
调用 JerseyReplicationClient#createReplicationClient(...)
静态方法,创建 JerseyReplicationClient 。点击 链接 查看带中文注释的方法代码。
5. EurekaHttpClientDecorator
com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator
,EurekaHttpClient 委托者抽象类。实现代码如下:
// 超过微信字数上限
#execute(…)
抽象方法,子类实现该方法,实现自己的特性。#register()
方法,实现向 Eureka-Server 注册应用实例。其他方法代码类似。调用
#execute(…)
方法,并将原有的注册实现通过 RequestExecutor 传递进去。子类在实现的
#execute(…)
方法,可以调用RequestExecutor#execute(…)
方法,继续执行原有逻辑。参考设计模式:《设计模式 ( 十九 ) 模板方法模式Template method(类行为型)》 。
RequestType ,请求类型枚举类。代码如下:
// EurekaHttpClientDecorator.java
public enum RequestType {
Register,
Cancel,
SendHeartBeat,
StatusUpdate,
DeleteStatusOverride,
GetApplications,
GetDelta,
GetVip,
GetSecureVip,
GetApplication,
GetInstance,
GetApplicationInstance
}RequestExecutor ,请求执行器接口。接口代码如下:
// EurekaHttpClientDecorator.java
// 超过微信字数上限
EurekaHttpClientDecorator 的每个实现类实现一个特性,代码非常非常非常清晰。
FROM 《委托模式》
委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承,它还使我们可以模拟mixin。
我们在上图的基础上,增加委托的关系,如下图( 打开大图 ):
请注意,每个委托着实现类,上面可能有类型为 EurekaHttpClientFactory 的属性,用于创建其委托的 EurekaHttpClient 。为什么会有 Factory ?例如,RetryableEurekaHttpClient 重试请求多个 Eureka-Server 地址时,每个 Eureka-Server 地址会创建一个 EurekaHttpClient 。所以,下文涉及到 EurekaHttpClientFactory 和委托的 EurekaHttpClient 的地方,你都需要仔细理解。
5.1 MetricsCollectingEurekaHttpClient
com.netflix.discovery.shared.transport.decorator.MetricsCollectingEurekaHttpClient
,监控指标收集 EurekaHttpClient ,配合 Netflix Servo 实现监控信息采集。
#execute()
方法,代码如下:
// 超过微信字数上限
第 10 行 :调用
RequestExecutor#execute(…)
方法,继续执行请求。`delegate` 属性,对应 JerseyApplicationClient 。
5.2 RedirectingEurekaHttpClient
com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient
,寻找非 302 重定向的 Eureka-Server 的 EurekaHttpClient 。
#execute()
方法,代码如下:
// 超过微信字数上限
注意:和我们理解的常规的 302 状态返回处理不同!!!
整个分成两部分:【第 4 至 15 行】、【第 16 至 24 行】。
当返回非 302 状态码时,找到非返回 302 状态码的 Eureka-Server 。
当返回 302 状态码时,向新的重定向的 Eureka-Server 执行请求直到成功找到或超过最大次数。
前者,意味着未找到非返回 302 状态码的 Eureka-Server ,此时通过在原始传递进来的 `serviceUrls` 执行请求,寻找非 302 状态码返回的 Eureka-Server。
后者,意味着当前已经找到非返回 302 状态码的 Eureka-Server ,直接执行请求。注意 :此时 Eureka-Server 再返回 302 状态码,不再处理。
目前 Eureka 1.x 的 Eureka-Server 不存在返回 302 状态码,猜测和 Eureka 2.X TODO[0028]:写入集群和读取集群 有关。
【前者】第 5 行 :使用初始的
serviceEndpoint
( 相当于serviceUrls
) 创建委托 EurekaHttpClient 。【前者】第 7 行 :调用
#executeOnNewServer(…)
方法,通过执行请求的方式,寻找非 302 状态码返回的 Eureka-Server。实现代码,点击 链接 查看带中文注释的代码实现。【前者】【前者】第 9 行 :关闭原有的
delegateRef
( 因为此处可能存在并发,多个线程都找到非 302 状态码返回的 Eureka-Server ),并设置当前成功非 302 请求的 EurekaHttpClient 到delegateRef
。【前者】第 13 行 :关闭
currentEurekaClientRef
,当请求发生异常或者超过最大重定向次数。【后者】第 18 行 :意味着当前已经找到非返回 302 状态码的 Eureka-Server ,直接执行请求。
【后者】第 21 至 22 行 :执行请求发生异常,关闭
currentEurekaClient
,后面要重新非返回 302 状态码的 Eureka-Server 。
5.2.1 工厂
RedirectingEurekaHttpClient 提供 #createFactory(...)
静态方法获得创建其的工厂,点击 链接 查看。
5.3 RetryableEurekaHttpClient
com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient
,支持向多个 Eureka-Server 请求重试的 EurekaHttpClient 。
#execute()
方法,代码如下:
// 超过微信字数上限
第 10 行 :当前
currentHttpClient
不存在,意味着原有delegate
不存在向 Eureka-Server 成功请求的 EurekaHttpClient 。此时需要从配置中的 Eureka-Server 数组重试请求,获得可以请求的 Eureka-Server 。
如果已经存在请求成功的
delegate
,直接使用它进行执行请求。第 11 至 17 行 :调用
#getHostCandidates()
方法,获得候选的 Eureka-ServerserviceUrls
数组。实现代码如下:// 超过微信字数上限
第 10 行 :最小可用的阀值,配置
eureka.retryableClientQuarantineRefreshPercentage
来设置百分比,默认值:0.66
。最 13 至 15 行 :
quarantineSet
数量超过阀值,清空quarantineSet
,全部candidateHosts
重试。第 17 至 24 行 :
quarantineSet
数量未超过阀值,移除candidateHosts
中在quarantineSet
的元素。第 3 行 :调用
ClusterResolver#getClusterEndpoints()
方法,获得候选的 Eureka-Server 地址数组(candidateHosts
)。注意:该方法返回的 Eureka-Server 地址数组,使用以本机 IP 为随机种子,达到不同 IP 的应用实例获得的数组顺序不同,而相同 IP 的应用实例获得的数组顺序一致,效果类似基于 IP HASH 的负载均衡算法。实现该功能的代码,在 《Eureka 源码解析 —— EndPoint 与 解析器》搜索关键字【ResolverUtils#randomize(…)】 详细解析。第 6 行 :调用
Set#retainAll()
方法,移除隔离的故障 Eureka-Server 地址数组(quarantineSet
) 中不在candidateHosts
的元素。第 8 至 24 行 :在保证最小可用的
candidateHosts
,移除在quarantineSet
的元素。第 19 至 22 行 :超过
candidateHosts
上限,全部 Eureka-Server 请求失败,抛出异常。第 24 至 26 行 :创建委托的 EurekaHttpClient ,用于下面请求执行。
第 31 行 :执行请求。
第 33 行 :调用
ServerStatusEvaluator#accept()
方法,判断响应状态码和请求类型是否能够接受。实现代码如下:// ServerStatusEvaluators.java
// 超过微信字数上限第 34 行 :请求成功,设置
delegate
。下次请求,优先使用delegate
,失败才进行候选的 Eureka-Server 地址数组重试。第 47 行 :请求失败,
delegate
若等于currentHttpClient
,进行清除。第 50 至 52 行 :请求失败,将请求的 Eureka-Server 地址添加到
quarantineSet
。总结来说:
【第一步】若当前有请求成功的 EurekaHttpClient ,继续使用。若请求失败,执行【第二步】。
【第二步】若当前无请求成功的 EurekaHttpClient ,获取候选的 Eureka-Server 地址数组顺序创建新的 EurekaHttpClient,直到成功,或者超过最大重试次数。当请求成功,保存该 EurekaHttpClient ,下次继续使用,直到请求失败。
5.3.1 工厂
RetryableEurekaHttpClient 提供 #createFactory(...)
静态方法获得创建其的工厂,点击 链接 查看。
5.4 SessionedEurekaHttpClient
com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient
,支持会话的 EurekaHttpClient 。执行定期的重建会话,防止一个 Eureka-Client 永远只连接一个特定的 Eureka-Server 。反过来,这也保证了 Eureka-Server 集群变更时,Eureka-Client 对 Eureka-Server 连接的负载均衡。
#execute(...)
,代码如下:
// 超过微信字数上限
第 7 至 12 行 :超过当前会话时间,关闭当前委托的 EurekaHttpClient 。
增加会话过期的随机性,实现所有 Eureka-Client 的会话过期重连的发生时间更加离散,避免集中时间过期。目前猜测这么做的目的和 TODO[0028]:写入集群和读取集群 有关,即返回 302 。关联 1.x new transport enhancements 。
第 10 行 :调用
#randomizeSessionDuration(...)
方法,计算计算下一次会话超时时长,公式为sessionDurationMs * (0.5, 1.5)
,代码如下:protected long randomizeSessionDuration(long sessionDurationMs) {
long delta = (long) (sessionDurationMs * (random.nextDouble() - 0.5));
return sessionDurationMs + delta;
}第 15 至 18 行 :获得委托的 EurekaHttpClient 。若不存在,创建新的委托的 EurekaHttpClient 。
TransportUtils#getOrSetAnotherClient(...)
方法代码如下:// 超过微信字数上限
该方法实现,获得
eurekaHttpClientRef
里的 EurekaHttpClient 。若获取不到,将another
设置到eurekaHttpClientRef
。当有多个线程设置时,有且只有一个线程设置成功,另外的设置失败的线程们,意味着当前eurekaHttpClientRef
有 EurekaHttpClient ,返回eurekaHttpClientRef
。目前该方法存在 BUG ,失败的线程直接返回
existing
的是null
,需要修改成return eurekaHttpClientRef.get()
。模拟重现该 BUG 代码如下 :第 19 行 :执行请求。
5.4.1 没有工厂
在 SessionedEurekaHttpClient 类里,没有实现创建其的工厂。在 「6. 创建网络通讯客户端」搜索 canonicalClientFactory
,可以看到 EurekaHttpClients#canonicalClientFactory(...)
方法,内部有 SessionedEurekaHttpClient 的创建工厂。
6. 创建网络通讯客户端
对于 Eureka-Server 来说,调用 JerseyReplicationClient#createReplicationClient(...)
静态方法即可创建用于 Eureka-Server 集群内,Eureka-Server 请求 其它的Eureka-Server 的网络通信客户端。
对于 Eureka-Client 来说,分成用于注册应用实例( registrationClient
)和查询注册信息( newQueryClient
)的两个不同网络通信客户端。在 DiscoveryClient 初始化时进行创建,代码如下:
// DiscoveryClient.class
// 超过微信字数上限
第 18 至 27 行 :调用
Jersey1TransportClientFactories#newTransportClientFactory(...)
方法,创建registrationClient
和queryClient
公用的委托的 EurekaHttpClientFactory ,代码如下:// Jersey1TransportClientFactories.java
// 超过微信字数上限在 TransportClientFactory 里委托 JerseyEurekaHttpClientFactory 。
第 34 至 49 行 :调用
EurekaHttpClients#registrationClientFactory(...)
方法,创建registrationClient
的 EurekaHttpClientFactory ,代码如下 :// EurekaHttpClients.java
// 超过微信字数上限第 51 至 71 行 :调用
EurekaHttpClients#queryClientFactory(...)
方法,创建queryClient
的 EurekaHttpClientFactory ,代码如下 :// EurekaHttpClients.java
// 超过微信字数上限
666. 彩蛋
这次真的是彩蛋,我们将整体调用关系调整如下如下( 打开大图 ):
胖友,你学会了么?
胖友,分享我的公众号( 芋道源码 ) 给你的胖友可好?
如果你对 Dubbo 感兴趣,欢迎加入我的知识星球一起交流。
目前在知识星球(https://t.zsxq.com/2VbiaEu)更新了如下 Dubbo 源码解析如下:
01. 调试环境搭建
02. 项目结构一览
03. 配置 Configuration
04. 核心流程一览
05. 拓展机制 SPI
06. 线程池
07. 服务暴露 Export
08. 服务引用 Refer
09. 注册中心 Registry
10. 动态编译 Compile
11. 动态代理 Proxy
12. 服务调用 Invoke
13. 调用特性
14. 过滤器 Filter
15. NIO 服务器
16. P2P 服务器
17. HTTP 服务器
18. 序列化 Serialization
19. 集群容错 Cluster
20. 优雅停机
21. 日志适配
22. 状态检查
23. 监控中心 Monitor
24. 管理中心 Admin
25. 运维命令 QOS
26. 链路追踪 Tracing
...
一共 60 篇++
源码不易↓↓↓↓↓
点赞支持老艿艿↓↓