【一起學源碼-微服務】Nexflix Eureka 源碼六:在眼花繚亂的代碼中,EurekaClient是如何註冊的?...

前言

上一講已經講解了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 {

}

這裏有兩個關鍵點:

  1. 此類實現了Runnable接口,說白了就是執行一個異步線程
  2. 該類作用是:用於將本地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,不知道是我沒找對地方還是什麼原因,我並沒有找到具體執行的實現類。

image.png

最後網上查了下,具體執行的實現類是: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 和公衆號:壹枝花算不算浪漫,如若轉載請標明來源!

感興趣的小夥伴可關注個人公衆號:壹枝花算不算浪漫

22.jpg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章