目录

一、Nacos服务端服务注册

1、服务端调用接口

2、服务注册

instanceServiceV2.registerInstance

EphemeralClientOperationServiceImpl.registerInstance

ServiceManager

clientManager

Client实例AbstractClient

ClientOperationEvent.ClientRegisterServiceEvent

二、Nacos服务端健康检查

1、长连接

2、健康检查

三、Nacos客户端服务发现

订阅处理流程

四、Nacos客户端服务订阅机制

1、Nacos订阅概述

2、定时任务开启

3、定时任务执行内容


一、Nacos服务端服务注册

1、服务端调用接口

客户端在注册服务的时候实际上是调用的NamingService.registerInstance这个方法来完成实例的注册。本质上讲服务注册就是调用的对应接口nacos/v1/ns/instance。

Nacos架构

Spring Cloud Alibaba - Nacos源码分析(二)-LMLPHP

nacos-naming模块下的controllers中的InstanceController,注册实例与它有关。

@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + UtilsAndCommons.NACOS_NAMING_INSTANCE_CONTEXT)
public class InstanceController {
...
    @CanDistro
    @PostMapping
    @Secured(action = ActionTypes.WRITE)
    public String register(HttpServletRequest request) throws Exception {
        
        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);
        
        final Instance instance = HttpRequestInstanceBuilder.newBuilder()
                .setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();
        //注册服务实例
        getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
        NotifyCenter.publishEvent(new RegisterInstanceTraceEvent(System.currentTimeMillis(), "", false, namespaceId,
                NamingUtils.getGroupName(serviceName), NamingUtils.getServiceName(serviceName), instance.getIp(),
                instance.getPort()));
        return "ok";
    }
....
//----------------------------------------------------
public class UtilsAndCommons {
    
    // ********************** Nacos HTTP Context ************************ \\
    
    public static final String NACOS_SERVER_CONTEXT = "/nacos";
    
    public static final String NACOS_SERVER_VERSION = "/v1";
    
    public static final String NACOS_SERVER_VERSION_2 = "/v2";
    
    public static final String DEFAULT_NACOS_NAMING_CONTEXT = NACOS_SERVER_VERSION + "/ns";
    
    public static final String DEFAULT_NACOS_NAMING_CONTEXT_V2 = NACOS_SERVER_VERSION_2 + "/ns";
    
    public static final String NACOS_NAMING_CONTEXT = DEFAULT_NACOS_NAMING_CONTEXT;
    
    public static final String NACOS_NAMING_CATALOG_CONTEXT = "/catalog";
    
    public static final String NACOS_NAMING_INSTANCE_CONTEXT = "/instance";
...

getInstanceOperator(),就是判断是否采用Grpc协议,很明显这个位置走的是instanceServiceV2

    private InstanceOperator getInstanceOperator() {
        return instanceServiceV2;
    }

2、服务注册

instanceServiceV2.registerInstance

实际上instanceServiceV2就是InstanceOperatorClientImpl,所以我们来看这里面的registerInstance方法

    @Override
    public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
        NamingUtils.checkInstanceIsLegal(instance);
        //判断是否为瞬时对象(临时客户端)
        boolean ephemeral = instance.isEphemeral();
        //获取客户端ID
        String clientId = IpPortBasedClient.getClientId(instance.toInetAddr(), ephemeral);
        //通过客户端ID创建客户端连接
        createIpPortClientIfAbsent(clientId);
        //获取服务
        Service service = getService(namespaceId, serviceName, ephemeral);
        //具体注册服务
        clientOperationService.registerInstance(service, instance, clientId);
    }

Nacos2.0以后新增Client模型一个客户端gRPC长连接对应一个Client,每个Client有自己唯一的id(clientId)。Client负责管理一个客户端的服务实例注册Publish和服务订阅Subscribe。

public interface Client {
    // 客户端id/gRPC的connectionId
    String getClientId();

    // 是否临时客户端
    boolean isEphemeral();
    // 客户端更新时间
    void setLastUpdatedTime();
    long getLastUpdatedTime();

    // 服务实例注册/注销/查询
    boolean addServiceInstance(Service service, InstancePublishInfo instancePublishInfo);
    InstancePublishInfo removeServiceInstance(Service service);
    InstancePublishInfo getInstancePublishInfo(Service service);
    Collection<Service> getAllPublishedService();

    // 服务订阅/取消订阅/查询订阅
    boolean addServiceSubscriber(Service service, Subscriber subscriber);
    boolean removeServiceSubscriber(Service service);
    Subscriber getSubscriber(Service service);
    Collection<Service> getAllSubscribeService();
    // 生成同步给其他节点的client数据
    ClientSyncData generateSyncData();
    // 是否过期
    boolean isExpire(long currentTime);
    // 释放资源
    void release();
}

EphemeralClientOperationServiceImpl.registerInstance

EphemeralClientOperationServiceImpl实际负责处理服务注册,那我们来看具体方法

    @Override
    public void registerInstance(Service service, Instance instance, String clientId) throws NacosException {
        NamingUtils.checkInstanceIsLegal(instance);
        //确保Service单例存在
        Service singleton = ServiceManager.getInstance().getSingleton(service);
        if (!singleton.isEphemeral()) {
            throw new NacosRuntimeException(NacosException.INVALID_PARAM,
                    String.format("Current service %s is persistent service, can't register ephemeral instance.",
                            singleton.getGroupedServiceName()));
        }
        //根据客户端id,找到客户端
        Client client = clientManager.getClient(clientId);
        if (!clientIsLegal(client, clientId)) {
            return;
        }
        //客户端Instance模型,转换为服务端Instance模型
        InstancePublishInfo instanceInfo = getPublishInfo(instance);
        //将Instance储存到Client里
        client.addServiceInstance(singleton, instanceInfo);
        client.setLastUpdatedTime();
        client.recalculateRevision();
        //建立Service与ClientId的关系
        NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
        NotifyCenter
                .publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
    }

ServiceManager

Service的容器是ServiceManager,但是在com.alibaba.nacos.naming.core.v2包下,容器中Service都是单例。

public class ServiceManager {
    
    private static final ServiceManager INSTANCE = new ServiceManager();
    //单例Service,可以查看Service的equals和hasCode方法
    private final ConcurrentHashMap<Service, Service> singletonRepository;
    //namespace下的所有service
    private final ConcurrentHashMap<String, Set<Service>> namespaceSingletonMaps;
    
    private ServiceManager() {
        singletonRepository = new ConcurrentHashMap<>(1 << 10);
        namespaceSingletonMaps = new ConcurrentHashMap<>(1 << 2);
    }
    
    public static ServiceManager getInstance() {
        return INSTANCE;
    }
    
    public Set<Service> getSingletons(String namespace) {
        return namespaceSingletonMaps.getOrDefault(namespace, new HashSet<>(1));
    }
    
    /**
     * Get singleton service. Put to manager if no singleton.
     *
     * @param service new service
     * @return if service is exist, return exist service, otherwise return new service
     */
//通过Map储存单例的Service
    public Service getSingleton(Service service) {
        singletonRepository.computeIfAbsent(service, key -> {
            NotifyCenter.publishEvent(new MetadataEvent.ServiceMetadataEvent(service, false));
            return service;
        });
        Service result = singletonRepository.get(service);
        namespaceSingletonMaps.computeIfAbsent(result.getNamespace(), namespace -> new ConcurrentHashSet<>());
        namespaceSingletonMaps.get(result.getNamespace()).add(result);
        return result;
    }
...

所以从这个位置可以看出,当调用这个注册方法的时候ServiceManager负责管理Service单例

clientManager

这是一个接口这里我们要看它对应的一个实现类ConnectionBasedClientManager,这个实现类负责管理长连接clientId与Client模型的映射关系

// 根据clientId查询Client
    @Override
    public Client getClient(String clientId) {
        return clients.get(clientId);
    }

Client实例AbstractClient

负责存储当前客户端的服务注册表,即Service与Instance的关系。注意对于单个客户端来说,同一个服务只能注册一个实例

public abstract class AbstractClient implements Client {
...
    @Override
    public boolean addServiceInstance(Service service, InstancePublishInfo instancePublishInfo) {
        if (null == publishers.put(service, instancePublishInfo)) {
            if (instancePublishInfo instanceof BatchInstancePublishInfo) {
                MetricsMonitor.incrementIpCountWithBatchRegister(instancePublishInfo);
            } else {
                MetricsMonitor.incrementInstanceCount();
            }
        }
        NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(this));
        Loggers.SRV_LOG.info("Client change for service {}, {}", service, getClientId());
        return true;
    }

ClientOperationEvent.ClientRegisterServiceEvent

这里的目的是为了过滤目标服务得到最终Instance列表建立Service与Client的关系,建立Service与Client的关系就是为了加速查询。
发布ClientRegisterServiceEvent事件,ClientServiceIndexesManager监听,ClientServiceIndexesManager维护了两个索引:
- Service与发布clientId
- Service与订阅clientId

@Component
public class ClientServiceIndexesManager extends SmartSubscriber {
    
    private final ConcurrentMap<Service, Set<String>> publisherIndexes = new ConcurrentHashMap<>();
    
    private final ConcurrentMap<Service, Set<String>> subscriberIndexes = new ConcurrentHashMap<>();
    
    public ClientServiceIndexesManager() {
        NotifyCenter.registerSubscriber(this, NamingEventPublisherFactory.getInstance());
    }
...
    private void handleClientOperation(ClientOperationEvent event) {
        Service service = event.getService();
        String clientId = event.getClientId();
        if (event instanceof ClientOperationEvent.ClientRegisterServiceEvent) {
            addPublisherIndexes(service, clientId);
        } else if (event instanceof ClientOperationEvent.ClientDeregisterServiceEvent) {
            removePublisherIndexes(service, clientId);
        } else if (event instanceof ClientOperationEvent.ClientSubscribeServiceEvent) {
            addSubscriberIndexes(service, clientId);
        } else if (event instanceof ClientOperationEvent.ClientUnsubscribeServiceEvent) {
            removeSubscriberIndexes(service, clientId);
        }
    }
//建立Service与发布Client的关系
    private void addPublisherIndexes(Service service, String clientId) {
        publisherIndexes.computeIfAbsent(service, key -> new ConcurrentHashSet<>());
        publisherIndexes.get(service).add(clientId);
        NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true));
    }

这个索引关系建立以后,还会触发ServiceChangedEvent,代表服务注册表变更。对于注册表变更紧接着还要做两个事情:1.通知订阅客户端 2.Nacos集群数据同步。

二、Nacos服务端健康检查

1、长连接

长连接,指在一个连接上可以连续发送多个[数据包],在连接保持期间,如果没有数据包发送,需要双方发链路检测包。

注册中心客户端2.0之后使用gRPC代替http,会与服务端建立长连接,但仍然保留了对旧http客户端的支持。

NamingClientProxy接口负责底层通讯,调用服务端接口。有三个实现类:

  1. NamingClientProxyDelegate:代理类,对所有NacosNamingService中的方法进行代理,根据实际情况选择http或gRPC协议请求服务端。
  2. NamingGrpcClientProxy:底层通讯基于gRPC长连接。
  3. NamingHttpClientProxy:底层通讯基于http短连接。使用的都是老代码基本没改,原来1.0NamingProxy重命名过来的。

以客户端服务注册为例,NamingClientProxyDelegate代理了registerService方法。
 

public class NacosNamingService implements NamingService {
...
    private NamingClientProxy clientProxy;
...
    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        NamingUtils.checkInstanceIsLegal(instance);
        clientProxy.registerService(serviceName, groupName, instance);
    }

NamingClientProxyDelegate会根据instance实例是否是临时节点而选择不同的协议。
临时instance:gRPC
持久instance:http

public class NamingClientProxyDelegate implements NamingClientProxy {
...
    private final NamingHttpClientProxy httpClientProxy;
    
    private final NamingGrpcClientProxy grpcClientProxy;
...
    @Override
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
    }
...
// 临时节点,走grpc长连接;持久节点,走http短连接
    private NamingClientProxy getExecuteClientProxy(Instance instance) {
        return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
    }

2、健康检查

        在之前的1.x版本中临时实例走Distro协议内存存储,客户端向注册中心发送心跳来维持自身healthy状态,持久实例走Raft协议持久化存储,服务端定时与客户端建立tcp连接做健康检查。
        但是2.0版本以后持久化实例没有什么变化,但是2.0临时实例不在使用心跳,而是通过长连接是否存活来判断实例是否健康。

ConnectionManager负责管理所有客户端的长连接。
每3s检测所有超过20s没发生过通讯的客户端,向客户端发起ClientDetectionRequest探测请求,如果客户端在1s内成功响应,则检测通过,否则执行unregister方法移除Connection。
如果客户端持续与服务端通讯,服务端是不需要主动探活的。

@Service
public class ConnectionManager {
...
    Map<String, Connection> connections = new ConcurrentHashMap<>();
...
    @PostConstruct
    public void start() {
        
        initConnectionEjector();
        // Start UnHealthy Connection Expel Task.:启动不健康连接排除功能
        RpcScheduledExecutor.COMMON_SERVER_EXECUTOR.scheduleWithFixedDelay(() -> {
            runtimeConnectionEjector.doEject();
        }, 1000L, 3000L, TimeUnit.MILLISECONDS);
        
    }

    /**
     * init connection ejector.
     */
    public void initConnectionEjector() {
        String connectionRuntimeEjector = null;
        try {
            connectionRuntimeEjector = ControlConfigs.getInstance().getConnectionRuntimeEjector();
            Collection<RuntimeConnectionEjector> ejectors = NacosServiceLoader.load(RuntimeConnectionEjector.class);
            for (RuntimeConnectionEjector runtimeConnectionEjectorLoad : ejectors) {
                if (runtimeConnectionEjectorLoad.getName().equalsIgnoreCase(connectionRuntimeEjector)) {
                    Loggers.CONNECTION.info("Found connection runtime ejector for name {}", connectionRuntimeEjector);
                    runtimeConnectionEjectorLoad.setConnectionManager(this);
                    runtimeConnectionEjector = runtimeConnectionEjectorLoad;
                }
            }
        } catch (Throwable throwable) {
            Loggers.CONNECTION.warn("Fail to load  runtime ejector ", throwable);
        }
        
        if (runtimeConnectionEjector == null) {
            Loggers.CONNECTION
                    .info("Fail to find connection runtime ejector for name {},use default", connectionRuntimeEjector);
            NacosRuntimeConnectionEjector nacosRuntimeConnectionEjector = new NacosRuntimeConnectionEjector();
            nacosRuntimeConnectionEjector.setConnectionManager(this);
            runtimeConnectionEjector = nacosRuntimeConnectionEjector;
        }
    }
    //注销(移出)连接方法
    public synchronized void unregister(String connectionId) {
        Connection remove = this.connections.remove(connectionId);
        if (remove != null) {
            String clientIp = remove.getMetaInfo().clientIp;
            AtomicInteger atomicInteger = connectionForClientIp.get(clientIp);
            if (atomicInteger != null) {
                int count = atomicInteger.decrementAndGet();
                if (count <= 0) {
                    connectionForClientIp.remove(clientIp);
                }
            }
            remove.close();
            LOGGER.info("[{}]Connection unregistered successfully. ", connectionId);
            clientConnectionEventListenerRegistry.notifyClientDisConnected(remove);
        }
    }

移除connection后,继承ClientConnectionEventListener的ConnectionBasedClientManager会移除Client,发布ClientDisconnectEvent事件。

@Component("connectionBasedClientManager")
public class ConnectionBasedClientManager extends ClientConnectionEventListener implements ClientManager {
...
    @Override
    public boolean clientDisconnected(String clientId) {
        Loggers.SRV_LOG.info("Client connection {} disconnect, remove instances and subscribers", clientId);
        ConnectionBasedClient client = clients.remove(clientId);
        if (null == client) {
            return true;
        }
        client.release();
        boolean isResponsible = isResponsibleClient(client);
        NotifyCenter.publishEvent(new ClientOperationEvent.ClientReleaseEvent(client, isResponsible));
        NotifyCenter.publishEvent(new ClientEvent.ClientDisconnectEvent(client, isResponsible));
        return true;
    }

ClientDisconnectEvent会触发几个事件:
1)Distro协议:同步移除的client数据。
2)清除两个索引缓存:ClientServiceIndexesManager中Service与发布Client的关系;ServiceStorage中Service与Instance的关系。
3)服务订阅:ClientDisconnectEvent会间接触发ServiceChangedEvent事件,将服务变更通知客户端。

三、Nacos客户端服务发现

Nacos客户端的服务发现,其实就是封装参数、调用服务接口、获得返回实例列表。

但是如果我们要是细化这个流程,会发现不仅包括了通过NamingService获取服务列表,在获取服务列表的过程中还涉及到通信流程协议(Http or gPRC)、订阅流程、故障转移流程等。

public class NamingExample {
...
System.out.println("instances after register: " + naming.getAllInstances("nacos.test.3"));

关注getAllInstances方法,那我们就需要看一下这个方法的具体操作,当然这其中需要经过一系列的重载方法调用
其实这里的方法比入口多出了几个参数,这里不仅有服务名称,还有分组名、集群列表、是否订阅,重载方法中的其他参数已经在各种重载方法的调用过程中设置了默认值,比如:
​分组名称默认:DEFAULT_GROUOP
集群列表:默认为空数组
是否订阅:订阅

public class NacosNamingService implements NamingService {
...
    @Override
    public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,
            boolean subscribe) throws NacosException {
        ServiceInfo serviceInfo;
        String clusterString = StringUtils.join(clusters, ",");
        // 是否是订阅模式
        if (subscribe) {
            // 先从客户端缓存获取服务信息
            serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString);
            if (null == serviceInfo || !clientProxy.isSubscribed(serviceName, groupName, clusterString)) {
                // 如果本地缓存不存在服务信息,则进行订阅
                serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
            }
        } else {
            // 如果未订阅服务信息,则直接从服务器进行查询
            serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
        }
        // 从服务信息中获取实例列表
        List<Instance> list;
        if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
            return new ArrayList<>();
        }
        return list;
    }

这个流程基本逻辑为:
如果是订阅模式,则直接从本地缓存获取服务信息(ServiceInfo),然后从中获取实例列表,这是因为订阅机制会自动同步服务器实例的变化到本地。如果本地缓存中没有,那说明是首次调用,则进行订阅,在订阅完成后会获得服务信息。
​如果是非订阅模式,那就直接请求服务器端,获得服务信息。

订阅处理流程

在刚才的流程中,涉及到了订阅的逻辑,入口代码为获取实例列表中的方法:

serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);

以下是具体分析。首先这里的clientProxy是NamingClientProxy类的对象,对应的实现类是NamingClientProxyDelegate对应subscribe实现如下:

public class NamingClientProxyDelegate implements NamingClientProxy {
...
    @Override
    public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException {
        NAMING_LOGGER.info("[SUBSCRIBE-SERVICE] service:{}, group:{}, clusters:{} ", serviceName, groupName, clusters);
        String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName);
        String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters);
        // 定时调度UpdateTask
        serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters);
        // 获取缓存中的ServiceInfo
        ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
        if (null == result || !isSubscribed(serviceName, groupName, clusters)) {
            // 如果为null,则进行订阅逻辑处理,基于gRPC协议
            result = grpcClientProxy.subscribe(serviceName, groupName, clusters);
        }
        // ServiceInfo本地缓存处理
        serviceInfoHolder.processServiceInfo(result);
        return result;
    }

具体流程分析:
1. 订阅方法先开启定时任务,这个定时任务的主要作用就是用来定时同步服务端的实例信息,并进行本地缓存更新等操作,但是如果是首次这里将会直接返回来走下一步。
2. 判断本地缓存是否存在,如果本地缓存存在ServiceInfo信息,则直接返回。如果不存在,则默认采用gRPC协议进行订阅,并返回ServiceInfo。
3. grpcClientProxy的subscribe订阅方法就是直接向服务器发送了一个订阅请求,并返回结果。
4. 最后,ServiceInfo本地缓存处理。这里会将获得的最新ServiceInfo与本地内存中的ServiceInfo进行比较,更新,发布变更时间,磁盘文件存储等操作。其实,这一步的操作,在订阅定时任务中也进行了处理。

四、Nacos客户端服务订阅机制

1、Nacos订阅概述

Nacos的订阅机制,如果用一句话来描述就是:Nacos客户端通过一个定时任务,每6秒从注册中心获取实例列表,当发现实例发生变化时,发布变更事件,订阅者进行业务处理(更新实例,更改本地缓存)。

2、定时任务开启

其实订阅本质上就是服务发现的一种方式,也就是在服务发现的时候执行订阅方法,触发定时任务去拉取服务端的数据。
NacosNamingService中暴露的许多重载的subscribe,重载的目的就是让大家少写一些参数,这些参数呢,Nacos给默认处理了。最终这些重载方法都会调用到下面这个方法:

public class NacosNamingService implements NamingService {
...
    @Override
    public void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener)
            throws NacosException {
        if (null == listener) {
            return;
        }
        String clusterString = StringUtils.join(clusters, ",");
        changeNotifier.registerListener(groupName, serviceName, clusterString, listener);
        clientProxy.subscribe(serviceName, groupName, clusterString);
    }

        这里我们先来看subscribe方法,大家可能有些眼熟它是clientProxy类型调用的方法,实际上就是NamingClientProxyDelegate.subscribe(),所以其实这里和之前的服务发现中调用的是一个方法,这里其实是在做服务列表的查询,所以得出结论查询和订阅都调用了同一个方法

public class NamingClientProxyDelegate implements NamingClientProxy {
...
    @Override
    public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException {
        NAMING_LOGGER.info("[SUBSCRIBE-SERVICE] service:{}, group:{}, clusters:{} ", serviceName, groupName, clusters);
        String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName);
        String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters);
        // 定时调度UpdateTask
        serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters);
        // 获取缓存中的ServiceInfo
        ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
        if (null == result || !isSubscribed(serviceName, groupName, clusters)) {
            // 如果为null,则进行订阅逻辑处理,基于gRPC协议
            result = grpcClientProxy.subscribe(serviceName, groupName, clusters);
        }
        // ServiceInfo本地缓存处理
        serviceInfoHolder.processServiceInfo(result);
        return result;
    }

        但是这里我们要关注这里的任务调度,该方法包含了构建serviceKey、通过serviceKey判断重复、最后添加UpdateTask,而其中的addTask的实现就是发起了一个定时任务:

public class ServiceInfoUpdateService implements Closeable {
...
    public void scheduleUpdateIfAbsent(String serviceName, String groupName, String clusters) {
        if (!asyncQuerySubscribeService) {
            return;
        }
        String serviceKey = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
        if (futureMap.get(serviceKey) != null) {
            return;
        }
        synchronized (futureMap) {
            if (futureMap.get(serviceKey) != null) {
                return;
            }
            //构建UpdateTask
            ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, groupName, clusters));
            futureMap.put(serviceKey, future);
        }
    }

定时任务延迟一秒执行:

    private synchronized ScheduledFuture<?> addTask(UpdateTask task) {
        return executor.schedule(task, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
    }

所以在这里我们得出结论,核心为:调用订阅方法和发起定时任务。

3、定时任务执行内容

UpdateTask封装了订阅机制的核心业务逻辑

Spring Cloud Alibaba - Nacos源码分析(二)-LMLPHP

public class ServiceInfoUpdateService implements Closeable {
    private static final long DEFAULT_DELAY = 1000L;
    
    private static final int DEFAULT_UPDATE_CACHE_TIME_MULTIPLE = 6;
...
        @Override
        public void run() {
            long delayTime = DEFAULT_DELAY;
            
            try {
                // 判断是服务是否订阅和未开启过定时任务,如果订阅过直接不在执行
                if (!changeNotifier.isSubscribed(groupName, serviceName, clusters) && !futureMap.containsKey(
                        serviceKey)) {
                    NAMING_LOGGER.info("update task is stopped, service:{}, clusters:{}", groupedServiceName, clusters);
                    isCancel = true;
                    return;
                }
                // 获取缓存的service信息
                ServiceInfo serviceObj = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
                // 如果为空
                if (serviceObj == null) {
                    // 根据serviceName从注册中心服务端获取Service信息
                    serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
                    // 处理本地缓存
                    serviceInfoHolder.processServiceInfo(serviceObj);
                    lastRefTime = serviceObj.getLastRefTime();
                    return;
                }
                // 过期服务,服务的最新更新时间小于等于缓存刷新(最后一次拉取数据的时间)时间,从注册中心重新查询
                if (serviceObj.getLastRefTime() <= lastRefTime) {
                    serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
                    // 处理本地缓存
                    serviceInfoHolder.processServiceInfo(serviceObj);
                }
                //刷新更新时间
                lastRefTime = serviceObj.getLastRefTime();
                if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
                    incFailCount();
                    return;
                }
                // 下次更新缓存时间设置,默认6秒
                // TODO multiple time can be configured.
                delayTime = serviceObj.getCacheMillis() * DEFAULT_UPDATE_CACHE_TIME_MULTIPLE;
                // 重置失败数量为0(可能会出现失败情况,没有ServiceInfo,连接失败)
                resetFailCount();
            } catch (NacosException e) {
                handleNacosException(e);
            } catch (Throwable e) {
                handleUnknownException(e);
            } finally {
                if (!isCancel) {
                    // 下次调度刷新时间,下次执行的时间与failCount有关,failCount=0,则下次调度时间为6秒,最长为1分钟
                    // 即当无异常情况下缓存实例的刷新时间是6秒
                    executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60),
                            TimeUnit.MILLISECONDS);
                }
            }
        }

 业务逻辑最后会计算下一次定时任务的执行时间,通过delayTime来延迟执行。delayTime默认为 1000L * 6,也就是6秒。而在finally里面真的发起下一次定时任务。当出现异常时,下次执行的时间与失败次数有关,但最长不超过1分钟。

总结:

1. 订阅方法的调用,并进行EventListener的注册,后面UpdateTask要用来进行判断;
2. 通过委托代理类来处理订阅逻辑,此处与获取实例列表方法使用了同一个方法;
3. 通过定时任务执行UpdateTask方法,默认执行间隔为6秒,当发生异常时会延长,但不超过1分钟;
5. UpdateTask方法中会比较本地是否存在缓存,缓存是否过期。当不存在或过期时,查询注册中心,获取最新实例,更新最后获取时间,处理ServiceInfo。
6. 重新计算定时任务时间,循环执行流程。

Spring Cloud Alibaba - Nacos

天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣。

06-03 21:47