Nacos源码分析(一)配置中心

目录

ServerWorker的创建过程

源码分析

总结

ClientWorker的创建过程

源码分析

总结


ServerWorker的创建过程

源码分析

1 我们以com.alibaba.nacos.example.ConfigExample这个测试类进行分析。

public static void main(String[] args) throws NacosException, InterruptedException {
    String serverAddr = "localhost";
    String dataId = "test";
    String group = "DEFAULT_GROUP";
    Properties properties = new Properties();
    properties.put("serverAddr", serverAddr);
    ConfigService configService = NacosFactory.createConfigService(properties);
    String content = configService.getConfig(dataId, group, 5000);
    System.out.println(content);
    configService.addListener(dataId, group, new Listener() {
        @Override
        public void receiveConfigInfo(String configInfo) {
            System.out.println("receive:" + configInfo);
        }

        @Override
        public Executor getExecutor() {
            return null;
        }
    });

    boolean isPublishOk = configService.publishConfig(dataId, group, "content");
    System.out.println(isPublishOk);

    Thread.sleep(3000);
    content = configService.getConfig(dataId, group, 5000);
    System.out.println(content);

    boolean isRemoveOk = configService.removeConfig(dataId, group);
    System.out.println(isRemoveOk);
    Thread.sleep(3000);

    content = configService.getConfig(dataId, group, 5000);
    System.out.println(content);
    Thread.sleep(300000);

}

1构建配置Properties对象,添加serverAddr
2创建ConfigService实例,这个是nacos作为配置中心时的客户端接口。
3根据dataId和group获取配置内容。 配置资源的资源坐标由dataId、group、namespace唯一确定。configService在初始化时给了默认的namespace。
4添加监听器
5发布、获取、删除配置等操作


2 看一下如何得到ConfigService实例。

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

里面调用ConfigFactory工厂类获得实例对象:

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);
    }
}
  1. 通过Class.forName得到NacosConfigService的Class对象
  2. 拿到以Properties为参数的构造方法
  3. 通过反射创建实例对象并返回

3 下面看一下NacosConfigService这个的构造方法:

package com.alibaba.nacos.client.config;
public NacosConfigService(Properties properties) throws NacosException {
    ValidatorUtils.checkInitParam(properties);
    String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
    if (StringUtils.isBlank(encodeTmp)) {
        this.encode = Constants.ENCODE;
    } else {
        this.encode = encodeTmp.trim();
    }
    initNamespace(properties);

    // ServerHttpAgent http代理
    // MetricsHttpAgent 又代理了一次
    // 另外,屏蔽了集群逻辑。提供的方法只是统一的http调用。
    this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
    this.agent.start();
    this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
  1. 配置检查
  2. 设置编码格式
  3. 初始化namespace
  4. 构造http代理并start
  5. 创建worker

4 MetricsHttpAgent类实际上就是对ServerHttpAgent进行代理,包装了一层prometheus用于指标采集,以httpGet为例:

@Override
public HttpRestResult<String> httpGet(String path, Map<String, String> headers, Map<String, String> paramValues,
        String encode, long readTimeoutMs) throws Exception {
    //时间数据采集
    Histogram.Timer timer = MetricsMonitor.getConfigRequestMonitor("GET", path, "NA");
    HttpRestResult<String> result;
    try {
        result = httpAgent.httpGet(path, headers, paramValues, encode, readTimeoutMs);
    } catch (IOException e) {
        throw e;
    } finally {
        timer.observeDuration();
        timer.close();
    }
    
    return result;
}

主要是针对时间数据采集。

5 回到第3步,我们看ServerHttpAgent类:

public ServerHttpAgent(Properties properties) throws NacosException {
    // 集群管理类
    this.serverListMgr = new ServerListManager(properties);
    // 安全认证
    this.securityProxy = new SecurityProxy(properties, NACOS_RESTTEMPLATE);
    // 命名空间
    this.namespaceId = properties.getProperty(PropertyKeyConst.NAMESPACE);

    // 初始化配置 encoding、maxRetry、ak、sk
    // ak、sk在登录认证时未用到。发送get、post、delete请求时需要验证
    init(properties);
    // 登录认证
    this.securityProxy.login(this.serverListMgr.getServerUrls());
    
    // init executorService
    // daemon线程
    this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("com.alibaba.nacos.client.config.security.updater");
            t.setDaemon(true);
            return t;
        }
    });

    // 5秒一次的登录认证(会校验是否在token窗口内,如果不在,则重新获取token),刷新token和token窗口
    this.executorService.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            securityProxy.login(serverListMgr.getServerUrls());
        }
    }, 0, this.securityInfoRefreshIntervalMills, TimeUnit.MILLISECONDS);
    
}
  1. 构造集群管理类ServerListManager
  2. 然后是安全代理类SecurityProxy
  3. namespace
  4. 初始化encoding编码、maxRetry最大重试次数、ak、sk等配置
  5. 通过securityProxy进行登录认证,然后启动个任务,每隔5秒重新调用login接口进行认证

6 我们看一下securityProxy.login

public boolean login(List<String> servers) {
    
    try {
        // token刷新窗口范围内,返回true
        if ((System.currentTimeMillis() - lastRefreshTime) < TimeUnit.SECONDS
                .toMillis(tokenTtl - tokenRefreshWindow)) {
            return true;
        }

        // 集群每个server进行登录验证
        for (String server : servers) {
            if (login(server)) {
                lastRefreshTime = System.currentTimeMillis();
                return true;
            }
        }
    } catch (Throwable ignore) {
    }
    
    return false;
}

这里有个时间窗口。如果当前任务执行的时间还在token窗口内,说明token还没有过期,此时不需要重新认证,否则重新登录认证并更新lastRefreshTime。

public boolean login(String server) throws UnsupportedEncodingException {
    
    if (StringUtils.isNotBlank(username)) {
        Map<String, String> params = new HashMap<String, String>(2);
        Map<String, String> bodyMap = new HashMap<String, String>(2);
        params.put("username", username);
        bodyMap.put("password", URLEncoder.encode(password, "utf-8"));
        String url = "http://" + server + contextPath + LOGIN_URL;
        
        if (server.contains(Constants.HTTP_PREFIX)) {
            url = server + contextPath + LOGIN_URL;
        }
        try {
            HttpRestResult<String> restResult = nacosRestTemplate
                    .postForm(url, Header.EMPTY, Query.newInstance().initParams(params), bodyMap, String.class);
            if (!restResult.ok()) {
                SECURITY_LOGGER.error("login failed: {}", JacksonUtils.toJson(restResult));
                return false;
            }
            JsonNode obj = JacksonUtils.toObj(restResult.getData());
            if (obj.has(Constants.ACCESS_TOKEN)) {
                accessToken = obj.get(Constants.ACCESS_TOKEN).asText();
                tokenTtl = obj.get(Constants.TOKEN_TTL).asInt();
                tokenRefreshWindow = tokenTtl / 10;
            }
        } catch (Exception e) {
            SECURITY_LOGGER.error("[SecurityProxy] login http request failed"
                    + " url: {}, params: {}, bodyMap: {}, errorMsg: {}", url, params, bodyMap, e.getMessage());
            return false;
        }
    }
    return true;
}

最终的登录认证实现,url是http://server/contextPath/v1/auth/users/login

7 而nacosRestTemplate是在初始化securityProxy时构造的:

package com.alibaba.nacos.client.config.http;
private static final NacosRestTemplate NACOS_RESTTEMPLATE = ConfigHttpClientManager.getInstance()
        .getNacosRestTemplate();
package com.alibaba.nacos.client.config.impl;
private static final NacosRestTemplate NACOS_REST_TEMPLATE;

static {
    NACOS_REST_TEMPLATE = HttpClientBeanHolder.getNacosRestTemplate(HTTP_CLIENT_FACTORY);
    NACOS_REST_TEMPLATE.getInterceptors().add(new LimiterHttpClientRequestInterceptor());
}

主要是加了一个拦截器。

package com.alibaba.nacos.common.http;
@Override
public NacosRestTemplate createNacosRestTemplate() {
    HttpClientConfig httpClientConfig = buildHttpClientConfig();
    final JdkHttpClientRequest clientRequest = new JdkHttpClientRequest(httpClientConfig);
    
    // enable ssl
    initTls(new BiConsumer<SSLContext, HostnameVerifier>() {
        @Override
        public void accept(SSLContext sslContext, HostnameVerifier hostnameVerifier) {
            clientRequest.setSSLContext(loadSSLContext());
            clientRequest.replaceSSLHostnameVerifier(hostnameVerifier);
        }
    }, new TlsFileWatcher.FileChangeListener() {
        @Override
        public void onChanged(String filePath) {
            clientRequest.setSSLContext(loadSSLContext());
        }
    });
    
    return new NacosRestTemplate(assignLogger(), clientRequest);
}

也就是requestClient的类型是JdkHttpClientRequest。

8 再看NacosRestTemplate
public <T> HttpRestResult<T> get(String url, Header header, Query query, Type responseType) throws Exception {
    return execute(url, HttpMethod.GET, new RequestHttpEntity(header, query), responseType);
}
private <T> HttpRestResult<T> execute(String url, String httpMethod, RequestHttpEntity requestEntity,
            Type responseType) throws Exception {
    URI uri = HttpUtils.buildUri(url, requestEntity.getQuery());
    if (logger.isDebugEnabled()) {
        logger.debug("HTTP method: {}, url: {}, body: {}", httpMethod, uri, requestEntity.getBody());
    }

    ResponseHandler<T> responseHandler = super.selectResponseHandler(responseType);
    HttpClientResponse response = null;
    try {
        response = this.requestClient().execute(uri, httpMethod, requestEntity);
        return responseHandler.handle(response);
    } finally {
        if (response != null) {
            response.close();
        }
    }
}

9 再看看requestClient的execute方法:

public HttpClientResponse execute(URI uri, String httpMethod, RequestHttpEntity requestHttpEntity)
        throws Exception {
    final Object body = requestHttpEntity.getBody();
    final Header headers = requestHttpEntity.getHeaders();
    replaceDefaultConfig(requestHttpEntity.getHttpClientConfig());
    
    HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
    Map<String, String> headerMap = headers.getHeader();
    if (headerMap != null && headerMap.size() > 0) {
        for (Map.Entry<String, String> entry : headerMap.entrySet()) {
            conn.setRequestProperty(entry.getKey(), entry.getValue());
        }
    }
    
    conn.setConnectTimeout(this.httpClientConfig.getConTimeOutMillis());
    conn.setReadTimeout(this.httpClientConfig.getReadTimeOutMillis());
    conn.setRequestMethod(httpMethod);
    if (body != null) {
        String contentType = headers.getValue(HttpHeaderConsts.CONTENT_TYPE);
        String bodyStr = JacksonUtils.toJson(body);
        if (MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)) {
            Map<String, String> map = JacksonUtils.toObj(bodyStr, HashMap.class);
            bodyStr = HttpUtils.encodingParams(map, headers.getCharset());
        }
        if (bodyStr != null) {
            conn.setDoOutput(true);
            byte[] b = bodyStr.getBytes();
            conn.setRequestProperty("Content-Length", String.valueOf(b.length));
            conn.getOutputStream().write(b, 0, b.length);
            conn.getOutputStream().flush();
            conn.getOutputStream().close();
        }
    }
    conn.connect();
    return new JdkHttpClientResponse(conn);
}

至此nacos的客户端到服务端的网络通信模型就明确了。

10 回来继续看NacosConfigService的初始化(第3步),当创建完ServerHttpAgent后,紧接着调用了start方法:

@Override
public void start() throws NacosException {
    serverListMgr.start();
}

就是服务管理列表的start方法:

public synchronized void start() throws NacosException {
    
    if (isStarted || isFixed) {
        return;
    }
    
    GetServerListTask getServersTask = new GetServerListTask(addressServerUrl);
    // 这里不是新线程,是直接调用run方法。
    for (int i = 0; i < initServerlistRetryTimes && serverUrls.isEmpty(); ++i) {
        getServersTask.run();
        try {
            this.wait((i + 1) * 100L);
        } catch (Exception e) {
            LOGGER.warn("get serverlist fail,url: {}", addressServerUrl);
        }
    }
    
    if (serverUrls.isEmpty()) {
        LOGGER.error("[init-serverlist] fail to get NACOS-server serverlist! env: {}, url: {}", name,
                addressServerUrl);
        throw new NacosException(NacosException.SERVER_ERROR,
                "fail to get NACOS-server serverlist! env:" + name + ", not connnect url:" + addressServerUrl);
    }
    
    // executor schedules the timer task
    // 30秒执行一次任务,更新服务状态
    this.executorService.scheduleWithFixedDelay(getServersTask, 0L, 30L, TimeUnit.SECONDS);
    isStarted = true;
}

先run一下,然后丢掉线程池里每隔30秒再run一下

11 看看run的方法

class GetServerListTask implements Runnable {
    
    final String url;
    
    GetServerListTask(String url) {
        this.url = url;
    }
    
    @Override
    public void run() {
        /*
         get serverlist from nameserver
         */
        try {
            updateIfChanged(getApacheServerList(url, name));
        } catch (Exception e) {
            LOGGER.error("[" + name + "][update-serverlist] failed to update serverlist from address server!", e);
        }
    }
}

12 获取服务列表,如果有变更就更新

private void updateIfChanged(List<String> newList) {
    if (null == newList || newList.isEmpty()) {
        LOGGER.warn("[update-serverlist] current serverlist from address server is empty!!!");
        return;
    }
    
    List<String> newServerAddrList = new ArrayList<String>();
    for (String server : newList) {
        if (server.startsWith(HTTP) || server.startsWith(HTTPS)) {
            newServerAddrList.add(server);
        } else {
            newServerAddrList.add(HTTP + server);
        }
    }
    
    /*
     no change
     */
    if (newServerAddrList.equals(serverUrls)) {
        return;
    }
    serverUrls = new ArrayList<String>(newServerAddrList);
    iterator = iterator();
    currentServerAddr = iterator.next();
    
    // Using unified event processor, NotifyCenter
    // 服务列表发生变更,发布事件消息
    NotifyCenter.publishEvent(new ServerlistChangeEvent());
    LOGGER.info("[{}] [update-serverlist] serverlist updated to {}", name, serverUrls);
}

通知中心发布服务列表变更事件ServerlistChangeEvent

总结

1 创建ConfigService对象时,会先创建一个ServerHttpAgent对象,初始化对象时会进行和服务端的认证通信,并启动一个定时任务动态更新token保证token不失效。
2 调用ServerHttpAgent的start方法,主要是监控服务列表是否有变更,当发现有变更时发布ServerlistChangeEvent事件。

ClientWorker的创建过程

源码分析

1 回到上一节的第3步

this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);

2 看下ClientWorker的初始化做了什么

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
        final Properties properties) {
    // http代理
    this.agent = agent;
    // 过滤器
    this.configFilterChainManager = configFilterChainManager;
    
    // Initialize the timeout parameter
    // 初始化配置
    init(properties);

    this.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;
        }
    });

    // cpu核数的线程,用来做长轮询的,每次检查配置,如果LongPollingRunnable任务的配置缓存超过一定数量,默认3000个,就要去开启一个新任务去检查配置
    // Runtime.getRuntime().availableProcessors()获取cpu核数
    this.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;
                }
            });

    // 10毫秒的任务,检查配置信息 LongPollingRunnable
    this.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);
}

这里定义了两个线程池,executorService这里只定义了,还没放入线程,用来做长轮询的;executor每10毫秒执行一次checkConfigInfo方法。

3 看一下checkConfigInfo这个方法:

public void checkConfigInfo() {
    // Dispatch taskes.
    // 监听的数量
    int listenerSize = cacheMap.get().size();
    // Round up the longingTaskCount.
    // 监听数量/3000 向上取整
    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
    if (longingTaskCount > currentLongingTaskCount) {
        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
            taskIdSet.add(i);
            // 循环3000次,建一个新的
            // LongPollingRunnable 长链接监听
            // 每个LongPollingRunnable默认可以负责3000个监听器的轮询
            executorService.execute(new LongPollingRunnable(i));
        }
    } else if (longingTaskCount < currentLongingTaskCount) {
        for (int i = longingTaskCount; i < (int) currentLongingTaskCount; i++) {
            taskIdSet.remove(i);
        }
    }
    currentLongingTaskCount = longingTaskCount;
}

ParamUtil.getPerTaskConfigSize()这个默认是3000。

这里的逻辑是这样的,根据监听器的数量建立长轮询任务,每3000个监听建一个任务并放入到executorService里。

监听器在cacheMap里,后面我们看addListener方法时会看到写入这个缓存的操作。

4 看一下LongPollingRunnable这个:

@Override
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) {
                cacheDatas.add(cacheData);
                try {
                    checkLocalConfig(cacheData);
                    //用本地配置
                    if (cacheData.isUseLocalConfigInfo()) {
                        //有改变的话会通知
                        cacheData.checkListenerMd5();
                    }
                } catch (Exception e) {
                    LOGGER.error("get local config info error", e);
                }
            }
        }

        //获取有变化的配置列表dataid+group,访问的url是/listener
        // check server config
        List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
        if (!CollectionUtils.isEmpty(changedGroupKeys)) {
            LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
        }

        //轮询有配置改变的,然后去获取内容
        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[] ct = getServerConfig(dataId, group, tenant, 3000L);
                CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
                //设置配置内容
                cache.setContent(ct[0]);
                if (null != ct[1]) {
                    //设置配置类型
                    cache.setType(ct[1]);
                }
                LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                        agent.getName(), dataId, group, tenant, cache.getMd5(),
                        ContentUtils.truncateContent(ct[0]), ct[1]);
            } 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);
            }
        }
        //不是初始化中的或者初始化集合里存在的
        for (CacheData cacheData : cacheDatas) {
            if (!cacheData.isInitializing() || inInitializingCacheList
                    .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                //检查是否有变化,有变化就通知
                cacheData.checkListenerMd5();
                //请求过了后就设置为不在初始化中,这样就会被挂起,如果服务器配置有更新,就会立即返回,这样就可以实现动态配置更新,又不会太多的空轮询消耗
                cacheData.setInitializing(false);
            }
        }
        inInitializingCacheList.clear();
        
        if (taskIdSet.contains(taskId)) {
            executorService.execute(this);
        }
        
    } catch (Throwable e) {
        
        // If the rotation training task is abnormal, the next execution time of the task will be punished
        LOGGER.error("longPolling error : ", e);
        executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
    }
}
1 当前taskId下的所有cacheData都取出来
2 本地配置验证,有需要发送监听通知checkListenerMd5
3 checkUpdateDataIds远程调用服务端检查是否有变更的,有的话getServerConfig获取新的配置,有需要的话发送监听通知checkListenerMd5
5 将当前线程再放回线程池中executorService.execute(this);

所谓的长轮询,简单来理解就是这个LongPollingRunnable线程会一直执行,每次都会去服务端获取和当前taskId相关的配置变更列表,如发现变更就取新的来进行更新。

5 我们看一下checkUpdateDataIds方法:

List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {
    //把配置信息都连起来,一次请求
    StringBuilder sb = new StringBuilder();
    for (CacheData cacheData : cacheDatas) {
        //不用本地的
        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()) {
                // It updates when cacheData occours in cacheMap by first time.
                // cacheData 首次出现在cacheMap中&首次check更新
                inInitializingCacheList
                        .add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
            }
        }
    }
    //是否是初始化的获取标记
    boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
    return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}

6 然后是checkUpdateConfigStr方法:

List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
    
    Map<String, String> params = new HashMap<String, String>(2);
    params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
    Map<String, String> headers = new HashMap<String, String>(2);
    headers.put("Long-Pulling-Timeout", "" + timeout);
    
    // told server do not hang me up if new initializing cacheData added in
    //是初始化的会设置一个请求头标记
    // 初始化时不挂起
    if (isInitializingCacheList) {
        headers.put("Long-Pulling-Timeout-No-Hangup", "true");
    }
    
    if (StringUtils.isBlank(probeUpdateString)) {
        return Collections.emptyList();
    }
    
    try {
        // In order to prevent the server from handling the delay of the client's long task,
        // increase the client's read timeout to avoid this problem.
        // 增加超时时间,防止被挂起,只有初始化的时候isInitializingCacheList=true不会挂起,
        // 应该是服务器看了请求头Long-Pulling-Timeout-No-Hangup才不会挂起
        long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
        HttpRestResult<String> result = agent
                .httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(),
                        readTimeoutMs);
        
        if (result.ok()) {
            setHealthServer(true);
            return parseUpdateDataIdResponse(result.getData());
        } else {
            setHealthServer(false);
            LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(),
                    result.getCode());
        }
    } catch (Exception e) {
        setHealthServer(false);
        LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
        throw e;
    }
    return Collections.emptyList();
}

可以看到就是/v1/cs/configs/listener接口。响应实际上是dataId和group集合,也就是配置的坐标资源列表。

7 回到第4步,看getServerConfig方法:

public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
        throws NacosException {
    String[] ct = new String[2];
    if (StringUtils.isBlank(group)) {
        group = Constants.DEFAULT_GROUP;
    }
    
    HttpRestResult<String> result = null;
    try {
        Map<String, String> params = new HashMap<String, String>(3);
        if (StringUtils.isBlank(tenant)) {
            params.put("dataId", dataId);
            params.put("group", group);
        } else {
            params.put("dataId", dataId);
            params.put("group", group);
            params.put("tenant", tenant);
        }
        result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
    } catch (Exception ex) {
        String message = String
                .format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s",
                        agent.getName(), dataId, group, tenant);
        LOGGER.error(message, ex);
        throw new NacosException(NacosException.SERVER_ERROR, ex);
    }
    
    switch (result.getCode()) {
        case HttpURLConnection.HTTP_OK:
            LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
            ct[0] = result.getData();
            if (result.getHeader().getValue(CONFIG_TYPE) != null) {
                ct[1] = result.getHeader().getValue(CONFIG_TYPE);
            } else {
                ct[1] = ConfigType.TEXT.getType();
            }
            return ct;
        case HttpURLConnection.HTTP_NOT_FOUND:
            LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
            return ct;
        case HttpURLConnection.HTTP_CONFLICT: {
            LOGGER.error(
                    "[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, "
                            + "tenant={}", agent.getName(), dataId, group, tenant);
            throw new NacosException(NacosException.CONFLICT,
                    "data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
        }
        case HttpURLConnection.HTTP_FORBIDDEN: {
            LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", agent.getName(),
                    dataId, group, tenant);
            throw new NacosException(result.getCode(), result.getMessage());
        }
        default: {
            LOGGER.error("[{}] [sub-server-error]  dataId={}, group={}, tenant={}, code={}", agent.getName(),
                    dataId, group, tenant, result.getCode());
            throw new NacosException(result.getCode(),
                    "http error, code=" + result.getCode() + ",dataId=" + dataId + ",group=" + group + ",tenant="
                            + tenant);
        }
    }
}

/v1/cs/configs接口的调用。取相应最新的配置数据。

8 最后是checkListenerMd5方法:

void checkListenerMd5() {
    for (ManagerListenerWrap wrap : listeners) {
        //有改变的话就通知
        if (!md5.equals(wrap.lastCallMd5)) {
            safeNotifyListener(dataId, group, content, type, md5, wrap);
        }
    }
}

9 MD5校验,有变更就通知

private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
        final String md5, final ManagerListenerWrap listenerWrap) {

    /**
     * 创建了一个任务,封装好信息,调用监听器的receiveConfigInfo方法接受数据处理。然后修改内容和MD5。
     * 这里他设置了一下类加载器,包装和监听器的类加载器一样,可能跟SPI反射调用相关。
     */
    final Listener listener = listenerWrap.listener;
    
    Runnable job = new Runnable() {
        @Override
        public void run() {
            ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
            ClassLoader appClassLoader = listener.getClass().getClassLoader();
            try {
                if (listener instanceof AbstractSharedListener) {
                    AbstractSharedListener adapter = (AbstractSharedListener) listener;
                    adapter.fillContext(dataId, group);
                    LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
                }
                // 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
                Thread.currentThread().setContextClassLoader(appClassLoader);
                
                ConfigResponse cr = new ConfigResponse();
                cr.setDataId(dataId);
                cr.setGroup(group);
                cr.setContent(content);
                configFilterChainManager.doFilter(null, cr);
                String contentTmp = cr.getContent();
                listener.receiveConfigInfo(contentTmp);
                
                // compare lastContent and content
                if (listener instanceof AbstractConfigChangeListener) {
                    Map data = ConfigChangeHandler.getInstance()
                            .parseChangeData(listenerWrap.lastContent, content, type);
                    ConfigChangeEvent event = new ConfigChangeEvent(data);
                    ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
                    listenerWrap.lastContent = content;
                }
                
                listenerWrap.lastCallMd5 = md5;
                LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
                        listener);
            } catch (NacosException ex) {
                LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}",
                        name, dataId, group, md5, listener, ex.getErrCode(), ex.getErrMsg());
            } catch (Throwable t) {
                LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId,
                        group, md5, listener, t.getCause());
            } finally {
                Thread.currentThread().setContextClassLoader(myClassLoader);
            }
        }
    };
    
    final long startNotify = System.currentTimeMillis();
    try {
        if (null != listener.getExecutor()) {
            listener.getExecutor().execute(job);
        } else {
            job.run();
        }
    } catch (Throwable t) {
        LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId,
                group, md5, listener, t.getCause());
    }
    final long finishNotify = System.currentTimeMillis();
    LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
            name, (finishNotify - startNotify), dataId, group, md5, listener);
}

如果监听器有线程池,则把通知任务丢线程池中,如果没有直接job.run.

job里listener.receiveConfigInfo(contentTmp);调用了监听器的receiveConfigInfo方法,也就是我们addListener时定义的实现。

10  回到第4步,看addListener方法:

@Override
public void addListener(String dataId, String group, Listener listener) throws NacosException {
    worker.addTenantListeners(dataId, group, Arrays.asList(listener));
}
public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners)
        throws NacosException {
    group = null2defaultGroup(group);
    String tenant = agent.getTenant();
    CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
    for (Listener listener : listeners) {
        cache.addListener(listener);
    }
}
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);
        // multiple listeners on the same dataid+group and race condition,so
        // double check again
        // other listener thread beat me to set to cacheMap
        if (null != cacheFromMap) {
            cache = cacheFromMap;
            // reset so that server not hang this check
            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) {
                String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                cache.setContent(ct[0]);
            }
        }
        
        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;
}

11 这里就是cacheMap的添加过程。

public void addListener(Listener listener) {
    if (null == listener) {
        throw new IllegalArgumentException("listener is null");
    }
    /**
     * 根据传入的类型,调用不同的ManagerListenerWrap构造函数
     */
    ManagerListenerWrap wrap =
            (listener instanceof AbstractConfigChangeListener) ? new ManagerListenerWrap(listener, md5, content)
                    : new ManagerListenerWrap(listener, md5);

    // listeners 是CopyOnWriteArrayList
    // 可以在写的时候提高性能,写的时候是复制一份去改的,原来的数据也能读,
    // 但是是旧的值,不过没关系,一般只修改一个元素,不影响到其他元素,其他元素照样可以读,旧的更新的是一样的数据
    if (listeners.addIfAbsent(wrap)) {
        LOGGER.info("[{}] [add-listener] ok, tenant={}, dataId={}, group={}, cnt={}", name, tenant, dataId, group,
                listeners.size());
    }
}

我们一下listeners的定义

是一个CopyOnWriteArrayList,这样保证了在添加监听的时候不会引起建立长轮询任务的地方出问题。因为listeners在写的时候是copy了一个副本,读的地方还是使用原来的list。

总结

ClientWorker的创建过程 – 主要是构建长轮询任务

长轮询任务如何建立的 – 3000个监听一组启动一个长轮询任务。任务跑完会写回到线程池中继续再次使用。

配置变更如何通知的 – 先去服务端拿到有变更的配置坐标,然后再发起调用获取配置内容。

另外ConfigService对配置的增删改查就是调用对应的服务端接口,这个就没什么好分析的了,要注意一个本地缓存的问题,优先使用本地缓存,是缓存文件。

 LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);


 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Nacos配置中心控制台的源码分析可以帮助我们深入理解其实现细节和工作原理。以下是一个大致的源码分析过程: 1. 入口类分析:首先,我们需要找到Nacos配置中心控制台的入口类。该类通常是一个Spring Boot应用的启动类,负责初始化和启动整个应用。我们可以查找包含main方法的类,或者在启动脚本中找到应用的入口点。 2. 依赖分析:接下来,我们需要分析应用所依赖的第三方库和框架。查看应用的pom.xml文件或者build.gradle文件,可以获取到所依赖的各个库和对应版本。这些依赖通常包括Spring框架、Nacos客户端等。 3. 配置加载与解析:Nacos配置中心控制台需要加载和解析配置,包括数据库配置、Nacos服务地址配置等。我们可以查找相关的配置文件或者代码片段,了解配置的加载和解析过程。 4. 控制器与路由:控制台通常提供了一些Web接口供前端调用。我们可以查找控制器类,分析其中的方法和注解,了解各个接口的功能和路由规则。 5. 页面模板与前端交互:配置中心控制台通常包含一些页面模板和与前端的交互逻辑。我们可以查找相关的HTML、CSS和JavaScript文件,分析页面的结构和交互逻辑。 6. 调用Nacos API:控制台需要与Nacos服务器进行通信,调用Nacos的API获取和修改配置信息。我们可以查找相关的API调用,了解控制台是如何与Nacos服务器进行通信的。 通过以上分析,我们可以逐步了解Nacos配置中心控制台的实现细节和工作原理。需要注意的是,具体的源码分析过程会因项目结构和代码风格而有所不同。以上只是一个大致的指导,具体分析还需根据实际情况来进行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值