1. 程式人生 > 其它 >Nacos原始碼系列—關於服務註冊的那些事

Nacos原始碼系列—關於服務註冊的那些事

點贊再看,養成習慣,微信搜尋【牧小農】關注我獲取更多資訊,風裡雨裡,小農等你,很高興能夠成為你的朋友。
專案原始碼地址:公眾號回覆 nacos,即可免費獲取原始碼

簡介

首先我們在看Nacos原始碼之前,要先想想為什麼我們要讀原始碼?是為了裝杯?還是為了在心儀的女神面前給她娓娓道來展示自己的程式碼功底?當然不全是!

這都不是我們讀原始碼的最終目的。作為一名技術人,上面的都是浮雲,真正激勵我們的應該是能夠提升我們技術功底和整體技術大局觀。此乃大道也!閒言少敘,接下來我們就來看一看,看原始碼究竟有什麼好處

  • 提升技術功底: 當我們去看原始碼的時候,能夠學習原始碼裡面優秀的設計思想,還含有設計模式和併發程式設計技術,解決問題的思路,能夠知其所以然
  • 新技術學習能力: 當我們看多了原始碼,對於一個新技術或者框架的掌握速度會有大幅度提升,能根據經驗或官網資料快速掌握底層的實現,技術更新迭代也可以更快的入手
  • 快速解決問題能力: 遇到問題,尤其是框架原始碼的問題,能夠更快速的定位
  • 面試獲取更高成功率: 現在出去面試,一般中高階一點的,都會問到框架原始碼級別的實現,如果能夠說出來,可以提升面試的成功率和薪資待遇,原始碼面試是區別程式設計師水平另一面鏡子
  • 認識更多圈子: 多活躍開源社群,熟讀原始碼後多思考,發現問題或需求主動參與開源技術研發,與圈內大牛成為為朋友

閱讀原始碼的方法

  1. 搭建入門demo: 我們可以先看一下官網提供的文件,搭建Demo,快速掌握框架的基本使用
  2. 看核心程式碼: 對於初次看原始碼的同學,不要太過於關注原始碼的細枝末節,先把主要核心流程梳理出來,找到其入口,分析靜態程式碼,如果遇到問題,可以進行斷點除錯。
  3. 繪圖和筆記: 梳理好核心功能後,可以用流程圖記錄下來,好記性不如爛筆頭,同時對關鍵的原始碼部分可以進行備註,分析引數的變化。同時要善於用Debug,來觀看原始碼的執行過程
  4. 複習總結: 當我們把框架的所有功能點的原始碼都分析完成後,回到主流程在梳理一遍,最後在自己腦袋中形成一個閉環,這樣原始碼的核心內容和主流程就基本上理解了。

Nacos核心功能點

服務註冊: Nacos Client會通過傳送REST請求的方式向Nacos Server註冊自己的服務,提供自身的元資料,比如IP地址,埠等資訊。Nacos Server接收到註冊請求後,就會把這些元資料資訊儲存在一個雙層的記憶體Map中。

服務心跳: 在服務註冊後,Nacos Client會維護一個定時心跳來支援通知Nacos Server,說明服務一直處於可用狀態,防止被剔除。預設5s傳送一次心跳。

服務健康檢查: Nacos Server會開啟一個定時任務用來檢查註冊服務例項的健康狀況,對於超過15s沒有收到客戶端心跳的例項會將它的healthy屬性設定為false(客戶端服務發現時不會發現)。如果某個例項超過30秒沒有收到心跳,直接剔除該例項(被剔除的例項如果恢復傳送心跳則會重新註冊)

服務發現: 服務消費者(Nacos Client)在呼叫服務提供者的服務時,會發送一個REST請求給Nacos Server,獲取上面註冊的服務清單,並且快取在Nacos Client本地,同時在Nacos Client本地開啟一個定時任務定時拉取服務端最新的登錄檔資訊更新到本地快取

服務同步: Nacos Server叢集之間會互相同步服務例項,用來保證服務資訊的一致性

Nacos原始碼下載

首先我們需要將Nacos的原始碼下載下來,下載地址:https://github.com/alibaba/nacos

我們將原始碼下下來以後,匯入到idea中

proto編譯

當我們匯入成功以後,會出現程式包com.alibaba.nacos.consistency.entity不存在的錯誤提示,這是因為Nacos底層的資料通訊會基於protobuf對資料做序列化和反序列化,需要先將proto檔案編譯為對應的Java程式碼。

最簡單的 不安裝任何的東西 idea2021.2已經捆綁安裝了這個。

可以通過mvn copmpile來在target自動生成他們。

Nacos缺少Istio依賴問題解決

我們只需要在檔案根目錄下執行以下命令即可:

mvn clean package -Dmaven.test.skip=true -Dcheckstyle.skip=true

做完以上兩步,我們就可以啟動Nacos的了

啟動Nacos

首先我們找到 nacos-console這個模組,這個就是我們的管理後臺,找到它的啟動類,因為Nacos預設為叢集啟動,所以我們要設定它為單機啟動,方便演示

設定命令:

-Dnacos.standalone=true -Dnacos.home=E:\test\nacos

啟動成功後,賬號密碼:nacos/nacos

到這裡我們Nacos的原始碼啟動就完成了。

開啟原始碼

我們先從客戶端服務的註冊開始說起,我們可以先想一想如果Nacos客戶端要註冊,會把什麼資訊傳遞給伺服器?
這裡我們可以看到在 nacos-client下的NamingTest有這麼一些資訊


@Ignore
public class NamingTest {
    
    @Test
    public void testServiceList() throws Exception {
        
        //Nacos Server連線資訊
        Properties properties = new Properties();
        //Nacos伺服器地址
        properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
        //連線Nacos服務的使用者名稱
        properties.put(PropertyKeyConst.USERNAME, "nacos");
        //連線Nacos服務的密碼
        properties.put(PropertyKeyConst.PASSWORD, "nacos");
        
        //例項資訊
        Instance instance = new Instance();
        //例項IP,提供給消費者進行通訊的地址
        instance.setIp("1.1.1.1");
        //埠,提供給消費者訪問的埠
        instance.setPort(800);
        //權重,當前例項的許可權,浮點型別(預設1.0D)
        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);
        
        ThreadUtils.sleep(5000L);
        
        List<Instance> list = namingService.getAllInstances("nacos.test.1");
        
        System.out.println(list);
        
        ThreadUtils.sleep(30000L);
        //        ExpressionSelector expressionSelector = new ExpressionSelector();
        //        expressionSelector.setExpression("INSTANCE.metadata.registerSource = 'dubbo'");
        //        ListView<String> serviceList = namingService.getServicesOfServer(1, 10, expressionSelector);
        
    }
}

上面就是客戶端註冊的一個測試類,模仿了真實的服務註冊到Nacos的過程,包括NacosServer連線、例項的建立、例項屬性的賦值、註冊例項,所以在這個其中包含了服務註冊的核心程式碼,從這裡我們可以大致看出,它包含了兩個類的資訊: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:例項的服務資訊;

元資料:

元資料型別為HashMap,從當前Demo我們能夠看到的資料只有兩個

  • netType:網路型別,這裡設定的值為external(外網)
  • version Nacos版本,這裡為2.0

除此之外,我們在Instance類中還可以看到一些預設資訊,這些方法都是通過get方法進行提供的

  //心跳間隙的key,預設為5s,也就是預設5秒進行一次心跳
    public long getInstanceHeartBeatInterval() {
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
                Constants.DEFAULT_HEART_BEAT_INTERVAL);
    }

    //心跳超時的key,預設為15s,也就是預設15秒收不到心跳,例項將會標記為不健康;
    public long getInstanceHeartBeatTimeOut() {
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
                Constants.DEFAULT_HEART_BEAT_TIMEOUT);
    }

    //例項IP被刪除的key,預設為30s,也就是30秒收不到心跳,例項將會被移除;
    public long getIpDeleteTimeout() {
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
                Constants.DEFAULT_IP_DELETE_TIMEOUT);
    }

    //例項ID生成器key,預設為simple;
    public String getInstanceIdGenerator() {
        return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR,
                Constants.DEFAULT_INSTANCE_ID_GENERATOR);
    }

為什麼要說這個呢?從這些引數中我們就可以瞭解到,我們服務的心跳間隙是多少以及超時時間,傳遞什麼引數配置什麼引數,以此來了解我們的例項是否健康。同時我們也可以看到一個比較關鍵且核心的類,是真正建立例項的類 ——NamingService

NamingService

NamingService是Nacos對外提供的一個統一的介面,當我們點進去檢視,可以看到大概一下幾個方法,這些方法提供了不同的過載方法,方便我們用於不同的場景。

//服務例項註冊
void registerInstance(...) throws NacosException;

//服務例項登出
void deregisterInstance(...) throws NacosException;

//獲取服務例項列表
List<Instance> getAllInstances(...) throws NacosException;

//查詢健康服務例項
List<Instance> selectInstances(...) throws NacosException;

//查詢叢集中健康的服務例項
List<Instance> selectInstances(....List<String> clusters....)throws NacosException;

//使用負載均衡策略選擇一個健康的服務例項
Instance selectOneHealthyInstance(...) throws NacosException;

//訂閱服務事件
void subscribe(...) throws NacosException;

//取消訂閱服務事件
void unsubscribe(...) throws NacosException;

//獲取所有(或指定)服務名稱
ListView<String> getServicesOfServer(...) throws NacosException;

//獲取所有訂閱的服務
List<ServiceInfo> getSubscribeServices() throws NacosException;
 
//獲取Nacos服務的狀態
String getServerStatus();
 
//主動關閉服務
void shutDown() throws NacosException;

NamingService的例項化是通過NacosFactory.createNamingService(properties);實現的,內部原始碼是通過反射來實現例項化過程

 NamingService namingService = NacosFactory.createNamingService(properties);
 

    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的具體實現

//呼叫registerInstance方法
namingService.registerInstance("nacos.test.1", instance);
 @Override
    public void registerInstance(String serviceName, Instance instance) throws NacosException {
        //預設的分組為“DEFAULT_GROUP” 
        registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
    }
 @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        //檢查心跳時間是否正常
        NamingUtils.checkInstanceIsLegal(instance);
        //通過代理註冊服務
        clientProxy.registerService(serviceName, groupName, instance);
    }

心跳檢測程式碼

   //心跳間隙超過限制 返回錯誤
        if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()
                || instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) {
            throw new NacosException(NacosException.INVALID_PARAM,
                    "Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'.");
        }

通過代理註冊服務,我們瞭解到clientProxy代理介面是通過NamingClientProxyDelegate來完成,我們可以在init構造方法中得出,具體的例項物件

  private void init(Properties properties) throws NacosException {
        //使用NamingClientProxyDelegate來完成
         this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
    }

NamingClientProxyDelegate實現
NamingClientProxyDelegate中,真正呼叫註冊服務的並不是代理實現類,而且先判斷當前例項是否為瞬時物件後,來選擇對應的客戶端代理來進行請求。

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

如果當前實力是瞬時物件,則採用gRPC協議(NamingGrpcClientProxy)進行請求,否則採用Http協議(NamingHttpClientProxy),預設為瞬時物件,在2.0版本中預設採用gRPC協議進行與Nacos服務進行互動

    //判斷當前例項是否為瞬時物件
      private NamingClientProxy getExecuteClientProxy(Instance instance) {
        return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
    }

NamingGrpcClientProxy中的實現

    @Override
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
                instance);
        //資料的快取
        redoService.cacheInstanceForRedo(serviceName, groupName, instance);
        //gRPC進行服務呼叫
        doRegisterService(serviceName, groupName, instance);
    }

大體關係圖如下所示:

Nacos客戶端在專案的應用

  1. 我們想要讓某一個服務註冊到Nacos中,首先要引入一個依賴:
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
  1. 在依賴中,去檢視SpringBoot自動裝配檔案自動裝配檔案META-INF/spring.factories
  1. 通過SpringBoot的自動裝配來載入EnableAutoConfiguration對應的類,這裡我們可以看到很多有關於Nacos相關的類,怎麼知道哪個是我們真正需要關心的類,服務在註冊的時候走的是哪個,一般自動裝配,我們都會找到帶有“Auto”關鍵字的檔案進行檢視,然後在結合我們需要找的,我們是客戶端註冊服務,所以我們大體可以定位到NacosServiceRegistryAutoConfiguration這個檔案
  1. 檢視NacosServiceRegistryAutoConfiguration原始碼,在這裡我們只需要關注最核心的nacosAutoServiceRegistration方法


而我們真正關心的只有三個類NacosAutoServiceRegistration類是註冊的核心,我們來看一下它的繼承關係

	@Bean
	@ConditionalOnBean(AutoServiceRegistrationProperties.class)
	public NacosAutoServiceRegistration nacosAutoServiceRegistration(
			NacosServiceRegistry registry,
			AutoServiceRegistrationProperties autoServiceRegistrationProperties,
			NacosRegistration registration) {
          return new NacosAutoServiceRegistration(registry,
          autoServiceRegistrationProperties, registration);
	}
  1. 從上述內容中我們可以知道,Nacos服務自動註冊是從NacosServiceRegistryAutoConfiguration類開始的,並自動註冊到NacosAutoServiceRegistration類中。

在下圖中我們可以看到,主要是呼叫了super 方法,所以我們繼續檢視該類的構造方法:AbstractAutoServiceRegistration

public class NacosAutoServiceRegistration
		extends AbstractAutoServiceRegistration<Registration> {

      public NacosAutoServiceRegistration(ServiceRegistry<Registration> serviceRegistry,
          AutoServiceRegistrationProperties autoServiceRegistrationProperties,
          NacosRegistration registration) {
        super(serviceRegistry, autoServiceRegistrationProperties);
        this.registration = registration;
      }

}

AbstractAutoServiceRegistration實現了ApplicationListener介面,用來監聽Spring容器啟動過程中WebServerInitializedEvent事件,一般如果我們實現這個類的時候,會實現一個方法onApplicationEvent(),這個方法會在我們專案啟動的時候觸發

	@Override
	@SuppressWarnings("deprecation")
	public void onApplicationEvent(WebServerInitializedEvent event) {
		bind(event);
	}

由此我們可以看到bind裡面的這個方法

	@Deprecated
	public void bind(WebServerInitializedEvent event) {
  //獲取 ApplicationContext物件
		ApplicationContext context = event.getApplicationContext();
    //判斷服務的 Namespace
		if (context instanceof ConfigurableWebServerApplicationContext) {
			if ("management".equals(((ConfigurableWebServerApplicationContext) context).getServerNamespace())) {
				return;
			}
		}
    //記錄當前服務的埠
		this.port.compareAndSet(0, event.getWebServer().getPort());
    //【核心】啟動註冊流程
		this.start();
	}

start()方法呼叫register();方法來註冊服務

	public void start() {
		if (!isEnabled()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Discovery Lifecycle disabled. Not starting");
			}
			return;
		}

		// only initialize if nonSecurePort is greater than 0 and it isn't already running
		// because of containerPortInitializer below
    //如果服務是沒有執行狀態時,進行初始化
		if (!this.running.get()) {
    //釋出服務開始註冊事件
			this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
      //【核心】註冊服務
			register();
			if (shouldRegisterManagement()) {
				registerManagement();
			}
      //釋出註冊完成事件
			this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration()));
      //服務狀態設定為執行狀態
			this.running.compareAndSet(false, true);
		}

	}

NacosServiceRegistry.register()方法,如下所示:

@Override
	public void register(Registration registration) {
 //判斷ServiceId是否為空
		if (StringUtils.isEmpty(registration.getServiceId())) {
			log.warn("No service to register for nacos client...");
			return;
		}
 //獲取Nacos的服務資訊
		NamingService namingService = namingService();
  //獲取服務ID和分組
		String serviceId = registration.getServiceId();
		String group = nacosDiscoveryProperties.getGroup();
    //構建instance例項(IP/Port/Weight/clusterName.....)
		Instance instance = getNacosInstanceFromRegistration(registration);

		try {
     //向服務端註冊此服務
			namingService.registerInstance(serviceId, group, instance);
			log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
					instance.getIp(), instance.getPort());
		}
		catch (Exception e) {
			if (nacosDiscoveryProperties.isFailFast()) {
				log.error("nacos registry, {} register failed...{},", serviceId,
						registration.toString(), e);
				rethrowRuntimeException(e);
			}
			else {
				log.warn("Failfast is false. {} register failed...{},", serviceId,
						registration.toString(), e);
			}
		}
	}

NacosNamingService.registerInstance()方法,如下:

    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        //檢查超時引數是否異常,心跳超時時間(15s)必須大於心跳間隙(5s)
        NamingUtils.checkInstanceIsLegal(instance);
        //拼接服務名,格式:groupName@@serviceName
        String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
        //判斷是否為臨時例項,預設為true
        if (instance.isEphemeral()) {
            //臨時例項,定時向Nacos服務傳送心跳
            BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
            beatReactor.addBeatInfo(groupedServiceName, beatInfo);
        }
        //【核心】傳送註冊服務例項請求
        serverProxy.registerService(groupedServiceName, groupName, instance);
    }

registerService中我們可以看到Nacos服務註冊介面需要的完整引數

    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        
        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
                instance);
        
        final Map<String, String> params = new HashMap<String, String>(16);
        //環境
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        //服務名稱
        params.put(CommonParams.SERVICE_NAME, serviceName);
        //分組名稱
        params.put(CommonParams.GROUP_NAME, groupName);
        //叢集名稱
        params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
        //當前例項IP
        params.put("ip", instance.getIp());
        //當前例項埠
        params.put("port", String.valueOf(instance.getPort()));
        //權重
        params.put("weight", String.valueOf(instance.getWeight()));
        params.put("enable", String.valueOf(instance.isEnabled()));
        params.put("healthy", String.valueOf(instance.isHealthy()));
        params.put("ephemeral", String.valueOf(instance.isEphemeral()));
        params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
        
        reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
        
    }

補充

在這裡我們會發現我們請求例項介面的地址為/nacos/v1/ns/instance,其實這個在官網中也有提供對應的地址給我們,並且是對應的

客戶端註冊流程圖

總結

以上就是Nacos的客戶端註冊流程,如果您對文中有疑問或者問題,歡迎在下方留言,小農看見了會第一時間回覆,大家加油~