开源 Serverless 框架 Laf 性能优化实践
介紹
Laf 是一個完全開源的 Serverless 框架,Laf 的 Node.js 運行時容器 (以下簡稱為 Runtime) 是 Laf 的函數執行環境,依托于 Express.js 框架。采用容器進程常駐的方式,每一個應用對應于一個或多個容器 (彈性伸縮下),底層使用了 Node.js 的 vm 模塊,使用 MongoDB 的 watch() 方法來監聽函數變更事件,以實現函數發布和配置發布。
Node.js vm 模塊
Node.js 的 vm 模塊是一個提供虛擬機功能的模塊,用于在 Node.js 環境中創建一個獨立的 JavaScript 執行環境。它允許在應用程序中運行和控制一段 JavaScript 代碼,同時提供了一些安全性和隔離性。
這個模塊包括一些可用于創建隔離的執行環境的函數,使得代碼能夠在獨立的上下文中運行,防止對主應用程序的影響。這在某些情況下可以提供更高的安全性,例如在沙盒環境中執行用戶提供的代碼,或者實現一些動態加載和執行代碼的需求。
原文鏈接:https://forum.laf.run/d/1146
為什么要優化
目前 Laf 的函數運行時存在以下問題:
- 頻繁使用 Node.js vm 模塊重復創建 vm,vm 創建執行的過程中,CPU 消耗很高。在以下對 runtime 的 CPU 火焰圖分析可見,在函數執行過程中,有兩部分 CPU 執行時間較長,分別是輸出函數請求日志和 vm 創建執行過程。
- 有時候遇到復雜的函數嵌套引用的時候,會導致循環引用,內存遲遲無法回收,造成內存泄漏,導致 OOM Killed。
- 交由 runtime 自己通過 HTTP 調用的形式,異步請求持久化函數日志,性能損耗大,QPS 直接減半。
- 函數引擎這塊的邏輯越來越復雜和臃腫,維護難度很大,急需重構。
如何優化
在前面的分析中,我們知道,當前造成性能瓶頸的原因主要有兩點:
- 為了實現隔離,vm 模塊重復創建,CPU 消耗高,特別是當函數引用達到一定規模時。另一方面,復雜的引用下,甚至會發生內存難以回收造成內存泄漏的問題。
- 頻繁打印函數請求日志,依賴單線程的 Node.js 通過異步請求處理 console.log 等日志,導致實際業務請求吞吐量下降。
因此,我們采用以下優化思路:
-
日志方面:使用標準輸出的形式輸出日志,交由 K8s 自己采集日志,而不由 runtime 自己處理。
-
函數引擎方面:第一次函數調用時,構建并緩存函數模塊,下次調用直接取出使用,不需要重復編譯,這塊更改需要確保以下因素:
- 保證這個緩存的函數模塊是無狀態,即 y = f(x),輸入相同的 x,則必然輸出確定的 y。
- 函數發布時,要及時清理緩存的函數模塊。
優化前后架構對比分析
- 優化前:
- 優化后:
優化步驟
- 改造日志方案為容器日志標準輸出,交由 K8s 收集,完全去除日志的有狀態依賴。
- 重構函數引擎,建立函數模塊,每一個函數模塊的導出都是一個 JS 對象,無論是代碼還是引用的第三方包,都被視作為一個 Module,在代碼中只會存在一份,等同于原生的 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 請求:
數據量 測試結果 QPS 10 并發請求 1000 次 110 100 并發請求 1000 次 122 -
WebSocket 連接
每秒創建 100 個 websocket 連接,當創建 1 萬個 websocket 連接時,資源占用情況如下:
真實案例
某個跑在 laf 上的應用,日活數十萬,原來需要 4 個 G 的內存,優化后,內存降至 512 MB 以下,CPU 只需要不到 1 核。
附加彩蛋
除此之外,我們還做了不少額外的工作:
- 日志支持根據不同 Level,以不同的顏色輸出。
- 通過重定向自定義依賴安裝路徑,現在支持安裝和內置依賴版本不同的依賴包。
- 攔截器現在支持類似 koa 洋蔥圈結構的前攔截和后攔截的寫法,詳情查看 Laf 文檔。
- ...
總結
通過優化 Laf 運行時,我們在將每個應用的成本降低至原來的 1/10 的同時,還大大提高了性能和穩定性,成功把 Laf 的價格打了下來 ~
總結
以上是生活随笔為你收集整理的开源 Serverless 框架 Laf 性能优化实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 懒人福音,谷歌让机器训练机器,用大语言模
- 下一篇: 性价比拉满!哪吒V全系优惠1万元 限量1