天天看點

《跟二師兄學Nacos吧》第1篇 Nacos用戶端服務注冊源碼分析

開篇構想

在此之前,已經寫了十多篇Nacos的文章,感覺Nacos還值得更深入的學習一下。于是萌生了寫一個Nacos源碼系列專欄的文章。

寫作的目标呢,有兩個:第一,能夠系統的學習Nacos知識;第二,能夠基于Nacos學到涉及到的知識點或面;

展現形式呢,也有兩個:第一,單篇足夠簡單且又有價值;第二,發現代碼中的新穎之處;

源碼版本資訊

目前在生産實踐中建議大家采用1.4.2版本,但作為技術研究,本系列文章會基于2.0.2版本來僅僅講解。這是兩個跨度比較大的版本,建議大家配合源碼進行學習。

關于源碼拉取,環境搭建部分就不再贅述。下面就開始本篇文章,講解Nacos服務注冊的用戶端部分。

服務注冊資訊

講到服務注冊,我們先要了解一下Nacos都會将什麼資訊傳遞給伺服器。直接從Nacos Client項目的NamingTest看起:

Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
properties.put(PropertyKeyConst.USERNAME, "nacos");
properties.put(PropertyKeyConst.PASSWORD, "nacos");

Instance instance = new Instance();
instance.setIp("1.1.1.1");
instance.setPort(800);
instance.setWeight(2);
Map<String, String> map = new HashMap<String, String>();
map.put("netType", "external");
map.put("version", "2.0");
instance.setMetadata(map);

NamingService namingService = NacosFactory.createNamingService(properties);
namingService.registerInstance("nacos.test.1", instance);           

複制

這是服務注冊的核心所有代碼。僅從此處的代碼分析,可以看出,Nacos注冊服務執行個體時,包含了兩大類資訊:Nacos Server連接配接資訊和執行個體資訊。

Nacos Server連接配接資訊

Nacos Server連接配接資訊,存儲在Properties當中,包含以下資訊:

  • Server位址:Nacos伺服器位址,屬性的key為serverAddr;
  • 使用者名:連接配接Nacos服務的使用者名,屬性key為username,預設值為nacos;
  • 密碼:連接配接Nacos服務的密碼,屬性key為password,預設值為nacos;

執行個體資訊

注冊執行個體資訊用Instance對象承載,注冊的執行個體資訊又分兩部分:執行個體基礎資訊和中繼資料。

執行個體基礎資訊包括:

  • instanceId:執行個體的唯一ID;
  • ip:執行個體IP,提供給消費者進行通信的位址;
  • port: 端口,提供給消費者通路的端口;
  • weight:權重,目前執行個體的權限,浮點類型(預設1.0D);
  • healthy:健康狀況,預設true;
  • enabled:執行個體是否準備好接收請求,預設true;
  • ephemeral:執行個體是否為瞬時的,預設為true;
  • clusterName:執行個體所屬的叢集名稱;
  • serviceName:執行個體的服務資訊;

Instance類包含了執行個體的基礎資訊之外,還包含了用于存儲中繼資料的metadata(描述資料的資料),類型為HashMap。

從Demo中放了兩個資料:

  • netType:顧名思義,網絡類型,這裡的值為external,也就是外網的意思;
  • version:版本,Nacos的版本,這裡是2.0這個大版本。

除了Demo中這些“自定義”的資訊,在Instance類中還定義了一些預設資訊,這些資訊通過get方法提供:

public long getInstanceHeartBeatInterval() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
            Constants.DEFAULT_HEART_BEAT_INTERVAL);
}

public long getInstanceHeartBeatTimeOut() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
            Constants.DEFAULT_HEART_BEAT_TIMEOUT);
}

public long getIpDeleteTimeout() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
            Constants.DEFAULT_IP_DELETE_TIMEOUT);
}

public String getInstanceIdGenerator() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR,
            Constants.DEFAULT_INSTANCE_ID_GENERATOR);
}           

複制

上面的get方法在需要中繼資料預設值時會被用到:

  • preserved.heart.beat.interval:心跳間隙的key,預設為5s,也就是預設5秒進行一次心跳;
  • preserved.heart.beat.timeout:心跳逾時的key,預設為15s,也就是預設15秒收不到心跳,執行個體将會标記為不健康;
  • preserved.ip.delete.timeout:執行個體IP被删除的key,預設為30s,也就是30秒收不到心跳,執行個體将會被移除;
  • preserved.instance.id.generator:執行個體ID生成器key,預設為simple;

這些都是Nacos預設提供的值,也就是目前執行個體注冊時會告訴Nacos Server說:我的心跳間隙、心跳逾時等對應的值是多少,你按照這個值來判斷我這個執行個體是否健康。當然,如果你想讓心跳“加速”,出現故障快速被移除,那可以跳短心跳間隙和逾時時間。但這也意味着給Nacos服務帶來一定的壓力。

有了這些資訊,我們基本是已經知道注冊執行個體時需要傳遞什麼參數,需要配置什麼參數了。

NamingService接口

NamingService接口是Nacos命名服務對外提供的一個統一接口,看對應的源碼就可以發現,它提供了大量執行個體相關的接口方法,比如:

  • 服務執行個體注冊;
  • 服務執行個體登出;
  • 擷取服務執行個體清單;
  • 擷取服務單個執行個體;
  • 訂閱服務事件;
  • 取消訂閱服務事件;
  • 擷取所有(或指定)服務名稱;
  • 擷取所有訂閱的服務;
  • 擷取Nacos服務的狀态;
  • 主動關閉服務;

其中部分功能提供了大量的重載方法,應用于不同場景和不同類型執行個體或服務的篩選。這個就不逐一說明,按照需要或注釋進行使用即可。

NamingService的執行個體化是通過NamingFactory類和上面提到的Nacos服務資訊:

public static NamingService createNamingService(Properties properties) throws NacosException {
    try {
        Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
        Constructor constructor = driverImplClass.getConstructor(Properties.class);
        return (NamingService) constructor.newInstance(properties);
    } catch (Throwable e) {
        throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
    }
}           

複制

很明顯,這裡采用了反射的機制來執行個體化NamingService,接口的具體實作類為NacosNamingService類。

NacosNamingService的實作

在示例代碼中使用了NamingService#registerInstance方法來進行服務執行個體的注冊,該方法接收兩個參數,服務名稱和執行個體對象。

@Override
public void registerInstance(String serviceName, Instance instance) throws NacosException {
    registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
}           

複制

這個方法的最大作用是設定了目前執行個體的分組資訊。我們知道,在Nacos中,通過Namespace、group、Service、Cluster等一層層的将執行個體進行環境的隔離。在這裡設定了預設的分組為“DEFAULT_GROUP”。

緊接着調用的registerInstance方法如下:

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    NamingUtils.checkInstanceIsLegal(instance);
    clientProxy.registerService(serviceName, groupName, instance);
}           

複制

這個方法實作了兩個功能:第一,檢查心跳時間設定的對不對,配置的逾時時間總不能比心跳間隔還短吧。第二,通過NamingClientProxy這個代理來執行服務注冊操作。

反觀NacosNamingService構造方法,會發現NamingClientProxy這個代理接口的具體實作是有NamingClientProxyDelegate來完成的。

NamingClientProxyDelegate中實作

NamingClientProxy調用registerService實際上調用的就是NamingClientProxyDelegate的對應方法:

@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}           

複制

真正調用注冊服務的并不是代理實作類,而是根據目前執行個體是否為瞬時對象,來選擇對應的用戶端代理來進行請求的:

private NamingClientProxy getExecuteClientProxy(Instance instance) {
    return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
}           

複制

如果目前執行個體為瞬時對象,則采用gRPC協定(NamingGrpcClientProxy)進行請求,否則采用http協定(NamingHttpClientProxy)進行請求。預設為瞬時對象,也就是說,2.0版本中預設采用了gRPC協定進行與Nacos服務進行互動。

NamingGrpcClientProxy中實作

關于gRPC協定這部分我們會單獨進行講解,這裡暫時不做拓展。主要看其registerService方法實作:

@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
            instance);
    namingGrpcConnectionEventListener.cacheInstanceForRedo(serviceName, groupName, instance);
    InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,
            NamingRemoteConstants.REGISTER_INSTANCE, instance);
    requestToServer(request, Response.class);
}           

複制

在NamingGrpcClientProxy中做了兩件事,一件事是通過事件監聽器緩存了目前注冊的執行個體資訊用于恢複。緩存的資料結構為ConcurrentMap<String, Instance>,key為“serviceName@@groupName”,value就是前面封裝的執行個體資訊。

另外一件事就是封裝了參數,基于gRPC協定進行服務的調用和結果的處理。

流程圖

下面來看一張流程圖,來彙總一下上面講到的整個業務邏輯:

《跟二師兄學Nacos吧》第1篇 Nacos用戶端服務注冊源碼分析

小結

關于Nacos源碼分析的開篇就寫這麼多,主要分析了服務注冊需要哪些次元的資訊、用戶端提供的核心服務處理類(NamingService)以及注冊通信協定的選擇。其中的一些内容還可以細化,比如gRPC協定的實作等,我們後續文章繼續進行呈現。

部落客簡介:《SpringBoot技術内幕》技術圖書作者,酷愛鑽研技術,寫技術幹貨文章。