Nacos 1.4.1注册中心源码深度解析-服务注册

6 篇文章 0 订阅
6 篇文章 0 订阅

对于nacos的基本概念和使用就不做说明了。直接来最干的干货,我们来看下源码。注意版本使用的是nacos1.4.1

这里说的1.4.1是指客户端,nacos-discovery对应2.1.0.RELEASE

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

服务注册

客户端注册

在微服务使用nacos是需要在pom文件里添加依赖。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
    <version>1.4.1.RELEASE</version>
</dependency>

这个依赖会引入 spring-cloud-alibaba-nacos-discovery的jar包。如果你学习过SpringBoot的自动装配一定知道SpringBoot会使用SPI机制,加载jar包中的spring.factories文件,对配置为EnableAutoConfiguration的类进行自动装配。如果没有学过SpringBoot的自动装配也没关系,现在我已经告诉你了。知道有这么回事就行了。

在这些自动装配的类中有一个NacosDiscoveryAutoConfiguration就是我们要找的服务注册相关的类了。别问我怎么知道的,猜的!怎么猜的?因为这个名字和jar包的名字最像,而且带了AutoConfiguration,又在第一个位置,不是它是谁。

进来以后我们发现nacosAutoServiceRegistration方法有两个参数,刚好是上面的两个Bean,无疑nacosAutoServiceRegistration方法才是这个类里最核心的方法。

nacosAutoServiceRegistration方法向容器中添加了一个NacosAutoServiceRegistration的Bean。我们进入到NacosAutoServiceRegistration这个类来看看。先来看下类图。

nacosAutoServiceRegistration类实现了ApplicationListener接口。这是一个监听用的接口,实现了这个接口就必须要实现onApplicationEvent方法。org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration#onApplicationEvent监听了WebServerInitializedEvent事件

分支流程忽略,继续看核心的内容。进入start方法。进去以后一看就知道register是核心。register有两个实现。可以通过debug来看下进到哪里了。还可以分析下当前我们跟踪的这个类的是哪个,这里一猜就是Nacos的。

继续super.register(); -> com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register。

来看看有什么,实例、注册 都有了。这里就是服务注册的地方了。想要知道启动的时候是怎么调用到这里的,可以在这里打个断点,看下调用链就知道了。 简单说就是启动的时候发布了一个事件,然后出发了nacos这里的监听。

实例信息就是当前启动的实例的ip,端口,集群等信息。

继续看registerInstance方法,一路往下走认准register关键字不迷路。到了com.alibaba.nacos.client.naming.net.NamingProxy#registerService

构建了一个POST请求,url为“/nacos/v1/ns/instance”

我们到nacos的官网看下,这个api就是注册实例了。 https://nacos.io/zh-cn/docs/open-api.html

继续深入直到com.alibaba.nacos.client.naming.net.NamingProxy#reqApi 看下里面的callServer方法。看到这句执行请求的方法。

HttpRestResult<String> restResult = nacosRestTemplate
        .exchangeForm(url, header, Query.newInstance().initParams(params), body, method, String.class);

跟着execute往下走,到com.alibaba.nacos.common.http.client.NacosRestTemplate#execute

response = this.requestClient().execute(uri, httpMethod, requestEntity);

最终使用了JdkHttpClientRequest发送请求。

好了就到这吧,不在深入了。

服务端注册

这里说的服务端不是指提供服务的一端,而是指nacos的注册中心服务。上面我们知道了客户端启动时发送了一个请求向服务端注册。那么服务端是如何处理这个注册的呢?

前面我们知道客户端注册时发送的路径是/nacos/v1/ns/instance,我们来看下服务端接收的入口在哪里。

源码下载: https://github.com/alibaba/nacos/releases/tag/1.4.1

在这了。怎么知道的呢? 猜呗,看请求的路径大概去猜下,然后去翻翻代码找一找。不会猜又找不到怎么办? 我告诉你,你不就知道了。找到就好不必纠结方法。

这里是用的是Rest服务,POST请求对应的是com.alibaba.nacos.naming.controllers.InstanceController#register方法。

我们现在是要注册一个实例,所以跟代码的时候盯着instance就可以了,看看导致被传到哪里去了,做了什么事情。进入registerInstance。

createEmptyService

先来看看第一个方法createEmptyService

com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent

--> com.alibaba.nacos.naming.core.ServiceManager#getService

从命名空间中获取服务,首次进来这里肯定是获取不到的,返回null

public Service getService(String namespaceId, String serviceName) {
    if (serviceMap.get(namespaceId) == null) {
        return null;
    }
    return chooseServiceMap(namespaceId).get(serviceName);
}

我们重点关注下这个serviceMap。这就是注册表。用来存储注册到nacos注册中心的服务。

/**
 * Map(namespace, Map(group::serviceName, Service)).
 */
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();

返回上一层,既然getService没有拿到,那下面就要创建service,填充值然后放入到map中了。

我们来看下putServiceAndInit

private void putServiceAndInit(Service service) throws NacosException {
    putService(service);
    service.init();
    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());
}

首先putService就是将service放入map中

public void putService(Service service) {
    if (!serviceMap.containsKey(service.getNamespaceId())) {
        synchronized (putServiceLock) {
            if (!serviceMap.containsKey(service.getNamespaceId())) {
                serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
            }
        }
    }
    serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
}

service.init()是健康检查用的,后面再来看。

addInstance

我们在回到com.alibaba.nacos.naming.core.ServiceManager#registerInstance。

public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
    
    createEmptyService(namespaceId, serviceName, instance.isEphemeral());
    
    Service service = getService(namespaceId, serviceName);
    
    if (service == null) {
        throw new NacosException(NacosException.INVALID_PARAM,
                "service not found, namespace: " + namespaceId + ", service: " + serviceName);
    }
    
    addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}

createEmptyService已经看完了,在看下下面的addInstance方法。

public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
        throws NacosException {
    
    String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
    
    Service service = getService(namespaceId, serviceName);
    
    synchronized (service) {
        List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
        
        Instances instances = new Instances();
        instances.setInstanceList(instanceList);
        
        consistencyService.put(key, instances);
    }
}

buildInstanceListKey

首先我们来看下buildInstanceListKey。这个代码的作用是设置一下当前的实例是临时实例还是持久化实例。默认ephemeral为true,就是临时实例。我们知道nacos支持CP和AP两种模式,这个设置就决定了使用的是哪种模式。基础概念这里就不讲了。只带大家看看源码。

addIpAddresses

接着看下addIpAddresses,首先通过addIpAddresses获取到了一个实例的列表,然后添加到实例中,最后调用了consistencyService.put。

我们也能大概猜到,这里就是将新注册的实例添加到了实例的一个列表里,然后保持起来。我们进入addIpAddresses来看下。

addIpAddresses --> updateIpAddresses 进入到updateIpAddresses ,这个方法就不详细看了,重点关注下这段。这里会对实例做一个更新,判断是删除,修改还是新增,然后更新下instanceMap

if (UtilsAndCommons.UPDATE_INSTANCE_ACTION_REMOVE.equals(action)) {
    instanceMap.remove(instance.getDatumKey());
} else {
    Instance oldInstance = instanceMap.get(instance.getDatumKey());
    if (oldInstance != null) {
        instance.setInstanceId(oldInstance.getInstanceId());
    } else {
        instance.setInstanceId(instance.generateInstanceId(currentInstanceIds));
    }
    instanceMap.put(instance.getDatumKey(), instance);
}

consistencyService.put

回到上层来看下consistencyService.put。我们看到put方法有很多实现类,那我们怎么知道调到了哪里去了呢?

有两种方法: 1. debug 简单直接。但是如果是不允许debug的时候就歇菜了。

如何去分析静态源码

这里重点来说下第二种方法,如何去分析静态源码。

首先我们看下consistencyService的声明的地方。我们看到consistencyService上面指定了Bean的名字是consistencyDelegate

@Resource(name = "consistencyDelegate")
private ConsistencyService consistencyService;

全局搜索下就可以看到了。DelegateConsistencyServiceImpl被注册的名字就是consistencyService。是不是豁然开朗!

然后我们进入到com.alibaba.nacos.naming.consistency.DelegateConsistencyServiceImpl#put。发现又懵了,又是刚才那几个家伙,这回又要去哪里呢?

还是和上面一样,分析下mapConsistencyService(key)到底拿到了哪个service。进入到mapConsistencyService,一个三元表达式,可能返回两个service。

ephemeralConsistencyService 临时实例,对应AP

persistentConsistencyService 持久化实例,对应CP

private ConsistencyService mapConsistencyService(String key) {
    return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
}

那究竟选了哪个呢? 在进入到 matchEphemeralKey --> matchEphemeralInstanceListKey 。这里判断key是不是有“ephemeral.” 看下上面的那段代码熟不熟悉,这不就是上面addInstance方法里刚看到的吗!也就是注册实例的时候根据是否为临时实例生成的key。

回到上一步,我们来看下AP的情况,这里就取了ephemeralConsistencyService

private ConsistencyService mapConsistencyService(String key) {
    return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
}

我们回到com.alibaba.nacos.naming.consistency.DelegateConsistencyServiceImpl#put。发现这里没有ephemeralConsistencyService啊。好难啊!

我们进入到ephemeralConsistencyService,发现它是一个接口。那么我们再来看下它的实现类。

class DistroConsistencyServiceImpl implements EphemeralConsistencyService

真相大白了。兜兜转转我们终于找到了“真凶” -- DistroConsistencyServiceImpl。

回顾下调用链路

com.alibaba.nacos.naming.core.ServiceManager#addInstance

-->consistencyService.put

-->com.alibaba.nacos.naming.consistency.DelegateConsistencyServiceImpl#put

-->com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#put

插播一下Distro是阿里实现的分布式协议,在nacos中用于AP架构。如果是CP架构这里会使用Raft协议。进入RaftConsistencyServiceImpl。

onPut

DistroConsistencyServiceImpl#put中我们在进入onPut(key, value)。

-->com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#put

-->com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#onPut

-->dataStore.put(key, datum);

这里会将Instances存入到dataMap,key,value就是在最初的addInstance生成的,大家可以自己看下。这个dataMap会在后面使用,先有个印象。

onPut方法中还有一行

notifier.addTask(key, DataOperation.CHANGE);

notifier.addTask(key, DataOperation.CHANGE);

这里将key和action封装成了task,放入了阻塞队列中。

既然有存,就一定有取得地方。授之以渔,教下大家方法。点下task,看下哪里在使用。offer是放入,对应的take就是取出了。

没得说,进到handle里看看吧。

handle

dataStore.get ,记得上面dataMap.put吗?这里就是通过key将dataMap里的Instances取出来了。然后调用了onChange

com.alibaba.nacos.naming.core.Service#onChange 我们只看下核心 --> com.alibaba.nacos.naming.core.Service#updateIPs -->com.alibaba.nacos.naming.core.Cluster#updateIps

其他的逻辑先跳过,看下注册的逻辑,最后会到这里,将实例放入到了一个Set中。

我们前面说过注册列表是一个双重的Map结构

/**
 * Map(namespace, Map(group::serviceName, Service)).
 */
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();

最里面的是一个service,其实service里还有一个Map。Map里放入了一个Cluster。这个就是上面的那张图中的Cluster了。这下是不是串起来了。

回顾下这张图,是不是茅塞顿开!

到这里整个服务注册流程就结束了!

不怕虐,还不过瘾的同学可以继续看看,我们来做一些扩展。

Notifier任务的执行

我们看到服务端实例注册的时候使用了一个阻塞队列进行解耦。我们来详细了解下这套异步架构是如何工作的。

首先我们看到com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#onPut 向阻塞队列添加了一个任务。这里使用的是notifier

来看下这个notifier。在程序启动的时候init方法上添加了@PostConstruct注解(spring的扩展点)。在Bean初始化后就会执行GlobalExecutor.submitDistroNotifyTask

-->

public static void submitDistroNotifyTask(Runnable runnable) {
    DISTRO_NOTIFY_EXECUTOR.submit(runnable);
}

DISTRO_NOTIFY_EXECUTOR是一个定时任务线程池执行器。

private static final ScheduledExecutorService DISTRO_NOTIFY_EXECUTOR = ExecutorFactory.Managed
        .newSingleScheduledExecutorService(ClassUtils.getCanonicalName(NamingApp.class),
                new NameThreadFactory("com.alibaba.nacos.naming.distro.notifier"));

所以程序启动后,就会调用到com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl.Notifier#run 。这里是一个死循环,如果有实例注册了就会被take出来去注册。

@Override
public void run() {
    Loggers.DISTRO.info("distro notifier started");
    
    for (; ; ) {
        try {
            Pair<String, DataOperation> pair = tasks.take();
            handle(pair);
        } catch (Throwable e) {
            Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
        }
    }
}

这样就完成了注册。

因为这里是一个阻塞队列,所以当队列是空的时候,take方法就会阻塞,不会空循环浪费性能。

为什么搞这么多弯弯绕绕,不直接注册呢?搞这么复杂还加了个阻塞队列。

这里主要是考虑到如果同时来注册的客户端太多,服务端会响应不及时,造成客户端阻塞。所以用个异步,加快了对客户端的响应速度。

如何防止多节点读写并发冲突(COW 写时复制)

我们在修改实例的时候会涉及到注册表的操作。如果注册表正在修改过程中,客户端来读取注册表可能就会读到脏数据了。那么如何解决并发冲突呢?一般就会考虑加锁了,但是加锁是会影响性能的。有没有更好的方法呢?

copy on write 了解下!来上源码 com.alibaba.nacos.naming.core.Cluster#updateIps

整个流程:

1.取出之前的实例,放入到oldMap中

2.将新的实例信息和旧的做比较

3.判断出修改、新增、删除的实例,并进行处理

4.直接将新的ips替换掉之前的

写时复制的思想就是在整个修改的过程中copy出来一份新的数据进行操作,在整个过程中不影响旧的数据。都处理好以后在直接替换掉旧的数据。避免在修改数据的过程中出现“脏读”。

再补充说明下,因为updateIps是由上面的Notifier任务来触发的,这里是一个单线程操作,不会有并发问题。

ServiceChangeEvent事件发布

我们再来想一个问题,服务注册好了以后,是如何通知到消费者的呢?

在服务端执行updateIPs方法时会调用getPushService().serviceChanged(this);发布一个ServiceChangeEvent事件。

com.alibaba.nacos.naming.core.Service#updateIPs --> getPushService().serviceChanged(this); --> publishEvent

this.applicationContext.publishEvent(new ServiceChangeEvent(this, service));

全局搜索下这个事件是在哪里消费的

com.alibaba.nacos.naming.push.PushService#onApplicationEvent在这个方法里会将最新的实例信息推送到消费者。

udpPush(ackEntry);

udpPush会通过服务注册时客户端注册到服务端的信息回调客户端,通知服务实例的变更。

这里和ZK不通,nacos使用的短连接,ZK使用的是长链接。 nacos使用的是UDP,ZK使用的是TCP。

好了,就卷到这里吧。源码很难啃,需要耐着性子都研究几遍了!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值