Nacos客户端服务发现源码分析

总体流程

 

主要是通过namingService.getAllInstances来获取

NamingTest.java

@Ignore
public class NamingTest {
    @Test
    public void testServiceList() throws Exception {
        ......
        //获取服务端所有服务实例,根据服务名称
        List list = namingService.getAllInstances("nacos.test.1");
        ......
    }
}

namingService.getAllInstances方法

该方法提供了好几个重载的版本,主要是多了如下参数:

类型 解释
serviceName String 服务名
groupName String 分组名,默认为DEFAULT_GROUOP
clusters List 集群列表,默认为空数组
subscribe boolean 是否为订阅,默认为true
@Override
public List getAllInstances(String serviceName, String groupName, List clusters,
                                      boolean subscribe) throws NacosException {
    ServiceInfo serviceInfo;
    String clusterString = StringUtils.join(clusters, ",");
    // 是否是订阅模式
    if (subscribe) {
        // 先从客户端缓存获取服务信息
        serviceInfo = serviceInfoHolder.getServiceInfo(serviceName,  , clusterString);
        if (null == serviceInfo) {
            // 如果本地缓存不存在服务信息,则进行订阅
            serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
        }
    } else {
        // 如果不是订阅模式,则直接从服务器进行查询
        serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
    }
    // 从服务信息中获取实例列表
    List list;
    if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
        return new ArrayList();
    }
    return list;
}

对于客户端来说,一般就是注册和订阅服务(订阅机制会自动同步服务器实例的变化到本地。如果本地缓存中没有,那说明是首次调用,则进行订阅,在订阅完成后会获得服务信息)。

非订阅相当于就是查询服务列表,所以我们着重的是订阅

NamingClientProxy.subscribe订阅服务

@Override
public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException {
    String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName);
    String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters);
    // 定时调度UpdateTask,默认6秒, 最多每隔60秒更新一次,
    serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters);
    // 获取缓存中的ServiceInfo,这里在调用该方法之前判断了一次,所以是双重判断
    ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
    if (null == result) {
        // 如果为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进行比较,更新,发布变更时间,磁盘文件存储等操作。其实,这一步的操作,在订阅定时任务中也进行了处理。

定时更新订阅服务的任务解析

@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);
            return;
        }
    
        // 获取缓存的service信息
        ServiceInfo serviceObj = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
        // 如果为空
        if (serviceObj == null) {
            // 根据serviceName从注册中心服务端获取Service信息
            serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
            // 处理本地缓存,并发布InstancesChangeEvent
            serviceInfoHolder.processServiceInfo(serviceObj);
            lastRefTime = serviceObj.getLastRefTime();
            return;
        }
    
        // 过期服务,服务的最新更新时间小于等于缓存刷新(最后一次拉取数据的时间)时间,从注册中心重新查询
        if (serviceObj.getLastRefTime() <= lastRefTime) {
            serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
            // 处理本地缓存,并发布InstancesChangeEvent
            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 (Throwable e) {
        incFailCount();
        NAMING_LOGGER.warn("[NA] failed to update serviceName: {}", groupedServiceName, e);
    } finally {
        // 下次调度刷新时间,下次执行的时间与failCount有关,failCount=0,则下次调度时间为6秒,最长为1分钟
        // 即当无异常情况下缓存实例的刷新时间是6秒
        executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60), TimeUnit.MILLISECONDS);
    }
}

客户端订阅InstancesChangeEvent事件机制

InstancesChangeEvent监听器

首先需要有订阅监听器, 监听是通过订阅是传入的。具体代码如下。

@Override
public void subscribe(String serviceName, String groupName, List clusters, EventListener listener)
    throws NacosException {
    ......
    //添加监听器
    changeNotifier.registerListener(groupName, serviceName, clusterString, listener);
    clientProxy.subscribe(serviceName, groupName, clusterString);
}
public void registerListener(String groupName, String serviceName, String clusters, EventListener listener) {
    String key = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
    ConcurrentHashSet eventListeners = listenerMap.get(key);
    if (eventListeners == null) {
        synchronized (lock) {
            eventListeners = listenerMap.get(key);
            if (eventListeners == null) {
                eventListeners = new ConcurrentHashSet();
                //将EventListener缓存到listenerMap
                listenerMap.put(key, eventListeners);
            }
        }
    }
    eventListeners.add(listener);
}

通过以上代码将监听器装入了一个listenerMap中,这个map中存的是服务的监听器列表

InstancesChangeEvent事件发布

触发InstancesChangeEvent事件是在更新服务时, 发现远程的服务和本地服务有变化时触发的,

public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
    String serviceKey = serviceInfo.getKey();
    if (serviceKey == null) {
        return null;
    }
    ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
    //拉取的服务信息不正常,则还是返回就的服务
    if (isEmptyOrErrorPush(serviceInfo)) {
        //empty or error push, just ignore
        return oldService;
    }
    serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
    //新的服务信息和就的服务信息对比,返回是否更改
    boolean changed = isChangedServiceInfo(oldService, serviceInfo);
    if (StringUtils.isBlank(serviceInfo.getJsonFromServer())) {
        serviceInfo.setJsonFromServer(JacksonUtils.toJson(serviceInfo));
    }
    MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());
    if (changed) {
        NAMING_LOGGER.info("current ips:({}) service: {} -> {}", serviceInfo.ipCount(), serviceInfo.getKey(),
                JacksonUtils.toJson(serviceInfo.getHosts()));
        //如果变更就发布InstancesChangeEvent事件
        NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
                serviceInfo.getClusters(), serviceInfo.getHosts()));
        DiskCache.write(serviceInfo, cacheDir);
    }
    return serviceInfo;
}

NotifyCenter(通知中心)发布事件机制

  1. 通知中心会在创建NacosNamingService时,初始化事件发布者和订阅者
public class NacosNamingService implements NamingService {
  ......
  private void init(Properties properties) throws NacosException {
      ......
      NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384);
      //添加关注者处理器, 会调用addSubscriber进行注册
      NotifyCenter.registerSubscriber(changeNotifier);
      ......
  }
  ......
}
  NotifyCenter注册订阅者处理器
private static void addSubscriber(final Subscriber consumer, Class subscribeType,EventPublisherFactory factory) {
    final String topic = ClassUtils.getCanonicalName(subscribeType);
    EventPublisher publisher = INSTANCE.publisherMap.get(topic);
    publisher.addSubscriber(consumer);
}
  1. 在客户端进行服务订阅时会添加监听器到订阅者changeNotifier中,registerListener方法会将监听器加入到一个Map中。
public class NacosNamingService implements NamingService {
  ......
  @Override
  public void subscribe(String serviceName, String groupName, List clusters, EventListener listener) throws NacosException {
      ......
      changeNotifier.registerListener(groupName, serviceName, clusterString, listener);
      ......
  }
  ......
}

public void registerListener(String groupName, String serviceName, String clusters, EventListener listener) {
    String key = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
    ConcurrentHashSet eventListeners = listenerMap.get(key);
    if (eventListeners == null) {
        synchronized (lock) {
            eventListeners = listenerMap.get(key);
            if (eventListeners == null) {
                eventListeners = new ConcurrentHashSet();
                //将EventListener缓存到listenerMap
                listenerMap.put(key, eventListeners);
            }
        }
    }
    eventListeners.add(listener);
}
  1. 在定时任务中发现服务有变化时通过NotifyCenter触发事件
public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
    ......
    // 添加实例变更事件,会被订阅者执行
    NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
                                                       serviceInfo.getClusters(), serviceInfo.getHosts()));
    ......
}
  1. publishEvent方法中根据事件类型获取事件发布者然后发布,这里的事件发布者就是在1步骤进行注册的
private static boolean publishEvent(final Class eventType, final Event event) {
    ......
    // 根据InstancesChangeEvent事件类型,获得对应的CanonicalName
    final String topic = ClassUtils.getCanonicalName(eventType);

    // 将CanonicalName作为Key,从NotifyCenter#publisherMap中获取对应的事件发布者(EventPublisher)
    EventPublisher publisher = INSTANCE.publisherMap.get(topic);
    if (publisher != null) {
        // 事件发布者publisher发布事件(InstancesChangeEvent)
        return publisher.publish(event);
    }
    ......
}
  1. 事件发布者发布事件者处理事件

    这里的事件发布者通过追踪代码发现实际是由NamingEventPublisher处理的。通过代码可以看出,这里NamingEventPublisher 实际上是一个线程,而且会有一个queue来负责管理事件,当队列queue添加失败时直接由当前线程继续处理事件(退化成同步处理了)

public class NamingEventPublisher extends Thread implements ShardedEventPublisher {
    ......
    public boolean publish(Event event) {
        .....
        boolean success = this.queue.offer(event);
        if (!success) {
            handleEvent(event);
            return true;
        }
        return true;
    }
    ......
}
  1. NamingEventPublisher处理事件

    它既然是一个线程,那么肯定有run方法,那么就应该去消费掉通过publish发布的事件

    事件通过该线程后就会得到处理,关注者就能获得事件回调了。

public class NamingEventPublisher extends Thread implements ShardedEventPublisher {    
    ......
    @Override
    public void run() {
        //执行等待订阅者就绪
        waitSubscriberForInit();
        //处理收到的事件
        handleEvents();
        ......
    }
    ......
    private void handleEvents() {
        while (!shutdown) {
            ......
            final Event event = queue.take();
            handleEvent(event);
            ......
        }
    }
    ......    
    private void handleEvent(Event event) {
        Class eventType = event.getClass();
        Set> subscribers = subscribes.get(eventType);
        ......
        for (Subscriber subscriber : subscribers) {
            notifySubscriber(subscriber, event);
        }
    }
    ......
    //该方法会尝试使用关注者的执行线程池去处理任务,如果没有线程池就在当前线程处理   
    @Override
    public void notifySubscriber(Subscriber subscriber, Event event) {
        final Runnable job = () -> subscriber.onEvent(event);
        final Executor executor = subscriber.executor();
        if (executor != null) {
            executor.execute(job);
        } else {
            job.run();
        }
    }
    ......
}

你可能感兴趣的:(Nacos专栏,微服务,java,服务发现)