对于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。
好了,就卷到这里吧。源码很难啃,需要耐着性子都研究几遍了!