说说Egg.js中的多进程增强模型(二)

说说Egg.js中的多进程增强模型(一)中我们了解到了多进程模型之间的通信方式和各个类之间的关系,可以用下面👇这张图进行回顾:

conclusion.jpeg

所有对于APIClient的方法调用,最终都会将调用执行到follower.js / leader.js这两个实例中,在follower.js中会通过tcp将方法调用发送给leader.js,在leader.js中无论是APIClient或是tcp请求过来的方法调用都会调用内部的_realClient

第一篇的整个主从模式的介绍还是非常笼统的,整体上对于多进程模型以及类关系图有一个全貌的印象,这样我们在使用Clueter-client类库时就不会只是调用一个黑盒了。但是类库真正的细节,制定的规则和约束还是需要具体分析的,这也是本篇的重点:

思路整理

跨进程调用协议

worker进程调用agent进程内实例的方法,双方肯定需要进行协议的约定,这样当接受请求时才能执行正确的调用逻辑并返回相应的数据。

API调用

对于一个业务客户端(如:zookeeper客户端 -> zkClient)的调用,每一个Worker进程都希望自己是独占的,如原生API一般的使用多进程模型(如:zkClient.getData(path)调用,多进程中依然可以调用同样的api)。因此多进程模型需要考虑的一点就是不能改变这一使用习惯。

API代理

worker中所有关于原生client的调用都是需要经过底层的协议转换之后请求agent中的leader进行执行,不可能每一个方法都去编写这样的逻辑,需要将所有的方法调用最终全部代理到一个方法或者若干个确定的方法上,这样只要在底层一次性实现相关的协议转换和tcp请求处理的逻辑,上层业务完全透明。

源码分析

经过上面的思路整理,我们就可以在代码中找到相应的实现,以及也会清晰的明白为什么会需要有这些类,以及每个类存在的职责。代码分析我们还是从上层使用到底层实现这一的顺序来分析比较顺畅。

api_client.js --> APIClientBase

APIClientBase类是库给业务提供的一个基类,业务层的每一个worker所持有的APIClient都是继承这个基类,这个类就是用来解决上面👆所提到的“API调用”的问题,业务层在这个类中需要对原生client的API进行定义,不用真正实现,只需要像下面这个直接调用_client即可:

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

通过上一篇文章的分析我们知道这里的_client属性实际是client.js 内定义的 ClusterClient类

client.js --> ClusterClient

由上面的代码我们知道,getData这个方法会直接调用 ClusterClientgetData方法,这样问题就来了,ClusterClient作为一个底层的API代理类不可能实现所有的业务需要的API。进到ClusterClient内部会发现有下面几个方法:

  /**
   * 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) { ... }这个方法,这个方法的实现很像是一个动态代理,是不是所有的方法都收敛到这个方法上了呢?如果真的是这样的话,那么必须要对其进行hook或者其它heck的方式,一般做这种事情都是在实例创建的时候干的,我们就去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逻辑里面会根据descriptors这个Map内存储的内容做方法自动创建.
  2. descriptors内存放的内容来源是APIClient --> delegates方法返回内容、autoGenerateMethods数组固定值以及RegistryClient内的异步方法。
  3. 经过_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 调用相关

协议的定义在/protocol目录内,底层tcp的调用是基于另一个库 tcp-base。调用的细节在源码follower.js / leader.js中都可以清晰看到。

补充

如果是完全自己编写一个插件业务(如:etcd的client),那么RegistryClient可以直接作为原生API的实现类,然后在APIClient的delegates方法然后一个api的mapping并定义相应的mock api。但是往往在真实开发过程中,业务的client的已经有实现好的Node包,而Egg插件只需要封装它就行,那么这样就需要将RegistryClient作为业务client的代理类,再次进行调用静态或动态转发,具体可以看一下我写的Cat的egg插件egg-cat-client

总结: 经过整个调用链路的梳理和底层一些规则的说明,我们已经对这样一个多进程的实现了然于胸了,这样在真实的开发使用中才可以写出更加符合自己需要的代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容