說(shuō)說(shuō)Egg.js中的多進(jìn)程增強(qiáng)模型(二)

說(shuō)說(shuō)Egg.js中的多進(jìn)程增強(qiáng)模型(一)中我們了解到了多進(jìn)程模型之間的通信方式和各個(gè)類(lèi)之間的關(guān)系,可以用下面??這張圖進(jìn)行回顧:

conclusion.jpeg

所有對(duì)于APIClient的方法調(diào)用,最終都會(huì)將調(diào)用執(zhí)行到follower.js / leader.js這兩個(gè)實(shí)例中,在follower.js中會(huì)通過(guò)tcp將方法調(diào)用發(fā)送給leader.js,在leader.js中無(wú)論是APIClient或是tcp請(qǐng)求過(guò)來(lái)的方法調(diào)用都會(huì)調(diào)用內(nèi)部的_realClient。

第一篇的整個(gè)主從模式的介紹還是非常籠統(tǒng)的,整體上對(duì)于多進(jìn)程模型以及類(lèi)關(guān)系圖有一個(gè)全貌的印象,這樣我們?cè)谑褂?code>Clueter-client類(lèi)庫(kù)時(shí)就不會(huì)只是調(diào)用一個(gè)黑盒了。但是類(lèi)庫(kù)真正的細(xì)節(jié),制定的規(guī)則和約束還是需要具體分析的,這也是本篇的重點(diǎn):

思路整理

跨進(jìn)程調(diào)用協(xié)議

worker進(jìn)程調(diào)用agent進(jìn)程內(nèi)實(shí)例的方法,雙方肯定需要進(jìn)行協(xié)議的約定,這樣當(dāng)接受請(qǐng)求時(shí)才能執(zhí)行正確的調(diào)用邏輯并返回相應(yīng)的數(shù)據(jù)。

API調(diào)用

對(duì)于一個(gè)業(yè)務(wù)客戶(hù)端(如:zookeeper客戶(hù)端 -> zkClient)的調(diào)用,每一個(gè)Worker進(jìn)程都希望自己是獨(dú)占的,如原生API一般的使用多進(jìn)程模型(如:zkClient.getData(path)調(diào)用,多進(jìn)程中依然可以調(diào)用同樣的api)。因此多進(jìn)程模型需要考慮的一點(diǎn)就是不能改變這一使用習(xí)慣。

API代理

worker中所有關(guān)于原生client的調(diào)用都是需要經(jīng)過(guò)底層的協(xié)議轉(zhuǎn)換之后請(qǐng)求agent中的leader進(jìn)行執(zhí)行,不可能每一個(gè)方法都去編寫(xiě)這樣的邏輯,需要將所有的方法調(diào)用最終全部代理到一個(gè)方法或者若干個(gè)確定的方法上,這樣只要在底層一次性實(shí)現(xiàn)相關(guān)的協(xié)議轉(zhuǎn)換和tcp請(qǐng)求處理的邏輯,上層業(yè)務(wù)完全透明。

源碼分析

經(jīng)過(guò)上面的思路整理,我們就可以在代碼中找到相應(yīng)的實(shí)現(xiàn),以及也會(huì)清晰的明白為什么會(huì)需要有這些類(lèi),以及每個(gè)類(lèi)存在的職責(zé)。代碼分析我們還是從上層使用到底層實(shí)現(xiàn)這一的順序來(lái)分析比較順暢。

api_client.js --> APIClientBase

APIClientBase類(lèi)是庫(kù)給業(yè)務(wù)提供的一個(gè)基類(lèi),業(yè)務(wù)層的每一個(gè)worker所持有的APIClient都是繼承這個(gè)基類(lèi),這個(gè)類(lèi)就是用來(lái)解決上面??所提到的“API調(diào)用”的問(wèn)題,業(yè)務(wù)層在這個(gè)類(lèi)中需要對(duì)原生client的API進(jìn)行定義,不用真正實(shí)現(xiàn),只需要像下面這個(gè)直接調(diào)用_client即可:

APIClient extends APIClientBase {
  getData(path)  {
    this._client.getData(path);
  }
}

通過(guò)上一篇文章的分析我們知道這里的_client屬性實(shí)際是client.js 內(nèi)定義的 ClusterClient類(lèi)

client.js --> ClusterClient

由上面的代碼我們知道,getData這個(gè)方法會(huì)直接調(diào)用 ClusterClientgetData方法,這樣問(wèn)題就來(lái)了,ClusterClient作為一個(gè)底層的API代理類(lèi)不可能實(shí)現(xiàn)所有的業(yè)務(wù)需要的API。進(jìn)到ClusterClient內(nèi)部會(huì)發(fā)現(xiàn)有下面幾個(gè)方法:

  /**
   * do subscribe
   *
   * @param {Object} reg - subscription info
   * @param {Function} listener - callback function
   * @return {void}
   */
 [subscribe](reg, listener) { ... }

  /**
   * do unSubscribe
   *
   * @param {Object} reg - subscription info
   * @param {Function} listener - callback function
   * @return {void}
   */
  [unSubscribe](reg, listener) { ... }

  /**
   * do publish
   *
   * @param {Object} reg - publish info
   * @return {void}
   */
  [publish](reg) { ... }

  /**
   * invoke a method asynchronously
   *
   * @param {String} method - the method name
   * @param {Array} args - the arguments list
   * @param {Function} callback - callback function
   * @return {void}
   */
  [invoke](method, args, callback) { ... }


 async [close]() { ... }

這幾個(gè)方法的內(nèi)部都是調(diào)用了innerClient,這之后就是本篇開(kāi)始梳理的流程。那么既然CluserClient只有這個(gè)幾個(gè)方法,怎么可以成功調(diào)用getData(path)? 也許我們觀察到了[invoke](method, args, callback) { ... }這個(gè)方法,這個(gè)方法的實(shí)現(xiàn)很像是一個(gè)動(dòng)態(tài)代理,是不是所有的方法都收斂到這個(gè)方法上了呢?如果真的是這樣的話(huà),那么必須要對(duì)其進(jìn)行hook或者其它heck的方式,一般做這種事情都是在實(shí)例創(chuàng)建的時(shí)候干的,我們就去index.js --> ClientWrapper的create方法(刪減):

const autoGenerateMethods = [
  'subscribe',
  'unSubscribe',
  'publish',
  'close',
];
...

  create(...args) {
    ...
    // auto generate description
    if (this._options.autoGenerate) {
      this._generateDescriptors();
    }

    for (const name of descriptors.keys()) {
      let value;
      const descriptor = descriptors.get(name);
      switch (descriptor.type) {
        case 'override':
          value = descriptor.value;
          break;
        case 'delegate':
          if (/^invoke|invokeOneway$/.test(descriptor.to)) {
            if (is.generatorFunction(proto[name])) {
              value = function* (...args) {
                return yield cb => { client[symbols.invoke](name, args, cb); };
              };
            } else if (is.function(proto[name])) {
              if (descriptor.to === 'invoke') {
                value = (...args) => {
                  let cb;
                  if (is.function(args[args.length - 1])) {
                    cb = args.pop();
                  }
                  // whether callback or promise
                  if (cb) {
                    client[symbols.invoke](name, args, cb);
                  } else {
                    return new Promise((resolve, reject) => {
                      client[symbols.invoke](name, args, function(err) {
                        if (err) {
                          reject(err);
                        } else {
                          resolve.apply(null, Array.from(arguments).slice(1));
                        }
                      });
                    });
                  }
                };
              } else {
                value = (...args) => {
                  client[symbols.invoke](name, args);
                };
              }
            } else {
              throw new Error(`[ClusterClient] api: ${name} not implement in client`);
            }
          } else {
            value = client[Symbol.for(`ClusterClient#${descriptor.to}`)];
          }
          break;
        default:
          break;
      }
      Object.defineProperty(client, name, {
        value,
        writable: true,
        enumerable: true,
        configurable: true,
      });
    }
    return client;
  }

  _generateDescriptors() {
    const clientClass = this._clientClass;
    const proto = clientClass.prototype;

    const needGenerateMethods = new Set(autoGenerateMethods);
    for (const entry of this._descriptors.entries()) {
      const key = entry[0];
      const value = entry[1];
      if (needGenerateMethods.has(key) ||
        (value.type === 'delegate' && needGenerateMethods.has(value.to))) {
        needGenerateMethods.delete(key);
      }
    }
    for (const method of needGenerateMethods.values()) {
      if (is.function(proto[method])) {
        this.delegate(method, method);
      }
    }

    const keys = Reflect.ownKeys(proto)
      .filter(key => typeof key !== 'symbol' &&
        !key.startsWith('_') &&
        !this._descriptors.has(key));

    for (const key of keys) {
      const descriptor = Reflect.getOwnPropertyDescriptor(proto, key);
      if (descriptor.value &&
        (is.generatorFunction(descriptor.value) || is.asyncFunction(descriptor.value))) {
        this.delegate(key);
      }
    }
  }
}

這里一下子就明朗了:

  1. create邏輯里面會(huì)根據(jù)descriptors這個(gè)Map內(nèi)存儲(chǔ)的內(nèi)容做方法自動(dòng)創(chuàng)建.
  2. descriptors內(nèi)存放的內(nèi)容來(lái)源是APIClient --> delegates方法返回內(nèi)容、autoGenerateMethods數(shù)組固定值以及RegistryClient內(nèi)的異步方法。
  3. 經(jīng)過(guò)_generateDescriptor之后所有的方法最終都會(huì)被歸類(lèi)( subscribe/unSubscribe/publish/close/invoke/invokeOneway)正好對(duì)應(yīng)到前面ClusterClient類(lèi)的5個(gè)方法(invoke|invokeOneway 都對(duì)應(yīng) [invoke])。
  4. 歸類(lèi)好的descriptors在create內(nèi)所有 invoke|invokeOneway會(huì)被全部指向 ClusterClient --> [invoke]。

上面的那個(gè)例子補(bǔ)充完整如下:

APIClient extends APIClientBase {

  get delegates() {
    return {
      'getData':'invoke'
    }
  }

  getData(path, callback)  {
    this._client.getData(path, callback);
  }
}

tcp 調(diào)用相關(guān)

協(xié)議的定義在/protocol目錄內(nèi),底層tcp的調(diào)用是基于另一個(gè)庫(kù) tcp-base。調(diào)用的細(xì)節(jié)在源碼follower.js / leader.js中都可以清晰看到。

補(bǔ)充

如果是完全自己編寫(xiě)一個(gè)插件業(yè)務(wù)(如:etcd的client),那么RegistryClient可以直接作為原生API的實(shí)現(xiàn)類(lèi),然后在APIClient的delegates方法然后一個(gè)api的mapping并定義相應(yīng)的mock api。但是往往在真實(shí)開(kāi)發(fā)過(guò)程中,業(yè)務(wù)的client的已經(jīng)有實(shí)現(xiàn)好的Node包,而Egg插件只需要封裝它就行,那么這樣就需要將RegistryClient作為業(yè)務(wù)client的代理類(lèi),再次進(jìn)行調(diào)用靜態(tài)或動(dòng)態(tài)轉(zhuǎn)發(fā),具體可以看一下我寫(xiě)的Cat的egg插件egg-cat-client。

總結(jié): 經(jīng)過(guò)整個(gè)調(diào)用鏈路的梳理和底層一些規(guī)則的說(shuō)明,我們已經(jīng)對(duì)這樣一個(gè)多進(jìn)程的實(shí)現(xiàn)了然于胸了,這樣在真實(shí)的開(kāi)發(fā)使用中才可以寫(xiě)出更加符合自己需要的代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容