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的几个核心类及其关系如下图。我们分别来看一下这几个类。
core-class
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来实现客户端。如果对源码实现感兴趣,可以自己看下代码。