nacos基础-配置中心

3 篇文章 0 订阅
3 篇文章 0 订阅

        配置的动态刷新客户端有两种方式完成客户端主动Pull拉取配置,Nacos Server主动Push配置数据    

nacos采取的是Pull模式来完成配置的管理和动态刷新 

1: Pull模式: 长轮询机制      

        nacos采用了长轮询机制实现,客户端发起一个Pull拉取配置请求,nacos server建立一个延时任务队列,每隔29.5s处理一个任务,处理任务便是花费0.5s检查配置有没有变更。不管有没有变更,都返回配置数据给客户端。之所以叫长轮询,是因为客户端和nacos建立的连接是一个30s的长连接。      

缺点:没有实时性,配置无变化会发起空pull,连接浪费资源。不过这里的缺点在下面的push都解决了。    

2: Push模式:      

        当通过nacos dashboard或者nacos api修改了配置后,nacos检查pull任务队列,可能会在任务中29.5s内的任意时刻,开始处理任务,和上述流程一致,检查配置变更数据,返回给Pull请求最新配置。所以Push模式实质上还是利用了Pull请求的任务来完成。

PS: 客户端本身会有一个定时线程每隔10ms检查一次本地配置(硬盘中存储)和内存(JVM)配置是否一致。通过上述动态刷新的只是内存中的配置。

长轮询

        是由服务端控制响应客户端请求的返回时间,来减少客户端无效请求的一种优化手段,其实对于客户端来说与轮询的使用并没有本质上的区别。

        客户端发起请求后,服务端不会立即返回请求结果,而是将请求挂起等待一段时间,如果此段时间内服务端数据变更,立即响应客户端请求,若是一直无变化则等到指定的超时时间后响应请求,客户端重新发起长链接。

 

        客户端、控制台通过发送Http请求将配置数据注册到服务端,服务端持久化数据到Mysql。

        客户端拉取配置数据,并批量设置对dataId的监听发起长轮询请求,如服务端配置项变更立即响应请求,如无数据变更则将请求挂起一段时间,直到达到超时时间。为减少对服务端压力以及保证配置中心可用性,拉取到配置数据客户端会保存一份快照在本地文件中,优先读取。

客户端源码分析

客户端数据结构cacheMap,是个Map结构,key为groupKey,是由dataId, group, tenant(租户)拼接的字符串;value为CacheData对象,每个dataId都会持有一个CacheData对象。

/**
 * groupKey -> cacheData.
 */
private final AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(
        new HashMap<String, CacheData>());

1.获取配置

        先取本地快照文件中的配置,如果本地文件不存在或者内容为空,则再通过HTTP请求从远端拉取对应dataId配置数据,并保存到本地快照中,请求默认重试3次,超时时间

        获取配置有getConfig()和getConfigAndSignListener()这两个接口,但getConfig()只是发送普通的HTTP请求,而getConfigAndSignListener()则多了发起长轮询和对dataId数据变更注册监听的操作addTenantListenersWithContent()。

       
/**
 * 客户端获取配置
 *
 * @param dataId    dataId
 * @param group     group
 * @param timeoutMs read timeout
 * @return config value
 * @throws NacosException NacosException
 */
@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
    return getConfigInner(namespace, dataId, group, timeoutMs);
}

/**
 * 客户端获取配置并注册监听器。
   如果想在程序第一次开始获取配置的时候自己拉取,并且注册的监听器用于以后的配置更新,可以保持原代码不变,
   只需要添加系统参数:enableRemoteSyncConfig = "true" (但有网络开销); 因此我们建议您直接使用该接口
 *
 */
@Override
public String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener)
        throws NacosException {
    String content = getConfig(dataId, group, timeoutMs);
    worker.addTenantListenersWithContent(dataId, group, content, Arrays.asList(listener));
    return content;
}

2.注册监听

        客户端注册监听,先从cacheMap中拿到dataId对应的CacheData对象。

/**
 * Add listeners for tenant with content.
 *
 * @param dataId    dataId of data
 * @param group     group of data
 * @param content   content
 * @param listeners listeners
 * @throws NacosException nacos exception
 */
public void addTenantListenersWithContent(String dataId, String group, String content,
        List<? extends Listener> listeners) throws NacosException {
    group = blank2defaultGroup(group);
    String tenant = agent.getTenant();
    // 获取dataId对应的CacheData,如没有则向服务端发起长轮询请求获取配置
    CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
    synchronized (cache) {
        // 注册对dataId的数据变更监听
        cache.setContent(content);
        for (Listener listener : listeners) {
            cache.addListener(listener);
        }
        cache.setSyncWithServer(false);
        agent.notifyListenConfig();
    }
}

/**
 * 如没有向服务端发起长轮询请求获取配置,默认的Timeout时间为30s,
 * 并把返回的配置数据回填至CacheData对象的content字段,同时用content生成MD5值;再通过addListener()注册监听器。
 *
 * @param dataId data id if data
 * @param group  group of data
 * @param tenant tenant of data
 * @return cache data
 */
public CacheData addCacheDataIfAbsent(String dataId, String group, String tenant) throws NacosException {
    CacheData cache = getCache(dataId, group, tenant);
    if (null != cache) {
        return cache;
    }
    String key = GroupKey.getKeyTenant(dataId, group, tenant);
    synchronized (cacheMap) {
        CacheData cacheFromMap = getCache(dataId, group, tenant);
        // 多个监听器在同一个 dataid+group 和竞争条件下,所以再次仔细检查
        // double check again
        // other listener thread beat me to set to cacheMap
        if (null != cacheFromMap) {
            cache = cacheFromMap;
            // 重置以便服务器不会挂起此检查
            cache.setInitializing(true);
        } else {
            cache = new CacheData(configFilterChainManager, agent.getName(), dataId, group, tenant);
            int taskId = cacheMap.get().size() / (int) ParamUtil.getPerTaskConfigSize();
            cache.setTaskId(taskId);
            // fix issue # 1317
            if (enableRemoteSyncConfig) {
                ConfigResponse response = getServerConfig(dataId, group, tenant, 3000L, false);
                cache.setContent(response.getContent());
            }
        }

        Map<String, CacheData> copy = new HashMap<String, CacheData>(this.cacheMap.get());
        copy.put(key, cache);
        cacheMap.set(copy);
    }
    LOGGER.info("[{}] [subscribe] {}", agent.getName(), key);

    MetricsMonitor.getListenConfigCountMonitor().set(cacheMap.get().size());

    return cache;
}

/**
 * 其中listeners是对dataId所注册的所有监听器集合,其中的ManagerListenerWrap对象除了持有Listener监听类,还有一个lastCallMd5字段,这个属性很关键,它是判断服务端数据是否更变的重要条件。
 * 在添加监听的同时会将CacheData对象当前最新的md5值赋值给ManagerListenerWrap对象的lastCallMd5属性。
 * @param listener listener
 */
public void addListener(Listener listener) {
    if (null == listener) {
        throw new IllegalArgumentException("listener is null");
    }
    ManagerListenerWrap wrap =
            (listener instanceof AbstractConfigChangeListener) ? new ManagerListenerWrap(listener, md5, content)
                    : new ManagerListenerWrap(listener, md5);
    
    if (listeners.addIfAbsent(wrap)) {
        LOGGER.info("[{}] [add-listener] ok, tenant={}, dataId={}, group={}, cnt={}", name, tenant, dataId, group,
                listeners.size());
    }
}

3.变更通知

        客户端又是如何感知服务端数据已变更呢?

        我们还是从头看,NacosConfigService类的构造器中初始化了一个ClientWorker,而在ClientWorker类的构造器中又启动了一个线程池来轮询cacheMap。

@Override
public void startInternal() throws NacosException {
    executor.schedule(new Runnable() {
        @Override
        public void run() {
            while (true) {
                try {
                    listenExecutebell.poll(5L, TimeUnit.SECONDS);
                    executeConfigListen();
                } catch (InterruptedException e) {
                    LOGGER.info("[ rpc listen execute ] Interrupted, stop the config listen task now.");
                    break;
                } catch (Exception e) {
                    LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e);
                }
            }
        }
    }, 0L, TimeUnit.MILLISECONDS);
}


//executeConfigListen() 中调用checkListenerMd5()方法,检查cacheMap中dataId的CacheData对象内,
//MD5字段与注册的监听listener内的lastCallMd5值,不相同表示配置数据变更则触发safeNotifyListener方法,发送数据变更通知。

void checkListenerMd5() {
    for (ManagerListenerWrap wrap : listeners) {
        if (!md5.equals(wrap.lastCallMd5)) {
            safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
        }
    }
}
//safeNotifyListener()方法单独起线程,向所有对dataId注册过监听的客户端推送变更后的数据内容。
//客户端接收通知,直接实现receiveConfigInfo()方法接收回调数据,处理自身业务就可以了。

服务端源码分析

        Nacos配置中心的服务端源码主要在nacos-config项目的ConfigController类

1.处理长轮询

        服务端对外提供的监听接口地址/v1/cs/configs/listener,这个方法内容不多,顺着doPollingConfig往下看。

/**
 * 客户端侦听配置更改。
 */
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public void listener(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    
    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
    String probeModify = request.getParameter("Listening-Configs");
    if (StringUtils.isBlank(probeModify)) {
        LOGGER.warn("invalid probeModify is blank");
        throw new IllegalArgumentException("invalid probeModify");
    }
    
    probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);
    
    Map<String, String> clientMd5Map;
    try {
        clientMd5Map = MD5Util.getClientMd5Map(probeModify);
    } catch (Throwable e) {
        throw new IllegalArgumentException("invalid probeModify");
    }
    
    // 
    inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}

/**
 * 轮询接口.
 */
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
        Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {

    // 长轮询 ,服务端根据请求header中的Long-Pulling-Timeout属性来区分请求是长轮询还是短轮询
    if (LongPollingService.isSupportLongPolling(request)) {
        longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
        return HttpServletResponse.SC_OK + "";
    }

    // else 兼容短轮询逻辑
    List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);

    //  兼容短轮询result
    String oldResult = MD5Util.compareMd5OldResult(changedGroups);
    String newResult = MD5Util.compareMd5ResultString(changedGroups);

    String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);
    if (version == null) {
        version = "2.0.0";
    }
    int versionNum = Protocol.getVersionNumber(version);

    // 2.0.4版本以前, 返回值放入header中
    if (versionNum < START_LONG_POLLING_VERSION_NUM) {
        response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
        response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
    } else {
        request.setAttribute("content", newResult);
    }

    // Disable cache.
    response.setHeader("Pragma", "no-cache");
    response.setDateHeader("Expires", 0);
    response.setHeader("Cache-Control", "no-cache,no-store");
    response.setStatus(HttpServletResponse.SC_OK);
    return HttpServletResponse.SC_OK + "";
}

/**
 * Add LongPollingClient.
 *
 * @param req              HttpServletRequest.
 * @param rsp              HttpServletResponse.
 * @param clientMd5Map     clientMd5Map.
 * @param probeRequestSize probeRequestSize.
 */
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
        int probeRequestSize) {
    //客户端提交的请求超时时间:30s
    String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
    String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
    String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
    String tag = req.getHeader("Vipserver-Tag");
    //延迟时间:0.5
    int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);

    // 实际超时时间:29.5,提前500ms返回响应,为避免客户端超时
    long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
    if (isFixedPolling()) {
        timeout = Math.max(10000, getFixedPollingInterval());
        // Do nothing but set fix polling timeout.
    } else {
        long start = System.currentTimeMillis();
        //对客户端提交上来的groupkey的MD5与服务端当前的MD5比对,如md5值不同,则说明服务端的配置项发生过变更,直接将该groupkey放入changedGroupKeys集合并返回给客户端。
        List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
        if (changedGroups.size() > 0) {
            generateResponse(req, rsp, changedGroups);
            LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
                    RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                    changedGroups.size());
            return;
        } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
            LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
                    RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                    changedGroups.size());
            return;
        }
    }
    String ip = RequestUtil.getRemoteIp(req);
    //这里每个长轮询任务携带了一个asyncContext对象,使得每个请求可以延迟响应,等延时到达或者配置有变更之后,调用asyncContext.complete()响应完成。
    //  一定要由HTTP线程调用,否则离开后容器会立即发送响应
    //asyncContext 为 Servlet 3.0新增的特性,异步处理,使Servlet线程不再需要一直阻塞,等待业务处理完毕才输出响应;
    //可以先释放容器分配给请求的线程与相关资源,减轻系统负担,其响应将被延后,在处理完业务或者运算后再对客户端进行响应。
    final AsyncContext asyncContext = req.startAsync();
    //  AsyncContext.setTimeout()的超时时间不准,所以只能自己控制
    asyncContext.setTimeout(0L);

    //如未发生变更,则将客户端请求挂起,这个过程先创建一个名为ClientLongPolling的调度任务Runnable,并提交给scheduler定时线程池延后29.5s执行。
    ConfigExecutor.executeLongPolling(
            new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}

2.数据变更

        管理平台或者客户端更改配置项接位置ConfigController中的publishConfig方法。

@PostMapping(params = "import=true")
@Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class)
public RestResult<Map<String, Object>> importAndPublishConfig(HttpServletRequest request,
        @RequestParam(value = "src_user", required = false) String srcUser,
        @RequestParam(value = "namespace", required = false) String namespace,
        @RequestParam(value = "policy", defaultValue = "ABORT") SameConfigPolicy policy, MultipartFile file)
        throws NacosException {
    Map<String, Object> failedData = new HashMap<>(4);

    if (Objects.isNull(file)) {
        return RestResultUtils.buildResult(ResultCodeEnum.DATA_EMPTY, failedData);
    }

    namespace = NamespaceUtil.processNamespaceParameter(namespace);
    if (StringUtils.isNotBlank(namespace) && persistService.tenantInfoCountByTenantId(namespace) <= 0) {
        failedData.put("succCount", 0);
        return RestResultUtils.buildResult(ResultCodeEnum.NAMESPACE_NOT_EXIST, failedData);
    }
    List<ConfigAllInfo> configInfoList = new ArrayList<>();
    List<Map<String, String>> unrecognizedList = new ArrayList<>();
    try {
        ZipUtils.UnZipResult unziped = ZipUtils.unzip(file.getBytes());
        ZipUtils.ZipItem metaDataZipItem = unziped.getMetaDataItem();
        if (metaDataZipItem != null && Constants.CONFIG_EXPORT_METADATA_NEW.equals(metaDataZipItem.getItemName())) {
            // new export
            RestResult<Map<String, Object>> errorResult = parseImportDataV2(unziped, configInfoList,
                    unrecognizedList, namespace);
            if (errorResult != null) {
                return errorResult;
            }
        } else {
            RestResult<Map<String, Object>> errorResult = parseImportData(unziped, configInfoList, unrecognizedList,
                    namespace);
            if (errorResult != null) {
                return errorResult;
            }
        }
    } catch (IOException e) {
        failedData.put("succCount", 0);
        LOGGER.error("parsing data failed", e);
        return RestResultUtils.buildResult(ResultCodeEnum.PARSING_DATA_FAILED, failedData);
    }

    if (CollectionUtils.isEmpty(configInfoList)) {
        failedData.put("succCount", 0);
        return RestResultUtils.buildResult(ResultCodeEnum.DATA_EMPTY, failedData);
    }
    final String srcIp = RequestUtil.getRemoteIp(request);
    String requestIpApp = RequestUtil.getAppName(request);
    final Timestamp time = TimeUtils.getCurrentTime();
    //数据库修改
    Map<String, Object> saveResult = persistService
            .batchInsertOrUpdate(configInfoList, srcUser, srcIp, null, time, false, policy);
    for (ConfigInfo configInfo : configInfoList) {
        //数据变更事件Event
        ConfigChangePublisher.notifyConfigChange(
                new ConfigDataChangeEvent(false, configInfo.getDataId(), configInfo.getGroup(),
                        configInfo.getTenant(), time.getTime()));
        ConfigTraceService
                .logPersistenceEvent(configInfo.getDataId(), configInfo.getGroup(), configInfo.getTenant(),
                        requestIpApp, time.getTime(), InetUtils.getSelfIP(),
                        ConfigTraceService.PERSISTENCE_EVENT_PUB, configInfo.getContent());
    }
    // unrecognizedCount
    if (!unrecognizedList.isEmpty()) {
        saveResult.put("unrecognizedCount", unrecognizedList.size());
        saveResult.put("unrecognizedData", unrecognizedList);
    }
    return RestResultUtils.success("导入成功", saveResult);
}

长轮询方法中:LongPollingService的构造方法中,正好订阅了数据变更事件,并在事件触发时执行一个数据变更调度任务DataChangeTask

public LongPollingService() {
    allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();

    ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);

    // 将本地数据更改事件注册到通知中心。
    NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);

    // 订阅监听本地数据变更事件
    NotifyCenter.registerSubscriber(new Subscriber() {

        @Override
        public void onEvent(Event event) {
            if (isFixedPolling()) {
                // Ignore.
            } else {
                if (event instanceof LocalDataChangeEvent) {
                    LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                    //执行事件变更调度任务
                    ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                }
            }
        }

        @Override
        public Class<? extends Event> subscribeType() {
            return LocalDataChangeEvent.class;
        }
    });

}

/**
 * DataChangeTask内的主要逻辑就是遍历allSubs队列,allSubs队列中维护的是所有客户端的长轮询请求任务,
 * 从这些任务中找到包含当前发生变更的groupkey的ClientLongPolling任务,以此实现数据更变推送给客户端,并从allSubs队列中剔除此长轮询任务。
 */
 
 final Queue<ClientLongPolling> allSubs;
 
class DataChangeTask implements Runnable {

    @Override
    public void run() {
        try {
            ConfigCacheService.getContentBetaMd5(groupKey);
            for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
                ClientLongPolling clientSub = iter.next();
                if (clientSub.clientMd5Map.containsKey(groupKey)) {
                    // 如果发布的标签不在 beta 列表中,则跳过。
                    if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
                        continue;
                    }

                    // 如果已发布的标签不在标签列表中,则跳过。
                    if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                        continue;
                    }

                    getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                    iter.remove(); // 删除订阅者的关系。
                    LogUtil.CLIENT_LOG
                            .info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance",
                                    RequestUtil
                                            .getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()),
                                    "polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
                    clientSub.sendResponse(Arrays.asList(groupKey));
                }
            }

        } catch (Throwable t) {
            LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
        }
    }
    
    void sendResponse(List<String> changedGroups) {

    // Cancel time out task.
    if (null != asyncTimeoutFuture) {
        asyncTimeoutFuture.cancel(false);
    }
    generateResponse(changedGroups);
}

/**
 * 客户端响应response时,调用asyncContext.complete()结束了异步请求。
 * @param changedGroups
 */
void generateResponse(List<String> changedGroups) {
    if (null == changedGroups) {

        // 告诉客户端 发送 http 响应。
        asyncContext.complete();
        return;
    }

    HttpServletResponse response = (HttpServletResponse) asyncContext.getResponse();

    try {
        final String respString = MD5Util.compareMd5ResultString(changedGroups);

        // 禁用缓存。
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-cache,no-store");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().println(respString);
        asyncContext.complete();
    } catch (Exception ex) {
        PULL_LOG.error(ex.toString(), ex);
        asyncContext.complete();
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值