nacos入門系列之配置中心

z_paul發表於2021-09-09
之前學習了nacos註冊中心,今天繼續看看nacos的其他功能。
註冊中心連結 

配置的釋出與訂閱

我們先來看看如何使用nacos提供的api來實現配置的釋出與訂閱
釋出配置:
public class ConfigPub {

    public static void main(String[] args) throws NacosException {

        final String dataId="test";

        final String group="DEFAULT_GROUP";

        ConfigService configService= NacosFactory.createConfigService("localhost:8848");

        configService.publishConfig(dataId,group,"test config body");
    }
}
訂閱配置:
   public static void main(String[] args) throws NacosException, InterruptedException {

        final String dataId="test";

        final String group="DEFAULT_GROUP";

        ConfigService configService= NacosFactory.createConfigService("localhost:8848");

        configService.addListener(dataId, group, new Listener() {
            @Override
            public Executor getExecutor() {
                return null;
            }

            @Override
            public void receiveConfigInfo(String configInfo) {

                System.out.println("receiveConfigInfo:"+configInfo);
            }
        });

        Thread.sleep(Integer.MAX_VALUE);
    }
}
根據上面的demo可以看到透過dataId和group可以定位一個配置檔案。


深入瞭解配置釋出

1-釋出的配置資訊會透過http請求呼叫具體的服務

agent.httpPost(url, headers, params, encode, POST_TIMEOUT);
服務類為 ConfigController:處理配置相關的http請求
persistService
      .insertOrUpdateTag(configInfo, tag, srcIp, srcUser, time, false);
EventDispatcher.fireEvent(
      new ConfigDataChangeEvent(false, dataId, group, tenant, tag,
            time.getTime()));

可以看到釋出的配置首先會進行持久化,然後會觸發變更通知。

持久化這裡就不做分析,我們來看看fireEvent這個方法:

EventDispatcher.fireEvent:
static public void fireEvent(Event event) {
    if (null == event) {
        throw new IllegalArgumentException("event is null");
    }

    for (AbstractEventListener listener : getEntry(event.getClass()).listeners) {
        try {
            listener.onEvent(event);
        } catch (Exception e) {
            log.error(e.toString(), e);
        }
    }
}

這裡可以看到具體呼叫了listener.onEvent(event);
這裡只要找到AbstractEventListener 具體的實現類是哪個就可以。
AbstractEventListener主要有兩個實現類:
AsyncNotifyService
LongPollingService

我們可以透過event的型別去判斷,因為這裡onEvent的引數型別為ConfigDataChangeEvent,
所以我們可以清楚的知道我們要找的實現類是AsyncNotifyService。
每個AbstractEventListener初始化的時候都會先將自己加入到listeners中
final CopyOnWriteArrayList<AbstractEventListener> listeners;
public AbstractEventListener() {
    /**
     * automatic register
     */
    EventDispatcher.addEventListener(this);
}

我們可以直接看看AsyncNotifyService的onEvent方法:
public void onEvent(Event event) {

   // 併發產生 ConfigDataChangeEvent
   if (event instanceof ConfigDataChangeEvent) {
      ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
      long dumpTs = evt.lastModifiedTs;
      String dataId = evt.dataId;
      String group = evt.group;
      String tenant = evt.tenant;
      String tag = evt.tag;
      //Member{address='192.168.31.192:8848'}
      Collection<Member> ipList = memberManager.allMembers();

      // 其實這裡任何型別佇列都可以
      Queue<NotifySingleTask> queue = new LinkedList<NotifySingleTask>();
      for (Member member : ipList) {
         queue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs,
               member.getAddress(), evt.isBeta));
      }
      EXECUTOR.execute(new AsyncTask(httpclient, queue));
   }
}

上面的方法主要實現的是:
獲取所有的nacos服務節點,然後對其執行非同步任務AsyncTask。
AsyncTask中會從佇列中獲取每個節點的NotifySingleTask資訊,然後進行http請求,呼叫通知配置資訊改變
的服務。具體服務在CommunicationController中實現。

/**
 * 通知配置資訊改變
 */
@GetMapping("/dataChange")

這個方法放在後面分析。


深入瞭解配置訂閱

初始化:

NacosConfigService初始化的時候構造了ClientWorker,並且透過ClientWorker啟動了兩個執行緒池。
worker = new ClientWorker(agent, configFilterChainManager, properties);
第一個執行緒池每10ms執行一次checkConfigInfo();
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);

我們來看看checkConfigInfo具體是做什麼的
public void checkConfigInfo() {
    // 分任務
    int listenerSize = cacheMap.get().size();
    // 向上取整為批數,限制LongPollingRunnable處理配置的個數。
    int longingTaskCount =(int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
    if (longingTaskCount > currentLongingTaskCount) {
        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
            // 要判斷任務是否在執行 這塊需要好好想想。 
            //任務列表現在是無序的。變化過程可能有問題
            executorService.execute(new LongPollingRunnable(i));
            //這裡的i就代表taskId
        }
        currentLongingTaskCount = longingTaskCount;
    }
}

這裡主要的作用是提交LongPollingRunnable任務到第二個執行緒池中去執行。
並且每個LongPollingRunnable只會處理3000個配置。

我們來看看LongPollingRunnable的實現
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);
            ...
        }
    }
cacheMap中儲存了配置資訊,從磁碟中載入獲取。
透過taskId從 cacheMap中獲取需要被當前LongPollingRunnable任務處理的配置,放入到cacheDatas集合。

我們來看看是在哪裡設定的taskId
int taskId = cacheMap.get().size() / (int) ParamUtil.getPerTaskConfigSize();
cache.setTaskId(taskId);
可以看到這裡和上面相對應,每3000個配置的taskId是相同的。因為每個LongPollingRunnable執行緒會處理
3000個配置。


// check server config  向服務端請求變化的配置
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);

//從Server獲取值變化了的DataID列表。返回的物件裡只有dataId和group是有效的。 保證不返回NULL。
return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);

這裡訂閱配置的客戶端會向服務端傳送http長輪詢請求,來獲取變化的配置資訊
長輪詢請求不會立刻返回結果,而是當有配置發生變化時返回,設定了超時時間30s,如果超過了設定的
超時時間沒有配置更新,則會預設返回。然後重新發起一次長輪詢的請求。

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

長輪詢的週期預設為30s:
timeout=Math.max(NumberUtils.toInt(properties.getProperty(PropertyKeyConst.CONFIG_LONG_POLL_TIMEOUT),
    Constants.CONFIG_LONG_POLL_TIMEOUT), Constants.MIN_CONFIG_LONG_POLL_TIMEOUT);

具體服務實現類在ConfigController中:
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public void listener(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
   ....

   // do long-polling
   inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}

doPollingConfig方法:
// 服務端處理長輪詢請求
if (LongPollingService.isSupportLongPolling(request)) {
    longPollingService.addLongPollingClient(request, response, clientMd5Map, 
    probeRequestSize);
    return HttpServletResponse.SC_OK + "";
}

使用執行緒池處理請求:
scheduler.execute(
    new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, 
    appName, tag));

接著來看ClientLongPolling是一個執行緒實現類
首先會觸發一個延時任務,然後將自己加入到佇列:allSubs.add(this);
allSubs中維護了所有長輪訓請求。

那麼肯定會有一個地方去消費allSubs佇列中的請求.
這個消費的地方就是onEvent方法:
LongPollingService其實就是我們上面提到的AbstractEventListener,因此也實現了onEvent方法。

@Override
public void onEvent(Event event) {
    if (isFixedPolling()) {
        // ignore
    } else {
        if (event instanceof LocalDataChangeEvent) {
            LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
            scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, 
            evt.betaIps));
        }
    }
}

這個event方法就是去處理配置變化的情況,主要邏輯在DataChangeTask中:
從allSubs獲取維護的請求中相同dataId+group的請求,比如:(test+DEFAULT_GROUP)
然後進行這個對長輪詢的請求進行返回。
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
    ClientLongPolling clientSub = iter.next();
    //groupKey test+DEFAULT_GROUP
    if (clientSub.clientMd5Map.containsKey(groupKey)) {
        ......
        iter.remove(); // 刪除訂閱關係
        LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
        (System.currentTimeMillis() - changeTime),
        "in-advance",
        RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),
            "polling",
            clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
        clientSub.sendResponse(Arrays.asList(groupKey));
    }
}


那是哪裡觸發了LongPollingService裡面的onEvent 方法呢?
當然是在配置釋出後進行觸發的,還記得CommunicationController中的dataChange服務嗎?
配置釋出後會透過http請求呼叫nacos服務中的dataChange服務。透過dataChange服務就可以通知
nacos服務中儲存的長輪訓的請求了。

並且這個方法是獲取所有nacos服務節點去遍歷執行的,因此不管變更配置對應的長輪詢儲存在哪個節點,
都會可以被獲取到。

/**
 * 通知配置資訊改變
 */
@GetMapping("/dataChange")

此處會呼叫DumpService中的方法儲存配置檔案到磁碟,並快取md5.

DiskUtil.saveToDisk(dataId, group, tenant, content);

public static void updateMd5(String groupKey, String md5, long lastModifiedTs) {
    CacheItem cache = makeSure(groupKey);
    if (cache.md5 == null || !cache.md5.equals(md5)) {
        cache.md5 = md5;
        cache.lastModifiedTs = lastModifiedTs;
        EventDispatcher.fireEvent(new LocalDataChangeEvent(groupKey));
    }
}

可以看到當配置變更,就會觸發fireEvent的LocalDataChangeEvent事件。


總結

到這裡,配置中心整體是實現基本上告一段落,還要很多細節沒有涉及到,需要在真正的使用過程中來探索和發現。






來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/75/viewspace-2825790/,如需轉載,請註明出處,否則將追究法律責任。

相關文章