Nacos 2.0源码分析-Distro协议详解

温馨提示:
本文内容基于个人学习Nacos 2.0.1版本代码总结而来,因个人理解差异,不保证完全正确。如有理解错误之处欢迎各位拍砖指正,相互学习;转载请注明出处。

《Distro协议概览》这篇文章内简要的从全局角度来分析了Distro协议的整体面貌。若还未阅读过的同学可以去看一下,大脑中对相关的组件有个映像。在DistroProtocol章节介绍了Distro协议是从这里开始的,本文将围绕这个入口展开分析。

Distro协议的初始任务

DistroProtocol章节介绍道,从构造方法中它启动了一个startDistroTask()任务,内部又分为验证任务和同步任务。

private void startDistroTask() {
	if (EnvUtil.getStandaloneMode()) {
		isInitialized = true;
		return;
	}
	startVerifyTask();
	startLoadTask();
}

在真正开始分析任务的具体操作之前,请允许我先介绍【DistroProtocol】内部的一些属性:

/**
 * 节点管理器
 */
private final ServerMemberManager memberManager;

/**
 * Distro组件持有者
 */
private final DistroComponentHolder distroComponentHolder;

/**
 * Distro任务引擎持有者
 */
private final DistroTaskEngineHolder distroTaskEngineHolder;

其中【DistroComponentHolder】【DistroTaskEngineHolder】的基本概念已经在《Distro协议概览》一文中介绍过,请点击名称跳转查看。
此处介绍一下他们的初始化。

初始化Distro协议的组件

DistroHttpRegistry 是v1版本中的组件,这里介绍v1相关内容是想让各位读者直观的对比一下v1和v2的巨大差异。因为此篇文章先于《Distro协议概览》完成,并且当初学习的时候并未注意即便使用了2.0.1版本的代码,它还是默认开启了对v1的兼容。因此花了大量时间研究的却是v1相关的操作。不过通过对v1和v2的相关处理的分析可以更加明确v2的提升点。

DistroHttpRegistry

v1版本的组件注册器

package com.alibaba.nacos.naming.consistency.ephemeral.distro;

@Component
public class DistroHttpRegistry {

	// 一些Distro组件的集合
    private final DistroComponentHolder componentHolder;
	// 一些任务执行引擎的集合
    private final DistroTaskEngineHolder taskEngineHolder;
	// Distro协议http请求方式的数据对象
    private final DataStore dataStore;
	// 协议映射器
    private final DistroMapper distroMapper;
	// 一些全局配置
    private final GlobalConfig globalConfig;
	// Distro 一致性协议服务
    private final DistroConsistencyServiceImpl consistencyService;
	// Nacos节点管理器
    private final ServerMemberManager memberManager;

    public DistroHttpRegistry(DistroComponentHolder componentHolder, DistroTaskEngineHolder taskEngineHolder,
            DataStore dataStore, DistroMapper distroMapper, GlobalConfig globalConfig,
            DistroConsistencyServiceImpl consistencyService, ServerMemberManager memberManager) {
        this.componentHolder = componentHolder;
        this.taskEngineHolder = taskEngineHolder;
        this.dataStore = dataStore;
        this.distroMapper = distroMapper;
        this.globalConfig = globalConfig;
        this.consistencyService = consistencyService;
        this.memberManager = memberManager;
    }

    /**
     * Register necessary component to distro protocol for HTTP implement.
     */
    @PostConstruct
    public void doRegister() {
		// 注册com.alibaba.nacos.naming.iplist.类型数据的数据仓库实现
        componentHolder.registerDataStorage(KeyBuilder.INSTANCE_LIST_KEY_PREFIX, new DistroDataStorageImpl(dataStore, distroMapper));
		// 注册com.alibaba.nacos.naming.iplist.类型数据的数据传输代理对象实现
        componentHolder.registerTransportAgent(KeyBuilder.INSTANCE_LIST_KEY_PREFIX, new DistroHttpAgent(memberManager));
		// 注册com.alibaba.nacos.naming.iplist.类型的失败任务处理器
        componentHolder.registerFailedTaskHandler(KeyBuilder.INSTANCE_LIST_KEY_PREFIX, new DistroHttpCombinedKeyTaskFailedHandler(taskEngineHolder));
		// 注册com.alibaba.nacos.naming.iplist.类型的任务处理器
        taskEngineHolder.registerNacosTaskProcessor(KeyBuilder.INSTANCE_LIST_KEY_PREFIX, new DistroHttpDelayTaskProcessor(globalConfig, taskEngineHolder));
		// 注册com.alibaba.nacos.naming.iplist.类型的DistroData数据处理器
        componentHolder.registerDataProcessor(consistencyService);
    }
}

DistroClientComponentRegistry

v2版本的组件注册器,请对比v1的异同。

package com.alibaba.nacos.naming.consistency.ephemeral.distro.v2;

@Component
public class DistroClientComponentRegistry {
    
	// Nacos节点管理器
    private final ServerMemberManager serverMemberManager;
    
	// Distro协议对象
    private final DistroProtocol distroProtocol;
    
	// 一些Distro组件的集合
    private final DistroComponentHolder componentHolder;
	
    // 一些任务执行引擎的集合
    private final DistroTaskEngineHolder taskEngineHolder;
    
	// Nacos 客户端管理器
    private final ClientManager clientManager;
	
    // 集群rpc客户端代理对象
    private final ClusterRpcClientProxy clusterRpcClientProxy;
	
    // 版本升级判断
    private final UpgradeJudgement upgradeJudgement;
    
    public DistroClientComponentRegistry(ServerMemberManager serverMemberManager, DistroProtocol distroProtocol,
            DistroComponentHolder componentHolder, DistroTaskEngineHolder taskEngineHolder,
            ClientManagerDelegate clientManager, ClusterRpcClientProxy clusterRpcClientProxy,
            UpgradeJudgement upgradeJudgement) {
        this.serverMemberManager = serverMemberManager;
        this.distroProtocol = distroProtocol;
        this.componentHolder = componentHolder;
        this.taskEngineHolder = taskEngineHolder;
        this.clientManager = clientManager;
        this.clusterRpcClientProxy = clusterRpcClientProxy;
        this.upgradeJudgement = upgradeJudgement;
    }
    
    /**
     * Register necessary component to distro protocol for v2 {@link com.alibaba.nacos.naming.core.v2.client.Client}
     * implement.
     */
    @PostConstruct
    public void doRegister() {
        DistroClientDataProcessor dataProcessor = new DistroClientDataProcessor(clientManager, distroProtocol, upgradeJudgement);
        DistroTransportAgent transportAgent = new DistroClientTransportAgent(clusterRpcClientProxy, serverMemberManager);
        DistroClientTaskFailedHandler taskFailedHandler = new DistroClientTaskFailedHandler(taskEngineHolder);
		// 注册Nacos:Naming:v2:ClientData类型数据的数据仓库实现
        componentHolder.registerDataStorage(DistroClientDataProcessor.TYPE, dataProcessor);
		// 注册Nacos:Naming:v2:ClientData类型的DistroData数据处理器
        componentHolder.registerDataProcessor(dataProcessor);
		// 注册Nacos:Naming:v2:ClientData类型数据的数据传输代理对象实现
        componentHolder.registerTransportAgent(DistroClientDataProcessor.TYPE, transportAgent);
		// 注册Nacos:Naming:v2:ClientData类型的失败任务处理器
        componentHolder.registerFailedTaskHandler(DistroClientDataProcessor.TYPE, taskFailedHandler);
    }
}

DistroClientComponentRegistry注册了一些Nacos:Naming:v2:ClientData类型的操作对象。他们的功能和上一节的DistroHttpRegistry一样。区别就是实现类不同。根据DistroComponentHolder注册的内容来看,它所操作的数据和《Distro协议概览》中介绍的【Distro协议重要角色】刚好对应。它明确了v2版本中数据保存在哪里,由什么数据处理器来处理,用什么DistroTransportAgent来发送。同时请注意这几个注册方法在v1和v2版本注册时传递的key,在v1中它是com.alibaba.nacos.naming.iplist.,v2中它是Nacos:Naming:v2:ClientData,后面你会经常看见他们的。

节点数据验证

现在正式开始分析DistroProtocol在构造方法中启动的任务。

验证的执行流程

前面说道有v1和v2的实现,即便在2.0.*版本中也依然会兼容v1的逻辑,除非你关闭了这个兼容性。从这点来看,接下来的代码结构的良好设计为升级也提供了很好的基础。本节内容主要分析的就是面向接口编程

private void startVerifyTask() {
	
	GlobalExecutor.schedulePartitionDataTimedSync(
		new DistroVerifyTimedTask(memberManager, distroComponentHolder, distroTaskEngineHolder.getExecuteWorkersManager()), 
		DistroConfig.getInstance().getVerifyIntervalMillis()
	);
}

验证功能从startVerifyTask()方法开始启动,此处它构建了一个名为DistroVerifyTimedTask的定时任务,延迟5秒开始,间隔5秒轮询。

// DistroVerifyTimedTask.java

@Override
public void run() {
	try {
		// 获取除自身节点之外的其他节点
		List<Member> targetServer = serverMemberManager.allMembersWithoutSelf();
		if (Loggers.DISTRO.isDebugEnabled()) {
			Loggers.DISTRO.debug("server list is: {}", targetServer);
		}
		// 每一种类型的数据,都要向其他节点发起验证
		for (String each : distroComponentHolder.getDataStorageTypes()) {
			// 对dataStorage内的数据进行验证
			verifyForDataStorage(each, targetServer);
		}
	} catch (Exception e) {
		Loggers.DISTRO.error("[DISTRO-FAILED] verify task failed.", e);
	}
}

private void verifyForDataStorage(String type, List<Member> targetServer) {
	// 获取数据类型
	DistroDataStorage dataStorage = distroComponentHolder.findDataStorage(type);
	// 若数据还未同步完毕则不处理
	if (!dataStorage.isFinishInitial()) {
		Loggers.DISTRO.warn("data storage {} has not finished initial step, do not send verify data", dataStorage.getClass().getSimpleName());
		return;
	}
	// ① 获取验证数据
	List<DistroData> verifyData = dataStorage.getVerifyData();
	if (null == verifyData || verifyData.isEmpty()) {
		return;
	}
	// 对每个节点开启一个异步的线程来执行
	for (Member member : targetServer) {
		DistroTransportAgent agent = distroComponentHolder.findTransportAgent(type);
		if (null == agent) {
			continue;
		}
		executeTaskExecuteEngine.addTask(member.getAddress() + type, new DistroVerifyExecuteTask(agent, verifyData, member.getAddress(), type));
	}
}

DistroVerifyTimedTask任务中,对每一个节点的所有验证数据都创建了一个新的任务 DistroVerifyExecuteTask,由它来执行具体的验证工作。

重点:
被验证的数据集合从接口DistroDataStorage中被获取,并被传入DistroVerifyExecuteTask任务中。

// DistroVerifyExecuteTask.java 

@Override
public void run() {
	for (DistroData each : verifyData) {
		try {
			// 判断传输对象是否支持回调(若是http的则不支持,实际上没区别,当前2.0.1版本没有实现回调的实质内容)
			if (transportAgent.supportCallbackTransport()) {
				doSyncVerifyDataWithCallback(each);
			} else {
				doSyncVerifyData(each);
			}
		} catch (Exception e) {
			Loggers.DISTRO
					.error("[DISTRO-FAILED] verify data for type {} to {} failed.", resourceType, targetServer, e);
		}
	}
}

/**
 * 支持回调的同步数据验证
 * @param data
 */
private void doSyncVerifyDataWithCallback(DistroData data) {
	// 回调实际上,也没啥。。。基本算是空对象
	transportAgent.syncVerifyData(data, targetServer, new DistroVerifyCallback());
}

/**
 * 不支持回调的同步数据验证
 * @param data
 */
private void doSyncVerifyData(DistroData data) {
	transportAgent.syncVerifyData(data, targetServer);
}

每一个DistroVerifyExecuteTask任务都持有一组验证数据List<DistroData>和数据发送的目的地ServertargetServer,它将使用DistroTransportAgent为每一个DistroData执行一次验证操作。这里仅仅描述它的执行流程,不对细节作过多描述。

数据验证流程:
从DistroDataStorage获取验证数据 -> 使用DistroTransportAgent开始验证。
请注意他们都是接口,对应不同类型的数据将会有不同的实现。

通过下图可以比较直观的看到验证任务的创建流程。

Distro协议验证数据任务流程图

验证流程中的任务产生说明:

  • 当前节点的DistroVerifyTimedTask 会根据节点的数量来创建DistroVerifyExecuteTask,并向其传递自身负责的所有Client的clientId集合(clientId最终被包装成DistroData)。
  • 每一个DistroVerifyExecuteTask 会为传入的List中的每一个DistroData创建一个异步的rpc请求。

疑问:
这里是将为本节点中的所有Client单独都创建一个rpc请求,为何不一次性将所有client发送出去?难道是为了性能考量?

小技巧:
当你看到红色部分的Task它是被放入一个阻塞队列里面的时候,你就应该要想到它一定会有一个从队列不断获取任务来执行的操作。这样在你要分析这些产生的任务如何执行的时候就变的相当容易了

验证数据的类型

前面我们知道Distro协议数据交互的对象是DistroData。其内部的content保存的数据才是真正的需要验证的对象。在v1版本中的DistroData.content保存的是序列化后的Map<String, String>, key为serviceName,value为Service下的所有Instance的checksum值;v2版本中的DistroData.content保存的是序列化后的DistroClientVerifyInfo

public class DistroClientVerifyInfo implements Serializable {
    
    private static final long serialVersionUID = 2223964944788737629L;
    // 客户端ID
    private String clientId;
    // 修订版本号,验证时固定为0
    private long revision;
    
    // ...省略getter/setter
}

执行验证任务

相信通过前面的章节你已经知道验证任务会处理不同的数据类型,而不同的数据类型的处理方式也不相同。这里假定你已经充分理解不同类型数据的处理流程和实际的处理对象。本节的分析将直接使用对应的处理方法而不再重复它属于哪个具体的实例(这里的实例指的是接口的实例并非业务中的那个Instance)。

注意:
当你使用2.0.X版本的代码,并且关闭了双写服务,就表示你的Nacos集群会彻底使用2.0版本的特性,它将不再兼容1.版本,同时1.版本中的检查逻辑将不再被执行,也就是com.alibaba.nacos.naming.iplist.*类型的数据检查将不被执行。取而代之的是Nacos:Naming:v2:ClientData

v1版本的数据验证

在v1版本中数据验证是验证com.alibaba.nacos.naming.iplist.*类型, 验证主要目的是在v1版本中向其他节点发送checksum请求。用于报告自身节点内部的服务列表状态。发送方不用关注,只管将节点内的服务信息发送出去即可。但当前节点也会接收到其他节点发送来的checksum请求。

从DistroDataStorage获取验证数据
// DistroDataStorageImpl.java

@Override
public List<DistroData> getVerifyData() {
	// 用于保存属于当前节点处理的,且数据真实有效的Service
	Map<String, String> keyChecksums = new HashMap<>(64);
	// 遍历当前节点已有的datastore
	for (String key : dataStore.keys()) {
		// 若当前的服务不是本机处理,则排除
		if (!distroMapper.responsible(KeyBuilder.getServiceName(key))) {
			continue;
		}
		// 若当key对应的数据为空,则排除
		Datum datum = dataStore.get(key);
		if (datum == null) {
			continue;
		}
		keyChecksums.put(key, datum.value.getChecksum());
	}
	if (keyChecksums.isEmpty()) {
		return Collections.emptyList();
	}
	// 构建DistroData
	DistroKey distroKey = new DistroKey("checksum", KeyBuilder.INSTANCE_LIST_KEY_PREFIX);
	DistroData data = new DistroData(distroKey, ApplicationUtils.getBean(Serializer.class).serialize(keyChecksums));
	// 设置当前操作类型为Verify
	data.setType(DataOperation.VERIFY);
	return Collections.singletonList(data);
}

注意:
keyChecksums内部存储的是每个服务所对应实例列表的checksum验证字符串

使用DistroTransportAgent发送验证数据
// DistroHttpAgent.java

@Override
public boolean syncVerifyData(DistroData verifyData, String targetServer) {
	// 若本机节点缓存中没有targetServer,说明此节点已不具备服务能力,也没有报告的必要。
	if (!memberManager.hasMember(targetServer)) {
		return true;
	}
	// 发送checksum请求
	NamingProxy.syncCheckSums(verifyData.getContent(), targetServer);
	return true;
}

// NamingProxy.java

/**
 * Synchronize check sums.
 *
 * @param checksums checksum map bytes
 * @param server    server address
 */
public static void syncCheckSums(byte[] checksums, String server) {
	try {
		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(HttpHeaderConsts.CONNECTION, "Keep-Alive");
		// 请求示例:http://10.53.126.16:8848/nacos/v1/ns/distro/checksum?source=10.53.155.22:8848
		HttpClient.asyncHttpPutLarge("http://" + server + EnvUtil.getContextPath() + UtilsAndCommons.NACOS_NAMING_CONTEXT + TIMESTAMP_SYNC_URL + "?source=" + NetUtils.localServer(), headers, checksums,
				new Callback<String>() {
					@Override
					public void onReceive(RestResult<String> result) {
						if (!result.ok()) {
							Loggers.DISTRO.error("failed to req API: {}, code: {}, msg: {}", "http://" + server + EnvUtil.getContextPath() + UtilsAndCommons.NACOS_NAMING_CONTEXT + TIMESTAMP_SYNC_URL, result.getCode(), result.getMessage());
						}
					}
					
					@Override
					public void onError(Throwable throwable) {
						Loggers.DISTRO.error("failed to req API:" + "http://" + server + EnvUtil.getContextPath() + UtilsAndCommons.NACOS_NAMING_CONTEXT + TIMESTAMP_SYNC_URL, throwable);
					}
					
					@Override
					public void onCancel() {
					
					}
				});
	} catch (Exception e) {
		Loggers.DISTRO.warn("NamingProxy", e);
	}
}

提示:
请求路径为:http://其他节点的IP地址:其他节点的端口号/nacos/v1/ns/distro/checksum?source=本机的IP地址:本机的端口号
参数为:DistroData,DistroData内部包装的则是一个Map<服务名称,服务下实例的checksum验证字符串>

处理DistroTransportAgent的验证请求

naming模块下的DistroController用于接收HTTP方式的请求。请注意,此处的验证请求是处理其他节点发送来的请求,写在这里是为了便于理解具体的验证功能。一定要将此小节的内容看做是接收方,因为他们的角色不同,内部缓存的服务数据归属也不同。发送方发送的服务数据是属于在发送方注册的,而此处作为接收方它自己内部也有
自己负责的服务。弄清楚角色的关系才能理解接下来的验证流程。

// DistroController.java

/**
 * Checksum.
 *
 * @param source  source server
 * @param dataMap checksum map
 * @return 'ok'
 */
@PutMapping("/checksum")
public ResponseEntity syncChecksum(@RequestParam String source, @RequestBody Map<String, String> dataMap) {
	// 构建验证数据对象
	DistroHttpData distroHttpData = new DistroHttpData(createDistroKey(source), dataMap);
	// 开始验证
	distroProtocol.onVerify(distroHttpData, source);
	return ResponseEntity.ok("ok");
}

// DistroProtocol.java

/**
 * Receive verify data, find processor to process.
 * @param distroData    verify data
 * @param sourceAddress source server address, might be get data from source server
 * @return true if verify data successfully, otherwise false
 */
public boolean onVerify(DistroData distroData, String sourceAddress) {
	if (Loggers.DISTRO.isDebugEnabled()) {
		Loggers.DISTRO.debug("[DISTRO] Receive verify data type: {}, key: {}", distroData.getType(), distroData.getDistroKey());
	}
	// 根据不同类型获取不同的数据处理器
	String resourceType = distroData.getDistroKey().getResourceType();
	DistroDataProcessor dataProcessor = distroComponentHolder.findDataProcessor(resourceType);
	if (null == dataProcessor) {
		Loggers.DISTRO.warn("[DISTRO] Can't find verify data process for received data {}", resourceType);
		return false;
	}
	return dataProcessor.processVerifyData(distroData, sourceAddress);
}

// DistroConsistencyServiceImpl.java

@Override
public boolean processVerifyData(DistroData distroData, String sourceAddress) {
	DistroHttpData distroHttpData = (DistroHttpData) distroData;
	Map<String, String> verifyData = (Map<String, String>) distroHttpData.getDeserializedContent();
	onReceiveChecksums(verifyData, sourceAddress);
	return true;
}

筛选需要处理的服务

服务筛选的目的可以用7个字总结:个人自扫门前雪。

/**
 * Check sum when receive checksums request.
 * Service检查服务,用于从其他节点更新已变更的Service
 * @param checksumMap map of checksum   某个服务对应的checksum
 * @param server      source server request checksum    checksum请求的来源
 */
public void onReceiveChecksums(Map<String, String> checksumMap, String server) {
	// 若已包含此节点的信息,说明正在处理(处理完毕之后会清空)
	if (syncChecksumTasks.containsKey(server)) {
		// Already in process of this server:
		Loggers.DISTRO.warn("sync checksum task already in process with {}", server);
		return;
	}
	// 将当前要处理的节点信息暂存,表示当前正在处理
	syncChecksumTasks.put(server, "1");

	try {

		List<String> toUpdateKeys = new ArrayList<>();
		List<String> toRemoveKeys = new ArrayList<>();

		// 第一个for循环
		
		/**
		 * 判断当前节点中哪些服务需要去远程更新
		 * 需要更新的服务将被添加至toUpdateKeys
		 */
		for (Map.Entry<String, String> entry : checksumMap.entrySet()) {

			/**
			 * 判断当前的entry(指的是服务Service)是否由本节点处理, 若是本机负责的则没必要从其他节点发送过来
			 * 因为在本节点注册的服务最终任何新的操作都会被路由到本机,那么它的状态在本机就是最新的
			 */
			if (distroMapper.responsible(KeyBuilder.getServiceName(entry.getKey()))) {
				// this key should not be sent from remote server:
				Loggers.DISTRO.error("receive responsible key timestamp of " + entry.getKey() + " from " + server);
				// abort the procedure:
				return;
			}

			// 若当前节点不存在此服务,或服务是空的,或服务的实例列表(checksum验证字符串)跟传入的不一致,则标记此服务需要更新
			if (!dataStore.contains(entry.getKey()) ||
				dataStore.get(entry.getKey()).value == null ||
				!dataStore.get(entry.getKey()).value.getChecksum().equals(entry.getValue())) {
				// 添加到待更新列表
				toUpdateKeys.add(entry.getKey());
			}
		}

		
		// 第二个for循环
		
		/**
		 * 此处用于判断当前节点已有的服务是不是当前接收的请求的来源节点负责处理的,如果是的话,就说明他是最新的不应该被删除
		 */
		for (String key : dataStore.keys()) {
			// 不是请求方处理的服务就不处理
			if (!server.equals(distroMapper.mapSrv(KeyBuilder.getServiceName(key)))) {
				continue;
			}

			// 是请求方处理的服务但不在请求列表中,说明此服务在请求方已经被删除了
			if (!checksumMap.containsKey(key)) {
				toRemoveKeys.add(key);
			}
		}

		Loggers.DISTRO.info("to remove keys: {}, to update keys: {}, source: {}", toRemoveKeys, toUpdateKeys, server);

		// 删掉已经变更的服务
		for (String key : toRemoveKeys) {
			// 移除服务,并发送变更通知
			onRemove(key);
		}

		// 若没有需要更新的终止此流程
		if (toUpdateKeys.isEmpty()) {
			return;
		}

		// 经过上述流程的处理,目前本机缓存的DataStorage中保留的服务是:在其他节点注册的、服务。

		try {
			DistroHttpCombinedKey distroKey = new DistroHttpCombinedKey(KeyBuilder.INSTANCE_LIST_KEY_PREFIX, server);
			distroKey.getActualResourceTypes().addAll(toUpdateKeys);
			// 更新变更了的服务
			DistroData remoteData = distroProtocol.queryFromRemote(distroKey);
			if (null != remoteData) {
				processData(remoteData.getContent());
			}
		} catch (Exception e) {
			Loggers.DISTRO.error("get data from " + server + " failed!", e);
		}
	} finally {
		// 全部处理完毕之后,将syncChecksumTasks中标记正在处理的节点移除
		// Remove this 'in process' flag:
		syncChecksumTasks.remove(server);
	}
}

请各位看官务必详细阅读onReceiveChecksums(Map<String, String> checksumMap, String server)方法内的注释。当前的方法主要处理服务的3种场景:不需要处理的、需要更新的、需要删除的。

checksum的请求发送方发送的是它自身节点负责的数据,那么接收方接收到的也都不是自己负责的数据。因此在onReciveChecksums方法的第一个for循环中就对数据进行筛选,避免本机负责的数据由其他节点误发过来(这种可能性也有可能存在,那就是当本机暂时不可用,某服务本来是本机负责,又被转移到其他节点的时候,此观点暂未实际测试),这里处理的是不需要处理的数据。

接着在第一个for循环内部的第二个if语句中判断传递过来的服务信息是否存在,是否为空,是否和传递过来的不一致。若条件成立则将其放入需要更新toUPdateKeys集合内。第二个for循环则处理的是需要删除的服务,这里是处理本机内部缓存的服务,首先判断本机内的哪些服务不是由传递请求的这个节点处理的,对于这类服务忽略掉,因为它所属的节点也会来发送checksum请求,放在那次处理即可。若是传递请求的节点处理的但又没有在这次请求中带过来则需要标记删除,接着对标记删除的服务进行移除。

服务筛选示意图:

  • 假设现在有3个节点,A、B、C他们以不同颜色区分,每个节点内部DataStore缓存的服务列表假设都是一致每个节点的服务名称和颜色都与节点相关联。
  • 这里虚拟了一个服务的名称,用于描述它所属的节点、状态、以及它的实例数量以#分隔。实例数量用于表示某个服务在某一时刻的状态。
  • 每个节点内部保留着本节点的服务和其他节点的服务,但每个节点只保证自己的服务是最新的(和节点颜色相同的服务)。
  • 在发送checksum请求的时候,以服务名称作为key。

Checksum的筛选

上图表示了节点A向节点B发送checksum请求的时候所带参数和接收方的筛选结果

  • 节点A请求的时候携带了自身负责处理的红色服务,有多少发多少。
  • 节点B接收到请求之后发现节点A的3、4、5服务实例数量有变更,并且A节点传递过来的6服务节点B中没有,因此将3、4、5、6服务添加到toUpdateKeys列表中稍后用于更新
  • 节点B同时发现节点A此次没有携带2服务,说明2服务在节点A已经被删除了,因此将其添加到toRemoveKeys列表中稍后要从本机的DataStore中移除
  • 节点B中保存的C节点的服务,不管他,因为它们需要等待节点C向节点B发送checksum。

更新已变更服务

在对传入的服务进行筛选之后就开始更新。

// 组装DistroData查询对象
DistroHttpCombinedKey distroKey = new DistroHttpCombinedKey(KeyBuilder.INSTANCE_LIST_KEY_PREFIX, server);
distroKey.getActualResourceTypes().addAll(toUpdateKeys);
// 从请求发送方获取最新数据
DistroData remoteData = distroProtocol.queryFromRemote(distroKey);
if (null != remoteData) {
	// 处理查询结果
	processData(remoteData.getContent());
}

// DistroProtocol.java

/**
 * Query data from specified server.
 * 从指定的节点获取DistroData
 * @param distroKey data key
 * @return data
 */
public DistroData queryFromRemote(DistroKey distroKey) {
	if (null == distroKey.getTargetServer()) {
		Loggers.DISTRO.warn("[DISTRO] Can't query data from empty server");
		return null;
	}
	String resourceType = distroKey.getResourceType();
	DistroTransportAgent transportAgent = distroComponentHolder.findTransportAgent(resourceType);
	if (null == transportAgent) {
		Loggers.DISTRO.warn("[DISTRO] Can't find transport agent for key {}", resourceType);
		return null;
	}
	// 使用DistroTransportAgent获取数据
	return transportAgent.getData(distroKey, distroKey.getTargetServer());
}

根据当前处理的服务类型,可以得知这里的DistroTransportAgent为DistroHttpAgent。

// DistroTransportAgent.java

@Override
public DistroData getData(DistroKey key, String targetServer) {
	try {
		List<String> toUpdateKeys = null;
		if (key instanceof DistroHttpCombinedKey) {
			toUpdateKeys = ((DistroHttpCombinedKey) key).getActualResourceTypes();
		} else {
			toUpdateKeys = new ArrayList<>(1);
			toUpdateKeys.add(key.getResourceKey());
		}
		// 使用NamingProxy获取数据
		byte[] queriedData = NamingProxy.getData(toUpdateKeys, key.getTargetServer());
		return new DistroData(key, queriedData);
	} catch (Exception e) {
		throw new DistroException(String.format("Get data from %s failed.", key.getTargetServer()), e);
	}
}

// NamingProxy.java
/**
 * Get Data from other server.
 *
 * @param keys   keys of datum
 * @param server target server address
 * @return datum byte array
 * @throws Exception exception
 */
public static byte[] getData(List<String> keys, String server) throws Exception {

	// 组装http请求参数
	Map<String, String> params = new HashMap<>(8);
	params.put("keys", StringUtils.join(keys, ","));
	// 发送http请求
	RestResult<String> result = HttpClient.httpGetLarge("http://" + server + EnvUtil.getContextPath() + UtilsAndCommons.NACOS_NAMING_CONTEXT + DATA_GET_URL, new HashMap<>(8), JacksonUtils.toJson(params));

	// 处理请求结果
	if (result.ok()) {
		return result.getData().getBytes();
	}

	throw new IOException("failed to req API: " + "http://" + server + EnvUtil.getContextPath()
			+ UtilsAndCommons.NACOS_NAMING_CONTEXT + DATA_GET_URL + ". code: " + result.getCode() + " msg: "
			+ result.getMessage());
}

成功获取远端节点的DistroData数据之后将进行解析, 对于Service类型的数据这里做了两个操作:一是将获取的最新数据放入当前节点的DataStore内;二是对获取的最新服务开启健康检查。

// DistroConsistencyServiceImpl.java

private boolean processData(byte[] data) throws Exception {
	if (data.length > 0) {
		// 序列化将要解析的值
		Map<String, Datum<Instances>> datumMap = serializer.deserializeMap(data, Instances.class);
		
		// 此for循环主要功能是用于将获取到的Service添加到监听器列表
		for (Map.Entry<String, Datum<Instances>> entry : datumMap.entrySet()) {
			// 遍历并放入数据仓库中
			dataStore.put(entry.getKey(), entry.getValue());
			// ① 判断是否有当前服务的监听器,若有则触发监听器用于更新服务信息
			if (!listeners.containsKey(entry.getKey())) {
				// 因为当前的类是用于处理临时节点的同步信息的(详情请看v1版本ServiceManager内部的逻辑)
				// pretty sure the service not exist:
				if (switchDomain.isDefaultInstanceEphemeral()) {
					// 根据获取的数据信息构建一个v1版本的Service对象
					// create empty service
					Loggers.DISTRO.info("creating service {}", entry.getKey());
					Service service = new Service();
					String serviceName = KeyBuilder.getServiceName(entry.getKey());
					String namespaceId = KeyBuilder.getNamespace(entry.getKey());
					service.setName(serviceName);
					service.setNamespaceId(namespaceId);
					service.setGroupName(Constants.DEFAULT_GROUP);
					// now validate the service. if failed, exception will be thrown
					service.setLastModifiedMillis(System.currentTimeMillis());
					service.recalculateChecksum();
					// 尤其是要注意此处的key类型,因为listener集合内部可能存放多种类型的监听器
					// ② 获取ServiceManager,因为其也是一个监听器
					// The Listener corresponding to the key value must not be empty
					RecordListener listener = listeners.get(KeyBuilder.SERVICE_META_KEY_PREFIX).peek();
					if (Objects.isNull(listener)) {
						return false;
					}
					// 触发ServiceManager的onChange事件,
					listener.onChange(KeyBuilder.buildServiceMetaKey(namespaceId, serviceName), service);
				}
			}
		}
		
		// 此for循环主要用于触发监听器列表中的Service监听器
		for (Map.Entry<String, Datum<Instances>> entry : datumMap.entrySet()) {
			
			if (!listeners.containsKey(entry.getKey())) {
				// Should not happen:
				Loggers.DISTRO.warn("listener of {} not found.", entry.getKey());
				continue;
			}
			
			try {
				for (RecordListener listener : listeners.get(entry.getKey())) {
					listener.onChange(entry.getKey(), entry.getValue().value);
				}
			} catch (Exception e) {
				Loggers.DISTRO.error("[NACOS-DISTRO] error while execute listener of key: {}", entry.getKey(), e);
				continue;
			}
			
			// Update data store if listener executed successfully:
			dataStore.put(entry.getKey(), entry.getValue());
		}
	}
	return true;
}

提示:

  • 方法中标记的①②处主要就是用于在拿到其他节点的数据时判断当前节点有没有处理过这个服务(Service),若有处理过那么ServiceManager就会将本服务放入ConsistencyService的监听器列表属性中(当前类型的数据处理器是DistroConsistencyServiceImpl)。因为Service本身它也是一个监听器,请结合代码中的注释来理解此段代码的两个for循环。
  • ServiceManager 本身作为监听器加入listener时的key为:com.alibaba.nacos.naming.domains.meta.
  • Service 本身作为监听器加入listener时的key为:com.alibaba.nacos.naming.iplist.ephemeral.命名空间##分组名称@@服务名称

监听器列表示例数据:

listeners = {ConcurrentHashMap@13820}  size = 2
	"com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@TO_REMOTE_65_T001" -> {ConcurrentLinkedQueue@13869}  size = 1
		key = "com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@TO_REMOTE_65_T001"
		value = {ConcurrentLinkedQueue@13869}  size = 1
			0 = {Service@10668} "Service{name='DEFAULT_GROUP@@TO_REMOTE_65_T001', protectThreshold=0.0, appName='null', groupName='DEFAULT_GROUP', metadata={}}"
	"com.alibaba.nacos.naming.domains.meta." -> {ConcurrentLinkedQueue@13871}  size = 1
		key = "com.alibaba.nacos.naming.domains.meta."
		value = {ConcurrentLinkedQueue@13871}  size = 1
			0 = {ServiceManager@10671} 

ServiceManager.onChange()

// ServiceManager.java

@Override
public void onChange(String key, Service service) throws Exception {
	try {
		if (service == null) {
			Loggers.SRV_LOG.warn("received empty push from raft, key: {}", key);
			return;
		}
		
		// 检查namespace
		if (StringUtils.isBlank(service.getNamespaceId())) {
			service.setNamespaceId(Constants.DEFAULT_NAMESPACE_ID);
		}
		
		Loggers.RAFT.info("[RAFT-NOTIFIER] datum is changed, key: {}, value: {}", key, service);
		// 从ServiceManager内部的serviceMap缓存中获取
		Service oldDom = getService(service.getNamespaceId(), service.getName());
		// 若不为空,则更新
		if (oldDom != null) {
			oldDom.update(service);
			// 重新将其放入监听器列表
			// re-listen to handle the situation when the underlying listener is removed:
			consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), oldDom);
			consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), oldDom);
		} else {
			// 若之前没有缓存,添加并初始化
			putServiceAndInit(service);
		}
	} catch (Throwable e) {
		Loggers.SRV_LOG.error("[NACOS-SERVICE] error while processing service update", e);
	}
}

private void putServiceAndInit(Service service) throws NacosException {
	// 插入缓存列表
	putService(service);
	service = getService(service.getNamespaceId(), service.getName());
	// 初始化
	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) {
	// 判断namespace是否存在,若不存在创建一个namespace,并new一个新的Map用于存放namespace对应的service
	if (!serviceMap.containsKey(service.getNamespaceId())) {
		synchronized (putServiceLock) {
			if (!serviceMap.containsKey(service.getNamespaceId())) {
				serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
			}
		}
	}
	// 将service存放于它所属的namespace容器内部
	serviceMap.get(service.getNamespaceId()).putIfAbsent(service.getName(), service);
}

在ServiceManager的onChange方法内部主要就是更新本次获取的Service信息,并将其添加到监听器列表内部。最重要的是,当此次传递进来的Service不在缓存中的时候它触发了一个Service.init()方法。它将会开启Service和Cluster的心跳检查。不过他们开启的是针对v1版本的心跳检查,若版本是2.0.x的话,心跳检查将不会执行。

// Service.java

private ClientBeatCheckTask clientBeatCheckTask = new ClientBeatCheckTask(this);

public void init() {
	HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
	for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
		entry.getValue().setService(this);
		// 还开启了Cluster的健康检查
		entry.getValue().init();
	}
}

// Cluster.java

/**
 * Init cluster.
 */
public void init() {
	if (inited) {
		return;
	}
	checkTask = new HealthCheckTask(this);
	HealthCheckReactor.scheduleCheck(checkTask);
	inited = true;
}

// ClientBeatCheckTask.java

public void run() {
	try {
		// If upgrade to 2.0.X stop health check with v1
		if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) {
			return;
		}
		// 省略部分代码...

	} catch (Exception e) {
		Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
	}
}

// HealthCheckTask.java

@Override
public void run() {
	
	try {
		// If upgrade to 2.0.X stop health check with v1
		if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) {
			return;
		}
	} catch (Throwable e) {
		Loggers.SRV_LOG.error("[HEALTH-CHECK] error while process health check for {}:{}", cluster.getService().getName(), cluster.getName(), e);
	}
}

Service.onChange()

v2版本的数据验证

在v2版本中Nacos:Naming:v2:ClientData类型作为验证数据类型,相较于v1版本以Service为维度的数据验证在整体处理流程上简洁了许多,一次验证一个客户端提供的所有服务在逻辑上的完整性也很强。

从DistroDataStorage获取验证数据
// DistroClientDataProcessor.java

@Override
public List<DistroData> getVerifyData() {
	List<DistroData> result = new LinkedList<>();
	// 遍历当前节点缓存的所有client
	for (String each : clientManager.allClientId()) {
		Client client = clientManager.getClient(each);
		if (null == client || !client.isEphemeral()) {
			continue;
		}
		// 是本机负责的Client才进行处理
		if (clientManager.isResponsibleClient(client)) {
			// TODO add revision for client.
			DistroClientVerifyInfo verifyData = new DistroClientVerifyInfo(client.getClientId(), 0);
			DistroKey distroKey = new DistroKey(client.getClientId(), TYPE);
			DistroData data = new DistroData(distroKey, ApplicationUtils.getBean(Serializer.class).serialize(verifyData));
			data.setType(DataOperation.VERIFY);
			result.add(data);
		}
	}
	return result;
}

注意:

  • 在当前节点执行Nacos:Naming:v2:ClientData类型数据的验证任务时,它只会向集群中的其他节点发送自己负责的,且未被移除的数据。
使用DistroTransportAgent发送验证数据

提示:

  • 执行发送数据的这个方法调用的本身是在上层DistroVerifyExecuteTask的run方法的for循环内部的。也就是说为每个Client发送一次验证请求。
// DistroClientTransportAgent.java

@Override
public void syncVerifyData(DistroData verifyData, String targetServer, DistroCallback callback) {
	// 若此节点不在当前节点缓存中,直接返回,因为可能下线、或者过期,不需要验证了
	if (isNoExistTarget(targetServer)) {
		callback.onSuccess();
	}
	// 构建请求对象
	DistroDataRequest request = new DistroDataRequest(verifyData, DataOperation.VERIFY);
	Member member = memberManager.find(targetServer);
	try {
		// 创建一个回调对象(Wrapper实现了RequestCallBack接口)
		DistroVerifyCallbackWrapper wrapper = new DistroVerifyCallbackWrapper(targetServer, verifyData.getDistroKey().getResourceKey(), callback, member);
		// 使用集群Rpc请求对象发送异步任务
		clusterRpcClientProxy.asyncRequest(member, request, wrapper);
	} catch (NacosException nacosException) {
		callback.onFailed(nacosException);
	}
}

// ClusterRpcClientProxy.java

/**
 * aync send request to member with callback.
 * 其他节点发送异步请求并回调
 * @param member   member of server.
 * @param request  request.
 * @param callBack RequestCallBack.
 * @throws NacosException exception may throws.
 */
public void asyncRequest(Member member, Request request, RequestCallBack callBack) throws NacosException {
	// 获取目标节点对应的RpcClient
	RpcClient client = RpcClientFactory.getClient(memberClientKey(member));
	if (client != null) {
		// 通过Client发送异步请求
		client.asyncRequest(request, callBack);
	} else {
		throw new NacosException(CLIENT_INVALID_PARAM, "No rpc client related to member: " + member);
	}
}

// RpcClient.java

/**
 * send async request.
 *
 * @param request request.
 */
public void asyncRequest(Request request, RequestCallBack callback) throws NacosException {
	
	int retryTimes = 0;

	Exception exceptionToThrow = null;
	long start = System.currentTimeMillis();
	// 重试次数少于3次,且总执行时间小于3秒
	while (retryTimes < RETRY_TIMES && System.currentTimeMillis() < start + callback.getTimeout()) {
		boolean waitReconnect = false;
		try {
			if (this.currentConnection == null || !isRunning()) {
				waitReconnect = true;
				throw new NacosException(NacosException.CLIENT_INVALID_PARAM, "Client not connected.");
			}
			// 使用GrpcConnection发送异步请求
			this.currentConnection.asyncRequest(request, callback);
			return;
		} catch (Exception e) {
			if (waitReconnect) {
				try {
					//wait client to re connect.
					Thread.sleep(Math.min(100, callback.getTimeout() / 3));
				} catch (Exception exception) {
					//Do nothing.
				}
			}
			LoggerUtils.printIfErrorEnabled(LOGGER, "[{}]Send request fail, request={}, retryTimes={},errorMessage={}", name, request, retryTimes, e.getMessage());
			exceptionToThrow = e;

		}
		retryTimes++;
	}

	// 判断RpcClientStatus 是不是RUNNING状态,若不是则设置其为UNHEALTHY
	if (rpcClientStatus.compareAndSet(RpcClientStatus.RUNNING, RpcClientStatus.UNHEALTHY)) {
		// 若成功设置了UNHEALTHY,则置为失败
		switchServerAsyncOnRequestFail();
	}
	if (exceptionToThrow != null) {
		throw (exceptionToThrow instanceof NacosException) ? (NacosException) exceptionToThrow : new NacosException(SERVER_ERROR, exceptionToThrow);
	} else {
		throw new NacosException(SERVER_ERROR, "AsyncRequest fail, unknown error");
	}
}

// GrpcConnection.java

@Override
public void asyncRequest(Request request, final RequestCallBack requestCallBack) throws NacosException {
	// ① 转换为Grpc的请求载体对象
	Payload grpcRequest = GrpcUtils.convert(request);
	// 发送Grpc请求
	ListenableFuture<Payload> requestFuture = grpcFutureServiceStub.request(grpcRequest);

	//set callback .
	Futures.addCallback(requestFuture, new FutureCallback<Payload>() {
		@Override
		public void onSuccess(@Nullable Payload grpcResponse) {
			Response response = (Response) GrpcUtils.parse(grpcResponse);

			if (response != null) {
				if (response instanceof ErrorResponse) {
					requestCallBack.onException(new NacosException(response.getErrorCode(), response.getMessage()));
				} else {
					// 若成功返回,触发回调的onResponse对象,由回调函数处理后续逻辑
					requestCallBack.onResponse(response);
				}
			} else {
				requestCallBack.onException(new NacosException(ResponseCode.FAIL.getCode(), "response is null"));
			}
		}

		@Override
		public void onFailure(Throwable throwable) {
			if (throwable instanceof CancellationException) {
				requestCallBack.onException(
						new TimeoutException("Timeout after " + requestCallBack.getTimeout() + " milliseconds."));
			} else {
				requestCallBack.onException(throwable);
			}
		}
	}, requestCallBack.getExecutor() != null ? requestCallBack.getExecutor() : this.executor);
	// set timeout future.
	ListenableFuture<Payload> payloadListenableFuture = Futures
			.withTimeout(requestFuture, requestCallBack.getTimeout(), TimeUnit.MILLISECONDS,
					RpcScheduledExecutor.TIMEOUT_SCHEDULER);

}

这里不同于v1版本的checksum,在v2版本的验证任务中使用了rpc请求方式。和v1版本的请求逻辑相同,都是发送当前节点所负责的Client信息到其他节点。

注意:
在①标注的地方,它在转换request的时候作了一些必要的操作,包括构建Grpc请求对象(Payload)所需要的元数据,其中最需要注意的就是Metadata内部的type属性,接收方将根据这个属性进行判断,分别处理不同类型的请求。

处理DistroTransportAgent的验证请求

其他节点接收到验证请求,是如何处理呢?GrpcRequestAcceptor用于接收rpc请求。需要注意的是,它将接收所有类型的rpc请求,例如:ServerCheckRequestDistroDataRequestHealthCheckRequest等,具体可查看com.alibaba.nacos.api.remote.request包下的请求对象。此处我们只关注验证请求DistroDataRequest

// GrpcRequestAcceptor.java

@Override
public void request(Payload grpcRequest, StreamObserver<Payload> responseObserver) {
	// 如果有必要跟踪的话
	traceIfNecessary(grpcRequest, true);
	
	// 获取请求对象中的请求类型
	String type = grpcRequest.getMetadata().getType();

	// 若当前节点还未启动完毕,则直接返回
	//server is on starting.
	if (!ApplicationUtils.isStarted()) {
		Payload payloadResponse = GrpcUtils.convert(buildErrorResponse(NacosException.INVALID_SERVER_STATUS, "Server is starting,please try later."));
		traceIfNecessary(payloadResponse, false);
		responseObserver.onNext(payloadResponse);

		responseObserver.onCompleted();
		return;
	}

	// 判断请求是否为ServerCheckRequest
	// server check.
	if (ServerCheckRequest.class.getSimpleName().equals(type)) {
		// 构建一个ServerCheckResponse作为返回对象,并包装为Payload返回给调用方
		Payload serverCheckResponseP = GrpcUtils.convert(new ServerCheckResponse(CONTEXT_KEY_CONN_ID.get()));
		traceIfNecessary(serverCheckResponseP, false);
		responseObserver.onNext(serverCheckResponseP);
		responseObserver.onCompleted();
		return;
	}

	// 根据请求的类型,找到对应的处理器
	RequestHandler requestHandler = requestHandlerRegistry.getByRequestType(type);
	//no handler found.
	if (requestHandler == null) {
		// 若未找到类型对应的处理器,返回给调用方一个ErrorResponse
		Loggers.REMOTE_DIGEST.warn(String.format("[%s] No handler for request type : %s :", "grpc", type));
		Payload payloadResponse = GrpcUtils.convert(buildErrorResponse(NacosException.NO_HANDLER, "RequestHandler Not Found"));
		traceIfNecessary(payloadResponse, false);
		responseObserver.onNext(payloadResponse);
		responseObserver.onCompleted();
		return;
	}

	// 检查连接状态
	//check connection status.
	String connectionId = CONTEXT_KEY_CONN_ID.get();
	boolean requestValid = connectionManager.checkValid(connectionId);
	if (!requestValid) {
		Loggers.REMOTE_DIGEST.warn("[{}] Invalid connection Id ,connection [{}] is un registered ,", "grpc", connectionId);
		Payload payloadResponse = GrpcUtils.convert(buildErrorResponse(NacosException.UN_REGISTER, "Connection is unregistered."));
		traceIfNecessary(payloadResponse, false);
		responseObserver.onNext(payloadResponse);
		responseObserver.onCompleted();
		return;
	}

	// 接下来开始真正的处理请求逻辑

	// 解析请求对象
	Object parseObj = null;
	try {
		parseObj = GrpcUtils.parse(grpcRequest);
	} catch (Exception e) {
		// 解析失败返回
		Loggers.REMOTE_DIGEST.warn("[{}] Invalid request receive from connection [{}] ,error={}", "grpc", connectionId, e);
		Payload payloadResponse = GrpcUtils.convert(buildErrorResponse(NacosException.BAD_GATEWAY, e.getMessage()));
		traceIfNecessary(payloadResponse, false);
		responseObserver.onNext(payloadResponse);
		responseObserver.onCompleted();
		return;
	}

	// 请求对象为空,返回
	if (parseObj == null) {
		Loggers.REMOTE_DIGEST.warn("[{}] Invalid request receive  ,parse request is null", connectionId);
		Payload payloadResponse = GrpcUtils.convert(buildErrorResponse(NacosException.BAD_GATEWAY, "Invalid request"));
		traceIfNecessary(payloadResponse, false);
		responseObserver.onNext(payloadResponse);
		responseObserver.onCompleted();
	}

	// 请求对象货不对板,返回
	if (!(parseObj instanceof Request)) {
		Loggers.REMOTE_DIGEST.warn("[{}] Invalid request receive  ,parsed payload is not a request,parseObj={}", connectionId, parseObj);
		Payload payloadResponse = GrpcUtils.convert(buildErrorResponse(NacosException.BAD_GATEWAY, "Invalid request"));
		traceIfNecessary(payloadResponse, false);
		responseObserver.onNext(payloadResponse);
		responseObserver.onCompleted();
		return;
	}

	// 将接收的对象转换为请求对象
	Request request = (Request) parseObj;
	try {
		Connection connection = connectionManager.getConnection(CONTEXT_KEY_CONN_ID.get());
		RequestMeta requestMeta = new RequestMeta();
		requestMeta.setClientIp(connection.getMetaInfo().getClientIp());
		requestMeta.setConnectionId(CONTEXT_KEY_CONN_ID.get());
		requestMeta.setClientVersion(connection.getMetaInfo().getVersion());
		requestMeta.setLabels(connection.getMetaInfo().getLabels());
		connectionManager.refreshActiveTime(requestMeta.getConnectionId());
		// 使用对应的处理器进行处理
		Response response = requestHandler.handleRequest(request, requestMeta);
		Payload payloadResponse = GrpcUtils.convert(response);
		traceIfNecessary(payloadResponse, false);
		responseObserver.onNext(payloadResponse);
		responseObserver.onCompleted();
	} catch (Throwable e) {
		Loggers.REMOTE_DIGEST.error("[{}] Fail to handle request from connection [{}] ,error message :{}", "grpc", connectionId, e);
		Payload payloadResponse = GrpcUtils.convert(buildErrorResponse((e instanceof NacosException) ? ((NacosException) e).getErrCode() : ResponseCode.FAIL.getCode(), e.getMessage()));
		traceIfNecessary(payloadResponse, false);
		responseObserver.onNext(payloadResponse);
		responseObserver.onCompleted();
		return;
	}

}

最终的DistroData数据将会由对应的Handler来处理,并返回结果给调用方。

/**
 * Distro data request handler.
 * Distro协议数据的请求处理器,用于处理客户端发送来的Distro协议 rpc 请求
 * @author xiweng.yy
 */
@Component
public class DistroDataRequestHandler extends RequestHandler<DistroDataRequest, DistroDataResponse> {

    private final DistroProtocol distroProtocol;

    public DistroDataRequestHandler(DistroProtocol distroProtocol) {
        this.distroProtocol = distroProtocol;
    }

    @Override
    public DistroDataResponse handle(DistroDataRequest request, RequestMeta meta) throws NacosException {
        try {
            switch (request.getDataOperation()) {
                case VERIFY:
                    return handleVerify(request.getDistroData(), meta);
                case SNAPSHOT:
                    return handleSnapshot();
                case ADD:
                case CHANGE:
                case DELETE:
                    return handleSyncData(request.getDistroData());
                case QUERY:
                    return handleQueryData(request.getDistroData());
                default:
                    return new DistroDataResponse();
            }
        } catch (Exception e) {
            Loggers.DISTRO.error("[DISTRO-FAILED] distro handle with exception", e);
            DistroDataResponse result = new DistroDataResponse();
            result.setErrorCode(ResponseCode.FAIL.getCode());
            result.setMessage("handle distro request with exception");
            return result;
        }
    }

    /**
     * 处理验证信息
     * @param distroData    请求方发送来的验证信息
     * @param meta          请求元数据
     * @return
     */
    private DistroDataResponse handleVerify(DistroData distroData, RequestMeta meta) {
        DistroDataResponse result = new DistroDataResponse();
		// 使用DistroProtocol来处理
        if (!distroProtocol.onVerify(distroData, meta.getClientIp())) {
            result.setErrorInfo(ResponseCode.FAIL.getCode(), "[DISTRO-FAILED] distro data verify failed");
        }
        return result;
    }

    private DistroDataResponse handleSnapshot() {
        DistroDataResponse result = new DistroDataResponse();
        DistroData distroData = distroProtocol.onSnapshot(DistroClientDataProcessor.TYPE);
        result.setDistroData(distroData);
        return result;
    }

    private DistroDataResponse handleSyncData(DistroData distroData) {
        DistroDataResponse result = new DistroDataResponse();
        if (!distroProtocol.onReceive(distroData)) {
            result.setErrorCode(ResponseCode.FAIL.getCode());
            result.setMessage("[DISTRO-FAILED] distro data handle failed");
        }
        return result;
    }

    private DistroDataResponse handleQueryData(DistroData distroData) {
        DistroDataResponse result = new DistroDataResponse();
        DistroKey distroKey = distroData.getDistroKey();
        DistroData queryData = distroProtocol.onQuery(distroKey);
        result.setDistroData(queryData);
        return result;
    }
}

根据DistroDataRequestHandler.handle()方法可以看出来,它不止可以处理VERIFY请求,这里我们只分析VERIFY类型的请求处理。可以发现它调用了DistroProtocol.onVerify()来处理。

// DistroProtocol.java

/**
 * Receive verify data, find processor to process.
 * @param distroData    verify data
 * @param sourceAddress source server address, might be get data from source server
 * @return true if verify data successfully, otherwise false
 */
public boolean onVerify(DistroData distroData, String sourceAddress) {
	if (Loggers.DISTRO.isDebugEnabled()) {
		Loggers.DISTRO.debug("[DISTRO] Receive verify data type: {}, key: {}", distroData.getType(), distroData.getDistroKey());
	}
	// 根据此次处理的数据类型获取对应的处理器,此处我们处理的类型是Client类型(Nacos:Naming:v2:ClientData)
	String resourceType = distroData.getDistroKey().getResourceType();
	DistroDataProcessor dataProcessor = distroComponentHolder.findDataProcessor(resourceType);
	if (null == dataProcessor) {
		Loggers.DISTRO.warn("[DISTRO] Can't find verify data process for received data {}", resourceType);
		return false;
	}
	return dataProcessor.processVerifyData(distroData, sourceAddress);
}

// DistroClientDataProcessor.java

@Override
public boolean processVerifyData(DistroData distroData, String sourceAddress) {
	DistroClientVerifyInfo verifyData = ApplicationUtils.getBean(Serializer.class).deserialize(distroData.getContent(), DistroClientVerifyInfo.class);
	// 调用ClientManager来验证
	if (clientManager.verifyClient(verifyData.getClientId())) {
		return true;
	}
	Loggers.DISTRO.info("client {} is invalid, get new client from {}", verifyData.getClientId(), sourceAddress);
	return false;
}

// EphemeralIpPortClientManager.java

@Override
public boolean verifyClient(String clientId) {
	// 从客户端管理器中获取客户端实例
	IpPortBasedClient client = clients.get(clientId);
	if (null != client) {
		// 若不为空,启动一个心跳更新任务
		NamingExecuteTaskDispatcher.getInstance().dispatchAndExecuteTask(clientId, new ClientBeatUpdateTask(client));
		return true;
	}
	return false;
}


public class ClientBeatUpdateTask extends AbstractExecuteTask {

    private final IpPortBasedClient client;

    public ClientBeatUpdateTask(IpPortBasedClient client) {
        this.client = client;
    }

    @Override
    public void run() {
        // 获取当前时间,更新Client和Client下的Instance的最新活跃时间
        long currentTime = System.currentTimeMillis();
        for (InstancePublishInfo each : client.getAllInstancePublishInfo()) {
            ((HealthCheckInstancePublishInfo) each).setLastHeartBeatTime(currentTime);
        }
		// 更新client的最新更新时间
        client.setLastUpdatedTime();
    }
}

通过最后一步的ClientBeatUpdateTask可以发现,其他节点发送来的verify请求,实际上就是发送端的状态报告请求。Client.getAllInstancePublishInfo()返回的是某一个客户端下的所有实例信息列表,最后再更新Client自身的最新活跃时间。

验证任务功能总结:

  • 每个节点在应用启动完毕之后延迟5秒,之后间隔5秒向其他节点发送Verify请求。
  • 每个节点只发送自己负责的、且未被移除的Client,且每一个Client都会发送一次请求,请求参数里面只附带了Client的clientId属性。(意味着当前节点只会告诉其他节点我目前有这个Client正在提供服务,并未提供服务的具体信息。)
  • 接收Verify请求的节点从请求参数中获取clientId,并检查自身是否有这个Client,若此Client存在,则更新Client下的所有Instance、以及Client自身的最新活跃时间为当前时间。

节点数据同步

除了开启验证任务之外,还开启了一个数据加载的任务startLoadTask(),用于从其他节点同步数据到本节点。同时它也维护着本机节点是否初始化完成的一个状态isInitialized,当前节点从其他节点加载数据成功之后,将这个状态设置为true。表示当前节点已经同步完毕数据,可以参与正常服务;同时又设置了DistroDataStorageisFinishInitial属性为true,表示数据已准备好,验证任务可以执行了。

数据同步的执行流程

// DistroProtocol.java

private void startLoadTask() {
	DistroCallback loadCallback = new DistroCallback() {
		@Override
		public void onSuccess() {
			isInitialized = true;
		}

		@Override
		public void onFailed(Throwable throwable) {
			isInitialized = false;
		}
	};
	GlobalExecutor.submitLoadDataTask(new DistroLoadDataTask(memberManager, distroComponentHolder, DistroConfig.getInstance(), loadCallback));
}

加载任务的一开始创建了一个 DistroLoadDataTask 任务,并传入了一个在加载完毕之后更改当前节点Distro协议完成状态的回调函数。

// DistroLoadDataTask.java 

private void load() throws Exception {
	// 若出自身之外没有其他节点,则休眠1秒,可能其他节点还未启动完毕
	while (memberManager.allMembersWithoutSelf().isEmpty()) {
		Loggers.DISTRO.info("[DISTRO-INIT] waiting server list init...");
		TimeUnit.SECONDS.sleep(1);
	}
	// 若数据类型为空,说明distroComponentHolder的组件注册器还未初始化完毕(v1版本为DistroHttpRegistry, v2版本为DistroClientComponentRegistry)
	while (distroComponentHolder.getDataStorageTypes().isEmpty()) {
		Loggers.DISTRO.info("[DISTRO-INIT] waiting distro data storage register...");
		TimeUnit.SECONDS.sleep(1);
	}
	// 加载每个类型的数据
	for (String each : distroComponentHolder.getDataStorageTypes()) {
		if (!loadCompletedMap.containsKey(each) || !loadCompletedMap.get(each)) {
			// 调用加载方法,并标记已处理
			loadCompletedMap.put(each, loadAllDataSnapshotFromRemote(each));
		}
	}
}

/**
 * 从其他节点获取同步数据
 * @param resourceType
 * @return
 */
private boolean loadAllDataSnapshotFromRemote(String resourceType) {
	// 获取数据传输对象
	DistroTransportAgent transportAgent = distroComponentHolder.findTransportAgent(resourceType);
	// 获取数据处理器
	DistroDataProcessor dataProcessor = distroComponentHolder.findDataProcessor(resourceType);
	if (null == transportAgent || null == dataProcessor) {
		Loggers.DISTRO.warn("[DISTRO-INIT] Can't find component for type {}, transportAgent: {}, dataProcessor: {}", resourceType, transportAgent, dataProcessor);
		return false;
	}
	// 向每个节点请求数据
	for (Member each : memberManager.allMembersWithoutSelf()) {
		try {
			Loggers.DISTRO.info("[DISTRO-INIT] load snapshot {} from {}", resourceType, each.getAddress());
			// 获取到数据
			DistroData distroData = transportAgent.getDatumSnapshot(each.getAddress());
			// 解析数据
			boolean result = dataProcessor.processSnapshot(distroData);
			Loggers.DISTRO.info("[DISTRO-INIT] load snapshot {} from {} result: {}", resourceType, each.getAddress(), result);
			// 若解析成功,标记此类型数据已加载完毕
			if (result) {
				distroComponentHolder.findDataStorage(resourceType).finishInitial();
				return true;
			}
		} catch (Exception e) {
			Loggers.DISTRO.error("[DISTRO-INIT] load snapshot {} from {} failed.", resourceType, each.getAddress(), e);
		}
	}
	return false;
}

加载的流程实际上和验证类似,都是通过获取需要加载的数据类型,使用DistroTransportAgent获取数据,使用DistroDataProcessor来处理数据。

执行同步任务

提示:
对于v1版本的相关操作,后续将不再分析,感兴趣的同学可以自行研究。此处已经给出了数据加载流程,无非就是他们的处理逻辑不同。

v2版本的数据同步

真正的同步任务从DistroLoadDataTaskloadAllDataSnapshotFromRemote方法开始。

使用DistroTransportAgent获取数据
// DistroClientTransportAgent.java

public DistroData getDatumSnapshot(String targetServer) {
	// 从节点管理器获取目标节点信息
	Member member = memberManager.find(targetServer);
	// 判断目标服务器是否健康
	if (checkTargetServerStatusUnhealthy(member)) {
		throw new DistroException(String.format("[DISTRO] Cancel get snapshot caused by target server %s unhealthy", targetServer));
	}
	
	// 构建请求参数
	DistroDataRequest request = new DistroDataRequest();
	// 设置请求的操作类型为DataOperation.SNAPSHOT
	request.setDataOperation(DataOperation.SNAPSHOT);
	try {
		// 使用Rpc代理对象发送同步rpc请求
		Response response = clusterRpcClientProxy.sendRequest(member, request);
		if (checkResponse(response)) {
			return ((DistroDataResponse) response).getDistroData();
		} else {
			throw new DistroException(String.format("[DISTRO-FAILED] Get snapshot request to %s failed, code: %d, message: %s", targetServer, response.getErrorCode(), response.getMessage()));
		}
	} catch (NacosException e) {
		throw new DistroException("[DISTRO-FAILED] Get distro snapshot failed! ", e);
	}
}

使用DistroDataProcessor处理数据
// DistroClientDataProcessor.java

public boolean processSnapshot(DistroData distroData) {
	// 反序列化获取的DistroData为ClientSyncDatumSnapshot
	ClientSyncDatumSnapshot snapshot = ApplicationUtils.getBean(Serializer.class).deserialize(distroData.getContent(), ClientSyncDatumSnapshot.class);
	// 处理结果集,这里将返回远程节点负责的所有client以及client下面的service、instance信息
	for (ClientSyncData each : snapshot.getClientSyncDataList()) {
		// 每次处理一个client
		handlerClientSyncData(each);
	}
	return true;
}

private void handlerClientSyncData(ClientSyncData clientSyncData) {
	Loggers.DISTRO.info("[Client-Add] Received distro client sync data {}", clientSyncData.getClientId());
	// 因为是同步数据,因此创建IpPortBasedClient,并缓存
	clientManager.syncClientConnected(clientSyncData.getClientId(), clientSyncData.getAttributes());
	Client client = clientManager.getClient(clientSyncData.getClientId());
	// 升级此客户端的服务信息
	upgradeClient(client, clientSyncData);
}

private void upgradeClient(Client client, ClientSyncData clientSyncData) {
	// 当前处理的远端节点中的数据集合
	// 获取所有的namespace
	List<String> namespaces = clientSyncData.getNamespaces();
	// 获取所有的groupNames
	List<String> groupNames = clientSyncData.getGroupNames();
	// 获取所有的serviceNames
	List<String> serviceNames = clientSyncData.getServiceNames();
	// 获取所有的instance
	List<InstancePublishInfo> instances = clientSyncData.getInstancePublishInfos();
	// 已同步的服务集合
	Set<Service> syncedService = new HashSet<>();

	// ①
	for (int i = 0; i < namespaces.size(); i++) {
		// 从获取的数据中构建一个Service对象
		Service service = Service.newService(namespaces.get(i), groupNames.get(i), serviceNames.get(i));
		Service singleton = ServiceManager.getInstance().getSingleton(service);
		// 标记此service已被处理
		syncedService.add(singleton);
		// 获取当前的实例
		InstancePublishInfo instancePublishInfo = instances.get(i);
		// 判断是否已经包含当前实例
		if (!instancePublishInfo.equals(client.getInstancePublishInfo(singleton))) {
			// 不包含则添加
			client.addServiceInstance(singleton, instancePublishInfo);
			// 当前节点发布服务注册事件
			NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, client.getClientId()));
		}
	}
	// 若当前client内部已发布的service不在本次同步的列表内,说明已经过时了,要删掉
	for (Service each : client.getAllPublishedService()) {
		if (!syncedService.contains(each)) {
			client.removeServiceInstance(each);
			// 发布客户端下线事件
			NotifyCenter.publishEvent(new ClientOperationEvent.ClientDeregisterServiceEvent(each, client.getClientId()));
		}
	}
}

注释①标记处的处理逻辑和ClientSyncData这个对象存储的对象有关系,此处是存放的以Service为维度的信息,它将一个Service的全部信息分别保存,并保证所有列表中的数据顺序一致。

ClientSyncData示例数据:

clientSyncData = {ClientSyncData@12737} 
	clientId = "10.55.56.1:8888#true"
	attributes = {ClientSyncAttributes@12740} 
	namespaces = {ArrayList@12741}  size = 2
		0 = "public"
		1 = "public"
	groupNames = {ArrayList@12742}  size = 2
		0 = "DEFAULT_GROUP"
		1 = "DEFAULT_GROUP"
	serviceNames = {ArrayList@12743}  size = 2
		0 = "SERVICE_01"
		1 = "SERVICE_02"
	instancePublishInfos = {ArrayList@12744}  size = 2
		0 = {InstancePublishInfo@12941} "InstancePublishInfo{ip='10.55.56.1', port=8888, healthy=false}"
		1 = {InstancePublishInfo@12942} "InstancePublishInfo{ip='10.55.56.1', port=8888, healthy=false}"

通过示例数据可以看出在10.55.56.1这个客户端中有两个服务,他们都在同一个namespace、同一个group中,因为InstancePublishInfo是和Service一对一的关系,而一个客户端下的服务IP一定和客户端的IP是一致的,所以也会存在两条instance信息。upgradeClient的主要功能就是,将从其他节点获取的所有注册的服务注册到当前节点内。

TODO 这里有个疑问,首次同步数据只会执行一次拉取(若拉取失败则会再次拉取,若拉取过后没有数据也不会再次拉取),而且拉取的是某个节点负责的服务数据,为何当前节点要发布事件呢?服务的状态维护不是应该由它负责的节点来维护嘛,比如我拉取的A服务是B节点的,我同步过来就OK了,如果A服务下线了,B节点来触发变更不就行了。然后再通知其他节点下线。

同步数据功能总结:
在一个节点启动之后,会主动向集群中的其他节点发送数据同步请求,接收到结果之后将其注册到自身节点内。

DistroProtocol 功能总结:

  • 节点状态报告:能用于向其他节点报告自身的服务状态。
  • 数据同步: 从其他节点拉取服务数据注册。
posted @ 2021-07-22 00:49  不会发芽的种子  阅读(2802)  评论(1编辑  收藏  举报