服务注册
在上一章,我们说完了nacos客户端的自动注册流程,知道最后是客户端向服务端发送注册请求,完成了客户端的注册流程。 之前发送的URL: curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.10&port=8080',根据这个url,我们在服务端代码中找到对应方法
看上图,首先获取到服务的命名空间和服务名,再获取服务实例信息,调用serviceManger完成注册。而默认的命名空间就是
SericeManager 服务存储结构
再让我们深入到sericeManager中
我们可以看到,在manager中有一个Map对象,具体构成如下:
首先是一个 Map(namespace, Map(group::serviceName, Service)) 集合,集合的key为 namespace, value 为一个map,namespace 代表着什么呢,像一般项目中,会有dev、test、master即对应开发、测试、线上环境。这个时候,各个环境的配置都是一套,你就可以建立3个 namespace;而value 中 key代表这个分组下的服务,value就是这个服务的具体信息,在Service内部, 有一个 Map<String, Cluster>, 在 Cluster 内部是一个 Set<Instance> 集合,这个是什么意思呢,举个例子吧: 在nacos的设计中,是为了应付中大型项目的,比如阿里,他们一个服务可能在不同的地方都对应着一个集群,而在每个集群中这个服务的实例都会有多个。所以 Cluster 就是对应集群分片,而 Instance 就是具体的服务实例了。
具体注册逻辑
再让我们回到注册方法中,在里面做了以下事情
我们进入到创建service的方法中,这里为什么叫创建了一个空的service呢,就是只创建了service对象,对里面的Instance实例并没有进行初始化
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
//首先从缓存中获取
Service service = getService(namespaceId, serviceName);
//如果不存在,则创建
if (service == null) {
Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
service.setGroupName(NamingUtils.getGroupName(serviceName));
// now validate the service. if failed, exception will be thrown
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
if (cluster != null) {
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
//校验service 不能为空,命名是否符合规范
service.validate();
//添加service
putServiceAndInit(service);
//持久化
if (!local) {
addOrReplaceService(service);
}
}
}
在 putServiceAndInit() 方法中,做了以下几件事
private void putServiceAndInit(Service service) throws NacosException {
//添加service, 向map 中put一个空的service对象
putService(service);
//初始化service
service.init();
//新增service节点变化监听
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}
什么是节点变化监听呢, nacos 有一个 RecordListener 类,它提供了几个接口,当插入、新增、删除时分别做什么事,比如我们一般像map中新增数据就是直接调用 Map.put(), 而 nacos就做得比较高级,它是先添加一个对插入对象的监听器,当数据有变化时,再调用对应的事件方法。
public interface RecordListener<T extends Record> {
/**
* Determine if the listener was registered with this key.
*
* @param key candidate key
* @return true if the listener was registered with this key
*/
boolean interests(String key);
/**
* Determine if the listener is to be removed by matching the 'key'.
*
* @param key key to match
* @return true if match success
*/
boolean matchUnlistenKey(String key);
/**
* Action to do if data of target key has changed.
*
* @param key target key
* @param value data of the key
* @throws Exception exception
*/
void onChange(String key, T value) throws Exception;
/**
* Action to do if data of target key has been removed.
*
* @param key target key
* @throws Exception exception
*/
void onDelete(String key) throws Exception;
}
再让我们回到添加实例的方法中,主要做了 1 更新实例 2 给Service 中 clusterMap 赋值,将实例放入DataStore 中 。
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
//获取到唯一标识key
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
//获取service
Service service = getService(namespaceId, serviceName);
synchronized (service) {
//获取到更新的实例,主要对实例的健康状态进行维护
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
Instances instances = new Instances();
instances.setInstanceList(instanceList);
//给Service 中clusterMap赋值,将实例放入DataStore中
consistencyService.put(key, instances);
}
}
clusterMap 这个我们知道,就是上文中说的 Service内部结构,DataStore 又是什么呢? DataStore 是 nacos 维护的另一份关于 Instance 的集合,具体结构如下,里面有一个 Map 集合, key 就是 String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral) 组成的, value 是 Instances , Instances 中是一个 List<Instance> 集合。
其中 Datum 的数据构成: key 是一个 Sring ,value 是一个泛型, 这个泛型继承自 Record类
而它有3个实现类,分别是以下这3个
我们再来看下它具体是怎么给 Service 添加实例、将实例加入 DataStore 中的。从前面可以看到,它调用了 consistencyService.put(), 具体方法实现类有以下几个
DelegateConsistencyServiceImpl : 里面实现了 EphemeralConsistencyService 和 PersistentConsistencyServiceDelegateImpl ,即是一个一致性委托类,对临时和永久节点分别做委托
DistroConsistencyServiceImpl :一致性协议算法,使用 distro 算法将数据分为多块,每个 nacos 服务器负责一块,由它来负责做服务器生成、删除和同步的,同一时间内每个Nacos服务器都会接收到其他Nacos服务器的数据同步,所以每个Nacos服务器最终都会接收到有完整的数据。
PersistentConsistencyServiceDelegateImpl: 持久一致性服务委托。
PersistentServiceProcessor: 在集群模式下,持久化处理,启动Raft协议。
RaftConsistencyServiceImpl: 使用简化的Raft协议来维护Nacos集群的一致性状态。
StandalonePersistentServiceProcessor:独立模式下的持久服务操纵层。
但是在nacos 内部,一般都是调用 DistroConsistencyServiceImpl, 对集群模式下临时节点的处理,让我们进去看看
public void put(String key, Record value) throws NacosException {
// 添加一个 record
onPut(key, value);
//开始同步数据到所有远程服务器
distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
globalConfig.getTaskDispatchPeriod() / 2);
}
public void onPut(String key, Record value) {
//判断key的正确性
if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
Datum<Instances> datum = new Datum<>();
datum.value = (Instances) value;
datum.key = key;
datum.timestamp.incrementAndGet();
//向 dataStore 中添加
dataStore.put(key, datum);
}
if (!listeners.containsKey(key)) {
return;
}
// 根据操作类型创建一个 notifier 任务
notifier.addTask(key, DataOperation.CHANGE);
}
再让我们看看 Notifier 类具体做了什么事情
进入 onChange() 方法,最后我看可以看到这个方法
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
//获取到当前 clusterMap 中的 Instances
Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
for (String clusterName : clusterMap.keySet()) {
ipMap.put(clusterName, new ArrayList<>());
}
for (Instance instance : instances) {
try {
if (instance == null) {
Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
continue;
}
if (StringUtils.isEmpty(instance.getClusterName())) {
instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
}
if (!clusterMap.containsKey(instance.getClusterName())) {
Loggers.SRV_LOG
.warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
instance.getClusterName(), instance.toJson());
Cluster cluster = new Cluster(instance.getClusterName(), this);
cluster.init();
getClusterMap().put(instance.getClusterName(), cluster);
}
List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
if (clusterIPs == null) {
clusterIPs = new LinkedList<>();
ipMap.put(instance.getClusterName(), clusterIPs);
}
clusterIPs.add(instance);
} catch (Exception e) {
Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
}
}
for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
//make every ip mine
List<Instance> entryIPs = entry.getValue();
//更新instance
clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
}
setLastModifiedMillis(System.currentTimeMillis());
//发布service改变事件,同步到集群中的其它服务器去
getPushService().serviceChanged(this);
StringBuilder stringBuilder = new StringBuilder();
for (Instance instance : allIPs()) {
stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
}
Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
stringBuilder.toString());
}
主要看这两个方法 :
更新instance : clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
发布service改变事件,同步到集群中的其它服务器去: getPushService().serviceChanged(this);
我们看下它是怎么更新的 Instance
//判断节点是临时还是永久节点
Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;
//获取到老的节点
HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());
for (Instance ip : toUpdateInstances) {
oldIpMap.put(ip.getDatumKey(), ip);
}
//对实例做更新
List<Instance> updatedIPs = updatedIps(ips, oldIpMap.values());
if (updatedIPs.size() > 0) {
for (Instance ip : updatedIPs) {
Instance oldIP = oldIpMap.get(ip.getDatumKey());
// do not update the ip validation status of updated ips
// because the checker has the most precise result
// Only when ip is not marked, don't we update the health status of IP:
if (!ip.isMarked()) {
ip.setHealthy(oldIP.isHealthy());
}
if (ip.isHealthy() != oldIP.isHealthy()) {
// ip validation status updated
Loggers.EVT_LOG.info("{} {SYNC} IP-{} {}:{}@{}", getService().getName(),
(ip.isHealthy() ? "ENABLED" : "DISABLED"), ip.getIp(), ip.getPort(), getName());
}
if (ip.getWeight() != oldIP.getWeight()) {
// ip validation status updated
Loggers.EVT_LOG.info("{} {SYNC} {IP-UPDATED} {}->{}", getService().getName(), oldIP.toString(),
ip.toString());
}
}
}
//获取到新增的 Instance
List<Instance> newIPs = subtract(ips, oldIpMap.values());
if (newIPs.size() > 0) {
Loggers.EVT_LOG
.info("{} {SYNC} {IP-NEW} cluster: {}, new ips size: {}, content: {}", getService().getName(),
getName(), newIPs.size(), newIPs.toString());
for (Instance ip : newIPs) {
//重置健康状态
HealthCheckStatus.reset(ip);
}
}
//获取到死掉的 Instance
List<Instance> deadIPs = subtract(oldIpMap.values(), ips);
if (deadIPs.size() > 0) {
Loggers.EVT_LOG
.info("{} {SYNC} {IP-DEAD} cluster: {}, dead ips size: {}, content: {}", getService().getName(),
getName(), deadIPs.size(), deadIPs.toString());
for (Instance ip : deadIPs) {
HealthCheckStatus.remv(ip);
}
}
//更新 Instances
toUpdateInstances = new HashSet<>(ips);
if (ephemeral) {
ephemeralInstances = toUpdateInstances;
} else {
persistentInstances = toUpdateInstances;
}
}
再看下它是怎么做同步的
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));
}
首先发布了一个 ServiceChange 事件,
public void onApplicationEvent(ServiceChangeEvent event) {
Service service = event.getService();
String serviceName = service.getName();
String namespaceId = service.getNamespaceId();
//创建一个任务延迟10秒执行
Future future = GlobalExecutor.scheduleUdpSender(() -> {
try {
Loggers.PUSH.info(serviceName + " is changed, add it to push queue.");
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();
//获取到所有的客户端ip
for (PushClient client : clients.values()) {
if (client.zombie()) {
Loggers.PUSH.debug("client is zombie: " + client.toString());
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));
//调用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);
}
至此,nacos 服务端注册流程就完了!