Sails.js 是 node 下的一個優秀的 MVC 框架,可是使用 Sails 後,在流量增加時, node 進程有時忽然內存暴漲、保持高佔用。通過翻閱源碼後,發現這個問題與 session / GC 都有關係。javascript
PS: 若是是內存泄露引發的,則須要細心檢查代碼,肯定變量能正常回收。html
新建一個 sails app :java
# new sails app memory > sails new memeory > cd memory
修改 config/bootstrap.js
增長內存快照,寫入一個 xls(方便畫圖):node
var fs = require('fs'); // (see note below) setInterval(function takeSnapshot() { var mem = process.memoryUsage(); fs.appendFile('./memorysnapshot.xls', mem.rss / 1024 / 1024 + '\t' + mem.heapUsed / 1024 / 1024 + '\t' + mem.heapTotal / 1024 / 1024 + '\n', 'utf8'); }, 1000); // Snapshot every second
使用 pm2 啓動 sailspython
> pm2 start app.js > pm2 monit
使用壓測工具,10W 請求,100 併發mysql
# ab 壓測工具 > ab -n 100000 -c 100 http://127.0.0.1:1337/
內存佔用喜人c++
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 { "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 作了什麼。git
sails的源碼結構至關清晰:github
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
開始redis
... 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
的部分,定位到 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-session
express-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 進程的內存佔用也會蹭蹭的漲。