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 + '\t'
+ mem.heapUsed / 1024 / 1024 + '\t' + mem.heapTotal / 1024 / 1024 + '\n', '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-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 进程的内存占用也会蹭蹭的涨。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。