從說說Egg.js中的多進程增強模型(一)中我們了解到了多進程模型之間的通信方式和各個類之間的關系跟畅,可以用下面??這張圖進行回顧:
所有對于
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
這個方法會直接調用 ClusterClient
的getData
方法,這樣問題就來了苫费,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);
}
}
}
}
這里一下子就明朗了:
- create邏輯里面會根據(jù)
descriptors
這個Map內存儲的內容做方法自動創(chuàng)建. -
descriptors
內存放的內容來源是APIClient --> delegates
方法返回內容、autoGenerateMethods數(shù)組固定值以及RegistryClient
內的異步方法棠众。 - 經(jīng)過
_generateDescriptor
之后所有的方法最終都會被歸類(subscribe/unSubscribe/publish/close/invoke/invokeOneway
)正好對應到前面ClusterClient
類的5個方法(invoke|invokeOneway 都對應 [invoke])琳疏。 - 歸類好的
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ā)使用中才可以寫出更加符合自己需要的代碼。