Nacos服务端代码分析
InstanceController
进入InstanceController类,可以看到一个register方法,就是服务注册的方法了:
点击查看代码
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
//获取namespaceId
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
//尝试获取serviceName,其格式为 group_name@@service_name
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
// 解析出实例信息,封装为Instance对象
final Instance instance = parseInstance(request);
// 注册实例
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
而其中的registerInstance方法就是注册服务实例的方法:
我们可以继续跟进createEmptyService这个方法:
再继续跟进createServiceIfAbsent方法如果缺席就创建,也就是说不存在就创建,第一次来就创建。
进来createServiceIfAbsent以后,首先尝试从注册表中获取一次服务,跟进去
如果不为null则代表有,也就是组Map有,通过这个chooseServiceMap方法得到组map,跟进来:
其实就是从服务列表里面根据命名空间id获取Map。接着通过组的服务名获取Service:
如果为空,说明该服务是第一次注册,创建新的Service:
这个groupName其实是serviceName的一部分是两个@符号隔开的,我们再第一次创建服务的时候传进去的cluster是null,随意上图中的cluster肯定是null,里面的代码也即不会执行。
记下来就是putServiceAndInit,服务创建好就要把其put进来。继续跟进:
第一个方法putService,继续:
首先是判断一下serviceMap注册表是否有这个服务,这里其实是为了提高性能(单例模式就有这种判断),如果没有就设置一把锁,出于线程安全考虑,当我在创建的时候别人就不要创建了,否则出现并发写修改的安全问题,然后在这个同步代码块里面又判断一次有没有,其实就是为了保证线程安全,如果确实没有就往注册表里面put:
但这个ConcurrentSkipListMap并不是在同步代码块里填的,而是出来这个同步代码块,在这个:
serviceMap.get(service.getNamespaceId()).putIfAbsent(service.getName(), service);
这句代码填入的真正的服务,这样确保我们的服务一定能写进去。
其中的putIfAbsent表示:如果不存在才put,如果并发情况下有其他人put进来了,就不往里面put了。这就是保证put动作的有效性。
然后service已经有了,我们取一次,并进行初始化,其实是进行一个健康检测:
接下来进行一致性服务的监听:consistencyService
监听服务的变更:
到这里,putServiceAndInit这个服务注册和初始化就做完了:
但是要记得putServiceAndInit方法里面的两个监听器。后续分析会用到
继续往下执行createEmptyService后,我们需要添加实例到service中addInstance这个方法,跟进:
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);记住这些参数。
比较重要的是上图的同步代码块
点击查看代码
synchronized (service) {
// 拷贝注册表中旧的实例列表,然后结合新注册的实例,得到最终的实例列表
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
//封装实例列表到instances
Instances instances = new Instances();
instances.setInstanceList(instanceList);
//更新注册表(更新本地注册表、同步给nacos集群中的其他节点)
consistencyService.put(key, instances);
}
因为ConsistencyService本身是个接口,它有很多的实现,Ctrl+h可以看到如下图:
可以发现其默认注册的就是delegate这个,意思就是委托:
它并不是一种真实的实现,是一种装饰模式。我们第一次注册进来的就是它:consistencyDelegate
所以我们调用的put就是DelegateConsistencyServiceImpl的put方法:
这个key就是前面传过来的服务标识serviceId,它会根据key来得到哪个service:
临时实例采用的协议是Distro协议,默认就是临时协议实现类的这个put方法:
上图主要的操作是:
1.更新本地注册表
2.同步给Nacos集群中的其他节点
继续跟进这个onput方法:
我们关注下这个dataStore:
它其实里面维护了一个dataMap存放serviceId和Datum的
private Map<String, Datum> dataMap = new ConcurrentHashMap<>(1024);
我们再跟进addTask这个方法:
它将那些serviceId和实例数据包存放到了任务里等待DistroConsistencyServiceImpl初始化后利用线程池来执行里面的具体的中注册。
找到这个类的构造函数:
发现它会利用一个全局的线程池来执行notifier任务,继续跟如notifier这个run方法
可以发现里面有个for循环它会从阻塞队列中获取任务来执行,阻塞队列BlockingQueue是指:当队列中有元素时这个take会返回,当无元素时就会take会waite阻塞,线程释放cpu执行权在这等着被唤醒。所以这个一只循环的for无所谓,它不是盲等,它有等待唤醒机制的。因此,只要你往这个队列里面投放一个任务,take才会醒过来取这个任务,然后handle执行。而这个取的动作是用线程池异步执行的,也就是说这个活跟服务注册就没关系了,是一个单独的任务。
相当于在我们执行onPut操作的时候,先更新本地注册表,其实就是将任务放到队列里面就结束了,然后不管你有没有执行完,我接着执行同步nacos集群的操作,这就是异步更新的作用,这样就提高了注册的效率,性能。
回到put这个方法这:
distroProtocol是发行版协议的意思,sync就是同步的意思,进入sync:
可以发现,将数据同步到nacos集群的其他节点也是异步的。
所以,Nacos内部接受到注册的请求时,不会立即写数据,而是将服务注册的任务放入一个阻塞队列里面立即响应给客户端。然后利用线程池
读取阻塞队列中的任务,异步来完成本地实例和集群中其他实例的更新,从而提高并发能力。这个阻塞队列大小为1024。