導入
Laf は完全にオープンソースのサーバーレス フレームワークであり、Laf の Node.js ランタイム コンテナ (以下、< a i =3>ランタイム) は、 に依存する、Laf の関数実行環境です。 Express.js フレームワーク。コンテナ プロセスは常駐しており、各アプリケーションは 1 つ以上のコンテナに対応します (エラスティック スケーリングの下)。最下層は Node.js の vm モジュールを使用します。関数変更イベントをリッスンして関数公開と構成公開を実装する MongoDB の メソッド。 watch()
Node.js vm モジュール
Node.js の vm モジュールは、仮想マシン機能を提供するモジュールであり、独立した JavaScript 実行環境を作成するために使用されます。これにより、セキュリティと分離を提供しながら、アプリケーション内で JavaScript コードの一部を実行および制御できるようになります。 a>。
このモジュールには、分離された実行環境を作成するために使用できる関数が含まれており、独立したコンテキストでコードを実行して、ホストとの干渉を防ぐことができます。 . アプリケーションへの影響。これにより、サンドボックス環境でユーザーが指定したコードを実行したり、動的読み込みと実行コードの要件。
なぜ最適化する必要があるのか
現在、Laf の関数には実行時に次の問題があります。
- Node.js vm モジュールを頻繁に使用して vm を繰り返し作成します。vm の作成と実行のプロセス中に、CPU の消費量が非常に多くなります。次のランタイムの CPU フレーム グラフ分析からわかるように、関数の実行プロセス中に、実行に時間がかかる CPU の 2 つの部分、つまり 関数リクエスト ログの出力 とvm は実行プロセスを作成します。
- 複雑な関数のネストされた参照が発生すると、循環参照が発生し、長時間メモリをリサイクルできなくなり、 a>メモリ リークが発生し、OOM Killed が発生します。
- HTTP 呼び出しを通じて永続関数ログを非同期的にリクエストすることをランタイム自体に任せると、パフォーマンスが大幅に低下します。QPS は直接半分になります 。
- 関数エンジンのロジックはますます複雑かつ肥大化しており、維持が困難になっており、早急に再構築する必要があります。
最適化する方法
前の分析で、現在のパフォーマンスのボトルネックには主に 2 つの理由があることがわかりました。
- 分離を実現するために、vm モジュールが繰り返し作成され、特に関数参照が一定の規模に達した場合、CPU の消費量が高くなります。一方、複雑な参照では、メモリのリサイクルが困難なため、メモリ リークが発生することもあります。
- 関数リクエストのログを頻繁に出力し、シングルスレッドの Node.js に依存して非同期リクエストを通じて console.log などのログを処理すると、実際のビジネス リクエストのスループットが低下します。
したがって、次の最適化のアイデアを採用します。
ログ: 標準出力を使用してログを出力し、ランタイム自体によって処理されるのではなく、K8 自体がログを収集できるようにします。
ファンクション・エンジン: ファンクションが初めて呼び出されるとき、ファンクション・モジュールが構築されてキャッシュされ、次のコールはコンパイルを繰り返さずに直接取り出されて使用されます。この変更では、次の要素を確保する必要があります:
- このキャッシュされた汎用モジュールはステートレスであることが保証されており、y = f(x) 入力が同じ x であれば、特定の y が出力されます。
- 関数が公開されると、キャッシュされた関数モジュールを適時にクリアする必要があります。
最適化前後のアーキテクチャの比較分析
- 最適化前:
- 最適化:
最適化の手順
- ログ ソリューションはコンテナ ログ標準出力に変更され、K8s によって収集され、ログのステートフルな依存関係が完全に削除されます。
- ファンクション・エンジンを再構築し、ファンクション・モジュールを確立します。各ファンクション・モジュールのエクスポートはJSオブジェクトです。コードであっても、参照されるサードパーティ・パッケージであっても、モジュールとみなされます。コード内にはコピーが1つだけ存在します。ネイティブの require/export と同等です。
- コードを簡素化し、可能な限り再利用し、コアロジックを保持します。
- 汎用モジュールのステートフル部分を削除します。
- 関数の実行および関数の導入ポイントで、関数モジュール キャッシュを作成します。
- デバッグ モードの場合、関数が実行されるたびに関数モジュールが再構築され、実行ログがアクティブに収集されます。
コア関数呼び出しロジック
const vm = require('vm')
// 函数列表
const functionList = {
a: "const b = require('b'); const func = () => b(); module.exports = func",
b: "module.exports = () => 'hello world'"
}
// 函数模块缓存
const functionModuleCache = new Map()
// 构建函数模块
const buildFunctionModule = (name) => {
// 自定义 require 逻辑,用来加载函数
const customRequire = (specifier) => {
if (functionModuleCache.has(specifier)) {
return functionModuleCache.get(specifier)
}
if(functionList[specifier]) {
return buildFunctionModule(specifier)
}
return require(specifier)
}
// 全局上下文
const ctx = {
__require: customRequire,
module: {
exports: {},
}
}
// 重新定义 require
const wrapCode = code => {
return `
const require = (name) => {
return __require(name)
}
${code}
module.exports;
`
}
// 构建模块
const script = new vm.Script(wrapCode(functionList[name]))
const mod = script.runInNewContext(ctx)
// 缓存构建结果
functionModuleCache.set(name, mod)
return mod
}
// 简单写一个入口函数
const main = () => {
const func = buildFunctionModule('a')
const res = func()
console.log(res)
}
main()
最適化効果
圧力試験
以下では、圧力テストの例として、Laf アプリケーションの最小構成 0.1c 128m を取り上げます。
通常の HTTP リクエスト:
データ量 試験結果 SWC 10 件の同時リクエスト 1000 回 110 100 の同時リクエスト 1000 回 122 WebSocket接続
1 秒あたり 100 の WebSocket 接続が作成されます。10,000 の WebSocket 接続が作成されると、リソースの使用量は次のようになります。
実際のケースのシナリオ
laf で実行されるアプリケーション毎日数十万のユーザーがいるには、当初は 4 G のメモリが必要でした。最適化後は、 a>。 メモリは 512 MB を下回り、CPU に必要なコアは 1 未満です
追加のイースターエッグ
さらに、私たちは多くの追加作業も行いました。
- ログは、さまざまなレベルに応じてさまざまな色での出力をサポートします。
- リダイレクトによる依存関係のインストール パスのカスタマイズで、インストールと組み込みの依存関係バージョンの異なる依存関係パッケージがサポートされるようになりました。
- インターセプターは、koa オニオン リング 構造と同様に、インターセプト前とインターセプト後の書き込みをサポートするようになりました。詳細については、Laf のドキュメントを参照してください。 。
- ...
要約する
Laf ランタイムを最適化することで、各アプリケーションのコストを元のコストの 1/10 に削減すると同時に、次の点でも大幅に改善しました。パフォーマンスと安定性の点で、Laf の価格を下げることに成功しました~