作为后起之秀,Nacos基本要取代Eureka的位置,我没有在项目中使用过,只在本地跑了一下,处于好奇我想浅显的分析Nacos的实现,看看是否比Eureka更优秀
官网图
从图中可以看到,nacos似乎比eureka多了一致性协议,eureka是p2p,也就是非强一致性,貌似nacos解决了这个问题,nacos还有控制台,嗯这个确实更方便了
一、服务注册
1.1 客户端
eureka是利用SmartLifeCycle
接口的生命周期方法來完成调用的。
nacos是利用的事件监听机制来触发的。都是固定的套路,首先就是分析NacosDiscoveryAutoConfiguration
,然后里面有个@Bean
NacosAutoServiceRegistration
.
spring的时间监听某个时间是通过泛型来判断的ApplicationListener<WebServerInitializedEvent>
,也就是说是是web容器初始化完成就开始调用的。然后就是常规的封装注册信息,然后调用http接口
//临时节点特殊处理
if (instance.isEphemeral()) {
BeatInfo beatInfo = new BeatInfo();
beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
beatInfo.setIp(instance.getIp());
beatInfo.setPort(instance.getPort());
beatInfo.setCluster(instance.getClusterName());
beatInfo.setWeight(instance.getWeight());
beatInfo.setMetadata(instance.getMetadata());
beatInfo.setScheduled(false);
long instanceInterval = instance.getInstanceHeartBeatInterval();
beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);
beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
//调用http接口
serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
在调用http接口的时候,如果有多个服务端,他是随机选取的,如果随机选取的服务端调用失败,那么开始顺序的调用其他服务端接口
还有稍微有点特殊的地方就是,注册的时候它分临时节点还是持久化节点,如果是临时时节点他有额外的动作:立马加上心跳
心跳代码:
//搞了一个一次性的心跳
executorService.schedule(new BeatTask(beatInfo), 0, TimeUnit.MILLISECONDS);
//然后在BeatTask方法后有立马开启一个新的调度
run(){
long result = serverProxy.sendBeat(beatInfo);//发送http请求
long nextTime = result > 0 ? result : beatInfo.getPeriod();
//立马又开启一个
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
我目前有2个疑问
- 为什么只有临时节点会立马开启一个心跳?
- 他的心跳这种定时任务,一旦上次发生异常,那么下次不会有心跳发生了(因为不会立马开启一个新的调度)
1.2 服务端
读了服务端代码感觉他的方法名字起的有一点乱,而且逻辑有点复杂,而且还不写注释。
在InstanceController
的register()
是入口
1.1.2 创建空服务
服务的创建中存在很多逻辑,这也是我读源码的时候头疼的地方,功能耦合性太高,你创建服务就创建服务,可以再其他地方进行类似健康检查之类的工作,他基本都柔和到一起了,它主要包含下面三个大功能(一个创建服务动作,做了好多事情)
- 创建Service,然后存在注册表中
- 开启健康检查任务,主要是为了服务剔除
- 加入listener
private Map<String, CopyOnWriteArrayList<RecordListener>> listeners = new ConcurrentHashMap<>();
,listener的子类就是service,他有2个重要方法onChnage
和onDelete
,主要就是处理节点增删改之后的处理逻辑。这里加入listener,其实也是等待这处理的意思。nacos注册节点大思想就是异步的思想,比如现在他要写一个instance到内存,他是直接提交一个notification然后开启一个while死循环,然后这些listener逐个处理。
补充知识:关于注册,必须了解它的注册表,他的结构也非常复杂,必须通过官网的图解结合才能理解
翻译成代码就是Map<String, Map<String, Service>> serviceMap
,其中第一个key是namespace,第二个key是service名字,最终存的是Service
该类中有一个重要字段private Map<String, Cluster> clusterMap = new HashMap<>();
,这里就牵扯出了Cluster的概念。Cluster中有三个字段
//一个cluster有多个实例
@JSONField(serialize = false)
private Set<Instance> persistentInstances = new HashSet<>();
@JSONField(serialize = false)
private Set<Instance> ephemeralInstances = new HashSet<>();
//持有属于service的引用
@JSONField(serialize = false)
private Service service;
- namespace:是起到环境隔离的效果,比如dev stage prod
- service name:指的就是一个具体的服务,该服务可能有N个实例
- cluster:这个概念是最难理解的,下面是官网的图,可以理解成是的集合一组实例的集合,通过集群进行数据同步,而且可以看出serive和cluster是相互持有引用的
//创建空服务的入口
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
//创建空服务最终调用的方法
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();
if (cluster != null) {
//如果cluster不为空,那么service和cluster相互更新引用
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
service.validate();
if (local) {
//临时节点
putServiceAndInit(service);
} else {
//持久化节点
addOrReplaceService(service);
}
}
}
下面针对创建空服务三个功能进行具体分析
1. 存在注册表中
serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
2. 健康检查
nacos的定时任务粒度是service,也就是每个service就开启一个定时任务,这种控制带来的就是更灵敏。但是占用内存更高。
心跳是每5秒执行一次。
如果超过15秒没有心跳那么就会发布心跳超时任务
如果超过30秒没有心跳那么就删除该节点
3. 添加到listener中
//临时
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
//持久化
consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
什么是listener你?,看下面的源码,他就是一个map,key主要由namespace和service组成, service本身就是集成RecordListener
,所以CopyOnWriteArrayList
中存放的就是service,可以理解成他就是一个service的集合,
Map<String, CopyOnWriteArrayList<RecordListener>> listeners
单独分析listener没有意义,必须要结合notifier来分析
1.1.3 存储instance
上面已经创建了service,相当于给instance创建了个容器,下面就是朝容器里面放instance,这里又分为临时节点和持久化节点。
主要做了2部分内容,一个是向阻塞队列中插入值,然后有个while循环一直以阻塞的方式取改队列,然后和上面的listener结合起来进行节点状态变更的处理,主要就是判断是增加了节点,删除了节点等。另外一部分好像是服务同步的。
入口
consistencyService.put(key, instances);//这个就是存储instance和服务同步的入口
onPut(key, value);//加入一个新的notifier,然后异步的处理加入新的instance
taskDispatcher.addTask(key);//处理服务同步
这次只针对临时节点分析
这里主要是取阻塞队列之后的处理比较重要。主要切入方法是updateIPs
,针对这个源码有几个局部变量我分析一下
- clusterMap:key是cluster name,value是cluster对象
- ipMap:key是cluster名字,value是instance列表,这个方法看上去很长,其实都是在组织ipMap
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
for (String clusterName : clusterMap.keySet()) {
ipMap.put(clusterName, new ArrayList<>());
}
for (Instance instance : instances) {
...................
}
//上面代码都是在组织ipMap
//下面的代码就是循环调用cluster的updateIPs方法
for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
//make every ip mine
List<Instance> entryIPs = entry.getValue();
clusterMap.get(entry.getKey()).updateIPs(entryIPs, ephemeral);
}
}
搞了这么一大圈就是就是调用cluster的updateIPs
方法,因为cluster是instance的集合,nacos中添加一个instance做的逻辑太复杂了,按照道理添加实例无非就是向注册表中插入一个instance,但是他搞这么麻烦主要是他的那段代码也是为了适应更新操作。
- 找出更新的节点,然后进行处理
- 找出新增的节点,然后进行处理
- 找出要剔除的节点,然后进行处理
- 整个过程处处体现了copyOnWrite的思想
public void updateIPs(List<Instance> ips, boolean ephemeral) {
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) {
......
}
List<Instance> newIPs = subtract(ips, oldIPMap.values());
if (newIPs.size() > 0) {
....
}
List<Instance> deadIPs = subtract(oldIPMap.values(), ips);
if (deadIPs.size() > 0) {
....
}
toUpdateInstances = new HashSet<>(ips);
if (ephemeral) {
ephemeralInstances = toUpdateInstances;
} else {
persistentInstances = toUpdateInstances;
}
}
1.1.4 服务同步
核心代码在下面,关于syncData
的源码,在后面在分析
long timestamp = System.currentTimeMillis();
boolean success = NamingProxy.syncData(data, task.getTargetServer());
if (!success) {
SyncTask syncTask = new SyncTask();
syncTask.setKeys(task.getKeys());
syncTask.setRetryCount(task.getRetryCount() + 1);
syncTask.setLastExecuteTime(timestamp);
syncTask.setTargetServer(task.getTargetServer());
retrySync(syncTask);
} else {
// clear all flags of this task:
for (String key : task.getKeys()) {
taskMap.remove(buildKey(key, task.getTargetServer()));
}
}
至此整个临时节点的服务注册就算分析完了,我大致在总结一遍
- 创建空service对象,在空service对象中开启定时剔除任务,向listener中添加对象为了后面异步存储instance服务(注册表结构复杂,然后一个方法里面 功能太多)
- 存储instance对象,这里采用异步的方式从阻塞队列中取数据,然后还处理(异步是特色,copyOnWrite对整体性能进一步提高,但是我总是感觉一个方法里面的功能还是太多,导致功能不清晰)
- 增加同步任务(这个整体功能清晰,有spring的感觉)
二、服务续约
在分析服务端代码的时候发现,如果在心跳的时候发现没有instance,会调用注册接口,这个是为了啥?
if (instance == null) {
instance = new Instance();
instance.setPort(clientBeat.getPort());
instance.setIp(clientBeat.getIp());
instance.setWeight(clientBeat.getWeight());
instance.setMetadata(clientBeat.getMetadata());
instance.setClusterName(clusterName);
instance.setServiceName(serviceName);
instance.setInstanceId(instance.generateInstanceId());
instance.setEphemeral(clientBeat.isEphemeral());
serviceManager.registerInstance(namespaceId, serviceName, instance);
}
Service service = serviceManager.getService(namespaceId, serviceName);
if (service == null) {
throw new NacosException(NacosException.SERVER_ERROR, "service not found: " + serviceName + "@" + namespaceId);
}
service.processClientBeat(clientBeat);
在processClientBeant
的方法中,和eureka稍微不同的是,它也是采用异步处理的
HealthCheckReactor.scheduleNow(clientBeatProcessor);//异步的去处理
instance.setLastBeat(System.currentTimeMillis());//心跳核心代码
三、服务同步
public String onSyncDatum(HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String, Datum<Instances>> dataMap =
serializer.deserializeMap(entity.getBytes(), Instances.class);
for (Map.Entry<String, Datum<Instances>> entry : dataMap.entrySet()) {
if (KeyBuilder.matchEphemeralInstanceListKey(entry.getKey())) {
String namespaceId = KeyBuilder.getNamespace(entry.getKey());
String serviceName = KeyBuilder.getServiceName(entry.getKey());
if (!serviceManager.containService(namespaceId, serviceName)
&& switchDomain.isDefaultInstanceEphemeral()) {
//如果没有服务就创建一个控服务,在注册的时候已经分析过
serviceManager.createEmptyService(namespaceId, serviceName, true);
}
//前面注册的时候也分析过这个方法,这个方法就是加入一个notifier
consistencyService.onPut(entry.getKey(), entry.getValue().value);
}
}
return "ok";
}
服务同步的代码和注册的代码基本是一致的。没有服务就创建空服务,然后加入一个notifier异步的处理新的instance。
四、服务下线
其实在服务注册的时候已经分析过了,他的服务剔除是以service为单位的。