Nacos源码客户端系列第8篇客户端服务实例管理

目录

属性说明

初始化

服务查询

updateServiceNow(服务更新)

scheduleUpdateIfAbsent(服务定时更新)

 总结


学习目的:通过本篇学习,需要知道本地服务实例是怎么存储的,是怎么保证最新的,这里面涉及到的并发控制是怎么实现。

属性说明

public class HostReactor{

    private static final long DEFAULT_DELAY = 1000L;

    private static final long UPDATE_HOLD_INTERVAL = 5000L;

    private final Map<String, ScheduledFuture<?>> futureMap = new HashMap<String, 
         ScheduledFuture<?>>();

    //存储本地的服务信息
    private Map<String, ServiceInfo> serviceInfoMap;

    private Map<String, Object> updatingMap;

    private PushReceiver pushReceiver;

    private EventDispatcher eventDispatcher;

    private NamingProxy serverProxy;

    private FailoverReactor failoverReactor;

    private String cacheDir;

    private ScheduledExecutorService executor;

    ..... 
}

public class ServiceInfo {

    @JSONField(serialize = false)
    private String jsonFromServer = EMPTY;
    public static final String SPLITER = "@@";
    //服务名
    private String name;
    //组名
    private String groupName;
    //集群
    private String clusters;
    
    private long cacheMillis = 1000L;
    //实列列表
    @JSONField(name = "hosts")
    private List<Instance> hosts = new ArrayList<Instance>();
     
    private long lastRefTime = 0L;

    private String checksum = "";

    private volatile boolean allIPs = false;

    ....
}

serviceInfoMap:记录service信息 key 为 serviceName@@clusterName value: ServiceInfo 

map类型: ConcurrentHashMap,

updatingMap: 记录正在更新的service信息,记录service信息 key 为 serviceName@@clusterName  类型为ConcurrentHashMap, 用于并发控制同一时刻同样的service只有一个线程执行请求更新操作

pushReceiver:创建udp监听端口并监听Nacos Server 发送过来的数据

eventDispatcher:服务变更事件监听器 

cacheDir:服务持久化目录(在服务启动时如果设置了【namingLoadCacheAtStart】属性自动总该目录加载service列表  减轻服务刚启动时的请求的压力)

executor: 1秒钟执行一次的服务更新任务执行器

futureMap: 并发控制服务更新任务的执行, 保证同一时刻同一个服务只有一个更新任务在执行。

futureMap 和 updatingMap 都是控制并发,但是场景不一样控制的操作不一样,前者是控制定时更新服务任务的执行 后者是控制远程服务的请求

初始化

   

public HostReactor(EventDispatcher eventDispatcher, NamingProxy serverProxy, String 
    cacheDir) {
        this(eventDispatcher, serverProxy, cacheDir, false, 
                UtilAndComs.DEFAULT_POLLING_THREAD_COUNT);
     }


public HostReactor(EventDispatcher eventDispatcher, NamingProxy serverProxy, String 
       cacheDir, boolean loadCacheAtStart, int pollingThreadCount) {
  
        executor = new ScheduledThreadPoolExecutor(pollingThreadCount, new 
              ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setDaemon(true);
                thread.setName("com.alibaba.nacos.client.naming.updater");
                return thread;
            }
        });

        this.eventDispatcher = eventDispatcher;
        this.serverProxy = serverProxy;
        this.cacheDir = cacheDir;
        if (loadCacheAtStart) {
            this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo> 
                 (DiskCache.read(this.cacheDir));
        } else {
            this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(16);
        }

        this.updatingMap = new ConcurrentHashMap<String, Object>();
        this.failoverReactor = new FailoverReactor(this, cacheDir);
        this.pushReceiver = new PushReceiver(this);
    }

初始化方法里初始化了实例更新线程池:线程池大小默认为:cpu cores / 2

loadCacheAtStart:控制是否服务启动时候从缓存文件加载服务列表到serviceInfoMap中

这里面的serviceInfoMap是线程安全的是因为更新服务实例有多个场景 1、服务第一次查询的时候会更新 2、定时任务会定时更新任务 3、推送任务也会触发更新

那么updatingMap为啥也要并发安全呢? 

因为updatingMap的入口方法是服务实例查询,执行这个方法的线程肯定不只一个。

因此对这个对象的更新是需要线程安全的

服务查询

 public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {

        NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
        String key = ServiceInfo.getKey(serviceName, clusters);
        //failoverReactor 后面的章节讲解,这里先略过
        if (failoverReactor.isFailoverSwitch()) {
            return failoverReactor.getService(key);
        }
        //本地serverInfoMap中查询 
        ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);

        if (null == serviceObj) {
            serviceObj = new ServiceInfo(serviceName, clusters);

            serviceInfoMap.put(serviceObj.getKey(), serviceObj);

            updatingMap.put(serviceName, new Object());
            updateServiceNow(serviceName, clusters);
            updatingMap.remove(serviceName);

        } else if (updatingMap.containsKey(serviceName)) {

            if (UPDATE_HOLD_INTERVAL > 0) {
                // hold a moment waiting for update finish
                synchronized (serviceObj) {
                    try {
                        serviceObj.wait(UPDATE_HOLD_INTERVAL);
                    } catch (InterruptedException e) {
                        NAMING_LOGGER.error("[getServiceInfo] serviceName:" + 
                             serviceName + ", clusters:" + clusters, e);
                    }
                }
            }
        }
        //启动定时服务更新任务
        scheduleUpdateIfAbsent(serviceName, clusters);

        return serviceInfoMap.get(serviceObj.getKey());
    }


private ServiceInfo getServiceInfo0(String serviceName, String clusters) {

        String key = ServiceInfo.getKey(serviceName, clusters);

        return serviceInfoMap.get(key);
    }

如果本地serviceInfoMap中么有找到该服务,则调用方法 updateServiceNow(serviceName, clusters); 拉取服务并保存到本地serviceInfoMap中。并设置服务更新状态;

如果本地已经存在该服务且有线程在执行更新操作则等待,超时时间是5秒。等待updateServiceNow方法执行完毕后唤醒程序继续执行。

启动该服务的定制更新任务 并返回刚刚查找的serviceInfo.

if (oldService != null) {
                synchronized (oldService) {
                    oldService.notifyAll();
                }
            }

updateServiceNow(服务更新)

 public void updateServiceNow(String serviceName, String clusters) {
       //保存老的服务信息 用于后面finally块中幻灯等待该锁的线程 
       ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
        try {

            String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUDPPort(), false);

            if (StringUtils.isNotEmpty(result)) {
                processServiceJSON(result);
            }
        } catch (Exception e) {
            NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
        } finally {
            if (oldService != null) {
                synchronized (oldService) {
                    oldService.notifyAll();
                }
            }
        }
    }

 public ServiceInfo processServiceJSON(String json) {
        ServiceInfo serviceInfo = JSON.parseObject(json, ServiceInfo.class);
        ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
        if (serviceInfo.getHosts() == null || !serviceInfo.validate()) {
            //empty or error push, just ignore
            return oldService;
        }

        boolean changed = false;

            ...

            Set<Instance> modHosts = new HashSet<Instance>();
            Set<Instance> newHosts = new HashSet<Instance>();
            Set<Instance> remvHosts = new HashSet<Instance>();

            ...

            serviceInfo.setJsonFromServer(json);

            if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
                eventDispatcher.serviceChanged(serviceInfo);
                DiskCache.write(serviceInfo, cacheDir);
            }

        } else {
            changed = true;
            NAMING_LOGGER.info("init new ips(" + serviceInfo.ipCount() + ") service: " + 
            serviceInfo.getKey() + " -> " + JSON
                .toJSONString(serviceInfo.getHosts()));
            serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
            eventDispatcher.serviceChanged(serviceInfo);
            serviceInfo.setJsonFromServer(json);
            DiskCache.write(serviceInfo, cacheDir);
        }

        MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());
        ...

        return serviceInfo;
    }

通过NamingProxy请求远程服务列表信息,然后通过Json反序列化成ServiceInfo对象,校验新对象的有效的host信息是否不为空(ServiceInfo.validate() 方法逻辑应该是有问题的,用于返回true), 否则还是返回老的ServiceInfo。

进一步比较新老对象的差异(统计出新增 修改 删除的host 列表)如果新老对象有变化, 则触发服务变更事件eventDispatcher.serviceChanged(serviceInfo); 同时缓存新服务到本地serviceInfoMap 同时更新本地的文件缓存。

public ServiceInfo processServiceJSON(String json) {
        ServiceInfo serviceInfo = JSON.parseObject(json, ServiceInfo.class);
        ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
        if (serviceInfo.getHosts() == null || !serviceInfo.validate()) {
            return oldService;
        }
 
      .....
 
}

class ServiceInfo {

 ...

//这个有问题方法 用于返回true 
public boolean validate() {
        if (isAllIPs()) {
            return true;
        }

        List<Instance> validHosts = new ArrayList<Instance>();
        for (Instance host : hosts) {
            if (!host.isHealthy()) {
                continue;
            }

            for (int i = 0; i < host.getWeight(); i++) {
                validHosts.add(host);
            }
        }

        return true;
    }
 
  ...
}

scheduleUpdateIfAbsent(服务定时更新)

 public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
        if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
            return;
        }

        synchronized (futureMap) {
            if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
                return;
            }

            ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
            futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
        }
    }

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


public class UpdateTask implements Runnable {
        long lastRefTime = Long.MAX_VALUE;
        private String clusters;
        private String serviceName;

        public UpdateTask(String serviceName, String clusters) {
            this.serviceName = serviceName;
            this.clusters = clusters;
        }

        @Override
        public void run() {
            try {
                ServiceInfo serviceObj = 
                     serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));

                if (serviceObj == null) {
                    updateServiceNow(serviceName, clusters);
                    executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
                    return;
                }

                if (serviceObj.getLastRefTime() <= lastRefTime) {
                    updateServiceNow(serviceName, clusters);
                    serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, 
                    clusters));
                } else {
                    // if serviceName already updated by push, we should not override it
                    // since the push data may be different from pull through force push
                    refreshOnly(serviceName, clusters);
                }

                executor.schedule(this, serviceObj.getCacheMillis(), 
                    TimeUnit.MILLISECONDS);

                lastRefTime = serviceObj.getLastRefTime();
            } catch (Throwable e) {
                NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, 
                e);
            }

        }
    }

通过 futureMap 控制任务的并发,如果该服务更新任务还在执行就不处理。否则添加更新任务 注意这里用了synchronize 关键字做了二次校验,是典型的synchronize 用法,这里不赘述。新任务以1秒中后执行

执行逻辑:

如果当前缓存中没有那么直接从远程服务拉取服务,1秒钟后再次执行更新任务

如果缓存中服务的lastRefTime小于当前的lastRefTime字段,说明缓存中服务已经过期,需要重新拉取更新。

如果缓存中服务的lastRefTime大于当前的lastRefTime字段 则 执行refreshOnly(serviceName, clusters) 方法 但不做本地服务的任何更新操作

refreshOnly这个方法执行的意义目前没有发现,有同学知道的评论区请留言。

什么情况下缓存的数据会跟任务中的lastRefTime时间不一致呢?

比如服务端主动推送服务列表到客户端更新缓存

服务端主动推动逻辑下一章节解析

 总结

服务实例管理的逻辑比较复杂, 除了服务第一次查询时候都并发查询控制之外,还涉及到定时更新任务的并发控制、定时任务的逻辑以及获取到最新服务实例后新老数据的比较与事件触发,另外还涉及到锁的等待和唤醒。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值