Nacos Server 是如何通知 Nacos Client 服务下线?

前言:

在上一篇中,我们分析了 Nacos Server 端的服务注册流程,其中关于 Nacos Server 端如何通知 Nacos Client 端服务下线并没有深入分析,对于注册中心来说,Client 感知服务下线也是非常核心的部分,本篇我们从源码的层面来分析下 Nacos 的这个过程。

Nacos 系列文章传送门:

Nacos 初步认识和 Nacos 部署细节

Nacos 配置管理模型 – 命名空间(Namespace)、配置分组(Group)和配置集ID(Data ID)

Nacos 注册中心和配置中心【实战】

服务启动何时触发 Nacos 的注册流程?

Nacos Client 端服务注册流程源码分析

Nacos Server 端服务注册流程源码分析

Nacos 服务发现(订阅)源码分析(客户端)

Nacos 服务发现(订阅)源码分析(服务端)

PushService 类

前文我们分析了当服务实例状态不健康时候会通过 getPushService().serviceChanged(service) 这行代码发布一个事件,我们来看下这行代码,如下:

//com.alibaba.nacos.naming.push.PushService#serviceChanged
public void serviceChanged(Service service) {
	// merge some change events to reduce the push frequency:
	//合并一些变更事件以减少推送频率  也就是已经发送过 udp 包的 client 不再继续发送
	if (futureMap
			.containsKey(UtilsAndCommons.assembleFullServiceName(service.getNamespaceId(), service.getName()))) {
		//已经发送过 udp 包的 client 不再继续发送
		return;
	}
	//发布服务状态变更事件
	this.applicationContext.publishEvent(new ServiceChangeEvent(this, service));
}

通过源码我们可以看到这行代码底层调用了 PushService 类的 serviceChanged 方法,serviceChanged 方法的作用就是给外部调用发布ServiceChangeEvent 事件,PushService 类是一个事件监听类,它监听的事件 ServiceChangeEvent,根据监听器机制,PushService 类肯定有一个 onApplicationEvent 方法,我们来分析一下 onApplicationEvent 方法。

PushService#onApplicationEvent 方法源码分析

PushService#onApplicationEvent 方法会注册一个延时任务并将该 future 放入 futureMap,该延时任务会从 clientMap 获取指定namespaceId、 serviceName 的client 集合,遍历 client 集合,判断 client 是否是 zombie(僵尸) client,如果是的则移除该 client,否则创建 Receiver.AckEntry,然后通过 UDP 的方式推送给 client,执行完毕后会从 futureMap 移除该 future。

//com.alibaba.nacos.naming.push.PushService#onApplicationEvent
public void onApplicationEvent(ServiceChangeEvent event) {
	//从事件对象中获取到 service
	Service service = event.getService();
	//获取 servicename
	String serviceName = service.getName();
	//获取名称空间id
	String namespaceId = service.getNamespaceId();
	//使用延时任务 延时1 秒 通过 UDP 的方式来发送
	Future future = GlobalExecutor.scheduleUdpSender(() -> {
		try {
			Loggers.PUSH.info(serviceName + " is changed, add it to push queue.");
			//从缓存map中获取当前服务的内层map 内层map中存放着当前服务的所有Nacos Client的
			//根据 namespaceId 和 serviceName 获取对应的 client 信息
			ConcurrentMap<String, PushClient> clients = clientMap
					.get(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));
			//为空判断 如果为空 就没有必要推送了
			if (MapUtils.isEmpty(clients)) {
				return;
			}
			//创建缓存 map
			Map<String, Object> cache = new HashMap<>(16);
			//获取当前时间的 纳秒
			long lastRefTime = System.nanoTime();
			//遍历所有的 client 信息
			for (PushClient client : clients.values()) {
				//是否是僵尸客户端
				if (client.zombie()) {
					Loggers.PUSH.debug("client is zombie: " + client.toString());
					//如果是的话 就移除僵尸客户端
					clients.remove(client.toString());
					Loggers.PUSH.debug("client is zombie: " + client.toString());
					continue;
				}
				//ACK
				Receiver.AckEntry ackEntry;
				Loggers.PUSH.debug("push serviceName: {} to client: {}", serviceName, client.toString());
				//获取推送 key
				String key = getPushCacheKey(serviceName, client.getIp(), client.getAgent());
				byte[] compressData = null;
				Map<String, Object> data = null;
				//switchDomain.getDefaultPushCacheMillis() 默认是 10秒 因此不会进入 if
				if (switchDomain.getDefaultPushCacheMillis() >= 20000 && cache.containsKey(key)) {
					org.javatuples.Pair pair = (org.javatuples.Pair) cache.get(key);
					compressData = (byte[]) (pair.getValue0());
					data = (Map<String, Object>) pair.getValue1();
					
					Loggers.PUSH.debug("[PUSH-CACHE] cache hit: {}:{}", serviceName, client.getAddrStr());
				}
				//封装 ackEntry  将客户端信息封装到 ackEntry 
				if (compressData != null) {
					ackEntry = prepareAckEntry(client, compressData, data, lastRefTime);
				} else {
					//这里初始化了需要推送的 客户端
					ackEntry = prepareAckEntry(client, prepareHostsData(client), lastRefTime);
					if (ackEntry != null) {
						cache.put(key, new org.javatuples.Pair<>(ackEntry.origin.getData(), ackEntry.data));
					}
				}
				
				Loggers.PUSH.info("serviceName: {} changed, schedule push for: {}, agent: {}, key: {}",
						client.getServiceName(), client.getAddrStr(), client.getAgent(),
						(ackEntry == null ? null : ackEntry.key));
				//通过 udp 协议向 Nacos 客户端推送数据
				udpPush(ackEntry);
			}
		} catch (Exception e) {
			Loggers.PUSH.error("[NACOS-PUSH] failed to push serviceName: {} to client, error: {}", serviceName, e);
			
		} finally {
			//移除 future
			futureMap.remove(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));
		}
		
	}, 1000, TimeUnit.MILLISECONDS);
	
	//任务放入 futureMap  表示已经发送了 udp 到客户端的服务实例
	futureMap.put(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName), future);
	
}

PushClient 类

PushClient 类是 PushService 的内部类,PushClient 类封装了要推送的目标服务地址信息等,PushClient 类提供了zombie 方法来判断服务实例是否 zombie(僵尸)节点,根据距离 lastRefTime 的时间差是否超过 switchDomain 指定的该 serviceName 的 PushCacheMillis (默认为10秒),超过则判定为 zombie(僵尸)节点。

//com.alibaba.nacos.naming.push.PushService.PushClient
public class PushClient {
	
	
	private String namespaceId;
	
	private String serviceName;
	
	private String clusters;
	
	private String agent;
	
	private String tenant;
	
	private String app;
	
	private InetSocketAddress socketAddr;
	
	private DataSource dataSource;
	
	private Map<String, String[]> params;
	
	public Map<String, String[]> getParams() {
		return params;
	}
	
	public void setParams(Map<String, String[]> params) {
		this.params = params;
	}
	
	public long lastRefTime = System.currentTimeMillis();
	
	public PushClient(String namespaceId, String serviceName, String clusters, String agent,
			InetSocketAddress socketAddr, DataSource dataSource, String tenant, String app) {
		this.namespaceId = namespaceId;
		this.serviceName = serviceName;
		this.clusters = clusters;
		this.agent = agent;
		this.socketAddr = socketAddr;
		this.dataSource = dataSource;
		this.tenant = tenant;
		this.app = app;
	}
	
	public DataSource getDataSource() {
		return dataSource;
	}
	
	public boolean zombie() {
		return System.currentTimeMillis() - lastRefTime > switchDomain.getPushCacheMillis(serviceName);
	}
	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("serviceName: ").append(serviceName).append(", clusters: ").append(clusters).append(", address: ")
				.append(socketAddr).append(", agent: ").append(agent);
		return sb.toString();
	}
	
	public String getAgent() {
		return agent;
	}
	
	public String getAddrStr() {
		return socketAddr.getAddress().getHostAddress() + ":" + socketAddr.getPort();
	}
	
	public String getIp() {
		return socketAddr.getAddress().getHostAddress();
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(serviceName, clusters, socketAddr);
	}
	
	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof PushClient)) {
			return false;
		}
		
		PushClient other = (PushClient) obj;
		
		return serviceName.equals(other.serviceName) && clusters.equals(other.clusters) && socketAddr
				.equals(other.socketAddr);
	}
	
	public String getClusters() {
		return clusters;
	}
	
	public void setClusters(String clusters) {
		this.clusters = clusters;
	}
	
	public String getNamespaceId() {
		return namespaceId;
	}
	
	public void setNamespaceId(String namespaceId) {
		this.namespaceId = namespaceId;
	}
	
	public String getServiceName() {
		return serviceName;
	}
	
	public void setServiceName(String serviceName) {
		this.serviceName = serviceName;
	}
	
	public String getTenant() {
		return tenant;
	}
	
	public void setTenant(String tenant) {
		this.tenant = tenant;
	}
	
	public String getApp() {
		return app;
	}
	
	public void setApp(String app) {
		this.app = app;
	}
	
	public InetSocketAddress getSocketAddr() {
		return socketAddr;
	}
	
	public void refresh() {
		lastRefTime = System.currentTimeMillis();
	}
	
}

PushService#udpPush 方法源码分析

PushService#udpPush 方法根据 Receiver.AckEntry 的信息进行推送,如果重试次数大于 MAX_RETRY_TIMES(默认只能重试一次) 则终止推送,将其从 ackMap、udpSendTimeMap 中移除,如果可以推送,则将其 ackEntry.key 放入 ackMap、udpSendTimeMap,然后执行推送操作,并将推送次数加1(这里使用了 AtomicInteger 原子类保证线程安全),最后会注册 Retransmitter 延时 10 秒的推送任务,其实就是推送重试,如果这个过程中出现异常则将其从 ackMap、udpSendTimeMap 移除,并将推送失败标识加 1。

//com.alibaba.nacos.naming.push.PushService#udpPush
private static Receiver.AckEntry udpPush(Receiver.AckEntry ackEntry) {
	//ackEntry 为空判断 如果为空 直接返回 不进行  udp 推送
	if (ackEntry == null) {
		Loggers.PUSH.error("[NACOS-PUSH] ackEntry is null.");
		return null;
	}
	//重试次数大于 1 就不在推送了  
	if (ackEntry.getRetryTimes() > MAX_RETRY_TIMES) {
		Loggers.PUSH.warn("max re-push times reached, retry times {}, key: {}", ackEntry.retryTimes, ackEntry.key);
		//从 ackMap 中移除 因为不用推送了
		ackMap.remove(ackEntry.key);
		//从 udpSendTimeMap 移除 因为不用推送了
		udpSendTimeMap.remove(ackEntry.key);
		//推送失败标志 +1 
		failedPush += 1;
		return ackEntry;
	}
	
	try {
		if (!ackMap.containsKey(ackEntry.key)) {
			//ackMap 中没有当前推送的实例对象  推送总次数 +1
			totalPush++;
		}
		//更新 ackMap
		ackMap.put(ackEntry.key, ackEntry);
		//记录推送开始时间
		udpSendTimeMap.put(ackEntry.key, System.currentTimeMillis());
		
		Loggers.PUSH.info("send udp packet: " + ackEntry.key);
		//推送
		udpSocket.send(ackEntry.origin);
		//重试次数+1 AtomicInteger 类 
		ackEntry.increaseRetryTime();
		
		//提交了一个延迟任务 延迟时间 10 秒 Retransmitter 实现了 Runnable 接口 run 方法再次调用了  udpPush 方法
		GlobalExecutor.scheduleRetransmitter(new Retransmitter(ackEntry),
				TimeUnit.NANOSECONDS.toMillis(ACK_TIMEOUT_NANOS), TimeUnit.MILLISECONDS);
		
		return ackEntry;
	} catch (Exception e) {
		Loggers.PUSH.error("[NACOS-PUSH] failed to push data: {} to client: {}, error: {}", ackEntry.data,
				ackEntry.origin.getAddress().getHostAddress(), e);
		//推送异常 
		//从 ackMap 中移除
		ackMap.remove(ackEntry.key);
		//从 udpSendTimeMap 移除
		udpSendTimeMap.remove(ackEntry.key);
		//推送失败标志 +1
		failedPush += 1;
		
		return null;
	}
}

//Retransmitter 实现了 Runnable 接口 run 方法再次调用了  udpPush 方法
//com.alibaba.nacos.naming.push.PushService.Retransmitter
public static class Retransmitter implements Runnable {
	
	Receiver.AckEntry ackEntry;
	
	public Retransmitter(Receiver.AckEntry ackEntry) {
		this.ackEntry = ackEntry;
	}
	
	@Override
	public void run() {
		if (ackMap.containsKey(ackEntry.key)) {
			Loggers.PUSH.info("retry to push data, key: " + ackEntry.key);
			udpPush(ackEntry);
		}
	}
}

PushService.Receiver 类源码分析

在推送流程中有使用了 Receiver 类, Receiver 类是 PushService 的一个内部类,同时 Receiver 类实现了 Runnable 接口,是一个线程任务类,作用是处理 UDP 推送结果的,它的 run 方法使用 while 循环来执行 udpSocket.receive,然后解析 AckPacket,计算推送耗时,从 ackMap 移除该 ackKey,并更新 pushCostMap,同时从 udpSendTimeMap 移除该 ackKey。

public static class Receiver implements Runnable {
	
	@Override
	public void run() {
		while (true) {
			byte[] buffer = new byte[1024 * 64];
			//创建数据包 接受 udp 响应数据
			DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
			
			try {
				//udp 响应
				udpSocket.receive(packet);
				//响应json
				String json = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8).trim();
				//转换为 ACK 响应对象
				AckPacket ackPacket = JacksonUtils.toObj(json, AckPacket.class);
				//获取地址 端口等
				InetSocketAddress socketAddress = (InetSocketAddress) packet.getSocketAddress();
				String ip = socketAddress.getAddress().getHostAddress();
				int port = socketAddress.getPort();
				//当前纳秒时间-最后参考时间 是否大于 ACK_TIMEOUT_NANOS 默认10秒
				if (System.nanoTime() - ackPacket.lastRefTime > ACK_TIMEOUT_NANOS) {
					//记录 ack 时间过长
					Loggers.PUSH.warn("ack takes too long from {} ack json: {}", packet.getSocketAddress(), json);
				}
				//获取 ackKey
				String ackKey = getAckKey(ip, port, ackPacket.lastRefTime);
				//从 ackMap 中移除
				AckEntry ackEntry = ackMap.remove(ackKey);
				if (ackEntry == null) {
					//为空 抛出无法找到异常
					throw new IllegalStateException(
							"unable to find ackEntry for key: " + ackKey + ", ack json: " + json);
				}
				//计算推送耗时
				long pushCost = System.currentTimeMillis() - udpSendTimeMap.get(ackKey);
				
				Loggers.PUSH
						.info("received ack: {} from: {}:{}, cost: {} ms, unacked: {}, total push: {}", json, ip,
								port, pushCost, ackMap.size(), totalPush);
				//存入 map
				pushCostMap.put(ackKey, pushCost);
				
				//从 udpSendTimeMap 中移除
				udpSendTimeMap.remove(ackKey);
				
			} catch (Throwable e) {
				Loggers.PUSH.error("[NACOS-PUSH] error while receiving ack data", e);
			}
		}
	}
	
	public static class AckEntry {
		
		public AckEntry(String key, DatagramPacket packet) {
			this.key = key;
			this.origin = packet;
		}
		
		public void increaseRetryTime() {
			retryTimes.incrementAndGet();
		}
		
		public int getRetryTimes() {
			return retryTimes.get();
		}
		
		public String key;
		
		public DatagramPacket origin;
		
		private AtomicInteger retryTimes = new AtomicInteger(0);
		
		public Map<String, Object> data;
	}
	
	public static class AckPacket {
		
		public String type;
		
		public long lastRefTime;
		
		public String data;
	}
}

Receiver 类的 run 方法中使用了几个 Map,这几个 Map 都是 PushService 类的静态变量,这里单独解析一下,如下:

//存放所有已经发送了 UDP 但还没收到客户端的 ACK 响应的数据包
private static volatile ConcurrentMap<String, Receiver.AckEntry> ackMap = new ConcurrentHashMap<>();

//存放的是所有的 UDP 客户端 nacos 服务端需要往客户端推送数据 所以需要知道有哪些客户端 那这些客户端数据又是怎么来的?
private static ConcurrentMap<String, ConcurrentMap<String, PushClient>> clientMap = new ConcurrentHashMap<>();

//存放每个数据包开始发送的时间
private static volatile ConcurrentMap<String, Long> udpSendTimeMap = new ConcurrentHashMap<>();

//存放每个数据包的推送耗时
public static volatile ConcurrentMap<String, Long> pushCostMap = new ConcurrentHashMap<>();

PushService 类的静态代码块

Nacos 除了在服务实例状态发生变更时候移出僵尸客户端,还会在 PushService 类被加载初始化的的时候,通过静态代码块的方式开启一个守护线程(Receiver ),来处理来自 Nocos Client 的 ack ,同时也会开启一个定时任务,每隔 20 执行一次检查,看是否有僵尸客户端,如果有则移出僵尸客户端。

//PushService 类的静态代码块
static {
	try {
		//udp 数据包
		udpSocket = new DatagramSocket();
		//接受者对象
		Receiver receiver = new Receiver();
		//创建一个线程
		Thread inThread = new Thread(receiver);
		//设置为为守护线程
		inThread.setDaemon(true);
		inThread.setName("com.alibaba.nacos.naming.push.receiver");
		//启动 receiver 线程 循环接收来自客户端的 ack 
		inThread.start();
		//启动定时任务 20 秒执行一次 主要是移除僵尸客户端
		GlobalExecutor.scheduleRetransmitter(() -> {
			try {
				//移出僵尸客户端
				removeClientIfZombie();
			} catch (Throwable e) {
				Loggers.PUSH.warn("[NACOS-PUSH] failed to remove client zombie");
			}
		}, 0, 20, TimeUnit.SECONDS);
		
	} catch (SocketException e) {
		Loggers.SRV_LOG.error("[NACOS-PUSH] failed to init push service");
	}
}

//com.alibaba.nacos.naming.push.PushService#removeClientIfZombie
private static void removeClientIfZombie() {
	
	int size = 0;
	//获取所有客户端对象
	for (Map.Entry<String, ConcurrentMap<String, PushClient>> entry : clientMap.entrySet()) {
		ConcurrentMap<String, PushClient> clientConcurrentMap = entry.getValue();
		for (Map.Entry<String, PushClient> entry1 : clientConcurrentMap.entrySet()) {
			PushClient client = entry1.getValue();
			//距离最后一次心跳大于10 秒 则为僵尸客户端
			if (client.zombie()) {
				//移除僵尸客户端
				clientConcurrentMap.remove(entry1.getKey());
			}
		}
		
		size += clientConcurrentMap.size();
	}
	
	if (Loggers.PUSH.isDebugEnabled()) {
		Loggers.PUSH.debug("[NACOS-PUSH] clientMap size: {}", size);
	}
	
}

UDP 推送客户端何时初始化的?

我们知道 udpPush(ackEntry) 这行代码通过 UDP 通知 Client 的,那 ackEntry 对象肯定包含了 Client 信息,udpPush(ackEntry) 这行代码是在 PushService#onApplicationEvent 方法中调用的,在 PushService#onApplicationEvent 方法中我们找到了如下代码:

if (compressData != null) {
	ackEntry = prepareAckEntry(client, compressData, data, lastRefTime);
} else {
	//重点关注  prepareHostsData(client) 方法
	ackEntry = prepareAckEntry(client, prepareHostsData(client), lastRefTime);
	if (ackEntry != null) {
		cache.put(key, new org.javatuples.Pair<>(ackEntry.origin.getData(), ackEntry.data));
	}
}

//com.alibaba.nacos.naming.push.PushService#prepareHostsData
private static Map<String, Object> prepareHostsData(PushClient client) throws Exception {
	Map<String, Object> cmd = new HashMap<String, Object>(2);
	cmd.put("type", "dom");
	//重点关注 client.getDataSource().getData(client) 这行代码
	cmd.put("data", client.getDataSource().getData(client));
	
	return cmd;
}



//com.alibaba.nacos.naming.push.DataSource#getData
@Override
public String getData(PushService.PushClient client) {
	
	ObjectNode result = JacksonUtils.createEmptyJsonNode();
	try {
		//重点关注这行代码
		result = doSrvIpxt(client.getNamespaceId(), client.getServiceName(), client.getAgent(),
				client.getClusters(), client.getSocketAddr().getAddress().getHostAddress(), 0,
				StringUtils.EMPTY, false, StringUtils.EMPTY, StringUtils.EMPTY, false);
	} catch (Exception e) {
		String serviceNameField = "name";
		String lastRefTimeField = "lastRefTime";
		if (result.get(serviceNameField) == null) {
			String serviceName = client.getServiceName();
			if (serviceName == null) {
				serviceName = StringUtils.trimToEmpty(serviceName);
			}
			result.put(serviceNameField, serviceName);
			result.put(lastRefTimeField, System.currentTimeMillis());
		}
		Loggers.SRV_LOG.warn("PUSH-SERVICE: service is not modified", e);
	}
	
	// overdrive the cache millis to push mode
	result.put("cacheMillis", switchDomain.getPushCacheMillis(client.getServiceName()));
	
	return result.toString();
}


//com.alibaba.nacos.naming.controllers.InstanceController#doSrvIpxt
public ObjectNode doSrvIpxt(String namespaceId, String serviceName, String agent, String clusters, String clientIP,
		int udpPort, String env, boolean isCheck, String app, String tid, boolean healthyOnly) throws Exception {
	
	ClientInfo clientInfo = new ClientInfo(agent);
	ObjectNode result = JacksonUtils.createEmptyJsonNode();
	Service service = serviceManager.getService(namespaceId, serviceName);
	long cacheMillis = switchDomain.getDefaultCacheMillis();
	
	// now try to enable the push
	try {
		if (udpPort > 0 && pushService.canEnablePush(agent)) {
			//重点关注这行代码
			pushService
					.addClient(namespaceId, serviceName, clusters, agent, new InetSocketAddress(clientIP, udpPort),
							pushDataSource, tid, app);
			cacheMillis = switchDomain.getPushCacheMillis(serviceName);
		}
	} catch (Exception e) {
		Loggers.SRV_LOG
				.error("[NACOS-API] failed to added push client {}, {}:{}", clientInfo, clientIP, udpPort, e);
		cacheMillis = switchDomain.getDefaultCacheMillis();
	}
	
	if (service == null) {
		if (Loggers.SRV_LOG.isDebugEnabled()) {
			Loggers.SRV_LOG.debug("no instance to serve for service: {}", serviceName);
		}
		result.put("name", serviceName);
		result.put("clusters", clusters);
		result.put("cacheMillis", cacheMillis);
		result.replace("hosts", JacksonUtils.createEmptyArrayNode());
		return result;
	}
	
	checkIfDisabled(service);
	
	List<Instance> srvedIPs;
	
	srvedIPs = service.srvIPs(Arrays.asList(StringUtils.split(clusters, ",")));
	
	// filter ips using selector:
	if (service.getSelector() != null && StringUtils.isNotBlank(clientIP)) {
		srvedIPs = service.getSelector().select(clientIP, srvedIPs);
	}
	
	if (CollectionUtils.isEmpty(srvedIPs)) {
		
		if (Loggers.SRV_LOG.isDebugEnabled()) {
			Loggers.SRV_LOG.debug("no instance to serve for service: {}", serviceName);
		}
		
		if (clientInfo.type == ClientInfo.ClientType.JAVA
				&& clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {
			result.put("dom", serviceName);
		} else {
			result.put("dom", NamingUtils.getServiceName(serviceName));
		}
		
		result.put("name", serviceName);
		result.put("cacheMillis", cacheMillis);
		result.put("lastRefTime", System.currentTimeMillis());
		result.put("checksum", service.getChecksum());
		result.put("useSpecifiedURL", false);
		result.put("clusters", clusters);
		result.put("env", env);
		result.set("hosts", JacksonUtils.createEmptyArrayNode());
		result.set("metadata", JacksonUtils.transferToJsonNode(service.getMetadata()));
		return result;
	}
	
	Map<Boolean, List<Instance>> ipMap = new HashMap<>(2);
	ipMap.put(Boolean.TRUE, new ArrayList<>());
	ipMap.put(Boolean.FALSE, new ArrayList<>());
	
	for (Instance ip : srvedIPs) {
		ipMap.get(ip.isHealthy()).add(ip);
	}
	
	if (isCheck) {
		result.put("reachProtectThreshold", false);
	}
	
	double threshold = service.getProtectThreshold();
	
	if ((float) ipMap.get(Boolean.TRUE).size() / srvedIPs.size() <= threshold) {
		
		Loggers.SRV_LOG.warn("protect threshold reached, return all ips, service: {}", serviceName);
		if (isCheck) {
			result.put("reachProtectThreshold", true);
		}
		
		ipMap.get(Boolean.TRUE).addAll(ipMap.get(Boolean.FALSE));
		ipMap.get(Boolean.FALSE).clear();
	}
	
	if (isCheck) {
		result.put("protectThreshold", service.getProtectThreshold());
		result.put("reachLocalSiteCallThreshold", false);
		
		return JacksonUtils.createEmptyJsonNode();
	}
	
	ArrayNode hosts = JacksonUtils.createEmptyArrayNode();
	
	for (Map.Entry<Boolean, List<Instance>> entry : ipMap.entrySet()) {
		List<Instance> ips = entry.getValue();
		
		if (healthyOnly && !entry.getKey()) {
			continue;
		}
		
		for (Instance instance : ips) {
			
			// remove disabled instance:
			if (!instance.isEnabled()) {
				continue;
			}
			
			ObjectNode ipObj = JacksonUtils.createEmptyJsonNode();
			
			ipObj.put("ip", instance.getIp());
			ipObj.put("port", instance.getPort());
			// deprecated since nacos 1.0.0:
			ipObj.put("valid", entry.getKey());
			ipObj.put("healthy", entry.getKey());
			ipObj.put("marked", instance.isMarked());
			ipObj.put("instanceId", instance.getInstanceId());
			ipObj.set("metadata", JacksonUtils.transferToJsonNode(instance.getMetadata()));
			ipObj.put("enabled", instance.isEnabled());
			ipObj.put("weight", instance.getWeight());
			ipObj.put("clusterName", instance.getClusterName());
			if (clientInfo.type == ClientInfo.ClientType.JAVA
					&& clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {
				ipObj.put("serviceName", instance.getServiceName());
			} else {
				ipObj.put("serviceName", NamingUtils.getServiceName(instance.getServiceName()));
			}
			
			ipObj.put("ephemeral", instance.isEphemeral());
			hosts.add(ipObj);
			
		}
	}
	
	result.replace("hosts", hosts);
	if (clientInfo.type == ClientInfo.ClientType.JAVA
			&& clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {
		result.put("dom", serviceName);
	} else {
		result.put("dom", NamingUtils.getServiceName(serviceName));
	}
	result.put("name", serviceName);
	result.put("cacheMillis", cacheMillis);
	result.put("lastRefTime", System.currentTimeMillis());
	result.put("checksum", service.getChecksum());
	result.put("useSpecifiedURL", false);
	result.put("clusters", clusters);
	result.put("env", env);
	result.replace("metadata", JacksonUtils.transferToJsonNode(service.getMetadata()));
	return result;
}


//com.alibaba.nacos.naming.push.PushService#addClient(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.net.InetSocketAddress, com.alibaba.nacos.naming.push.DataSource, java.lang.String, java.lang.String)
public void addClient(String namespaceId, String serviceName, String clusters, String agent,
		InetSocketAddress socketAddr, DataSource dataSource, String tenant, String app) {
	
	PushClient client = new PushClient(namespaceId, serviceName, clusters, agent, socketAddr, dataSource, tenant,
			app);
	//重点关注
	addClient(client);
}
    
//com.alibaba.nacos.naming.push.PushService#addClient(com.alibaba.nacos.naming.push.PushService.PushClient)
public void addClient(PushClient client) {
	// client is stored by key 'serviceName' because notify event is driven by serviceName change
	String serviceKey = UtilsAndCommons.assembleFullServiceName(client.getNamespaceId(), client.getServiceName());
	ConcurrentMap<String, PushClient> clients = clientMap.get(serviceKey);
	if (clients == null) {
		//重点关注
		clientMap.putIfAbsent(serviceKey, new ConcurrentHashMap<>(1024));
		clients = clientMap.get(serviceKey);
	}
	
	PushClient oldClient = clients.get(client.toString());
	if (oldClient != null) {
		oldClient.refresh();
	} else {
		PushClient res = clients.putIfAbsent(client.toString(), client);
		if (res != null) {
			Loggers.PUSH.warn("client: {} already associated with key {}", res.getAddrStr(), res.toString());
		}
		Loggers.PUSH.debug("client: {} added for serviceName: {}", client.getAddrStr(), client.getServiceName());
	}
}



跟踪了这么多代码,只能说往 clientMap 存入了客户端,好像并不能说明这是 clientMap 的初始化,在 PushService#onApplicationEvent 中开始就遍历了 clientMap,很好奇这个 clientMap 是什么时候初始化的,希望各位大佬能够帮我解答。。。。。。

总结:

本篇分析的代码基本都是在 PushService 类中,主要围绕了 UDP 客户端维护,UDP 客户端推送, UDP 客户端接收这三大块展开,其中大量使用定时任务调度和异步线程。

UDP 客户端维护:

PushService 类通过静态代码块的方式开启一个定时任务(20秒一次),专门用来维护 clientMap(存放所有需要进行 UDP 推送的客户端),如果户端从初始化到响应 ACK 的时间间隔超过了10秒,就判定为僵尸客户端,从 clientMap 中移除,下次就不会再往这个客户端推送了。

UDP 客户端推送:

当服务端注册表中实例发送了变更时,会发布 ServiceChangeEvent 事件,PushService#onApplicationEvent 会监听,监听到之后就会向客户端推送通知,如果发送失败或者超过10秒没收到 ack,就会间隔10s进行重试,默认重试一次,超过1次就不再发送。

UDP 客户端接收:

PushService 类通过静态代码块的方式开启了一个守护线程 Receiver,用于循环接收来自客户端的 ack,使用 ackMap 维护所有已发送 UDP 包但还没有进行ack响应的包,如果接收到客户端的 ack 响应,就从 ackMap 中移除,并更新 pushCostMap,同时从 udpSendTimeMap 移除该 ackKey。

欢迎提出建议及对错误的地方指出纠正。

  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值