Nacos客户端主要具有如下能力
比较“3.服务订阅”和“5.获取服务实例”,区别在于,“3.服务订阅”不仅跟踪了服务实例的变更,而且还要调用相关的监听器;而“5.获取服务实例”只跟踪服务实例的变更。
另外,需要注意的是,Nacos中的服务注册数据被设计为五层结构,包括Namespace、Group、Service、Cluster、Instance,其中Namespace和Group两层由客户端从应用配置中读取,达到应用隔离的目的,客户端注册和订阅实例的范围在Service及以下。
客户端初始化主要完成如下工作
private void init(Properties properties) {
namespace = InitUtils.initNamespaceForNaming(properties);
// 把构造函数的服务器地址赋值给内部对象
initServerAddr(properties);
// 获取服务器API路径
InitUtils.initWebRootContext();
// 获取本地缓存地址
initCacheDir();
// 获取日志文件名称
initLogName(properties);
// 服务实例变更通知器
eventDispatcher = new EventDispatcher();
// 服务器代理
serverProxy = new NamingProxy(namespace, endpoint, serverList, properties);
// 心跳反应器
beatReactor = new BeatReactor(serverProxy, initClientBeatThreadCount(properties));
// 实例反应器
hostReactor = new HostReactor(eventDispatcher, serverProxy, cacheDir, isLoadCacheAtStart(properties),
initPollingThreadCount(properties));
}
// EventDispatcher
public class EventDispatcher {
.....
// 变更的服务信息,共享内存,用于通知服务变更事件
private BlockingQueue<ServiceInfo> changedServices = new LinkedBlockingQueue<ServiceInfo>();
// Service对应的监听器
private ConcurrentMap<String, List<EventListener>> observerMap
= new ConcurrentHashMap<String, List<EventListener>>();
......
}
如上,changedServices是一个共享内存,用于服务事件变更通知;observerMap用于存放应用定义的监听器,key是服务标识,value是监听器列表
// NameProxy
public class NamingProxy {
.....
private List<String> serverList;
......
}
如上,serverList是Nacos集群服务器列表
// BeatReactor
public class BeatReactor {
.....
public final Map<String, BeatInfo> dom2Beat = new ConcurrentHashMap<String, BeatInfo>();
......
}
如上,dom2Beat用于存放心跳信息,可以通过这些信息控制停止心跳线程,例如摘除实例时,就可以通过BeatInfo.setStopped()来停止心跳
// HostReactor
public class HostReactor {
.....
private Map<String, ServiceInfo> serviceInfoMap;
......
}
如上,serviceInfoMap用于存放订阅的服务实例信息
客户端完成注册服务实例时,启动一个心跳线程(AP模式下启动,CP模式不启动,CP模式由服务器主动检查客户端健康状态),该线程不断发送心跳信息到Nacos服务端,Nacos服务端检查该实例已注册之后,返回成功响应信息,如果Nacos返回不成功,则会重新注册该实例,达到补偿注册实例的效果,代码片段如下
//NacosNamingService
//添加心跳并注册服务实例
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
if (instance.isEphemeral()) {
BeatInfo beatInfo = new BeatInfo();
beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
beatInfo.setIp(instance.getIp());
beatInfo.setPort(instance.getPort());
beatInfo.setCluster(instance.getClusterName());
beatInfo.setWeight(instance.getWeight());
beatInfo.setMetadata(instance.getMetadata());
beatInfo.setScheduled(false);
beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
//添加心跳,这里会启动一个心跳线程
beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
// 注册服务实例
serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}
// BeatReactor.BeatTask
public void run() {
if (beatInfo.isStopped()) {
return;
}
long nextTime = beatInfo.getPeriod();
try {
// 发送心跳
JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
long interval = result.getIntValue("clientBeatInterval");
boolean lightBeatEnabled = false;
if (result.containsKey(CommonParams.LIGHT_BEAT_ENABLED)) {
lightBeatEnabled = result.getBooleanValue(CommonParams.LIGHT_BEAT_ENABLED);
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0) {
nextTime = interval;
}
int code = NamingResponseCode.OK;
if (result.containsKey(CommonParams.CODE)) {
code = result.getIntValue(CommonParams.CODE);
}
if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
//服务注册不成功时进行补偿
Instance instance = new Instance();
instance.setPort(beatInfo.getPort());
instance.setIp(beatInfo.getIp());
instance.setWeight(beatInfo.getWeight());
instance.setMetadata(beatInfo.getMetadata());
instance.setClusterName(beatInfo.getCluster());
instance.setServiceName(beatInfo.getServiceName());
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(true);
try {
//补偿注册实例
serverProxy.registerService(beatInfo.getServiceName(),
NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
} catch (Exception ignore) {
}
}
} catch (NacosException ne) {
NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
JSON.toJSONString(beatInfo), ne.getErrCode(), ne.getErrMsg());
}
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}
通过API请求服务端摘除注册实例,并停止心跳线程(AP模式)
// NacosNamingService
// 调用API实现服务实例摘除
public void deregisterInstance(String serviceName, String groupName, String ip, int port, String clusterName) throws NacosException {
Instance instance = new Instance();
instance.setIp(ip);
instance.setPort(port);
instance.setClusterName(clusterName);
deregisterInstance(serviceName, groupName, instance);
}
服务订阅完成如下几件事
// EventDispatcher
public class EventDispatcher {
.....
// 变更的服务信息,共享内存,用于通知服务变更事件
private BlockingQueue<ServiceInfo> changedServices = new LinkedBlockingQueue<ServiceInfo>();
// Service对应的监听器
private ConcurrentMap<String, List<EventListener>> observerMap
= new ConcurrentHashMap<String, List<EventListener>>();
......
}
在getServiceInfo中,会调用HostReactor的updateServiceNow方法,从服务端获取服务实例列表,如下
// HostReactor
public void updateServiceNow(String serviceName, String clusters) {
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUDPPort(), false);
if (StringUtils.isNotEmpty(result)) {
processServiceJSON(result);
}
} catch (Exception e) {
NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
} finally {
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
}
其中,ServerProxy.queryList方法会调用服务端的list接口,服务端的list接口会把该客户端放入监听列表中,服务实例发生变化时,通知该客户端。queryList方法如下
//NamingProxy
public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly)
throws NacosException {
final Map<String, String> params = new HashMap<String, String>(8);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, serviceName);
params.put("clusters", clusters);
params.put("udpPort", String.valueOf(udpPort));
params.put("clientIP", NetUtils.localIP());
params.put("healthyOnly", String.valueOf(healthyOnly));
// 调用服务端list接口
return reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/list", params, HttpMethod.GET);
}
调用getServiceInfo获取的服务实例信息保存在HostReactor的内部对象serviceInfoMap里
// NacosNamingService
// 服务订阅方法
public void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener) throws NacosException {
// 调用getServiceInfo获取服务实例
// 添加监听器
eventDispatcher.addListener(hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ",")), StringUtils.join(clusters, ","), listener);
}
// HostReactor
public class HostReactor {
......
private Map<String, ServiceInfo> serviceInfoMap;
......
}
另外,在getServiceInfo中,也会放入类EventDispatcher的内部对象changedServices中,触发服务实例变更通知线程工作
从服务实例变更通知器添中删除监听器
// NacosNamingService
public void unsubscribe(String serviceName, String groupName, List<String> clusters, EventListener listener) throws NacosException {
eventDispatcher.removeListener(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","), listener);
}
根据服务名获取服务实例列表,调用HostReactor的getServiceInfo,进而调用ServerProxy.queryList,通过服务端的list接口查询服务实例列表,并同时监听该服务的实例变更信息,参看上面“服务订阅”的说明
本文基于Nacos的1.2.0版本