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的源碼下載下來,下載地址://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的客戶端註冊流程,如果您對文中有疑問或者問題,歡迎在下方留言,小農看見了會第一時間回復,大家加油~