tegg v3 - 性能飛躍
本文作者是螞蟻集團(tuán)前端工程師零弌,介紹了 tegg v3 究竟有多快,快在哪,以及 tegg v3 如何做到性能飛躍。
AsyncLocalStorage 與 eggjs
AsyncLocalStorage?[1]?可以在一個(gè) AsyncFunction 以及其相關(guān)的異步操作內(nèi)安全的獲取到在 store 內(nèi)存儲(chǔ)的變量。我們通過(guò)一段簡(jiǎn)單的代碼就可以看到其演示效果。
AsyncLocalStorage 演示
發(fā)起兩次 HTTP 請(qǐng)求
打印內(nèi)容為
AsyncLocalStorage 實(shí)現(xiàn)原理
nodejs 通過(guò) v8 提供的 Promise lifecycle hooks?[2]?實(shí)現(xiàn)了 Promise 執(zhí)行追蹤?[3]?。也就是說(shuō)在一個(gè) Promise 實(shí)例化、resolve、reject、then、catch 時(shí)均能被追蹤到,AsyncLocalStorage 通過(guò)這些 hook 將 asyncId 進(jìn)行了傳播,因此可以通過(guò)一個(gè) storage 在異步函數(shù)中安全的獲取到當(dāng)前的上下文變量。
node 中除了 Promise 之外還有 timer、fs、net 這些模塊也能執(zhí)行移步操作,比如上面演示代碼中就是有 http server 和 setTimeout。node 通過(guò) AsyncResource?[4]?這層抽象對(duì)這些方式進(jìn)行了實(shí)現(xiàn),如果有自己的 addon 實(shí)現(xiàn),也可以通過(guò)這個(gè)類來(lái)實(shí)現(xiàn)。
egg
koa 已支持?[5]?,通過(guò) asyncLocalStorage 參數(shù)開啟。
egg 已支持?[6]?,通過(guò) app.currentContext 即可獲取當(dāng)前 egg context。
??Benchmark
測(cè)試地址:https://github.com/eggjs/tegg_benchmark/actions/runs/4025979558
測(cè)試場(chǎng)景
項(xiàng)目規(guī)模測(cè)試
通過(guò)在項(xiàng)目里創(chuàng)建大量的 controller 和 service 來(lái)模擬項(xiàng)目規(guī)模。Benchmark 分別測(cè)試了 1, 10, 100, 1000, 10000 個(gè) controller/service 的情況。

業(yè)務(wù)復(fù)雜度測(cè)試
通過(guò)在 controller 訪問(wèn) service 來(lái)模擬業(yè)務(wù)復(fù)雜度。Benchmark 分別測(cè)試了 1, 10, 100, 1000, 10000 個(gè) service 的情況。

結(jié)論
tegg v3 不會(huì)因?yàn)轫?xiàng)目規(guī)模擴(kuò)張而引起性能衰退。
tegg v3 因?yàn)闃I(yè)務(wù)復(fù)雜度擴(kuò)張而引起的性能衰退比 egg/tegg v1 慢。
Profile
CPU
egg 項(xiàng)目規(guī)模復(fù)雜度(1w)
性能熱點(diǎn)集中在 egg 中的 defineProperty 和 ClassLoader,原因是 controller/service 過(guò)多導(dǎo)致。

tegg 項(xiàng)目規(guī)模復(fù)雜度(1w)
熱點(diǎn)耗時(shí)在 node 本身,gc、async_hook。

內(nèi)存
egg 項(xiàng)目規(guī)模復(fù)雜度(1w)
由于 controller/service 數(shù)量過(guò)多,導(dǎo)致存在內(nèi)存分配瓶頸??梢钥吹?controller 每次實(shí)例化都會(huì)占用大量?jī)?nèi)存。

tegg 項(xiàng)目規(guī)模復(fù)雜度(1w)
目前內(nèi)存壓力存在于 async_hook。

如何飛躍
tegg 注入原理
tegg 實(shí)例代碼,HelloWorldController 注入了 Foo 類, Foo 類注入了 Tracer 類。
@HTTPController()
class?HelloWorldController?{
??@Inject()
??private?readonly?foo:?Foo;
}
@Context()
class?Foo?{
??@Inject()
??private?readonly?tracer:?Tracer;
}
請(qǐng)求在進(jìn)入框架執(zhí)行階段后,首先會(huì)找到入口類。本例中為 HelloWorldController,并根據(jù)對(duì)象圖實(shí)例化所有的對(duì)象。

tegg v1 性能瓶頸
每個(gè)請(qǐng)求需要?jiǎng)?chuàng)建 10001 個(gè)對(duì)象,需要分配大量的內(nèi)存,導(dǎo)致 gc 壓力很大。

tegg v3 優(yōu)化原理
減少實(shí)例化對(duì)象,看示例代碼,HelloWorkerController, Foo 與請(qǐng)求上下文是無(wú)關(guān)的,只有 Tracer 是與請(qǐng)求上下文相關(guān)。因此 HelloWorldController, Foo 不應(yīng)該需要每次都實(shí)例化,這對(duì)于 CPU 和 內(nèi)存 都是利好。

使用 AsyncLocalStorage 來(lái)代理對(duì)象,HelloWorldController、Foo 將會(huì)改造成單例模式,如何在不同的請(qǐng)求中獲取到正確的對(duì)象將會(huì)是一個(gè)問(wèn)題。我們需要將 tracer 改造成一個(gè)代理,通過(guò) ctxStorage 來(lái)獲取到正確的對(duì)象。
優(yōu)化后效果
從 10001 到 0。極大的降低了內(nèi)存壓力,tegg v1 的堆大小從 200M 到 1G 波動(dòng),tegg v3 可以穩(wěn)定在 200M。

tegg v3 代碼改造
修改注解
僅需將 @ContextProto() 替換為 @SingletonProto。
~~@ContextProto()~~
@SingletonProto()
class?Foo?{
??@Inject()
??private?readonly?tracer:?Tracer;
}
實(shí)現(xiàn)有狀態(tài)
Foo 這個(gè)類的狀態(tài)和當(dāng)前上下文有關(guān),如果改成 Singleton 模式,所有上下文中共享會(huì)導(dǎo)致對(duì)象用串了,所以需要保持 ContextProto。
@ContextProto()
class?Foo?{
??state:?State;
??foo()?{
????this.state?=?'foo';
??}
??bar()?{
????this.state?=?'bar';
??}
}
單測(cè)改造
describe('test/index.test.ts',?()?=>?{
??let?foo:?Foo;
??beforeEach(async?()?=>?{
????foo?=?await?app.getEggObject(Foo);
??});
??it('should?work',?()?=>?{
????assert(foo.hello());
??});
});
?? 相關(guān)鏈接
[1] https://nodejs.org/dist/latest-v18.x/docs/api/async_context.html#class-asynclocalstorage
[2] https://docs.google.com/document/d/1rda3yKGHimKIhg5YeoAmCOtyURgsbTH_qaYR79FELlk/edit
[3]https://nodejs.org/dist/latest-v18.x/docs/api/async_hooks.html#promise-execution-tracking
[4]https://nodejs.org/dist/latest-v18.x/docs/api/async_context.html#class-asyncresource
[5]https://github.com/koajs/koa/pull/1721
[6]https://github.com/eggjs/egg-core/pull/251