摘要:是下的一個優秀的框架,但是使用后,在流量增長時,進程有時突然內存暴漲保持高占用。如果是內存泄露引起的,則需要細心檢查代碼,確定變量能正常回收。每個對象有自己產生的內存。譯注但是大對象內存區本身不是可執行的內存區。
Sails.js 是 node 下的一個優秀的 MVC 框架,但是使用 Sails 后,在流量增長時, node 進程有時突然內存暴漲、保持高占用。經過翻閱源碼后,發現這個問題與 session / GC 都有關系。
PS: 如果是內存泄露引起的,則需要細心檢查代碼,確定變量能正常回收。
舉個栗子新建一個 sails app :
# new sails app memory > sails new memeory > cd memory
修改 config/bootstrap.js 增加內存快照,寫入一個 xls(方便畫圖):
var fs = require("fs"); // (see note below) setInterval(function takeSnapshot() { var mem = process.memoryUsage(); fs.appendFile("./memorysnapshot.xls", mem.rss / 1024 / 1024 + " " + mem.heapUsed / 1024 / 1024 + " " + mem.heapTotal / 1024 / 1024 + " ", "utf8"); }, 1000); // Snapshot every second
使用 pm2 啟動 sails
> pm2 start app.js > pm2 monit
使用壓測工具,10W 請求,100 并發
# ab 壓測工具 > ab -n 100000 -c 100 http://127.0.0.1:1337/
內存占用喜人
Concurrency Level: 100 Time taken for tests: 276.154 seconds Complete requests: 100000 Failed requests: 0 Total transferred: 1094761464 bytes HTML transferred: 1044700000 bytes Requests per second: 362.12 [#/sec] (mean) Time per request: 276.154 [ms] (mean) Time per request: 2.762 [ms] (mean, across all concurrent requests) Transfer rate: 3871.40 [Kbytes/sec] received PM2 monitoring (To go further check out https://app.keymetrics.io) app [ ] 0 %%% [0] [fork_mode] [|||||||| ] 893.184 MB關閉 session
# 關閉 session { "hooks": { ... "session": false, ... } } # 壓測結果與之前并沒有什么區別 Requests per second: 381.06 [#/sec] (mean) # 但是內存很穩定,基本沒增加過 PM2 monitoring (To go further check out https://app.keymetrics.io) app [ ] 0 %%% [0] [fork_mode] [|||||||||||||| ] 162.609 MB
結果感人,關閉不必要的服務并沒有給訪問主頁帶來多大的性能提升,但是內存占用下降了非常多,下面就翻翻源碼看看 Sails 做了什么。
Sails 做了什么 源碼sails的源碼結構相當清晰:
sails@0.12.1 ├── bin/ # sails command 處理 ├── errors/ # 定義啟動加載錯誤 └─┬ lib/ ├─┬ app/ │ ├── configuration/ # 加載各種參數,補全默認參數 │ ├── private/ # 很多方法,最終都 bind 到 Sails │ ├── ... # other module, all bind to Sails │ ├── Sail.js # main entry │ └── index.js ├─┬ hook/ # 以下部分加載 sails 的相關配置 │ ├── blueprints/ │ ├── controllers/ │ ├── cors/ │ ├── csrf/ │ ├── grunt/ │ ├─┬ http/ │ │ ├── middleware/ # express middleware 加載的地方 │ │ ├── public/ # favicon.ico │ │ ├── start.js / # .listen(port) │ │ ├── initialize.js # load express │ │ └── ... │ ├── i18n/ │ ├── logger/ │ ├── moduleloader/ │ ├── orm/ │ ├── policies/ │ ├── pubsub/ │ ├── request/ │ ├── responses/ │ ├── services/ │ ├── session/ # session 加載的地方 │ ├── userconfig/ │ ├── userhook/ │ ├── views/ │ └── index.js └─┬ hook/ # router ├── bind.js # bind handler to router ├── req.js # sails.request object ├── res.js # Ensure that response object has a minimum set of reasonable defaults Used primarily as a test fixture. ├── ... # default handler config └── index.js啟動
從 app.js 開始
... sails = require("sails")
第一句 require 創建了一個新的 Sails() (sails/lib/Sails.js) 對象。
Sails 初始化的時候,巴拉巴拉綁定了一堆模塊/函數,并且繼承了 events.EventEmitter ,加載過程中使用 emit/on 來執行加載后的動作。
.lift之后 lift 啟動(其他啟動參數也最終都會調用到 lift):
... sails.lift(rc("sails")); # rc 讀取 .sailsrc 文件
sails/lib/lift.js 對 Sails 執行加載啟動:
... async.series([ function(cb) { sails.load(configOverride, cb); }, sails.initialize ], function sailsReady(err, async_data){ ... # 這里就會打印 sails 那艘小船 }) ....load
方法位于 sails/lib/app/load.js ,按順序加載直到最后啟動 Sails :
... async.auto({ config: [Configuration.load], # 默認 config hooks: ["config", loadHooks], # 加載 hooks registry: ["hooks", # 每個 hook 的 middleware 綁定到 sails.middleware function populateRegistry(cb) { ... } ], router: ["registry", sails.router.load] # 綁定 express router }, ready__(cb)); ...loadHooks
loadHooks 會加載 sails/lib/hooks/ 下所有需要加載的模塊:
... async.series({ moduleloader: ..., userconfig: ..., userhooks: ..., // other hooks
其中 sails/lib/hooks/moduleloader/ 定義了加載其他各個模塊的位置、方法:
configure: function() { sails.config.appPath = sails.config.appPath ? path.resolve(sails.config.appPath) : process.cwd() // path of config/controllers/policies/... ... }, // function of how to load other hooks loadUserConfig/loadUserHooks/loadBlueprints
除了 userhooks 每個 hook 加載均有時間限制:
var timeoutInterval = (sails.config[hooks[id].configKey || id] && sails.config[hooks[id].configKey || id]._hookTimeout) || sails.config.hookTimeout || 20000;
加載其他模塊的時候使用的是 async.each ,所以實際加載 hooks 是有個順序的(可以通過后面的 silly 日志看到):
async.each(_.without(_.keys(hooks), "userconfig", "moduleloader", "userhooks")...) // 而默認 hooks 位于 sails/lib/app/configuration/default-hooks.js module.exports = { "moduleloader": true, "logger": true, "request": true, "orm": true, ... }
注意
userhooks(用于加載項目 api/hooks/ 文件下的模塊)的加載順序為第二,而此時其他模塊均未加載,如果此時要設置 sails[${name}] ,注意屬性名不要和 sails 其他模塊名相同。
hooks/http/ 會根據項目配置 config/http.js 來加載各個 express 中間件,默認加載:
www: ..., // use "serve-static" to cache .tmp/public session: ..., // use express-session favicon: ..., // favicon.ico startRequestTimer: ..., // just set req._startTime = new Date() cookieParser: ..., compress: ..., // use `compression` bodyParser: ..., // Default use `skipper` handleBodyParserError: ..., // Allow simulation of PUT and DELETE HTTP methods for user agents methodOverride: (function() {...})(), // By default, the express router middleware is installed towards the end. router: app.router, poweredBy: ..., // 404 and 500 middleware should be after `router`, `www`, and `favicon` 404: function handleUnmatchedRequest(req, res, next) {...}, 500: function handleError(err, req, res, next) {...}
并且注冊了 ready :
// sails/lib/hooks/http/initialize.js ... sails.on("ready", startServer); ... // sails/lib/hooks/http/start.js // startSever 啟動 express ... var liftTimeout = sails.config.liftTimeout || 4000; // 超時 sails.hooks.http.server.listen(sails.config.port...) ...
?
.initialize待所有 .load 執行完畢之后,開始執行 sails.config.bootstrap :
// sails/lib/app/private/bootstrap.js ... // 超時 var timeoutMs = sails.config.bootstrapTimeout || 2000; // run ... // sails/lib/app/private/initialize.js // afterBootstrap ... // 調用 startServer sails.emit("ready"); ...
如果把 log 級別設置到 silly ,啟動的時候就可以看到 hooks/router 的加載信息:
# load hooks verbose: logger hook loaded successfully. verbose: request hook loaded successfully. verbose: Loading the app"s models and adapters... verbose: Loading app models... verbose: Loading app adapters... verbose: responses hook loaded successfully. verbose: controllers hook loaded successfully. verbose: Loading policy modules from app... verbose: Finished loading policy middleware logic. verbose: policies hook loaded successfully. verbose: services hook loaded successfully. verbose: cors hook loaded successfully. verbose: session hook loaded successfully. verbose: http hook loaded successfully. verbose: Starting ORM... verbose: orm hook loaded successfully. verbose: Built-in hooks are ready. # 以下是 register verbose: Instantiating registry... # 以下是 router verbose: Loading router... silly: Binding route :: all /* (REQUEST HOOK: addMixins) # ready verbose: All hooks were loaded successfully. # 打印小船
以上就是 Sails.js 的啟動過程,最終的 http 請求都是通過 express 來處理。
Session看完源碼,來具體看看 session 的部分,定位到 sails/lib/hooks/session/index.js 與 sails/lib/hooks/http/middleware/defaults.js 。
可以看到, Sails 的 session 默認使用 express-session 的 MemoryStore 作為默認 store :
function MemoryStore() { Store.call(this) this.sessions = Object.create(null) }
內存妥妥的要爆好嗎!
然而項目大都使用 mysql/redis 作 session 存儲,并不存在使用 memory 的情況。
express-sessionexpress-session 改寫了 red.end (http.ServerResponse) ,并根據條件判斷是否 .touch 和 .save session,memory/mysql/redis 三個 session 中間件有不同的實現:
.touch | .save | |
---|---|---|
MemoryStore | √ | √ |
RedisStore | √ | √ |
MysqlStore | × | √ |
那么問題來了,如果 store.save 排隊阻塞了,那么大量的 req/res 就會駐留在內存當中,當流量持續到來時,node 進程占用的內存就會哐哐哐的往上蹭!
垃圾回收session 與 req/res 只是保持的內存占用,當被垃圾回收處理之后,這部分內存就會回落。
然而 v8 的垃圾回收觸發存在一個閾值,并且各個分代區都設置了默認大小,直接在 heap.cc 就能看到:
Heap::Heap() : ... // semispace_size_ should be a power of 2 and old_generation_size_ should // be a multiple of Page::kPageSize. reserved_semispace_size_(8 * (kPointerSize / 4) * MB), max_semi_space_size_(8 * (kPointerSize / 4) * MB), initial_semispace_size_(Page::kPageSize), target_semispace_size_(Page::kPageSize), max_old_generation_size_(700ul * (kPointerSize / 4) * MB), initial_old_generation_size_(max_old_generation_size_ / kInitalOldGenerationLimitFactor), old_generation_size_configured_(false), max_executable_size_(256ul * (kPointerSize / 4) * MB), ...
v8 的 GC 是 “全停頓”(stop-the-world),對這幾個幾個不同的堆區,使用不同的垃圾回收算法:
新生區:大多數對象被分配在這里。新生區是一個很小的區域,垃圾回收在這個區域非常頻繁,與其他區域相獨立。
老生指針區:這里包含大多數可能存在指向其他對象的指針的對象。大多數在新生區存活一段時間之后的對象都會被挪到這里。
老生數據區:這里存放只包含原始數據的對象(這些對象沒有指向其他對象的指針)。字符串、封箱的數字以及未封箱的雙精度數字數組,在新生區存活一段時間后會被移動到這里。
大對象區:這里存放體積超越其他區大小的對象。每個對象有自己mmap產生的內存。垃圾回收器從不移動大對象。
代碼區:代碼對象,也就是包含JIT之后指令的對象,會被分配到這里。這是唯一擁有執行權限的內存區(不過如果代碼對象因過大而放在大對象區,則該大對象所對應的內存也是可執行的。譯注:但是大對象內存區本身不是可執行的內存區)。
Cell區、屬性Cell區、Map區:這些區域存放Cell、屬性Cell和Map,每個區域因為都是存放相同大小的元素,因此內存結構很簡單。
對于新生代快速 gc,而老生代則使用 Mark-Sweep(標記清除)和 Mark-Compact(標記整理),所以老生代的內存回收并不實時,在持續的訪問壓力下,老生代的占用會持續增長,并且垃圾內存并沒有立刻回收,所以整個 node 進程的內存占用也會蹭蹭的漲。
具體的垃圾回收詳解可以參加 這里 或者是 中文版 。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/79126.html
摘要:特別是內存,它將強烈的影響區塊鏈的運行速度,過小會造成區塊鏈網絡的嚴重擁堵。伴隨著區塊鏈對當今社會的逐步滲透,當達到一定的臨界點之后,這種影響將會是驚人的,我們拭目以待。 作者介紹:張其中,中科院碩士,連續創業者,樂家app創始人,花貓快問聯合創始人,鏈寶科技聯合創始人,關注EOS公鏈生態發展,致力于基于EOS的DAPP應用實踐與產品研究。 最近EOS又刷眼球了。讓EOS刷眼球的是EO...
摘要:一需求場景最近閑來無事,提出了一個要求,研究相關代碼并完成一個關于編輯圖片功能的性能優化,該功能的主要界面展示如下通過了幾分鐘的短暫試用,發現就是一個簡單的裁剪并保存用戶選擇并上傳的圖片作為用戶頭像的功能。一、需求場景: 最近閑來無事,boss提出了一個要求,研究相關代碼并完成一個關于編輯圖片功能的性能優化,該功能的主要界面展示如下: 通過了幾分鐘的短暫試用,發現就是一個簡單的裁剪并保存用...
摘要:作為語言的核心,存在于源碼目錄中的子目錄。年,和決定重寫代碼以解決這兩個問題。最終他倆把該項技術的核心引擎命名為,的意思即為。語法分析語法檢查。執行引擎執行這些。核心核心由兩部分組成和。 Zend Engine 作為 PHP 語言的核心, Zend Engine 存在于 PHP 源碼目錄中的 Zend 子目錄。 Why Zend Engine ? PHP3 采用的是邊解釋、邊...
摘要:基礎布局的中主要為部分,分別是用于搜索篩選和分頁的表單控件用于排序表格的表頭以及用于展示數據的。這也是前瞻發布之后,提出廢棄部分功能后許多人反應較為強烈的原因。 與上周的第一篇實踐教程一樣,在這篇文章中,我將繼續從一種常見的功能——表格入手,展示Vue.js中的一些優雅特性。同時也將對filter功能與computed屬性進行對比,說明各自的適用場景,也為vue2.0版本中即將刪除的部...
閱讀 2473·2021-11-24 09:39
閱讀 3406·2021-11-15 11:37
閱讀 2251·2021-10-08 10:04
閱讀 3965·2021-09-09 11:54
閱讀 1883·2021-08-18 10:24
閱讀 1034·2019-08-30 11:02
閱讀 1793·2019-08-29 18:45
閱讀 1651·2019-08-29 16:33