Sails.jsメモリの暴騰とソース分析


Sails.jsはnodeの優れたMVCフレームですが、Sailsを使って流量が増えると、nodeプロセスが突然メモリが暴騰したり、高占有率を維持したりします.ソースコードを調べてみたら、この問題はセッション/GCと関係があります.
PS:メモリリークによるものであれば、コードを慎重にチェックし、変数を正常に回収することができます.
くりを一つあげる
新しいsailsアプリ:
# 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 + '\t'
    + mem.heapUsed / 1024 / 1024 + '\t' + mem.heapTotal / 1024 / 1024 + '
', 'utf8'); }, 1000); // Snapshot every second
pm 2を使ってsailsを起動します.
> pm2 start app.js
> pm2 monit
圧力測定ツールを使用して、10 W要求、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
{
    "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のソースコードの構造はかなりはっきりしています.
[email protected]
├── 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')
第1の文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));
...
loadHooksloadHooksは、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は順序があります.
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,
  ...
}
注意
  • silly(プロジェクトuserhooksファイルの下にあるモジュールをロードするための)のロード順序は第二であるが、他のモジュールはロードされていない.この場合、api/hooks/を設定する場合、属性名はsails[${name}]の他のモジュール名と同じではないことに注意する.
  • sailsは、各hooks/http/のミドルウェアをプロジェクト構成config/http.jsに従ってロードし、デフォルトローディング:
    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) {...}
  • express:
    // 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...)
    ...
  • が登録されています.
    readyすべての.initializeが実行された後、実行を開始する.load:
    // sails/lib/app/private/bootstrap.js
    ...
    //   
    var timeoutMs = sails.config.bootstrapTimeout || 2000;
    // run
    ...
    
    // sails/lib/app/private/initialize.js
    // afterBootstrap
    ...
    //    startServer
    sails.emit('ready');
    ...
    ロゴレベルをsails.config.bootstrapに設定すると、起動時にsillyのローディング情報が表示されます.
    # 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の起動プロセスであり、最終的なhooks/routerの要求はhttpによって処理される.
    セッション
    ソースコードを読み終えて、具体的にexpressの部分を見にきて、sessionsails/lib/hooks/session/index.jsに位置します.
    Sailsのsails/lib/hooks/http/middleware/defaults.jsは、デフォルトsessionexpress-sessionを使用していることがわかる.
    function MemoryStore() {
      Store.call(this)
      this.sessions = Object.create(null)
    }
    メモリがちゃんとしています.爆発しますか?
    しかし、プロジェクトの多くはMemoryStoreをsessionとして記憶しており、storeを使用することはない.mysql/redismemoryexpress-sessionを書き換え、条件によってexpress-sessionred.end (http.ServerResponse)セッションがあるかどうかを判断し、.touchの3つのsessionの中間部品は異なる実装がある:.savememory/mysql/redisメモリストア


    Redis Store


    Mysql Store
    ×

    問題が来ました.もし.touchが列に並んで閉塞したら、大量の.saveはメモリの中に存在します.流量が持続的に到来すると、store.saveプロセスによって占有されたメモリはガタガタと上にこすります.
    ごみの回収req/resnodeは保持しているメモリだけが占有され、ゴミ回収処理された後、この部分のメモリは反落します.
    しかし、v 8のゴミ回収トリガには閾値があり、各サブエリアにはデフォルトサイズが設定されています.直接にhep.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),
          ...
    v 8のGCは「全休止」(stop-the-world)です.これらのいくつかの異なるヒープエリアに対して、異なるゴミ回収アルゴリズムを使います.
  • 新生区:多くの対象がここに割り当てられています.新生区は小さい地域です.ゴミの回収はこの地域でとても頻繁で、他の地域と独立しています.
  • 高齢者用のポインタエリア:ここには他のオブジェクトを指す可能性のあるほとんどのオブジェクトが含まれています.多くの新生区でしばらく生存した後の対象はここに移されます.
  • 旧データエリア:ここには元のデータだけを含むオブジェクトが格納されている(これらのオブジェクトは他のオブジェクトを指すポインタがない).文字列、封箱の数字と、封されていない箱の二重精度の数字配列は、新生エリアでしばらく生存した後にここに移動されます.
  • オブジェクトエリア:ここには他のエリアの大きさを超えるオブジェクトが格納されています.各オブジェクトには、自分のmmapから発生したメモリがあります.ゴミ回収器は大きなオブジェクトを移動しません.
  • コードエリア:コードオブジェクト、つまりJIT以降のコマンドを含むオブジェクトは、ここに割り当てられます.これは実行権限を持つ唯一のメモリエリアです.ただし、コードオブジェクトが大きすぎて対象エリアに置かれている場合には、その大きいオブジェクトに対応するメモリも実行可能です.
  • Cellエリア、属性Cellエリア、Mapエリア:これらの領域はCell、属性Cell、Mapを保存しています.各領域は同じサイズの要素を保存しているので、メモリ構造は簡単です.
  • 新生代の高速gcに対しては、旧世代はMark-SweepとMark-Comppactを使用していますので、老生世代のメモリ回収はリアルタイムではなく、継続的なアクセス圧力の下で、老生世代の占有率は持続的に増加しています.また、ゴミメモリはすぐに回収されていません.
    具体的なゴミ回収はここか中国語版に参加できます.