說說Egg.js中的多進程增強模型(二)

說說Egg.js中的多進程增強模型(一)中我們了解到了多進程模型之間的通信方式和各個類之間的關系跟畅,可以用下面??這張圖進行回顧:

conclusion.jpeg

所有對于APIClient的方法調用咽筋,最終都會將調用執(zhí)行到follower.js / leader.js這兩個實例中,在follower.js中會通過tcp將方法調用發(fā)送給leader.js徊件,在leader.js中無論是APIClient或是tcp請求過來的方法調用都會調用內部的_realClient奸攻。

第一篇的整個主從模式的介紹還是非常籠統(tǒng)的虱痕,整體上對于多進程模型以及類關系圖有一個全貌的印象睹耐,這樣我們在使用Clueter-client類庫時就不會只是調用一個黑盒了。但是類庫真正的細節(jié)部翘,制定的規(guī)則和約束還是需要具體分析的硝训,這也是本篇的重點:

思路整理

跨進程調用協(xié)議

worker進程調用agent進程內實例的方法,雙方肯定需要進行協(xié)議的約定新思,這樣當接受請求時才能執(zhí)行正確的調用邏輯并返回相應的數(shù)據(jù)窖梁。

API調用

對于一個業(yè)務客戶端(如:zookeeper客戶端 -> zkClient)的調用,每一個Worker進程都希望自己是獨占的夹囚,如原生API一般的使用多進程模型(如:zkClient.getData(path)調用纵刘,多進程中依然可以調用同樣的api)。因此多進程模型需要考慮的一點就是不能改變這一使用習慣荸哟。

API代理

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

源碼分析

經(jīng)過上面的思路整理,我們就可以在代碼中找到相應的實現(xiàn)掏父,以及也會清晰的明白為什么會需要有這些類笋轨,以及每個類存在的職責。代碼分析我們還是從上層使用到底層實現(xiàn)這一的順序來分析比較順暢赊淑。

api_client.js --> APIClientBase

APIClientBase類是庫給業(yè)務提供的一個基類爵政,業(yè)務層的每一個worker所持有的APIClient都是繼承這個基類,這個類就是用來解決上面??所提到的“API調用”的問題陶缺,業(yè)務層在這個類中需要對原生client的API進行定義钾挟,不用真正實現(xiàn),只需要像下面這個直接調用_client即可:

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

通過上一篇文章的分析我們知道這里的_client屬性實際是client.js 內定義的 ClusterClient類饱岸。

client.js --> ClusterClient

由上面的代碼我們知道掺出,getData這個方法會直接調用 ClusterClientgetData方法,這樣問題就來了苫费,ClusterClient作為一個底層的API代理類不可能實現(xiàn)所有的業(yè)務需要的API汤锨。進到ClusterClient內部會發(fā)現(xiàn)有下面幾個方法:

  /**
   * 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]() { ... }

這幾個方法的內部都是調用了innerClient,這之后就是本篇開始梳理的流程百框。那么既然CluserClient只有這個幾個方法闲礼,怎么可以成功調用getData(path)? 也許我們觀察到了[invoke](method, args, callback) { ... }這個方法,這個方法的實現(xiàn)很像是一個動態(tài)代理铐维,是不是所有的方法都收斂到這個方法上了呢柬泽?如果真的是這樣的話,那么必須要對其進行hook或者其它heck的方式嫁蛇,一般做這種事情都是在實例創(chuàng)建的時候干的锨并,我們就去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邏輯里面會根據(jù)descriptors這個Map內存儲的內容做方法自動創(chuàng)建.
  2. descriptors內存放的內容來源是APIClient --> delegates方法返回內容、autoGenerateMethods數(shù)組固定值以及RegistryClient內的異步方法棠众。
  3. 經(jīng)過_generateDescriptor之后所有的方法最終都會被歸類( subscribe/unSubscribe/publish/close/invoke/invokeOneway)正好對應到前面ClusterClient類的5個方法(invoke|invokeOneway 都對應 [invoke])琳疏。
  4. 歸類好的descriptors在create內所有 invoke|invokeOneway會被全部指向 ClusterClient --> [invoke]有决。

上面的那個例子補充完整如下:

APIClient extends APIClientBase {

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

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

tcp 調用相關

協(xié)議的定義在/protocol目錄內闸拿,底層tcp的調用是基于另一個庫 tcp-base。調用的細節(jié)在源碼follower.js / leader.js中都可以清晰看到书幕。

補充

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

總結: 經(jīng)過整個調用鏈路的梳理和底層一些規(guī)則的說明严衬,我們已經(jīng)對這樣一個多進程的實現(xiàn)了然于胸了澄者,這樣在真實的開發(fā)使用中才可以寫出更加符合自己需要的代碼。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末请琳,一起剝皮案震驚了整個濱河市粱挡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌俄精,老刑警劉巖询筏,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異竖慧,居然都是意外死亡嫌套,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門圾旨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灌危,“玉大人,你說我怎么就攤上這事碳胳∮买” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵挨约,是天一觀的道長味混。 經(jīng)常有香客問我,道長诫惭,這世上最難降的妖魔是什么翁锡? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮夕土,結果婚禮上馆衔,老公的妹妹穿的比我還像新娘。我一直安慰自己怨绣,他們只是感情好角溃,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著篮撑,像睡著了一般减细。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赢笨,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天未蝌,我揣著相機與錄音驮吱,去河邊找鬼。 笑死萧吠,一個胖子當著我的面吹牛左冬,可吹牛的內容都是我干的。 我是一名探鬼主播纸型,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼又碌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绊袋?” 一聲冷哼從身側響起毕匀,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎癌别,沒想到半個月后皂岔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡展姐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年躁垛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片圾笨。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡教馆,死狀恐怖,靈堂內的尸體忽然破棺而出擂达,到底是詐尸還是另有隱情土铺,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布板鬓,位于F島的核電站悲敷,受9級特大地震影響,放射性物質發(fā)生泄漏俭令。R本人自食惡果不足惜后德,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望抄腔。 院中可真熱鬧瓢湃,春花似錦、人聲如沸赫蛇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棍掐。三九已至藏雏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間作煌,已是汗流浹背掘殴。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粟誓,地道東北人奏寨。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像鹰服,于是被迫代替她去往敵國和親病瞳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內容