Nacos 是阿里巴巴的微服务开源项目,用于服务发现和配置管理,开源以来我就一直关注,在此准备以几篇文章来窥其全貌,但大段大段贴代码就没必要了,这里用自己的一些理解和总结来帮助大家理解。文章将基于截止目前最新发布的0.8版本,Nacos的使用方式参考官方文档即可,这里主要从原理和实现上来讲。
Nacos可以分为服务发现(Naming)和配置管理(Config)两块,而从使用上来说,又可分为Nacos服务端和客户端,第一篇先来聊下服务发现(Naming)的客户端。
我们从官方示例入手。
Properties properties = new Properties(); properties.setProperty("serverAddr", System.getProperty("serverAddr")); properties.setProperty("namespace", System.getProperty("namespace")); NamingService naming = NamingFactory.createNamingService(properties); naming.registerInstance("nacos.test.3", "11.11.11.11", 8888, "TEST1"); naming.registerInstance("nacos.test.3", "2.2.2.2", 9999, "DEFAULT"); System.out.println(naming.getAllInstances("nacos.test.3")); naming.deregisterInstance("nacos.test.3", "2.2.2.2", 9999, "DEFAULT"); System.out.println(naming.getAllInstances("nacos.test.3")); naming.subscribe("nacos.test.3", new EventListener() { @Override public void onEvent(Event event) { System.out.println(((NamingEvent)event).getServiceName()); System.out.println(((NamingEvent)event).getInstances()); } });
从官方示例可以了解到,对于我们使用者来说, NamingService
是Nacos对外提供给使用者的接口,其实现类为 com.alibaba.nacos.client.naming.NacosNamingService
,归纳起来, NamingService
提供了以下方法:
registerInstance
:注册实例。 deregisterInstance
:注销实例。 getAllInstances
:获取某一服务的所有实例。 selectInstances
:获取某一服务健康或不健康的实例。 selectOneHealthyInstance
:根据权重选择一个健康的实例。 getServerStatus
:检测服务端健康状态。 subscribe
:注册对某个服务的监听。 unsubscribe
:注销对某个服务的监听。 getSubscribeServices
:获取被监听的服务。 getServicesOfServer
:获取命名空间(namespace)下的所有服务名。【注:此方法有个小坑,参数 pageNo
要从1开始】 Naming Client的几个核心类及其关系如下图。我们分别来看一下这几个类。
NacosNamingService
是 NamingService
接口的实现类。实现了上面提到的那些方法。
此外, NacosNamingService
还起到了初始化其他核心类的作用,因为对外提供的方法都是委托给其他核心类处理的。按顺序将依次初始化 EventDispatcher
、 NamingProxy
、 BeatReactor
、 HostReactor
。
从 NacosNamingService
的构造函数我们也可以了解到,可以进行一些参数的自定义,总结如下(部分概念的含义可参考 官方文档 ):
key | 含义 | 默认值 |
---|---|---|
namespace | 命名空间 | public |
com.alibaba.nacos.naming .log.filename |
日志文件名 | naming.log |
com.alibaba.nacos.naming .log.level |
日志级别 | INFO |
com.alibaba.nacos.naming .cache.dir |
缓存目录(缓存服务信息) | {user.home}/nacos/ naming/{namespace} |
serverAddr | Nacos服务端地址 | 无 |
endpoint | 接入点 | 无 |
namingLoadCacheAtStart | 初始化时是否从缓存读取服务信息 | false |
namingClientBeatThreadCount | 心跳线程池线程数 | 1~CPU核心数的一半 |
namingPollingThreadCount | 服务更新线程池线程数 | 1~CPU核心数的一半 |
EventDispatcher
与其他事件分发的组件没什么不同,用于处理 subscribe
、 unsubscribe
等等与服务监听相关的方法,并分发 NamingEvent
到各Listener。
成员变量 ConcurrentMap<String, List<EventListener>> observerMap
保存了注册的Listener,key为 {服务名}@@{集群名}
,value为各个 EventListener
的列表。
EventDispatcher
会启动 1 个名为 com.alibaba.nacos.naming.client.listener
的线程用于处理事件的分发。
注意点:
NamingEvent
时,按照 subscribe(...)
方法的调用顺序串行依次调用 EventListener
的 onEvent(...)
方法。 subscribe(...)
方法会引起对应Service的事件分发。 NamingProxy
用于与Nacos服务端通信,注册服务、注销服务、发送心跳等都经由 NamingProxy
来请求服务端。
NamingProxy
会启动 1 个名为 com.alibaba.nacos.client.naming.serverlist.updater
的线程,用于定期调用 refreshSrvIfNeed()
方法更新Nacos服务端地址,默认间隔为 30秒 ,
对服务端API的调用将在后文总结。
注意点: refreshSrvIfNeed()
方法对Nacos服务端地址的更新仅在使用endpoint的时候才会进行实际更新,如果是通过 serverAddr
配置的Nacos服务端地址, refreshSrvIfNeed()
方法将不会进行任何操作。
BeatReactor
用于向Nacos服务端发送已注册服务的心跳。
成员变量 Map<String, BeatInfo> dom2Beat
中保存了需要发送的 BeatInfo
,key为 {serviceName}#{ip}#{port}
,value为对应的 BeatInfo
。
BeatReactor
会启动名为 com.alibaba.nacos.naming.beat.sender
的线程来发送心跳,默认线程数为1~CPU核心数的一半,可由 namingClientBeatThreadCount
参数指定。
默认情况下每 5秒 发送一次心跳,可根据Nacos服务端返回的 clientBeatInterval
的值调整心跳间隔。
注意点:0.8版本有一个小bug,客户端心跳间隔并不受服务端返回值的控制。我已提交PR,预计将在0.9版本修复。
HostReactor
用于获取、保存、更新各Service实例信息。
成员变量 Map<String, ServiceInfo> serviceInfoMap
中保存了已获取到的服务的信息,key为 {服务名}@@{集群名}
。
HostReactor
会启动名为 com.alibaba.nacos.client.naming.updater
的线程来更新服务信息,默认线程数为1~CPU核心数的一半,可由 namingPollingThreadCount
参数指定。定时任务 UpdateTask
会根据服务的 cacheMillis
值定时更新服务信息,默认值为 10秒 。该定时任务会在获取某一服务信息时创建,保存在成员变量 Map<String, ScheduledFuture<?>> futureMap
中。
PushReceiver
用于接收Nacos服务端的推送,初始化时会创建 DatagramSocket
使用UDP的方式接收推送。会启动 1 个名为 com.alibaba.nacos.naming.push.receiver
的线程。
用于故障转移,会启动 1 个名为 com.alibaba.nacos.naming.failover
的线程并定时读取名为 00-00---000-VIPSRV_FAILOVER_SWITCH-000---00-00
的文件,内容为 1
时表示开启,此时获取服务信息时会返回 FailoverReactor
缓存的服务信息。
根据服务实例的权重挑选一个实例,实现简单的负载均衡。
用于服务信息的持久化。
API汇总如下:
Method | URI | 含义 |
---|---|---|
POST | /nacos/v1/ns/instance | 注册实例 |
DELETE | /nacos/v1/ns/instance | 注销实例 |
GET | /nacos/v1/ns/instance/list | 获取实例列表 |
PUT | /nacos/v1/ns/instance/beat | 发送心跳 |
GET | /nacos/v1/ns/api/hello | Nacos服务端状态 |
GET | /nacos/v1/ns/service/list | 获取所有服务名 |
key | 含义 | 备注 |
---|---|---|
namespaceId | 命名空间 | 默认为public |
ip | 实例IP地址 | |
port | 实例端口 | |
weight | 权重 | 默认为1.0 |
enable | 是否开启 | 默认为true |
healthy | 健康状态 | 默认为true |
metadata | 其他信息 | |
serviceName | 服务名 | |
clusterName | 集群名 | 默认为DEFAULT |
请求示例: http://localhost:8848/nacos/v1/ns/instance?metadata=%7B%7D&namespaceId=public&port=8888&enable=true&healthy=true&ip=11.11.11.11&clusterName=TEST1&weight=1.0&serviceName=nacos.test.3&encoding=UTF-8&
返回示例:ok
key | 含义 | 备注 |
---|---|---|
namespaceId | 命名空间 | 默认为public |
ip | 实例IP地址 | |
port | 实例端口 | |
serviceName | 服务名 | |
clusterName | 集群名 | 默认为DEFAULT |
请求示例: http://localhost:8848/nacos/v1/ns/instance?cluster=DEFAULT&serviceName=nacos.test.3&encoding=UTF-8&namespaceId=public&port=9999&ip=2.2.2.2&
返回示例:ok
key | 含义 | 备注 |
---|---|---|
namespaceId | 命名空间 | 默认为public |
serviceName | 服务名 | |
clusters | 集群名 | 默认为DEFAULT |
udpPort | 监听的UPD端口号 | 由PushReceiver创建 |
clientIP | 客户端IP | |
healthyOnly | 是否只返回健康的实例 |
请求示例: http://localhost:8848/nacos/v1/ns/instance/list?healthyOnly=false&namespaceId=public&clientIP=172.16.20.114&serviceName=nacos.test.3&udpPort=53957&encoding=UTF-8&
返回示例:{“metadata”:{},”dom”:”nacos.test.3”,”cacheMillis”:10000,”useSpecifiedURL”:false,”hosts”:[{“valid”:true,”marked”:false,”metadata”:{},”instanceId”:”2.2.2.2#9999#DEFAULT#nacos.test.3”,”port”:9999,”ip”:”2.2.2.2”,”clusterName”:”DEFAULT”,”weight”:1.0,”serviceName”:”nacos.test.3”,”enabled”:true},{“valid”:true,”marked”:false,”metadata”:{},”instanceId”:”11.11.11.11#8888#TEST1#nacos.test.3”,”port”:8888,”ip”:”11.11.11.11”,”clusterName”:”TEST1”,”weight”:1.0,”serviceName”:”nacos.test.3”,”enabled”:true}],”checksum”:”bd1054e6afb8d10730d945d74c4ce4421550584589236”,”lastRefTime”:1550584589236,”env”:””,”clusters”:””}
key | 含义 | 备注 |
---|---|---|
namespaceId | 命名空间 | 默认为public |
serviceName | 服务名 | |
beat | BeatInfo的JSON字符串 |
BeatInfo
对象结构如下,与 Instance
对象类似:
field | 含义 | 备注 |
---|---|---|
port | 端口 | |
ip | IP地址 | |
weight | 权重 | |
metadata | 其他信息 | |
serviceName | 服务名 | |
clusterName | 集群名 | |
scheduled | 是否心跳中 | 这个是BeatReactor用来标识状态的 |
请求示例: http://localhost:8848/nacos/v1/ns/instance/beat?beat=%7B%22cluster%22%3A%22DEFAULT%22%2C%22ip%22%3A%222.2.2.2%22%2C%22metadata%22%3A%7B%7D%2C%22port%22%3A9999%2C%22scheduled%22%3Atrue%2C%22serviceName%22%3A%22nacos.test.3%22%2C%22weight%22%3A1.0%7D&serviceName=nacos.test.3&encoding=UTF-8&namespaceId=public&
返回示例:{“clientBeatInterval”:5000}
key | 含义 | 备注 |
---|---|---|
namespaceId | 命名空间 | 默认为public |
请求示例: http://localhost:8848/nacos/v1/ns/api/hello?encoding=UTF-8&namespaceId=public&
返回示例:{“msg”:”Hello! I am Nacos-Naming and healthy! total services: raft 2, local port:8848”}
key | 含义 | 备注 |
---|---|---|
namespaceId | 命名空间 | 默认为public |
pageNo | 页码 | 注意从1开始 |
pageSize | 返回数量 | |
selector | 过滤器 |
请求示例: http://localhost:8848/nacos/v1/ns/service/list?pageSize=100&encoding=UTF-8&namespaceId=public&pageNo=0&
返回示例:{“count”:1,”doms”:[“nacos.test.3”]}
Nacos服务发现的客户端较为简单,其他语言也可以参照其API来实现客户端。如果对源码实现感兴趣,可以自己看下代码。