一、引言
ok呀,上个章节我们讲了Nacos客户端的服务自动注册,今天我们来看看服务端接收到了客户端的服务注册请求,服务端都做了哪些事情~
二、目录
目录
一、引言
二、目录
三、回顾上节内容:
四、Nacos 服务代码入口分析
五、Nacos 服务端服务注册源码分析
六、本章小结
三、回顾上节内容:
上个章节我们从 Nacos 客户端源码开始讲解,从 Nacos Discover 依赖中,发现了spring.factories创建了相关的配置类,其中就包括了注册类NacosServiceRegistryAutoConfiguration ,这个配置类创建了三个bean对象。其中有一个bean对象实现了监听事件的方法。当Spring容器启动的时候,就会发布一个事件。这个bean对象就会监听到,从而执行真正的注册方法,执行的时候会发送一个健康检查延时任务,告诉Nacos服务端我这个服务还活着,健康检查延时任务每5s执行一次。
四、Nacos 服务代码入口分析
主线任务:找到注册中心模块代码的入口
我们打开Nacos 1.4.1 源码项目,看到了这么多模块,是不是有点懵逼,不知道怎么下手~
这个时候,我们就可以从Nacos的架构图来入手了。注册中心在架构图中叫 naming Server,那我们就在项目中找到 naming 的项目模块。
我们打开项目目录结构一看,这不纯纯就是Springboot项目嘛。我们要找接口肯定就在Controller的包下。
注册实例的接口地址为:/nacos/v1/ns/instance,一猜就是InstanceController类。再看下RequestMapping当中的请求地址,常量拼接起来:v1/ns/instance。正好跟我们要找的请求地址相对应,那代码就是在这个类当中了。
五、Nacos 服务端服务注册源码分析
主线任务:Nacos服务端接受到了实例注册请求,它做了什么事 ?
我们先看接受请求的代码,很简单。先从request当中获取参数,然后调用serviceManager.registerInstance() 方法。
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
// 接受客户端的参数
// 从request当中获取 namespaceId(命名空间ID)、serviceName(服务名称)、Instance实例
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
// Instance 里面就包含了客户端的 ip、port 等信息
final Instance instance = parseInstance(request);
// 上面代码就是获取参数
// 主线任务:调用服务注册的实现类
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
紧接着看serviceManager类中的registerInstance方法。因为我们第一次看,抓住主线任务代码看,分支代码后面可以再去细看。
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
// 不知道是创建了一个什么服务
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
// 根据namespaceId、serviceName获取 Service服务
Service service = getService(namespaceId, serviceName);
// service为空就抛出异常
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
// 上面都是分支代码
// 主线任务:添加服务实例
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
然后调用了 ServiceManager当中的 addInstance方法,这里key还是比较重要的,这个后面去讲一下。
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
// 根据namespaceId、serviceName、ephemeral,获取一个Key,在代码最后put进去
// 重点:后面再去补充一下这个Key的生成规则
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
// 根据namespaceId、serviceName,获取 Service服务
Service service = getService(namespaceId, serviceName);
// 锁住一个service
synchronized (service) {
// 这里提前说一下,ips 上层方法传过来的,是本次实例注册对应的Instance,也就是已开始从Request里面获取的参数信息。
// 最后会放在instanList里面,为什么这里是List,说明它不仅仅只有一个,还会包含之前已经注册的Instance,放在了一个List里面
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
// 创建一个 Instances 对象,并且把 instanceList 属性set进去
Instances instances = new Instances();
instances.setInstanceList(instanceList);
// 主线任务:调用了consistencyService.put 方法,把key和Instances对象当作参数传进去
consistencyService.put(key, instances);
}
}
然后接着往 consistencyService.put() 方法点击调用时,发现了有好几个类都实现该接口。怎么办 ?
两种办法:
- 第一种:Debug的方式,一步一步进去看,就知道对应哪个类了。
- 第二种:看这个属性是怎么注入的。
这里的话,我们采用第二种方法。如下代码,我们能看到这个对象来自consistencyDelegate,注入的时候已经指定对应的Bean了,Bean的名字肯定是唯一的。这里我们用IDEA快捷键跳转,一下子就能找到了。
然后接着调用DelegateConsistencyServiceImpl实现类当中的最后一步put方法了,根据不同的key选择不同对象进行put方法。
@Override
public void put(String key, Record value) throws NacosException {
// 先通过key选择不同的 Service,然后调用对应 Service 的 put 方法
mapConsistencyService(key).put(key, value);
}
private ConsistencyService mapConsistencyService(String key) {
return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
}
那这个 Record 参数是什么 ?
其实就是 Instances 对象实现了 Record 的接口,Instances 对象当中有个instanceList属性,这个属性包含了之前已经注册的实例和新需要注册的实例。
private List<Instance> instanceList = new ArrayList<>();
那这个 key 是怎么来的 ?生成规则是什么 ?(这里重点说明一下,不然都不知道方法接着该怎么走了)这个key的生成是在前面 addInstance() 的方法当中。
// namespaceId:命名空间id , serviceName: 服务名称,ephemeral:是否为临时实例
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
点进 KeyBuilder.buildInstanceListKey() 方法当中看,是根据 ephemeral 这个参数来生成不同的key。
ephemeral 这个参数看着有点眼熟,之前在讲 Nacos客户端 发送心跳的时候遇到过,根据 ephemeral 这个参数判断是否发送健康心跳。这个参数默认是为:true
那在服务端这个 ephemeral 参数是什么意思呢?我第一次看也有点懵逼,这个时候还是得看官方Open API文档。
public static String buildInstanceListKey(String namespaceId, String serviceName, boolean ephemeral) {
// 根据 ephemeral 来生成不同的 key
// ephemeral:是否为临时实例 默认为:true
return ephemeral ? buildEphemeralInstanceListKey(namespaceId, serviceName)
: buildPersistentInstanceListKey(namespaceId, serviceName);
}
看到文档上的说明,ephemeral:是否临时实例
那我们就可以得知,Naocs 客户端默认注册的实例就是临时实例。那到底什么是临时实例,这个就和AP、CP架构有关系了,这个后面看集群源码的时候再去说明,现在你只要知道,Nacos 默认注册的实例都是临时实例。
ok,那我们接着看 key的生成规则,既然 ephemeral 为true的话,走的是 buildEphemeralInstanceListKey(namespaceId, serviceName) 方法。
那拼接起来key的格式为:com.alibaba.nacos.naming.iplist.ephemeral. +namespaceId + ## + serviceName
private static String buildEphemeralInstanceListKey(String namespaceId, String serviceName) {
// com.alibaba.nacos.naming.iplist.ephemeral. +namespaceId + ## + serviceName
return INSTANCE_LIST_KEY_PREFIX + EPHEMERAL_KEY_PREFIX + namespaceId + NAMESPACE_KEY_CONNECTOR + serviceName;
}
那我们现在知道了 key 的大概生成规则是什么样了~
我们接着看 DelegateConsistencyServiceImpl 当中的mapConsistencyService方法。
那 key 是默认为 com.alibaba.nacos.naming.iplist.ephemeral. +namespaceId + ## + serviceName格式的,所以这里应该返回 ephemeralConsistencyService对象。
private ConsistencyService mapConsistencyService(String key) {
// 判断根据不同的key选择不同的对象进行返回
return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
}
public static boolean matchEphemeralKey(String key) {
// currently only instance list has ephemeral type:
return matchEphemeralInstanceListKey(key);
}
// 判断key包含com.alibaba.nacos.naming.iplist.ephemeral开头.就为true,返回 ephemeralConsistencyService 对象
public static boolean matchEphemeralInstanceListKey(String key) {
return key.startsWith(INSTANCE_LIST_KEY_PREFIX + EPHEMERAL_KEY_PREFIX);
}
那我们就着往 ephemeralConsistencyService 对象当中的put方法看。这个 Record 参数前面已经说过了。直接看核心代码 onPut() 方法。
@Override
public void put(String key, Record value) throws NacosException {
// 核心代码
onPut(key, value);
// 集群节点同步,这个后面文章中讲解
distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
globalConfig.getTaskDispatchPeriod() / 2);
}
onPut() 方法 创建Datum对象,把key、和 Instances都放入Datum对象里面去,最后调用了 dataStore.put 方法。点进去,接着往下看,如下图:
public void onPut(String key, Record value) {
// 这里还是判断刚刚那个 key 前缀,这里是为 true
if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
// 创建Datum对象,把key、和 Instances都放入Datum对象里面去
Datum<Instances> datum = new Datum<>();
datum.value = (Instances) value;
datum.key = key;
datum.timestamp.incrementAndGet();
// 最后添加到dataStore当中,这个 dataStore就是一个Map对象
dataStore.put(key, datum);
}
if (!listeners.containsKey(key)) {
return;
}
// 主线任务:添加任务
notifier.addTask(key, DataOperation.CHANGE);
}
DataStore 里面有个Map方法,put 方法也就是把刚刚创建的 Datum 当作 value 放了进去。key就是前面生成的key,value包含了 key 以及 instances 两部分。
最后我们来看下 addTask 方法,datumKey 还是前面 addInstance 方法生成的key,action 是 DataOperation.CHANGE。最后把把key、action包装成 Pair 对象,放入到阻塞队列当中就结束了。
private BlockingQueue<Pair<String, DataOperation>> tasks = new ArrayBlockingQueue<>(1024 * 1024);
/**
* 向队列中添加新的通知任务。
* @param datumKey data key
* @param action action for data
*/
public void addTask(String datumKey, DataOperation action) {
if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
return;
}
if (action == DataOperation.CHANGE) {
services.put(datumKey, StringUtils.EMPTY);
}
// 主线任务:taskks 是一个阻塞队列,并且把key、action包装成 Pair 对象,放入队列当中
tasks.offer(Pair.with(datumKey, action));
}
看完了代码思路不够清晰,这是本章节的分析图,可以根据分析图再过下代码:
六、本章小结
主线任务:Nacos服务端接受到了实例注册请求,它做了什么事 ?
通过源码分析可以得知,先从 request 请求当中接受从客户端传过来的参数。在 addInstance 方法当中生成了一个 key。根据调用注入接口bean得知,调用到了ConsistencyService接口下的DelegateConsistencyServiceImpl实现类当中的put方法。然后又根据不同 key 判断选择调用不同 Service。
最终默认调用到了 EphemeralConsistencyService 接口下的DistroConsistencyServiceImpl当中put方法,又在put方法当中调用了 onPut 方法,在onPut 方法当中,创建了Datum对象,把key、和 Instances都放入Datum对象里面去。放入了DataStore中的Map里。这个步骤有什么用?后面分析源码在进行说明
最后调用了addTask 方法,把key、action包装成 Pair 对象,放入阻塞队列当中。
你以为放入到阻塞队列就完了吗 ?答案:肯定不是,这个后面我们在进行分析,大家可以猜一猜后面都做了什么操作