前言
上一講已經講解了EurekaClient的啓動流程,到了這裏已經有6篇Eureka源碼分析的文章了,看了下之前的文章,感覺代碼成分太多,會影響閱讀,後面會只截取主要的代碼,加上註釋講解。
這一講看的是EurekaClient註冊的流程,當然也是一塊核心,標題爲什麼會寫上眼花繚亂呢?關於EurekaClient註冊的代碼,真的不是這麼容易被發現的。
如若轉載 請標明來源:一枝花算不算浪漫
源碼分析
如果是看過前面文章的同學,肯定會知道,Eureka Client啓動流程最後是初始化DiscoveryClient這個類,那麼我們就直接從這個類開始分析,後面代碼都只截取重要代碼,具體大家可以自行參照源碼。
DiscoveryClient.java
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider) {
// 省略部分代碼...
this.applicationInfoManager = applicationInfoManager;
// 創建一個配置實例,這裏面會有eureka的各種信息,看InstanceInfo類的註釋爲:The class that holds information required for registration with Eureka Server
// and to be discovered by other components.
InstanceInfo myInfo = applicationInfoManager.getInfo();
// 省略部分代碼...
try {
// 支持底層的eureka client跟eureka server進行網絡通信的組件
eurekaTransport = new EurekaTransport();
// 發送http請求,調用restful接口
scheduleServerEndpointTask(eurekaTransport, args);
} catch (Throwable e) {
throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
}
// 初始化調度任務
initScheduledTasks();
}
上面省略了很多代碼,這段代碼在之前的幾篇文章也都有提及,說實話看到這裏 仍然一臉悶逼,入冊的入口在哪呢?不急,下面慢慢分析。
DiscoveryClient.java
private void initScheduledTasks() {
// 省略大部分代碼,這段代碼是初始化eureka client的一些調度任務
// InstanceInfo replicator
// 創建服務拷貝副本
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
// 執行線程 InitialInstanceInfoReplicationIntervalSeconds默認爲40s
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
}
上面仍然是DiscoveryClient中的源碼,看方法名我們知道這裏肯定是初始化EurekaClient啓動時的相關定時任務的。
這裏主要是截取了instanceInfoReplicator
初始化和執行instanceInfoReplicator.start
的任務,
接着我們就可以順着這個線先看看InstatnceInfoReplicator
是何方神聖?
/**
* A task for updating and replicating the local instanceinfo to the remote server. Properties of this task are:
* - configured with a single update thread to guarantee sequential update to the remote server
* - update tasks can be scheduled on-demand via onDemandUpdate()
* - task processing is rate limited by burstSize
* - a new update task is always scheduled automatically after an earlier update task. However if an on-demand task
* is started, the scheduled automatic update task is discarded (and a new one will be scheduled after the new
* on-demand update).
*
* 用於將本地instanceinfo更新和複製到遠程服務器的任務。此任務的屬性是:
* -配置有單個更新線程以保證對遠程服務器的順序更新
* -可以通過onDemandUpdate()按需調度更新任務
* -任務處理的速率受burstSize的限制
* -新更新總是在較早的更新任務之後自動計劃任務。但是,如果啓動了按需任務*,則計劃的自動更新任務將被丟棄(並且將在新的按需更新之後安排新的任務)
*/
class InstanceInfoReplicator implements Runnable {
}
這裏有兩個關鍵點:
- 此類實現了
Runnable
接口,說白了就是執行一個異步線程 - 該類作用是:用於將本地instanceinfo更新和複製到遠程服務器的任務
看完這兩點,我又不禁陷入思考,我找的是eurekaClient註冊過程,咋還跑到這個裏面來了?不甘心,於是繼續往下看。
InstanceInfoReplicator.start()
public void start(int initialDelayMs) {
if (started.compareAndSet(false, true)) {
instanceInfo.setIsDirty(); // for initial register
Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
這個scheduler是一個調度任務線程池,會將this線程放入到線程池中,然後再指定時間後執行該線程的run方法。
InstanceInfoReplicator.run()
public void run() {
try {
// 刷新一下服務實例信息
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
看到這裏是不是有種豁然開朗的感覺?看到了register
就感覺到希望來了,這裏使用的是DiscoveryClient.register方法,其實這裏我們也可以先找DiscoveryClient中的register方法,然後再反查調用方,這也是一種好的思路呀。
DiscoveryClient.register
boolean register() throws Throwable {
logger.info(PREFIX + appPathIdentifier + ": registering service...");
EurekaHttpResponse<Void> httpResponse;
try {
// 回看eurekaTransport創建及初始化過程
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
這裏是使用eurekaTransport.registrationClient去進行註冊,我們在最開始DiscoveryClient構造方法中已經截取了eurekaTransport創建及初始化代碼,這裏再貼一下:
// 支持底層的eureka client跟eureka server進行網絡通信的組件
eurekaTransport = new EurekaTransport();
// 發送http請求,調用restful接口
scheduleServerEndpointTask(eurekaTransport, args);
private void scheduleServerEndpointTask(EurekaTransport eurekaTransport,
AbstractDiscoveryClientOptionalArgs args) {
// 省略大量代碼
// 如果需要抓取註冊表,讀取其他server的註冊信息
if (clientConfig.shouldRegisterWithEureka()) {
EurekaHttpClientFactory newRegistrationClientFactory = null;
EurekaHttpClient newRegistrationClient = null;
try {
newRegistrationClientFactory = EurekaHttpClients.registrationClientFactory(
eurekaTransport.bootstrapResolver,
eurekaTransport.transportClientFactory,
transportConfig
);
newRegistrationClient = newRegistrationClientFactory.newClient();
} catch (Exception e) {
logger.warn("Transport initialization failure", e);
}
// 將newRegistrationClient放入到eurekaTransport中
eurekaTransport.registrationClientFactory = newRegistrationClientFactory;
eurekaTransport.registrationClient = newRegistrationClient;
}
}
到了這裏,可以看到eurekaTransport.registrationClient實際就是EurekaHttpClient
,不知道是我沒找對地方還是什麼原因,我並沒有找到具體執行的實現類。
最後網上查了下,具體執行的實現類是:AbstractJersey2EurekaHttpClient
AbstractJersey2EurekaHttpClient.register()
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
Response response = null;
try {
// 發送請求,類似於:http://localhost:8080/v2/apps/ServiceA
// 發送的是post請求,服務實例的對象被打成了一個json發送,包括自己的主機、ip、端口號
// eureka server 就知道了這個ServiceA這個服務,有一個服務實例,比如是在192.168.31.109、host-01、8761端口
Builder resourceBuilder = jerseyClient.target(serviceUrl).path(urlPath).request();
addExtraProperties(resourceBuilder);
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.accept(MediaType.APPLICATION_JSON)
.acceptEncoding("gzip")
.post(Entity.json(info));
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey2 HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
到了這裏就已經真相大白了,可是 讀了一通發現這個代碼實在是不好理解,最後再總結一波才行。。。
總結
(1)DiscoveryClient構造函數會初始化EurekaClient相關的定時任務,定時任務裏面會啓動instanceInfo 互相複製的任務,就是InstanceInfoReplicator中的start()
(2)InstanceInfoReplicator的start()方法裏面,將自己作爲一個線程放到一個調度線程池中去了,默認是延遲40s去執行這個線程,還將isDirty設置爲了ture
(3)如果執行線程的時候,是執行run()方法,線程
(3)先是找EurekaClient.refreshInstanceInfo()這個方法,裏面其實是調用ApplicationInfoManager的一些方法刷新了一下服務實例的配置,看看配置有沒有改變,如果改變了,就刷新一下;用健康檢查器,檢查了一下狀態,將狀態設置到了ApplicationInfoManager中去,更新服務實例的狀態
(4)因爲之前設置過isDirty,所以這裏會執行進行服務註冊
(5)服務註冊的時候,是基於EurekaClient的reigster()方法去註冊的,調用的是底層的TransportClient的RegistrationClient,執行了register()方法,將InstanceInfo服務實例的信息,通過http請求,調用eureka server對外暴露的一個restful接口,將InstanceInfo給發送了過去。這裏找的是EurekaTransport,在構造的時候,調用了scheduleServerEndpointTask()方法,這個方法裏就初始化了專門用於註冊的RegistrationClient。
(6)找SessionedEurekaHttpClient調用register()方法,去進行註冊,底層最終使用的AbstractJersey2EurekaHttpClient的register方法實現的
(7)eureka大量的基於jersey框架,在eureka server上提供restful接口,在eureka client如果要發送請求到eureka server的話,一定是基於jersey框架,去發送的http restful接口調用的請求
(8)真正執行註冊請求的,就是eureka-client-jersey2工程裏的AbstractJersey2EurekaHttpClient,請求http://localhost:8080/v2/apps/ServiceA,將服務實例的信息發送過去
eureka client這一塊,在服務註冊的這塊代碼,很多槽點:
(1)服務註冊,不應該放在InstanceInfoReplicator裏面,語義不明朗
(2)負責發送請求的HttpClient,類體系過於複雜,導致人根本就找不到對應的Client,最後是根據他是使用jersey框架來進行restful接口暴露和調用,才能連蒙帶猜,找到真正發送服務註冊請求的地方
申明
本文章首發自本人博客:https://www.cnblogs.com/wang-meng 和公衆號:壹枝花算不算浪漫,如若轉載請標明來源!
感興趣的小夥伴可關注個人公衆號:壹枝花算不算浪漫