欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
0. 环境
- nacos版本:1.4.1
- Spring Cloud : 2020.0.2
- Spring Boot :2.4.4
- Spring Cloud alibaba: 2.2.5.RELEASE
在一文中我们介绍了nacos两种服务发现的方式,一种是直接去nacos服务器上拉取实例信息,另一种是订阅拉取,就是拉取的时候,订阅这个服务,一旦你订阅的服务实例发生变化,nacos服务端就会通知你。在上篇中关于服务实例变化,nacos服务端通知客户端这块内容我们还没有介绍,本文将介绍下服务变化通知这块的代码实现。
1. 关于客户端服务订阅的补充
在服务发现上篇中,我们只是使用了NamingService的getAllInstances 方法作为了服务发现获取服务实例列表的方式,其实nacos还支持订阅方式,通过提供订阅的服务名称与监听器(listener )就可以了,然后订阅的服务实例信息一旦发生变化,nacos服务端就会通知你这个client,client收到通知后就会调用你这监听器,执行对应的逻辑。
NamingService naming = NamingFactory.createNamingService(properties);
// 订阅orderService 服务
naming.subscribe("orderService", new EventListener() {
@Override
public void onEvent(Event event) {
System.out.println(((NamingEvent) event).getServiceName());
System.out.println(((NamingEvent) event).getInstances());
}
});
就是上面这个样子的,直接调用subscribe 的方法就对该服务订阅了,一旦“orderService”服务实例信息发生变化,nacos服务端就会使用udp协议通知你这个client,client收到通知后,判断与本地缓存的服务实例信息有没有变化,如果有变化的话,就会调用你这个事件监听器。
我们看下这个subscribe 方法背后的实现源码:
public void subscribe(String serviceName, String clusters, EventListener eventListener) {
notifier.registerListener(serviceName, clusters, eventListener);
getServiceInfo(serviceName, clusters);
}
首先 就是nacos服务端通知过来之后,client先会跟本地缓存的服务实例信息做一下比较,如果发生了变化,就会整一个服务实例变化的通知给EventDispatcher 组件,这个组件就会根据服务信息,然后找到对应的listener ,进行执行,它其实就是管着 listener 注册与调用listener 执行调用的。知道EventDispatcher组件作用之后,它的addListener方法就很简单了,就是将订阅服务信息与listener 放到一个map中。
public void registerListener(String serviceName, String clusters, EventListener listener) {
String key = ServiceInfo.getKey(serviceName, clusters);
ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
if (eventListeners == null) {
synchronized (lock) {
eventListeners = listenerMap.get(key);
if (eventListeners == null) {
eventListeners = new ConcurrentHashSet<EventListener>();
// 加入观察者map中
listenerMap.put(key, eventListeners);
}
}
}
eventListeners.add(listener);
}
然后 找到hostReactor 组件获取一下这个serviceInfo信息,这块代码是不是很熟悉,我们在上篇专门解析过,就是在getAllInstances 方法中需要订阅那个代码块中
就是这里,这个getServiceInfo方法主要就是看看本地有没有这个serviceInfo ,如果没有的话,就创建一个,并且立马向服务端拉取这个服务实例信息,另外带着订阅。然后就是将这个订阅搞个任务出来,定时的去服务端拉拉看看,这个任务就是防止某个nacos服务端实例挂了导致不能及时通知了,它时不时的去服务端刷新下订阅信息,就算是某个nacos 服务挂掉了,它也可以请求其他的nacos服务继续拉取实例,同时也告诉下nacos 服务端我这个客户端还活着,维护者一个连接那种感觉。这一块详细代码我们就不看了,主要是在上篇中已经详细介绍过了。
2. 服务端异步通知
在服务注册与服务下线时候,都是先找到ConsistencyService 一致性服务将实例信息存储到对应的存储器中,我们这里主要介绍临时节点的一致性服务,也就是找到DistroConsistencyServiceImpl 这个组件,将某个service下的所有实例信息存储到了DataStore(其实就是个map)里面去了,并且会创建一个任务放到一个队列中,整完就响应给客户端了。然后DistroConsistencyServiceImpl 有个Notifier 组件会不停的从队列中获取任务,执行任务,其实就是找到对应服务的listener进行通知,说你这个服务的实例信息发生了变化,就是调用service的onChange方法进行通知,好让这个service修改下自己里面维护的信息。在修改完的时候会有这么一行代码getPushService().serviceChanged(this);,这行代码就是找到Push服务,然后向那些订阅了这个服务的客户端推送最新的服务实例信息。
先来介绍下PushService 组件
2.1 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");
inThread.start();
// 20s执行一次
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");
}
}
先是创建了一个udp的socket,启动一个线程,执行Receiver 任务,这个任务其实就是接收客户端ack的,你给客户端推送过去了最新的实例信息,然后客户端收到之后也得给你个ack。接着就是启动一个定时任务20s一次,清理下僵尸客户端,就是处理掉几秒没刷新订阅的客户端,默认是10s。
我们看下这个serviceChange 服务改变方法:
public void serviceChanged(Service service) {
// merge some change events to reduce the push frequency:
// 合并一些变更事件减少推送频率
if (futureMap
.containsKey(UtilsAndCommons.assembleFullServiceName(service.getNamespaceId(), service.getName()))) {
return;
}
// 发布服务变更事件
this.applicationContext.publishEvent(new ServiceChangeEvent(this, service));
}
使用spring的事件通知发布了一个事件,PushService 这个服务其实也是一个ServiceChangeEvent 事件的观察者,实现了onApplicationEvent 方法。
我们看下它的具体实现,其实就是先生成一个future,然后将future放到一个任务调度线程池中执行,延迟1s,再把future放到这个futureMap中,也就是上面serviceChange方法里面那个判断future。看下这个future。
@Override
public void onApplicationEvent(ServiceChangeEvent event) {
Service service = event.getService();
String serviceName = service.getName();
String namespaceId = service.getNamespaceId();
// 启动一个定时操作,异步执行相关内容
Future future = GlobalExecutor.scheduleUdpSender(() -> {
try {
Loggers.PUSH.info(serviceName + " is changed, add it to push queue.");
// 从缓存map中获取当前服务的内层map,内层map中存放着当前服务的所有Nacos Client的
// UDP客户端PushClient
ConcurrentMap<String, PushClient> clients = clientMap
.get(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));
if (MapUtils.isEmpty(clients)) {
return;
}
Map<String, Object> cache = new HashMap<>(16);
// 更新最后引用时间
long lastRefTime = System.nanoTime();
// 遍历所有PushClient,向所有该服务的订阅者Nacos Client进行UDP推送
for (PushClient client : clients.values()) {
// 若当前PushClient为僵尸客户端
if (client.zombie()) {
Loggers.PUSH.debug("client is zombie: " + client.toString());
// 将该PushClient干掉
clients.remove(client.toString());
Loggers.PUSH.debug("client is zombie: " + client.toString());
continue;
}
Receiver.AckEntry ackEntry;
Loggers.PUSH.debug("push serviceName: {} to client: {}", serviceName, client.toString());
String key = getPushCacheKey(serviceName, client.getIp(), client.getAgent());
byte[] compressData = null;
Map<String, Object> data = null;
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());
}
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));
// todo UDP通信
udpPush(ackEntry);
}
} catch (Exception e) {
Loggers.PUSH.error("[NACOS-PUSH] failed to push serviceName: {} to client, error: {}", serviceName, e);
} finally {
futureMap.remove(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));
}
}, 1000, TimeUnit.MILLISECONDS);
futureMap.put(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName), future);
}
先是去clientMap这个订阅客户端map中获取这个服务的一堆订阅客户端。然后遍历这堆订阅者,先从缓存中获取,默认是不启用这个缓存的,没有的话,就自己准备这个数据,放入缓存一份, 最后是调用udpPush进行推送。
private static Receiver.AckEntry udpPush(Receiver.AckEntry ackEntry) {
if (ackEntry == null) {
Loggers.PUSH.error("[NACOS-PUSH] ackEntry is null.");
return null;
}
// 若UDP通信重试次数超出了最大阈值,则将该UDP通信从两个缓存map中干掉
if (ackEntry.getRetryTimes() > MAX_RETRY_TIMES) {
Loggers.PUSH.warn("max re-push times reached, retry times {}, key: {}", ackEntry.retryTimes, ackEntry.key);
ackMap.remove(ackEntry.key);
udpSendTimeMap.remove(ackEntry.key);
// 失败计数器加一
failedPush += 1;
return ackEntry;
}
try {
// 计数器加一
if (!ackMap.containsKey(ackEntry.key)) {
totalPush++;
}
ackMap.put(ackEntry.key, ackEntry);
udpSendTimeMap.put(ackEntry.key, System.currentTimeMillis());
Loggers.PUSH.info("send udp packet: " + ackEntry.key);
// todo 发送UDP
udpSocket.send(ackEntry.origin);
ackEntry.increaseRetryTime();
// 开启定时任务,进行UPD通信失败后的重新推送
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.remove(ackEntry.key);
udpSendTimeMap.remove(ackEntry.key);
failedPush += 1;
return null;
}
}
使用udpSocket进行推送,失败就重试,默认是重试1次的。
我们再来看下Receiver 的实现,它就是接收client ack的一个任务。
public static class Receiver implements Runnable {
@Override
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 = JacksonUtils.toObj(json, AckPacket.class);
InetSocketAddress socketAddress = (InetSocketAddress) packet.getSocketAddress();
String ip = socketAddress.getAddress().getHostAddress();
int port = socketAddress.getPort();
// 超过10s ack超时
if (System.nanoTime() - ackPacket.lastRefTime > ACK_TIMEOUT_NANOS) {
Loggers.PUSH.warn("ack takes too long from {} ack json: {}", packet.getSocketAddress(), json);
}
// ackKey
String ackKey = getAckKey(ip, port, ackPacket.lastRefTime);
// 移除
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);
// 推送耗时
pushCostMap.put(ackKey, pushCost);
// 移除推送事件
udpSendTimeMap.remove(ackKey);
} catch (Throwable e) {
Loggers.PUSH.error("[NACOS-PUSH] error while receiving ack data", e);
}
}
}
就是接收消息,进行序列化啥的。后面的一堆都是一些指标统计的东西。 接下来我们看下客户端收到通知是怎么处理的吧。
3. 客户端处理通知
在HostReactor组件中,有一个PushReceiver 是在HostReactor 实例化的时候创建的。先来看下这个PushReceiver组件
3.1 PushReceiver
public PushReceiver(HostReactor hostReactor) {
try {
this.hostReactor = hostReactor;
// 开启一个udpSocket
this.udpSocket = new DatagramSocket();
// 创建一个线程池
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;
}
});
// 异步执行当前PushReceiver任务,执行run方法
this.executorService.execute(this);
} catch (Exception e) {
NAMING_LOGGER.error("[NA] init udp socket failed", e);
}
}
在实例化的时候,开启一个udpSocket,然后整了一个调度线程池,将自己作为一个任务扔进去了。
@Override
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);
// 接收来自Nacos Server的UDP推送的数据,并封装到packet数据包中
udpSocket.receive(packet);
// 将数据包中的数据解码为JSON串
String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
// 将JSON串封装为PushPacket
PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
// 根据不同的数据类型,形成不现的ack
String ack;
if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
// todo 将来自Nacos Server发生变更的Service 更新到当前Nacos Client的本地注册表
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":" + """}";
}
// 向推送数据的Nacos Server进行响应(UDP推送)
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);
}
}
}
就是等着接收请求,然后对接收过来的信息进行反序列化,如果type是dom或者是service的话就会交给hostReactor组件的processServiceJson 来处理数据。最后生成ack 返回给nacos服务端。
我们看下这个hostReactor的processServiceJson 方法是怎样处理的
3.2 HostReactor#processServiceJson
关于这个方法我们不细看,它其实就是新的服务实例信息替换下本地缓存的那个服务实例信息,然后拿新通知过来的服务实例列表信息与 本地缓存的一份实例列表信息进行比较,看看有没有发生变化,如果有的话,就通知这个NotifyCenter。
// todo 来自Server的数据是最新 数据
public ServiceInfo processServiceJson(String json) {
...
// 当前注册表中存在该服务,想办法将来自server端的数据更新到本地注册表中
if (oldService != null) {
// 来自server的serviceInfo替换到注册表中的当前服务
serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
...
// 只要发生了变更,就将这个发生变更的serviceInfo记录到一个缓存队列
if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
// todo
NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
serviceInfo.getClusters(), serviceInfo.getHosts()));
DiskCache.write(serviceInfo, cacheDir);
}
// 本地注册表中没有这个服务,直接将来自Server的serviceInfo写入到本地注册表
} else {
...
// 将这个发生变更的serviceInfo记录到一个缓存队列
NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
serviceInfo.getClusters(), serviceInfo.getHosts()));
serviceInfo.setJsonFromServer(json);
DiskCache.write(serviceInfo, cacheDir);
}
...
return serviceInfo;
}
3.3 NotifyCenter.publishEvent
public static boolean publishEvent(final Event event) {
try {
// todo
return publishEvent(event.getClass(), event);
} catch (Throwable ex) {
LOGGER.error("There was an exception to the message publishing : {}", ex);
return false;
}
}
private static boolean publishEvent(final Class<? extends Event> eventType, final Event event) {
if (ClassUtils.isAssignableFrom(SlowEvent.class, eventType)) {
return INSTANCE.sharePublisher.publish(event);
}
final String topic = ClassUtils.getCanonicalName(eventType);
EventPublisher publisher = INSTANCE.publisherMap.get(topic);
if (publisher != null) {
// todo
return publisher.publish(event);
}
LOGGER.warn("There are no [{}] publishers for this event, please register", topic);
return false;
}
// com.alibaba.nacos.common.notify.DefaultPublisher#publish
@Override
public boolean publish(Event event) {
checkIsStart();
// todo 加入到队列中
boolean success = this.queue.offer(event);
if (!success) {
LOGGER.warn("Unable to plug in due to interruption, synchronize sending time, event : {}", event);
receiveEvent(event);
return true;
}
return true;
}
这个就是将serviceInfo信息添加到一个队列中,DefaultPublisher 组件中有个Notifier 的任务会不断从这个队列中获取serviceInfo ,通知那些订阅这个服务的listener。
@Override
public void run() {
openEventHandler();
}
void openEventHandler() {
try {
// This variable is defined to resolve the problem which message overstock in the queue.
int waitTimes = 60;
// To ensure that messages are not lost, enable EventHandler when
// waiting for the first Subscriber to register
for (; ; ) {
if (shutdown || hasSubscriber() || waitTimes <= 0) {
break;
}
ThreadUtils.sleep(1000L);
waitTimes--;
}
for (; ; ) {
if (shutdown) {
break;
}
// 从队列中获取
final Event event = queue.take();
// todo
receiveEvent(event);
UPDATER.compareAndSet(this, lastEventSequence, Math.max(lastEventSequence, event.sequence()));
}
} catch (Throwable ex) {
LOGGER.error("Event listener exception : {}", ex);
}
}
void receiveEvent(Event event) {
final long currentEventSequence = event.sequence();
// Notification single event listener
// 遍历
for (Subscriber subscriber : subscribers) {
// Whether to ignore expiration events
if (subscriber.ignoreExpireEvent() && lastEventSequence > currentEventSequence) {
LOGGER.debug("[NotifyCenter] the {} is unacceptable to this subscriber, because had expire",
event.getClass());
continue;
}
// Because unifying smartSubscriber and subscriber, so here need to think of compatibility.
// Remove original judge part of codes.
// 通知订阅者
notifySubscriber(subscriber, event);
}
}
可以看到就是从这个队列中取出serviceInfo,然后从观察者map中获取对应的lisenter集合,然后遍历这个集合,进行事件的通知。
4. nacos服务发现在dubbo中是怎样使用的
这里还要提一下nacos作为dubbo的注册中心是怎样服务发现的。
其实是使用的getAllInstances 与subscribe 结合的方式进行服务发现的。我们可以稍微看下源码
先是使用的getAllInstances 获取这个服务的实例信息,然后通知一下订阅者,接着就是向nacos服务端订阅这个服务信息
生成一个
eventListener ,然后调用subscribe进行订阅。
5. 总结
到这我们服务发现就介绍完了,本篇主要是介绍了下服务实例改变后nacos使用udp协议通知那些订阅了的客户端,客户端收到推送过来的实例信息,会改变本地的缓存信息,然后通知执行对应的listener ,它这里面大量使用了异步处理的方式,就是生成一个任务放到队列中,然后某个线程就专门从队列中获取任务,执行相应的任务处理逻辑,这是值得我们学习的地方。最后我们介绍了下nacos 服务发现在dubbo框架中的应用。
参考文章
nacos-1.4.1源码分析(注释)
springcloud-source-study学习github地址
深度解析nacos注册中心
mac系统如何安装nacos