詳解nacos註冊中心服務註冊流程

說起註冊中心,我們首先要知道註冊中心是用來做什麼的,註冊中心一般都是用在微服務架構中,而微服務架構風格是一種將一個單一應用程序開發爲一組小型服務的方法,每個服務運行在自己的進程中,服務間通信通常採用HTTP的方式,這些服務共用一個最小型的集中式的管理。這個最小型的集中式管理的組件就是服務註冊中心。

一、nacos 簡介

本文的目的在於詳解 nacos 註冊中心的服務註冊流程,所以首先需要對 nacos 有個基本的瞭解。nacos 提供了一組簡單易用的特性集,幫助您快速實現動態服務發現、服務配置、服務元數據及流量管理。Nacos 幫助您更敏捷和容易地構建、交付和管理微服務平臺。nacos 是構建以“服務”爲中心的現代應用架構 (例如微服務範式、雲原生範式) 的服務基礎設施。

nacos 基本架構

二、nacos 註冊中心

nacos 服務註冊中心,它是服務,其實例及元數據的數據庫。服務實例在啓動時註冊到服務註冊表,並在關閉時註銷。服務和路由器的客戶端查詢服務註冊表以查找服務的可用實例。服務註冊中心可能會調用服務實例的健康檢查 API 來驗證它是否能夠處理請求。

nacos 服務註冊表結構:Map<namespace, Map<group::serviceName, Service>>

三、demo 搭建

瞭解了 nacos 的基本架構和服務註冊中心的功能之後,接下來就要來詳解服務註冊的流程原理了,首先建立一個 nacos 客戶端工程,springboot 版本選擇2.1.0,springcloud 版本選擇 Greenwich

<spring-boot-version>2.1.0.RELEASE</spring-boot-version>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
複製代碼

隨後引入 springcloud,springboot 和 springcloud-alibaba 的依賴

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-boot-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

創建服務提供者模塊,在服務提供者模塊的 pom 文件中引入 spring-cloud-starter-alibaba-nacos-discovery 依賴,在啓動類中加上 @EnableDiscoveryClient 註解,使得註冊中心可以發現該服務,運行啓動類,可以看到 nacos 控制檯中註冊了該服務

[]( imgchr.com/i/rloNjg )

點擊詳情,可以看到服務詳情,其中臨時實例屬性爲true,代表服務是臨時實例,nacos 註冊中心可以設置註冊的實例是臨時實例還是持久化實例,默認服務都是臨時實例。

[]( imgchr.com/i/rlTdxO )

四、原理詳解

搭建好demo並且在 nacos 控制檯上看到效果之後,接下來就要來分析服務註冊的原理了,要先知道原理,最好的辦法就是分析源碼,首先要知道 nacos 客戶端是什麼時候向服務端去註冊的,其次需要知道服務端是怎麼執行服務註冊的,對於客戶端來說,由於引入了 spring-cloud-starter-alibaba-nacos-discovery 依賴,自然源碼要到這裏去找。PS:源碼分析部分內容較多,望大家理解。

根據 springboot 自動配置原理,很容易可以想到, 要到 spring.factories 文件下去找相關的 AutoConfiguration 配置類,果不其然,我們找到了 NacosDiscoveryAutoConfiguration 這個配置類

從圖中我們看到這個配置類中有三個方法上有 @Bean 註解,熟悉 Spring 的小夥伴們都知道,@Bean 註解表示產生一個 Bean 對象,然後這個 Bean 對象交給 Spring 管理,同時我們又看到最後一個 nacosAutoServiceRegistration 這個方法中的參數裏有上面兩個方法產生的對象,說明上面兩個方法的實現不是很重要,直接來看 nacosAutoServiceRegistration 的實現。

public NacosAutoServiceRegistration(ServiceRegistry<Registration> serviceRegistry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
//父類先初始化,先看看父類的實現
super(serviceRegistry, autoServiceRegistrationProperties);
this.registration = registration;
}
public abstract class AbstractAutoServiceRegistration<R extends Registration>
implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent>

我們看到父類實現了 ApplicationListener 接口,而實現該接口必須重新其 onApplicationEvent() 方法,我們看到方法中又調用了 bind() 方法

public void bind(WebServerInitializedEvent event) {
ApplicationContext context = event.getApplicationContext();
if (context instanceof ConfigurableWebServerApplicationContext) {
if ("management".equals(
((ConfigurableWebServerApplicationContext) context).getServerNamespace())) {
return;
}
}
//CAS 原子操作
this.port.compareAndSet(0, event.getWebServer().getPort());
this.start();
}
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);
}

}

com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register

public void register(Registration registration) {

if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}

String serviceId = registration.getServiceId();

Instance instance = getNacosInstanceFromRegistration(registration);

try {
// 把 serviceId 和 instance傳入,開始註冊流程
namingService.registerInstance(serviceId, instance);
log.info("nacos registry, {} {}:{} register finished", serviceId,
instance.getIp(), instance.getPort());
}
catch (Exception e) {
log.error("nacos registry, {} register failed...{},", serviceId,
registration.toString(), e);
}
}

com.alibaba.nacos.client.naming.NacosNamingService#registerInstance(java.lang.String, java.lang.String, com.alibaba.nacos.api.naming.pojo.Instance)

public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//如果實例是臨時的,組裝beatInfo,發送心跳請求,因爲默認是臨時實例,所以肯定走到這段代碼
//爲什麼要發送心跳請求,因爲nacos在此時實現的是cap理論中的ap模式,即不保證強一致性,但會保證可用性,這就需要做到動態感知服務的上下線,所以要通過心跳來判斷服務是否還正常在線
if (instance.isEphemeral()) {
BeatInfo beatInfo = new BeatInfo();
beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
beatInfo.setIp(instance.getIp());
beatInfo.setPort(instance.getPort());
beatInfo.setCluster(instance.getClusterName());
beatInfo.setWeight(instance.getWeight());
beatInfo.setMetadata(instance.getMetadata());
beatInfo.setScheduled(false);
long instanceInterval = instance.getInstanceHeartBeatInterval();
beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);
//添加心跳信息進行處理
beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
//服務代理類註冊實例
serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}
  • 心跳請求部分

com.alibaba.nacos.client.naming.beat.BeatReactor#addBeatInfo

public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
dom2Beat.put(buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort()), beatInfo);
//定時任務發送心跳請求
executorService.schedule(new BeatTask(beatInfo), 0, TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}

com.alibaba.nacos.client.naming.beat.BeatReactor.BeatTask#run

public void run() {
if (beatInfo.isStopped()) {
return;
}
//服務代理類發送心跳,心跳時長爲5秒鐘,也就是每5秒發送一次心跳請求
long result = serverProxy.sendBeat(beatInfo);
long nextTime = result > 0 ? result : beatInfo.getPeriod();
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}

com.alibaba.nacos.client.naming.net.NamingProxy#sendBeat

public long sendBeat(BeatInfo beatInfo) {
try {
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
}
Map<String, String> params = new HashMap<String, String>(4);
params.put("beat", JSON.toJSONString(beatInfo));
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
//通過http的put請求,向服務端發送心跳請求,請求路徑爲/v1/ns/instance/beat
String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat", params, HttpMethod.PUT);
JSONObject jsonObject = JSON.parseObject(result);

if (jsonObject != null) {
return jsonObject.getLong("clientBeatInterval");
}
} catch (Exception e) {
NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: " + JSON.toJSONString(beatInfo), e);
}
return 0L;
}

看到這裏我們瞭解到,客戶端是通過 http 請求的方式,和服務端進行通信,由於還涉及註冊實例的源碼和服務端的源碼,心跳部分暫時告一段落,接下來回到服務代理類註冊實例

com.alibaba.nacos.client.naming.net.NamingProxy#registerService

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>(9);
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());
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", JSON.toJSONString(instance.getMetadata()));
//發送註冊實例請求,路徑爲/v1/ns/instance
reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, HttpMethod.POST);

}

到這裏,nacos 客戶端部分的源碼就分析完了,接下來開始分析 nacos 服務端的源碼

首先去到 nacos 的官方 github,clone 一份源碼到本地,選擇1.1.4版本,通過 maven 編譯後打開,源碼結構如下

之前我們在分析客戶端源碼的時候看到客戶端向服務端發送請求的部分都在 NacosNamingService 類中,相對應的在服務端應該也是到naming 的模塊下尋找代碼的入口,根據 springboot 開發接口的習慣,接口都是通過 controller 來實現調用的,所以直接找到controllers目錄,其中在 InstanceController 中,我們看到了 register() 的方法,也看到了 beat() 方法,接上面的 客戶端心跳發送部分,先分析 beat() 方法。

public JSONObject beat(HttpServletRequest request) throws Exception {

JSONObject result = new JSONObject();

result.put("clientBeatInterval", switchDomain.getClientBeatInterval());
String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID,
Constants.DEFAULT_NAMESPACE_ID);
String beat = WebUtils.required(request, "beat");
RsInfo clientBeat = JSON.parseObject(beat, RsInfo.class);

if (!switchDomain.isDefaultInstanceEphemeral() && !clientBeat.isEphemeral()) {
return result;
}

if (StringUtils.isBlank(clientBeat.getCluster())) {
clientBeat.setCluster(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
}

String clusterName = clientBeat.getCluster();

if (Loggers.SRV_LOG.isDebugEnabled()) {
Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}", clientBeat, serviceName);
}

Instance instance = serviceManager.getInstance(namespaceId, serviceName, clientBeat.getCluster(),
clientBeat.getIp(),
clientBeat.getPort());

if (instance == null) {
//如果實例爲空,組裝實例
instance = new Instance();
instance.setPort(clientBeat.getPort());
instance.setIp(clientBeat.getIp());
instance.setWeight(clientBeat.getWeight());
instance.setMetadata(clientBeat.getMetadata());
instance.setClusterName(clusterName);
instance.setServiceName(serviceName);
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(clientBeat.isEphemeral());
//註冊實例
//爲什麼要在心跳方法中再次註冊實例?因爲客戶端下線或停機重啓之後原先註冊表中的內容就沒有了,所以需要定時發送心跳的時候再註冊一次
serviceManager.registerInstance(namespaceId, serviceName, instance);
}

Service service = serviceManager.getService(namespaceId, serviceName);

if (service == null) {
throw new NacosException(NacosException.SERVER_ERROR,
"service not found: " + serviceName + "@" + namespaceId);
}
//處理客戶端心跳,這裏就不再展開閱讀了
service.processClientBeat(clientBeat);
result.put("clientBeatInterval", instance.getInstanceHeartBeatInterval());
return result;
}

分析完了 beat() 方法,接下來我們就開始分析最重要的 register() 方法

public String register(HttpServletRequest request) throws Exception {

String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
//註冊實例
serviceManager.registerInstance(namespaceId, serviceName, parseInstance(request));
return "ok";
}
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
//創建空的service
createEmptyService(namespaceId, serviceName, instance.isEphemeral());

Service service = getService(namespaceId, serviceName);

if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
//添加實例
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}

com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent

public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster) throws NacosException {
Service service = getService(namespaceId, serviceName);
if (service == null) {

Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
service.setGroupName(NamingUtils.getGroupName(serviceName));
// now validate the service. if failed, exception will be thrown
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
if (cluster != null) {
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
service.validate();
//放入service及初始化
putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
}

com.alibaba.nacos.naming.core.ServiceManager#putServiceAndInit

private void putServiceAndInit(Service service) throws NacosException {
//放入sevice
putService(service);
//service初始化
service.init();
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJSON());
}
public void putService(Service service) {
if (!serviceMap.containsKey(service.getNamespaceId())) {
synchronized (putServiceLock) {
if (!serviceMap.containsKey(service.getNamespaceId())) {
//serviceMap,就是nacos存放服務的內存註冊表
//service的結構爲Map<namespace, Map<group::serviceName, Service>>,其中,第一層map的key爲命名空間,第二層map的key爲service所在的組名加service名,對應之前的nacos服務註冊表結構圖
serviceMap.put(service.getNamespaceId(), new ConcurrentHashMap<>(16));
}
}
}
serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
}
public void init() {
//服務健康檢查定時任務,定時輪詢檢查服務是否還存在,不是特別重要,不做更多的深入分析
HealthCheckReactor.scheduleCheck(clientBeatCheckTask);

for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
entry.getValue().setService(this);
entry.getValue().init();
}
}

回到之前的添加實例的地方

com.alibaba.nacos.naming.core.ServiceManager#addInstance

public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) throws NacosException {

String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);

Service service = getService(namespaceId, serviceName);

synchronized (service) {
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);

Instances instances = new Instances();
instances.setInstanceList(instanceList);
//把instance放到一個一致性service中,此時方法這裏是個接口,有很多實現,找到定義consistencyService的地方發現存在@Resource(name = "consistencyDelegate")註解,所以應該去找delegate這個實現
consistencyService.put(key, instances);
}
}

com.alibaba.nacos.naming.consistency.DelegateConsistencyServiceImpl#put

public void put(String key, Record value) throws NacosException {
//一致性service的map,在這裏我們同樣發現有好幾個實現,還是通過尋找定義的地方,發現實現應爲DistroConsistencyServiceImpl這個實現
mapConsistencyService(key).put(key, value);
}

com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#put

public void put(String key, Record value) throws NacosException {
onPut(key, value);
taskDispatcher.addTask(key);
}

com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#onPut

public void onPut(String key, Record value) {

if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
Datum<Instances> datum = new Datum<>();
datum.value = (Instances) value;
datum.key = key;
datum.timestamp.incrementAndGet();
//把實例組裝成datum裝入datastore
dataStore.put(key, datum);
}

if (!listeners.containsKey(key)) {
return;
}
//異步任務,直接去到異步任務的run方法
notifier.addTask(key, ApplyAction.CHANGE);
}

com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl.Notifier#run

public void run() {
Loggers.DISTRO.info("distro notifier started");

while (true) {
try {

Pair pair = tasks.take();

if (pair == null) {
continue;
}

String datumKey = (String) pair.getValue0();
ApplyAction action = (ApplyAction) pair.getValue1();

services.remove(datumKey);

int count = 0;

if (!listeners.containsKey(datumKey)) {
continue;
}

for (RecordListener listener : listeners.get(datumKey)) {

count++;

try {
if (action == ApplyAction.CHANGE) {
//監聽器監聽change事件,也就是監聽實例列表變化事件,在此處實際就是註冊事件
listener.onChange(datumKey, dataStore.get(datumKey).value);
continue;
}

if (action == ApplyAction.DELETE) {
listener.onDelete(datumKey);
continue;
}
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] error while notifying listener of key: {}", datumKey, e);
}
}

if (Loggers.DISTRO.isDebugEnabled()) {
Loggers.DISTRO.debug("[NACOS-DISTRO] datum change notified, key: {}, listener count: {}, action: {}",
datumKey, count, action.name());
}
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
}
}
}

com.alibaba.nacos.naming.core.Service#onChange

public void onChange(String key, Instances value) throws Exception {

Loggers.SRV_LOG.info("[NACOS-RAFT] datum is changed, key: {}, value: {}", key, value);
//遍歷實例列表
for (Instance instance : value.getInstanceList()) {

if (instance == null) {
// Reject this abnormal instance list:
throw new RuntimeException("got null instance " + key);
}

if (instance.getWeight() > 10000.0D) {
instance.setWeight(10000.0D);
}

if (instance.getWeight() < 0.01D && instance.getWeight() > 0.0D) {
instance.setWeight(0.01D);
}
}
//更新所有的實例列表
updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));

recalculateChecksum();
}

com.alibaba.nacos.naming.core.Service#updateIPs

public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
for (String clusterName : clusterMap.keySet()) {
//複製一份實例Map放到ipMap裏,這裏複製一份的原因是利用了copyOnWrite思想,在寫入時寫入到副本中,這樣可以在併發讀時不需要加鎖,提高了併發讀的效率
ipMap.put(clusterName, new ArrayList<>());
}
//遍歷所有實例
for (Instance instance : instances) {
try {
if (instance == null) {
Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
continue;
}

if (StringUtils.isEmpty(instance.getClusterName())) {
instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
}

if (!clusterMap.containsKey(instance.getClusterName())) {
Loggers.SRV_LOG.warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
instance.getClusterName(), instance.toJSON());
Cluster cluster = new Cluster(instance.getClusterName(), this);
cluster.init();
getClusterMap().put(instance.getClusterName(), cluster);
}

List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
if (clusterIPs == null) {
clusterIPs = new LinkedList<>();
ipMap.put(instance.getClusterName(), clusterIPs);
}

clusterIPs.add(instance);
} catch (Exception e) {
Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
}
}

for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
//make every ip mine
List<Instance> entryIPs = entry.getValue();
//更新實例
clusterMap.get(entry.getKey()).updateIPs(entryIPs, ephemeral);
}

setLastModifiedMillis(System.currentTimeMillis());
getPushService().serviceChanged(this);
StringBuilder stringBuilder = new StringBuilder();

for (Instance instance : allIPs()) {
stringBuilder.append(instance.toIPAddr()).append("_").append(instance.isHealthy()).append(",");
}

Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}",
getNamespaceId(), getName(), stringBuilder.toString());

}

com.alibaba.nacos.naming.core.Cluster#updateIPs

這個方法就是最終更新所有實例的方法

public void updateIPs(List<Instance> ips, boolean ephemeral) {

Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;

HashMap<String, Instance> oldIPMap = new HashMap<>(toUpdateInstances.size());

for (Instance ip : toUpdateInstances) {
oldIPMap.put(ip.getDatumKey(), ip);
}

List<Instance> updatedIPs = updatedIPs(ips, oldIPMap.values());
if (updatedIPs.size() > 0) {
for (Instance ip : updatedIPs) {
Instance oldIP = oldIPMap.get(ip.getDatumKey());

// do not update the ip validation status of updated ips
// because the checker has the most precise result
// Only when ip is not marked, don't we update the health status of IP:
if (!ip.isMarked()) {
ip.setHealthy(oldIP.isHealthy());
}

if (ip.isHealthy() != oldIP.isHealthy()) {
// ip validation status updated
Loggers.EVT_LOG.info("{} {SYNC} IP-{} {}:{}@{}",
getService().getName(), (ip.isHealthy() ? "ENABLED" : "DISABLED"), ip.getIp(), ip.getPort(), getName());
}

if (ip.getWeight() != oldIP.getWeight()) {
// ip validation status updated
Loggers.EVT_LOG.info("{} {SYNC} {IP-UPDATED} {}->{}", getService().getName(), oldIP.toString(), ip.toString());
}
}
}

List<Instance> newIPs = subtract(ips, oldIPMap.values());
if (newIPs.size() > 0) {
Loggers.EVT_LOG.info("{} {SYNC} {IP-NEW} cluster: {}, new ips size: {}, content: {}",
getService().getName(), getName(), newIPs.size(), newIPs.toString());

for (Instance ip : newIPs) {
HealthCheckStatus.reset(ip);
}
}

List<Instance> deadIPs = subtract(oldIPMap.values(), ips);

if (deadIPs.size() > 0) {
Loggers.EVT_LOG.info("{} {SYNC} {IP-DEAD} cluster: {}, dead ips size: {}, content: {}",
getService().getName(), getName(), deadIPs.size(), deadIPs.toString());

for (Instance ip : deadIPs) {
HealthCheckStatus.remv(ip);
}
}

toUpdateInstances = new HashSet<>(ips);

if (ephemeral) {
//把臨時實例更新到cluster的ephemeralInstances上,服務發現最終查找到的實例就是這個屬性
ephemeralInstances = toUpdateInstances;
} else {
persistentInstances = toUpdateInstances;
}
}

講到這裏,實際上的 nacos 註冊中心服務註冊的流程就結束了,不過剛纔我們的流程是在 com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#put 中的 onPut 方法中一直往下走,在 onPut 方法之後還有一個 taskDispatcher.addTask(key) 方法,這個方法是用來做什麼的呢,這個方法的作用就是執行集羣同步的任務,之前我們說過 nacos 的臨時實例實現的是 cap 理論中的 ap 模式,也就是要保證可用性,所以在集羣架構中一定會存在一個定時同步的機制,接下來我們就來看看這個同步的方法。

com.alibaba.nacos.naming.consistency.ephemeral.distro.TaskDispatcher#addTask

public void addTask(String key) {
//往定時任務列表中添加任務
taskSchedulerList.get(UtilsAndCommons.shakeUp(key, cpuCoreCount)).addTask(key);
}

我們跳過添加任務的部分,直接來看定時任務的 run 方法

com.alibaba.nacos.naming.consistency.ephemeral.distro.TaskDispatcher.TaskScheduler#run

public void run() {

List<String> keys = new ArrayList<>();
while (true) {

try {

String key = queue.poll(partitionConfig.getTaskDispatchPeriod(),
TimeUnit.MILLISECONDS);

if (Loggers.DISTRO.isDebugEnabled() && StringUtils.isNotBlank(key)) {
Loggers.DISTRO.debug("got key: {}", key);
}

if (dataSyncer.getServers() == null || dataSyncer.getServers().isEmpty()) {
continue;
}

if (StringUtils.isBlank(key)) {
continue;
}

if (dataSize == 0) {
keys = new ArrayList<>();
}

keys.add(key);
//數據的數量+1
dataSize++;
//如果數據數量達到了批量同步的閾值或者距離上次同步時間超過了2000ms,提交一次同步任務
if (dataSize == partitionConfig.getBatchSyncKeyCount() ||
(System.currentTimeMillis() - lastDispatchTime) > partitionConfig.getTaskDispatchPeriod()) {

for (Server member : dataSyncer.getServers()) {
if (NetUtils.localServer().equals(member.getKey())) {
continue;
}
SyncTask syncTask = new SyncTask();
syncTask.setKeys(keys);
syncTask.setTargetServer(member.getKey());

if (Loggers.DISTRO.isDebugEnabled() && StringUtils.isNotBlank(key)) {
Loggers.DISTRO.debug("add sync task: {}", JSON.toJSONString(syncTask));
}

dataSyncer.submit(syncTask, 0);
}
lastDispatchTime = System.currentTimeMillis();
dataSize = 0;
}

} catch (Exception e) {
Loggers.DISTRO.error("dispatch sync task failed.", e);
}
}
}

com.alibaba.nacos.naming.consistency.ephemeral.distro.DataSyncer#submit

public void submit(SyncTask task, long delay) {

// If it's a new task:
if (task.getRetryCount() == 0) {
Iterator<String> iterator = task.getKeys().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
if (StringUtils.isNotBlank(taskMap.putIfAbsent(buildKey(key, task.getTargetServer()), key))) {
// associated key already exist:
if (Loggers.DISTRO.isDebugEnabled()) {
Loggers.DISTRO.debug("sync already in process, key: {}", key);
}
iterator.remove();
}
}
}

if (task.getKeys().isEmpty()) {
// all keys are removed:
return;
}
//執行器執行異步任務
GlobalExecutor.submitDataSync(() -> {
// 1. check the server
if (getServers() == null || getServers().isEmpty()) {
Loggers.SRV_LOG.warn("try to sync data but server list is empty.");
return;
}

List<String> keys = task.getKeys();

if (Loggers.SRV_LOG.isDebugEnabled()) {
Loggers.SRV_LOG.debug("try to sync data for this keys {}.", keys);
}
// 2. get the datums by keys and check the datum is empty or not
Map<String, Datum> datumMap = dataStore.batchGet(keys);
if (datumMap == null || datumMap.isEmpty()) {
// clear all flags of this task:
for (String key : keys) {
taskMap.remove(buildKey(key, task.getTargetServer()));
}
return;
}

byte[] data = serializer.serialize(datumMap);

long timestamp = System.currentTimeMillis();
//批量同步數據
boolean success = NamingProxy.syncData(data, task.getTargetServer());
if (!success) {
SyncTask syncTask = new SyncTask();
syncTask.setKeys(task.getKeys());
syncTask.setRetryCount(task.getRetryCount() + 1);
syncTask.setLastExecuteTime(timestamp);
syncTask.setTargetServer(task.getTargetServer());
retrySync(syncTask);
} else {
// clear all flags of this task:
for (String key : task.getKeys()) {
taskMap.remove(buildKey(key, task.getTargetServer()));
}
}
}, delay);
}

com.alibaba.nacos.naming.misc.NamingProxy#syncData

public static boolean syncData(byte[] data, String curServer) {
Map<String, String> headers = new HashMap<>(128);

headers.put(HttpHeaderConsts.CLIENT_VERSION_HEADER, VersionUtils.VERSION);
headers.put(HttpHeaderConsts.USER_AGENT_HEADER, UtilsAndCommons.SERVER_VERSION);
headers.put("Accept-Encoding", "gzip,deflate,sdch");
headers.put("Connection", "Keep-Alive");
headers.put("Content-Encoding", "gzip");

try {
//發送http請求同步數據,請求路徑爲/v1/ns/distro/datum,涉及到同步的內容本文不做深入的詳解
HttpClient.HttpResult result = HttpClient.httpPutLarge("http://" + curServer + RunningConfig.getContextPath()
+ UtilsAndCommons.NACOS_NAMING_CONTEXT + DATA_ON_SYNC_URL, headers, data);
if (HttpURLConnection.HTTP_OK == result.code) {
return true;
}
if (HttpURLConnection.HTTP_NOT_MODIFIED == result.code) {
return true;
}
throw new IOException("failed to req API:" + "http://" + curServer
+ RunningConfig.getContextPath()
+ UtilsAndCommons.NACOS_NAMING_CONTEXT + DATA_ON_SYNC_URL + ". code:"
+ result.code + " msg: " + result.content);
} catch (Exception e) {
Loggers.SRV_LOG.warn("NamingProxy", e);
}
return false;
}

到這裏,整個 nacos 註冊中心的服務註冊流程就已經分析完了,我畫了一張註冊流程的結構圖給大家


五、總結

通過上述的源碼閱讀與分析,我們詳細的瞭解了 nacos 註冊中心服務註冊的流程原理,看到了服務註冊功能需要有心跳任務和健康檢查任務,集羣同步任務,可以看到這些任務都被設計成了定時的異步任務,這樣做的好處在於可以保證不會在執行這些任務的時候引起不必要的阻塞,提升了系統的性能,而且在將服務添加進內存註冊表的時候還設計成了 copyOnWrite 的方式,保證了在讀多寫少的場景下整個註冊中心的併發性能。

本人文筆水平有限,本文如有錯誤或不足,還請大家多多指正,謝謝大家。

原文地址:  https://juejin.cn/post/6950867039284101151

本文分享自微信公衆號 - JAVA高級架構(gaojijiagou)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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