目录
(1)client接收udp通知
结合前面的文章,client在服务发现的时候,每次请求的时候会传递自己的clientIp+udpPort, 并且后续会定时去再次拉取服务实例信息。naming在发现服务实例有变化的时候,会通过clientIp+udpPort反向通知client,参考:2. nacos之服务注册_ZXH240651200的博客-CSDN博客 中的小节:(3.3) 服务变化时通过udp发送通知
接收处理udp通知的是PushReceiver。
启动:com.alibaba.nacos.client.naming.NacosNamingService#NacosNamingService(java.util.Properties)
--> com.alibaba.nacos.client.naming.NacosNamingService#init
--> com.alibaba.nacos.client.naming.core.HostReactor#HostReactor(com.alibaba.nacos.client.naming.net.NamingProxy, com.alibaba.nacos.client.naming.beat.BeatReactor, java.lang.String, boolean, boolean, int)
-->
this.pushReceiver = new PushReceiver(this);
public PushReceiver(HostReactor hostReactor) {
try {
this.hostReactor = hostReactor;
// 获取udp端口号配置
String udpPort = getPushReceiverUdpPort();
// 没有配置,就使用随机的端口号
if (StringUtils.isEmpty(udpPort)) {
this.udpSocket = new DatagramSocket();
} else {
this.udpSocket = new DatagramSocket(new InetSocketAddress(Integer.parseInt(udpPort)));
}
logger.info("========#PushReciver 启动upd监听:{}", udpSocket.getLocalPort());
this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("com.alibaba.nacos.naming.push.receiver");
return thread;
}
});
// 将自己提交到线程池,即启动
this.executorService.execute(this);
} catch (Exception e) {
NAMING_LOGGER.error("[NA] init udp socket failed", e);
}
}
public void run() {
while (!closed) {
try {
// byte[] is initialized with 0 full filled by default
byte[] buffer = new byte[UDP_MSS];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// 接收数据包
udpSocket.receive(packet);
String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
logger.info("#PushReceiver.run#upd socket 接收到通知:{}", json);
PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
String ack;
if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
// 处理服务类型的通知
hostReactor.processServiceJson(pushPacket.data);
// send ack to server
ack = "{\"type\": \"push-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime + "\", \"data\":"
+ "\"\"}";
} else if ("dump".equals(pushPacket.type)) {
// dump data to server
ack = "{\"type\": \"dump-ack\"" + ", \"lastRefTime\": \"" + pushPacket.lastRefTime + "\", \"data\":"
+ "\"" + StringUtils.escapeJavaScript(JacksonUtils.toJson(hostReactor.getServiceInfoMap()))
+ "\"}";
} else {
// do nothing send ack only
ack = "{\"type\": \"unknown-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\"\"}";
}
// 发送ack给naming
udpSocket.send(new DatagramPacket(ack.getBytes(UTF_8), ack.getBytes(UTF_8).length,
packet.getSocketAddress()));
} catch (Exception e) {
if (closed) {
return;
}
NAMING_LOGGER.error("[NA] error while receiving push data", e);
}
}
}
在接收到通知之后,走更新本地缓存更新的逻辑,然后发送ack给naming.
(2)naming处理ack
com/alibaba/nacos/naming/push/PushService.java
static {
try {
// 构建udpsocket
udpSocket = new DatagramSocket();
// 构造receiver线程
Receiver receiver = new Receiver();
Thread inThread = new Thread(receiver);
inThread.setDaemon(true);
inThread.setName("com.alibaba.nacos.naming.push.receiver");
inThread.start();
// 启动定时任务,定时清理长时间未更新的pushClient,
// 默认20s执行一次,超过10s就会认为超时,pushClient会被移除
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");
}
}
pushService通过静态代码块,初始化了udpsocket和处理线程,并且启动了一个线程去清理长时间未更新的pushClient
移除长时间未更细的pushClient
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();
// client的udp信息,长时间未更新,默认是10s,移除pushClient
if (client.zombie()) {
clientConcurrentMap.remove(entry1.getKey());
}
}
size += clientConcurrentMap.size();
}
if (Loggers.PUSH.isDebugEnabled()) {
Loggers.PUSH.debug("[NACOS-PUSH] clientMap size: {}", size);
}
}
处理ack
com.alibaba.nacos.naming.push.PushService.Receiver#run
public void run() {
while (true) {
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
// 接收消息
udpSocket.receive(packet);
String json = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8).trim();
// 转成ackPacket
AckPacket ackPacket = JacksonUtils.toObj(json, AckPacket.class);
InetSocketAddress socketAddress = (InetSocketAddress) packet.getSocketAddress();
String ip = socketAddress.getAddress().getHostAddress();
int port = socketAddress.getPort();
// 是否超时
if (System.nanoTime() - ackPacket.lastRefTime > ACK_TIMEOUT_NANOS) {
Loggers.PUSH.warn("ack takes too long from {} ack json: {}", packet.getSocketAddress(), json);
}
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);
// 保存push到ack的耗时
pushCostMap.put(ackKey, pushCost);
// 发送时间map中删除
udpSendTimeMap.remove(ackKey);
} catch (Throwable e) {
Loggers.PUSH.error("[NACOS-PUSH] error while receiving ack data", e);
}
}
接收到ack之后,会从ackMap中移除key,累计push耗时,从udpSendTimeMap中移除key
(3)naming处理ack超时
naming在发送udp数据包的时候,顺带提交了一个异步任务:
// 10s后,如果ackMap中,还有ackentry.key, 就重试发送
GlobalExecutor.scheduleRetransmitter(new Retransmitter(ackEntry),
TimeUnit.NANOSECONDS.toMillis(ACK_TIMEOUT_NANOS), TimeUnit.MILLISECONDS);
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);
}
}
}
如果10s,待接收ack的ackMap中还存在key,就会再次发送一次udp通知,通知累计重试次数,最大重试次数:MAX_RETRY_TIMES=1