资讯专栏INFORMATION COLUMN

Sails.js 内存暴涨 & 源码分析

antz / 1595人阅读

摘要:是下的一个优秀的框架,但是使用后,在流量增长时,进程有时突然内存暴涨保持高占用。如果是内存泄露引起的,则需要细心检查代码,确定变量能正常回收。每个对象有自己产生的内存。译注但是大对象内存区本身不是可执行的内存区。

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.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 进程的内存占用也会蹭蹭的涨。

具体的垃圾回收详解可以参加 这里 或者是 中文版 。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79126.html

相关文章

  • 【张其中】两周暴涨54倍的EOS内存,背后的Dapp是如何进行产品设计的?

    摘要:特别是内存,它将强烈的影响区块链的运行速度,过小会造成区块链网络的严重拥堵。伴随着区块链对当今社会的逐步渗透,当达到一定的临界点之后,这种影响将会是惊人的,我们拭目以待。 作者介绍:张其中,中科院硕士,连续创业者,乐家app创始人,花猫快问联合创始人,链宝科技联合创始人,关注EOS公链生态发展,致力于基于EOS的DAPP应用实践与产品研究。 最近EOS又刷眼球了。让EOS刷眼球的是EO...

    cpupro 评论0 收藏0
  • 巧用css的border属性完成对图片编辑功能的性能优化

    摘要:一需求场景最近闲来无事,提出了一个要求,研究相关代码并完成一个关于编辑图片功能的性能优化,该功能的主要界面展示如下通过了几分钟的短暂试用,发现就是一个简单的裁剪并保存用户选择并上传的图片作为用户头像的功能。一、需求场景: 最近闲来无事,boss提出了一个要求,研究相关代码并完成一个关于编辑图片功能的性能优化,该功能的主要界面展示如下: 通过了几分钟的短暂试用,发现就是一个简单的裁剪并保存用...

    番茄西红柿 评论0 收藏0
  • Zend Engine & PHP

    摘要:作为语言的核心,存在于源码目录中的子目录。年,和决定重写代码以解决这两个问题。最终他俩把该项技术的核心引擎命名为,的意思即为。语法分析语法检查。执行引擎执行这些。核心核心由两部分组成和。 Zend Engine 作为 PHP 语言的核心, Zend Engine 存在于 PHP 源码目录中的 Zend 子目录。 Why Zend Engine ? PHP3 采用的是边解释、边...

    GraphQuery 评论0 收藏0
  • Vue.js 实践(2):实现多条件筛选、搜索、排序及分页的表格功能

    摘要:基础布局的中主要为部分,分别是用于搜索筛选和分页的表单控件用于排序表格的表头以及用于展示数据的。这也是前瞻发布之后,提出废弃部分功能后许多人反应较为强烈的原因。 与上周的第一篇实践教程一样,在这篇文章中,我将继续从一种常见的功能——表格入手,展示Vue.js中的一些优雅特性。同时也将对filter功能与computed属性进行对比,说明各自的适用场景,也为vue2.0版本中即将删除的部...

    Profeel 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<