配置中心nacos的实现原理

配置中心nacos的实现原理
在了解nacos配置中心的实现原理之前,可以先思考一个问题,如果我们自己实现一个配置中心,需要考虑到哪些问题呢?

  • 服务器配置持久化存储
  • 客户端远程访问服务端的数据
  • 客户端本地缓存配置信息
  • 客户端与服务器端进行数据交互

有一个问题需要弄明白,Nacos 客户端是怎么实时获取到 Nacos 服务端的最新数据的
其实客户端和服务端之间的数据交互,无外乎两种情况:

  • 服务端推数据给客户端
  • 客户端从服务端拉数据

Nacos 的设计方式客户端主动去服务端拉取数据。这主要是为了避免,服务端为了维持心跳而耗费资源,而采用拉的方式,客户端只需要通过一个无状态的 http 请求即可获取到服务端的数据

基于猜想我们分析一下Nacos的源码

nacos是通过NacosFactory创建的ConfigService 来接收数据的,NacosFactory的createConfigService方法如下所示:

public static ConfigService createConfigService(Properties properties) throws NacosException {
        return ConfigFactory.createConfigService(properties);
}

public static ConfigService createConfigService(Properties properties) throws NacosException {
    try {
        Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
        Constructor constructor = driverImplClass.getConstructor(Properties.class);
        ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
        return vendorImpl;
    } catch (Throwable e) {
        throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
    }
}

本质上是通过反射实例化了一个NacosConfigService,需要注意的是,这里的ConfigService并不是单例模式
我们进入NacosConfigService的构造器方法:

public NacosConfigService(Properties properties) throws NacosException {
  ...
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        agent.start();
        worker = new ClientWorker(agent, configFilterChainManager, properties);
}

在这里NacosConfigService主要是初始化了两个对象,他们分别是:

  • HttpAgent
  • ClientWorker

HttpAgent作为参数传入到ClientWorker 中的,可以猜测到里面会用到agent做一些远程通信相关的事情。接下来我们看一下ClientWorker的构造器方法:

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;

        // Initialize the timeout parameter
        init(properties);
       //初始化一个定时调度的线程池,重写了threadfactory方法
        executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });
        //初始化一个定时调度的线程池,从里面的name名字来看,似乎和长轮训有关系。而这个长轮训应该是和nacos服务端的长轮训
        executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });
        //设置定时任务的执行频率,并且调用checkConfigInfo这个方法,猜测是定时去检测配置是否发生了变化
        //首次执行延迟时间为1毫秒、延迟时间为10毫秒
        executor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    checkConfigInfo();
                } catch (Throwable e) {
                    LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
                }
            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }

在这里 ClientWorker 创建了两个线程池:

  • 第一个线程池是定时调度的线程池用来执行定时任务的 executor,每隔 10ms 就会执行一次 checkConfigInfo() 方法(检查配置信息)

  • 第二个线程池似乎和nacos服务端之间的长轮询有关

我们顺着初始化的代码,继续往下看
checkConfigInfo()方法:用来检查服务端的配置信息是否发生了变化。如果发生了变化,则触发listener通知,源码如下:

public void checkConfigInfo() {
        // 分任务
        int listenerSize = cacheMap.get().size();
        // 向上取整为批数,监听的配置数量除以3000,得到一个整数,代表长轮训任务的数量
        int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
        //currentLongingTaskCount表示当前的长轮训任务数量,如果小于计算的结果,则可以继续创建
        if (longingTaskCount > currentLongingTaskCount) {
            for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
                // 要判断任务是否在执行  任务列表现在是无序的。变化过程可能有问题
                executorService.execute(new LongPollingRunnable(i));
            }
            currentLongingTaskCount = longingTaskCount; //更新当前长轮训任务数量
        }
    }

上面的代码可以看出长轮询的代码定义在LongPollingRunnable中,接下来我们去看看LongPollingRunnable的run方法
这个方法传递了一个taskid, tasked用来区分cacheMap中的任务批次, 保存到cacheDatas这个集合中,源码如下:

public void run() {

    List<CacheData> cacheDatas = new ArrayList<CacheData>();
    List<String> inInitializingCacheList = new ArrayList<String>();
    try {
        // check failover config
        for (CacheData cacheData : cacheMap.get().values()) {
            if (cacheData.getTaskId() == taskId) { //对cacheMap中的数据进行分批
                cacheDatas.add(cacheData);
                try {
                    checkLocalConfig(cacheData); //通过本地文件中缓存的数据和cacheData集合中的数据进行比对,判断是否出现数据变化
                    if (cacheData.isUseLocalConfigInfo()) {//如果有数据有变化,需要通知监听器
                        cacheData.checkListenerMd5();
                    }
                } catch (Exception e) {
                    LOGGER.error("get local config info error", e);
                }
            }
        }
		...
    }

checkLocalConfig方法主要是通过本地文件中缓存的数据和cacheData集合中的数据进行比对,判断是否出现数据变化,如果数据发生变化则拉取服务器数据

private void checkLocalConfig(CacheData cacheData) {
        final String dataId = cacheData.dataId;
        final String group = cacheData.group;
        final String tenant = cacheData.tenant;
        File path = LocalConfigInfoProcessor.getFailoverFile(agent.getName(), dataId, group, tenant);

        //本地缓存文件存在,并且isUseLocalConfigInfo为false
        if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
            //更新cacheData中的值
            String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
            String md5 = MD5.getInstance().getMD5String(content);
            cacheData.setUseLocalConfigInfo(true);
            cacheData.setLocalConfigInfoVersion(path.lastModified());
            cacheData.setContent(content);
  
            LOGGER.warn("[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
                agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
            return;
        }

        // 有 -> 没有。不通知业务监听器,从server拿到配置后通知。
        if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
            cacheData.setUseLocalConfigInfo(false);
            LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
                dataId, group, tenant);
            return;
        }

        // 有变更
        if (cacheData.isUseLocalConfigInfo() && path.exists()
            && cacheData.getLocalConfigInfoVersion() != path.lastModified()) {
            String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
            String md5 = MD5.getInstance().getMD5String(content);
            cacheData.setUseLocalConfigInfo(true);
            cacheData.setLocalConfigInfoVersion(path.lastModified());
            cacheData.setContent(content);
            LOGGER.warn("[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
                agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
        }
    }

当判断数据发生了变化之后,下一步就需要去服务器上获取最新的数据,检查哪些数据发生了变化,步骤如下:

//1.通过checkUpdateDataIds 从服务端获取发生变化的数据的DataID列表,保存在List<String>集合中
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
//2.遍历这些集合,调用getServerConfig()方法从远程服务器获得对应的内容,根据dataId、group、tenant等信息,使用http请求从远程服务器上获得配置信息,读取到数据之后缓存到本地文件中
for (String groupKey : changedGroupKeys) {
    String[] key = GroupKey.parseKey(groupKey);
    String dataId = key[0];
    String group = key[1];
    String tenant = null;
    if (key.length == 3) {
        tenant = key[2];
    }
    try {
        String content = getServerConfig(dataId, group, tenant, 3000L);//2
        CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
        //3.更新本地的cache,设置为服务器端返回的内容
        cache.setContent(content);
        LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}",
                    agent.getName(), dataId, group, tenant, cache.getMd5(),
                    ContentUtils.truncateContent(content));
    } catch (NacosException ioe) {
        String message = String.format(
            "[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
            agent.getName(), dataId, group, tenant);
        LOGGER.error(message, ioe);
    }
}
//4.最后遍历cacheDatas,找到变化的数据进行通知
for (CacheData cacheData : cacheDatas) {
    if (!cacheData.isInitializing() || inInitializingCacheList
        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
        cacheData.checkListenerMd5();
        cacheData.setInitializing(false);
    }
}
inInitializingCacheList.clear();
executorService.execute(this);

上面源码中的步骤一通过checkUpdateDataIds()方法从服务端获取发生变化的数据,具体过程是什么样的呢?

List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws IOException {
        StringBuilder sb = new StringBuilder();
        for (CacheData cacheData : cacheDatas) {
        //1.首先从cacheDatas集合中找到isUseLocalConfigInfo为false的缓存
            if (!cacheData.isUseLocalConfigInfo()) {
                sb.append(cacheData.dataId).append(WORD_SEPARATOR);
                sb.append(cacheData.group).append(WORD_SEPARATOR);
                if (StringUtils.isBlank(cacheData.tenant)) {
                    sb.append(cacheData.getMd5()).append(LINE_SEPARATOR);
                } else {
                    sb.append(cacheData.getMd5()).append(WORD_SEPARATOR);
                    sb.append(cacheData.getTenant()).append(LINE_SEPARATOR);
                }
                if (cacheData.isInitializing()) {
                    // cacheData 首次出现在cacheMap中&首次check更新
                    inInitializingCacheList
                        .add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
                }
            }
        }
        boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
        //2.调用checkUpdateConfigStr()方法,通过长轮训的方式,从远程服务器获得变化的数据进行返回
        return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
    }

checkUpdateConfigStr()方法,通过长轮训的方式,从远程服务器获得变化的数据进行返回,源码如下:

List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
        List<String> params = Arrays.asList(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);

        List<String> headers = new ArrayList<String>(2);
        headers.add("Long-Pulling-Timeout");
        headers.add("" + timeout);

        // told server do not hang me up if new initializing cacheData added in
        if (isInitializingCacheList) {
            headers.add("Long-Pulling-Timeout-No-Hangup");
            headers.add("true");
        }

        if (StringUtils.isBlank(probeUpdateString)) {
            return Collections.emptyList();
        }

        try {
            HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
                agent.getEncode(), timeout);

            if (HttpURLConnection.HTTP_OK == result.code) {
                setHealthServer(true);
                return parseUpdateDataIdResponse(result.content);
            } else {
                setHealthServer(false);
                LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(), result.code);
            }
        } catch (IOException e) {
            setHealthServer(false);
            LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
            throw e;
        }
        return Collections.emptyList();
    }

至此nacos客户端获得配置中心数据的关键流程就全部结束了,最后我们用一个流程图总结:

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值