
08篇 要给Nacos的UDP通信功能点个赞
学习不用那么功利,二师兄带你从更高维度轻松阅读源码~
Nacos在服务注册功能中使用到了UDP的通信方式,主要功能就是用来辅助服务实例变化时对客户端进行通知。然而,对于大多数使用Nacos的程序员来说,可能还不知道这个功能,更别说灵活运用了。
看完整个源码的实现,还是要为这一功能点个赞的,可以说非常巧妙和实用。但在实现上有一些不足,文末会进行指出。
本篇文章就带大家从源码层面来分析一下Nacos 2.0中是如何基于UDP协议来实现服务实例变更的通知。
UDP通知基本原理
在分析源码之前,先来从整体上看一下Nacos中UDP的实现原理。
Nacos UDP基本原理
我们知道,UDP协议通信是双向的,没有所谓的客户端和服务端,因此在客户端和服务器端都会开启UDP的监听。客户端是单独开启一个线程来处理UDP消息的。当采用HTTP协议与注册中心通信时,,在客户端调用服务订阅接口时,会将客户端的UPD信息(IP和端口)上送到注册中心,注册中心以PushClient对象来进行封装和存储。
当注册中心有实例变化时,会发布一个ServiceChangeEvent事件,注册中心监听到这个事件之后,会遍历存储的PushClient,基于UDP协议对客户端进行通知。客户端接收到UDP通知,即可更新本地缓存的实例列表。
前面我们已经知道,基于HTTP协议进行服务注册时,会有一个实例更新的时间差,因为是通过客户端定时拉取服务器中的实例列表。如果拉取太频繁,注册中心压力比较大,如果拉取的周期比较长,实例的变化又没办法快速感知到。而UDP协议的通知,恰恰弥补了这一缺点,所以说,要为基于UDP通知这个功能点个赞。
下面就来看看源码层面是如何实现的。
客户端UDP通知监听与处理
客户端在实例化NamingHttpClientProxy时,在其构造方法中会初始化PushReceiver。
PushReceiver的构造方法,如下:
PushReceiver的构造方法做了以下操作:
● 第一、持有ServiceInfoHolder对象引用;
● 第二、获取UDP端口;
● 第三、实例化DatagramSocket对象,用于发送和接收Socket数据;
● 第四,创建线程池,并执行PushReceiver(实现了Runnable接口);
既然PushReceiver实现了Runnable接口,run方法肯定是需要重新实现的:
PushReceiver#run方法主要处理了以下操作:
● 第一、构建DatagramPacket用于接收报文数据;
● 第二、通过DatagramSocket#receive方法阻塞等待报文的到来;
● 第三、DatagramSocket#receive接收到报文之后,方法继续执行;
● 第四、解析JSON格式的报文为PushPacket对象;
● 第五、判断报文类型,调用ServiceInfoHolder#processServiceInfo处理接收到的报文信息,在该方法中会将PushPacket转化为ServiceInfo对象;
● 第六、封装ACK信息(即应答报文信息);
● 第七、通过DatagramSocket发送应答报文;
上面我们看到了Nacos客户端是如何基于UDP进行报文的监听和处理的,但并未找到客户端是如何将UDP信息上送给注册中心的。下面我们就来梳理一下,上送UDP信息的逻辑。
客户端上送UDP信息
在NamingHttpClientProxy中存储了UDP_PORT_PARAM,即UDP的端口参数信息。
UDP端口信息通过实例查询类接口进行传递,比如:查询实例列表、查询单个健康实例、查询所有实例、订阅接口、订阅的更新任务UpdateTask等接口。在这些方法中都调用了NamingClientProxy#queryInstancesOfService方法。
NamingHttpClientProxy中的queryInstancesOfService方法实现:
但查看源码会发现,查询实例列表、查询单个健康实例、查询所有实例、订阅的更新任务UpdateTask中,UDP端口传递的参数值均为0。只有HTTP协议的订阅接口取值为PushReceiver中的UDP端口号。
在上面的代码中我们已经知道PushReceiver中有一个getPushReceiverUdpPort的方法:
很明显,UDP的端口是通过环境变量设置的,对应的key为“push.receiver.udp.port”。
而在1.4.2版本中,HostReactor中的NamingProxy成员变量的queryList方法也会传递UDP端口:
关于1.4.2版本中的实现,大家自行看源码即可,这里不再展开。
完成了客户端UDP基本信息的传递,再来看看服务器端是如何接收和存储这些信息的。
UDP服务存储
服务器端在获取实例列表的接口中,对UDP端口进行了处理。
在getInstanceOperator()方法中会获得当前采用的哪个协议,然后选择对应的处理类:
这里具体的实现类为InstanceOperatorServiceImpl:
当UDP端口大于0,且agent参数定义的客户端支持UDP,则将对应的客户端信息封装到InetSocketAddress对象中,然后放入NamingSubscriberServiceV1Impl中(该类已经被废弃,看后续如何调整该方法实现)。
在NamingSubscriberServiceV1Impl中,会将对应的参数封装为PushClient,存放在Map当中。
addClient方法会将PushClient信息存放到ConcurrentMap<String, ConcurrentMap<String, PushClient>>当中:
此时,UDP的IP、端口信息已经封装到PushClient当中,并存储在NamingSubscriberServiceV1Impl的成员变量当中。
注册中心的UDP通知
当服务端发现某个实例发生了变化,比如主动注销了,会发布一个ServiceChangeEvent事件,UdpPushService会监听到该事件,并进行业务处理。
在UdpPushService的onApplicationEvent方法中,会根据PushClient的具体情况进行移除或发送UDP通知。onApplicationEvent中核心逻辑代码如下:
事件处理的核心逻辑是就是先判断PushClient的状态信息,如果已经是僵尸客户端,则移除。然后将发送UDP的报文信息和接收客户端的信息封装为AckEntry对象,然后调用udpPush方法,进行UDP消息的发送。
注册中心的UDP接收
在看客户端源码的时候,我们看到客户端不仅会接收UDP请求,而且还会进行应答。那么注册中心怎么接收应答呢?也在UdpPushService类中,该类内部的静态代码块初始化一个UDP的DatagramSocket,用来接收消息:
Receiver是一个内部类,实现了Runnable接口,在其run方法中主要就是接收报文信息,然后进行报文消息的判断,根据判断结果,操作本地Map中数据。
UDP设计不足
文章最开始就写到,UDP的设计非常棒,即弥补了HTTP定时拉取的不足,又不至于太影响性能。但目前Nacos在UDP方面有一些不足,也可能是个人的吹毛求疵吧。
第一,文档中没有明确说明UDP的功能如何使用,这导致很多使用者在使用时并不知道UDP功能的存在,以及使用的限制条件。
第二,对云服务不友好。客户端的UDP端口可以自定义,但服务器端的UDP端口是随机获取到。在云服务中,即便是内网服务,UDP端口也是被防火墙限制的。如果服务端的UDP端口是随机获取(客户端默认也是),那么UDP的通信将直接被防火墙拦截掉,而用户根本看不到任何异常(UDP协议不关注客户端是否收到消息)。
至于这两点,说起来算是瑕不掩瑜,读完源码或读过我这篇文章的朋友大概已经知道怎么用了。后续可以给官方提一个Issue,看看是否可以改进。
小结
本文重点从三个方面讲解的Nacos基于UDP的服务实例变更通知:
第一,客户端监听UDP端口,当接收注册中心发来的服务实例变化,可以及时的更新本地的实例缓存;
第二,客户端通过订阅接口,将自身的UDP信息发送给注册中心,注册中心进行存储;
第三,注册中心中实例发生了变化,通过事件机制,将变更信息通过UDP协议发送给客户端。
经过本篇文章,想必你不仅了解了Nacos中UDP协议的通知机制。同时,也开拓了一个新的思路,即如何使用UDP,在什么场景下使用UDP,以及在云服务中使用UDP可能会存在的问题。如果这篇文章对你有帮助,关注或点赞都可以。
文章转载自公众号:程序员新视界
