Nacos源碼系列—關(guān)于服務注冊的那些事

簡介

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

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

  • 提升技術(shù)功底: 當我們?nèi)タ丛创a的時候,能夠?qū)W習源碼里面優(yōu)秀的設計思想,還含有設計模式和并發(fā)編程技術(shù),解決問題的思路,能夠知其所以然
  • 新技術(shù)學習能力: 當我們看多了源碼,對于一個新技術(shù)或者框架的掌握速度會有大幅度提升,能根據(jù)經(jīng)驗或官網(wǎng)資料快速掌握底層的實現(xiàn),技術(shù)更新迭代也可以更快的入手
  • 快速解決問題能力: 遇到問題,尤其是框架源碼的問題,能夠更快速的定位
  • 面試獲取更高成功率: 現(xiàn)在出去面試,一般中高級一點的,都會問到框架源碼級別的實現(xiàn),如果能夠說出來,可以提升面試的成功率和薪資待遇,源碼面試是區(qū)別程序員水平另一面鏡子
  • 認識更多圈子: 多活躍開源社區(qū),熟讀源碼后多思考,發(fā)現(xiàn)問題或需求主動參與開源技術(shù)研發(fā),與圈內(nèi)大牛成為為朋友

閱讀源碼的方法

  1. 搭建入門demo: 我們可以先看一下官網(wǎng)提供的文檔,搭建Demo,快速掌握框架的基本使用
  2. 看核心代碼: 對于初次看源碼的同學,不要太過于關(guān)注源碼的細枝末節(jié),先把主要核心流程梳理出來,找到其入口,分析靜態(tài)代碼,如果遇到問題,可以進行斷點調(diào)試。
  3. 繪圖和筆記: 梳理好核心功能后,可以用流程圖記錄下來,好記性不如爛筆頭,同時對關(guān)鍵的源碼部分可以進行備注,分析參數(shù)的變化。同時要善于用Debug,來觀看源碼的執(zhí)行過程
  4. 復習總結(jié): 當我們把框架的所有功能點的源碼都分析完成后,回到主流程在梳理一遍,最后在自己腦袋中形成一個閉環(huán),這樣源碼的核心內(nèi)容和主流程就基本上理解了。

Nacos核心功能點

服務注冊: Nacos Client會通過發(fā)送REST請求的方式向Nacos Server注冊自己的服務,提供自身的元數(shù)據(jù),比如IP地址,端口等信息。Nacos Server接收到注冊請求后,就會把這些元數(shù)據(jù)信息存儲在一個雙層的內(nèi)存Map中。

服務心跳: 在服務注冊后,Nacos Client會維護一個定時心跳來支持通知Nacos Server,說明服務一直處于可用狀態(tài),防止被剔除。默認5s發(fā)送一次心跳。

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

服務發(fā)現(xiàn): 服務消費者(Nacos Client)在調(diào)用服務提供者的服務時,會發(fā)送一個REST請求給Nacos Server,獲取上面注冊的服務清單,并且緩存在Nacos Client本地,同時在Nacos Client本地開啟一個定時任務定時拉取服務端最新的注冊表信息更新到本地緩存

服務同步: Nacos Server集群之間會互相同步服務實例,用來保證服務信息的一致性

Nacos源碼下載

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

我們將源碼下下來以后,導入到idea中

proto編譯

當我們導入成功以后,會出現(xiàn)程序包com.alibaba.nacos.consistency.entity不存在的錯誤提示,這是因為Nacos底層的數(shù)據(jù)通信會基于protobuf對數(shù)據(jù)做序列化和反序列化,需要先將proto文件編譯為對應的Java代碼。

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

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

Nacos缺少Istio依賴問題解決

我們只需要在文件根目錄下執(zhí)行以下命令即可:

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);
        //權(quán)重,當前實例的權(quán)限,浮點類型(默認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);

        //關(guān)鍵代碼 創(chuàng)建自己的實例
        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連接、實例的創(chuàng)建、實例屬性的賦值、注冊實例,所以在這個其中包含了服務注冊的核心代碼,從這里我們可以大致看出,它包含了兩個類的信息:Nacos Server連接信息和實例信息

Nacos Server連接信息:

從上述中我們可以看到有關(guān)于Nacos Server連接信息是存儲在Properties中,

  • Server地址:Nacos服務器地址,屬性的key為serverAddr;
  • 用戶名:連接Nacos服務的用戶名,屬性key為username,默認值為nacos;
  • 密碼:連接Nacos服務的密碼,屬性key為password,默認值為nacos;

實例信息:

從上述測試中我們可以看到注冊實例信息用instance進行承載,而實例信息又分為兩部分,一個是基礎(chǔ)實例信息,一個是元數(shù)據(jù)信息

實例基礎(chǔ)信息:

  • instanceId:實例的唯一ID;
  • ip:實例IP,提供給消費者進行通信的地址;
  • port: 端口,提供給消費者訪問的端口;
  • weight:權(quán)重,當前實例的權(quán)限,浮點類型(默認1.0D);
  • healthy:健康狀況,默認true;
  • enabled:實例是否準備好接收請求,默認true;
  • ephemeral:實例是否為瞬時的,默認為true;
  • clusterName:實例所屬的集群名稱;
  • serviceName:實例的服務信息;

元數(shù)據(jù):

元數(shù)據(jù)類型為HashMap,從當前Demo我們能夠看到的數(shù)據(jù)只有兩個

  • netType:網(wǎng)絡類型,這里設置的值為external(外網(wǎng))
  • 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);
    }

為什么要說這個呢?從這些參數(shù)中我們就可以了解到,我們服務的心跳間隙是多少以及超時時間,傳遞什么參數(shù)配置什么參數(shù),以此來了解我們的實例是否健康。同時我們也可以看到一個比較關(guān)鍵且核心的類,是真正創(chuàng)建實例的類 ——NamingService

NamingService

NamingService是Nacos對外提供的一個統(tǒng)一的接口,當我們點進去查看,可以看到大概一下幾個方法,這些方法提供了不同的重載方法,方便我們用于不同的場景。

//服務實例注冊
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服務的狀態(tài)
String getServerStatus();
 
//主動關(guān)閉服務
void shutDown() throws NacosException;

NamingService的實例化是通過NacosFactory.createNamingService(properties);實現(xiàn)的,內(nèi)部源碼是通過反射來實現(xiàn)實例化過程

 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的具體實現(xiàn)

//調(diào)用registerInstance方法
namingService.registerInstance("nacos.test.1", instance);
  • 1
  • 2
 @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構(gòu)造方法中得出,具體的實例對象

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

NamingClientProxyDelegate實現(xiàn)
NamingClientProxyDelegate中,真正調(diào)用注冊服務的并不是代理實現(xiàn)類,而且先判斷當前實例是否為瞬時對象后,來選擇對應的客戶端代理來進行請求。

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

如果當前實力是瞬時對象,則采用gRPC協(xié)議(NamingGrpcClientProxy)進行請求,否則采用Http協(xié)議(NamingHttpClientProxy),默認為瞬時對象,在2.0版本中默認采用gRPC協(xié)議進行與Nacos服務進行交互

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

NamingGrpcClientProxy中的實現(xiàn)

    @Override
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
                instance);
        //數(shù)據(jù)的緩存
        redoService.cacheInstanceForRedo(serviceName, groupName, instance);
        //gRPC進行服務調(diào)用
        doRegisterService(serviceName, groupName, instance);
    }

大體關(guān)系圖如下所示:

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對應的類,這里我們可以看到很多有關(guān)于Nacos相關(guān)的類,怎么知道哪個是我們真正需要關(guān)心的類,服務在注冊的時候走的是哪個,一般自動裝配,我們都會找到帶有“Auto”關(guān)鍵字的文件進行查看,然后在結(jié)合我們需要找的,我們是客戶端注冊服務,所以我們大體可以定位到NacosServiceRegistryAutoConfiguration這個文件

  1. 查看NacosServiceRegistryAutoConfiguration源碼,在這里我們只需要關(guān)注最核心的nacosAutoServiceRegistration方法


而我們真正關(guān)心的只有三個類NacosAutoServiceRegistration類是注冊的核心,我們來看一下它的繼承關(guān)系

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

在下圖中我們可以看到,主要是調(diào)用了super 方法,所以我們繼續(xù)查看該類的構(gòu)造方法:AbstractAutoServiceRegistration

public class NacosAutoServiceRegistration
		extends AbstractAutoServiceRegistration<Registration> {

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

}

AbstractAutoServiceRegistration實現(xiàn)了ApplicationListener接口,用來監(jiān)聽Spring容器啟動過程中WebServerInitializedEvent事件,一般如果我們實現(xiàn)這個類的時候,會實現(xiàn)一個方法onApplicationEvent(),這個方法會在我們項目啟動的時候觸發(fā)

	@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()方法調(diào)用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
    //如果服務是沒有運行狀態(tài)時,進行初始化
		if (!this.running.get()) {
    //發(fā)布服務開始注冊事件
			this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
      //【核心】注冊服務
			register();
			if (shouldRegisterManagement()) {
				registerManagement();
			}
      //發(fā)布注冊完成事件
			this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration()));
      //服務狀態(tài)設置為運行狀態(tài)
			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();
    //構(gòu)建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 {
        //檢查超時參數(shù)是否異常,心跳超時時間(15s)必須大于心跳間隙(5s)
        NamingUtils.checkInstanceIsLegal(instance);
        //拼接服務名,格式:groupName@@serviceName
        String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
        //判斷是否為臨時實例,默認為true
        if (instance.isEphemeral()) {
            //臨時實例,定時向Nacos服務發(fā)送心跳
            BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
            beatReactor.addBeatInfo(groupedServiceName, beatInfo);
        }
        //【核心】發(fā)送注冊服務實例請求
        serverProxy.registerService(groupedServiceName, groupName, instance);
    }

registerService中我們可以看到Nacos服務注冊接口需要的完整參數(shù)

    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);
        //環(huán)境
        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()));
        //權(quán)重
        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);
        
    }

補充

在這里我們會發(fā)現(xiàn)我們請求實例接口的地址為/nacos/v1/ns/instance,其實這個在官網(wǎng)中也有提供對應的地址給我們,并且是對應的

客戶端注冊流程圖

總結(jié)

以上就是Nacos的客戶端注冊流程,閱讀源碼并沒有我們想象中的那么難,道阻且長,行之將至,當你開始行動的時候,你就已經(jīng)開始進步了,別管學多少,如果您對文中有疑問或者問題,歡迎在下方留言,小農(nóng)看見了會第一時間回復,大家加油~


文章轉(zhuǎn)載自: https://muxiaonong.blog.csdn.net


公眾號:牧小農(nóng),微信掃碼關(guān)注或搜索公眾號名稱