Sails.js 內存暴漲 & 源碼分析

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

all

關閉 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

none

結果感人,關閉沒必要要的服務並無給訪問主頁帶來多大的性能提高,可是內存佔用降低了很是多,下面就翻翻源碼看看 Sails 作了什麼。git

Sails 作了什麼

源碼

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

看完源碼,來具體看看 session 的部分,定位到 sails/lib/hooks/session/index.jssails/lib/hooks/http/middleware/defaults.js

能夠看到, Sails 的 session 默認使用 express-sessionMemoryStore 做爲默認 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 進程佔用的內存就會哐哐哐的往上蹭!

垃圾回收

sessionreq/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 進程的內存佔用也會蹭蹭的漲。

具體的垃圾回收詳解能夠參加 這裏 或者是 中文版

相關文章
相關標籤/搜索