摘要:是下的一個(gè)優(yōu)秀的框架,但是使用后,在流量增長時(shí),進(jìn)程有時(shí)突然內(nèi)存暴漲保持高占用。如果是內(nèi)存泄露引起的,則需要細(xì)心檢查代碼,確定變量能正?;厥?。每個(gè)對象有自己產(chǎn)生的內(nèi)存。譯注但是大對象內(nèi)存區(qū)本身不是可執(zhí)行的內(nèi)存區(qū)。
Sails.js 是 node 下的一個(gè)優(yōu)秀的 MVC 框架,但是使用 Sails 后,在流量增長時(shí), node 進(jìn)程有時(shí)突然內(nèi)存暴漲、保持高占用。經(jīng)過翻閱源碼后,發(fā)現(xiàn)這個(gè)問題與 session / GC 都有關(guān)系。
PS: 如果是內(nèi)存泄露引起的,則需要細(xì)心檢查代碼,確定變量能正?;厥?。
舉個(gè)栗子新建一個(gè) sails app :
# new sails app memory > sails new memeory > cd memory
修改 config/bootstrap.js 增加內(nèi)存快照,寫入一個(gè) 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 啟動(dòng) sails
> pm2 start app.js > pm2 monit
使用壓測工具,10W 請求,100 并發(fā)
# ab 壓測工具 > ab -n 100000 -c 100 http://127.0.0.1:1337/
內(nèi)存占用喜人
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關(guān)閉 session
# 關(guān)閉 session { "hooks": { ... "session": false, ... } } # 壓測結(jié)果與之前并沒有什么區(qū)別 Requests per second: 381.06 [#/sec] (mean) # 但是內(nèi)存很穩(wěn)定,基本沒增加過 PM2 monitoring (To go further check out https://app.keymetrics.io) app [ ] 0 %%% [0] [fork_mode] [|||||||||||||| ] 162.609 MB
結(jié)果感人,關(guān)閉不必要的服務(wù)并沒有給訪問主頁帶來多大的性能提升,但是內(nèi)存占用下降了非常多,下面就翻翻源碼看看 Sails 做了什么。
Sails 做了什么 源碼sails的源碼結(jié)構(gòu)相當(dāng)清晰:
[email protected] ├── bin/ # sails command 處理 ├── errors/ # 定義啟動(dòng)加載錯(cuò)誤 └─┬ lib/ ├─┬ app/ │ ├── configuration/ # 加載各種參數(shù),補(bǔ)全默認(rèn)參數(shù) │ ├── private/ # 很多方法,最終都 bind 到 Sails │ ├── ... # other module, all bind to Sails │ ├── Sail.js # main entry │ └── index.js ├─┬ hook/ # 以下部分加載 sails 的相關(guān)配置 │ ├── 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啟動(dòng)
從 app.js 開始
... sails = require("sails")
第一句 require 創(chuàng)建了一個(gè)新的 Sails() (sails/lib/Sails.js) 對象。
Sails 初始化的時(shí)候,巴拉巴拉綁定了一堆模塊/函數(shù),并且繼承了 events.EventEmitter ,加載過程中使用 emit/on 來執(zhí)行加載后的動(dòng)作。
.lift之后 lift 啟動(dòng)(其他啟動(dòng)參數(shù)也最終都會(huì)調(diào)用到 lift):
... sails.lift(rc("sails")); # rc 讀取 .sailsrc 文件
sails/lib/lift.js 對 Sails 執(zhí)行加載啟動(dòng):
... async.series([ function(cb) { sails.load(configOverride, cb); }, sails.initialize ], function sailsReady(err, async_data){ ... # 這里就會(huì)打印 sails 那艘小船 }) ....load
方法位于 sails/lib/app/load.js ,按順序加載直到最后啟動(dòng) Sails :
... async.auto({ config: [Configuration.load], # 默認(rèn) config hooks: ["config", loadHooks], # 加載 hooks registry: ["hooks", # 每個(gè) hook 的 middleware 綁定到 sails.middleware function populateRegistry(cb) { ... } ], router: ["registry", sails.router.load] # 綁定 express router }, ready__(cb)); ...loadHooks
loadHooks 會(huì)加載 sails/lib/hooks/ 下所有需要加載的模塊:
... async.series({ moduleloader: ..., userconfig: ..., userhooks: ..., // other hooks
其中 sails/lib/hooks/moduleloader/ 定義了加載其他各個(gè)模塊的位置、方法:
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 每個(gè) hook 加載均有時(shí)間限制:
var timeoutInterval = (sails.config[hooks[id].configKey || id] && sails.config[hooks[id].configKey || id]._hookTimeout) || sails.config.hookTimeout || 20000;
加載其他模塊的時(shí)候使用的是 async.each ,所以實(shí)際加載 hooks 是有個(gè)順序的(可以通過后面的 silly 日志看到):
async.each(_.without(_.keys(hooks), "userconfig", "moduleloader", "userhooks")...) // 而默認(rèn) hooks 位于 sails/lib/app/configuration/default-hooks.js module.exports = { "moduleloader": true, "logger": true, "request": true, "orm": true, ... }
注意
userhooks(用于加載項(xiàng)目 api/hooks/ 文件下的模塊)的加載順序?yàn)榈诙?,而此時(shí)其他模塊均未加載,如果此時(shí)要設(shè)置 sails[${name}] ,注意屬性名不要和 sails 其他模塊名相同。
hooks/http/ 會(huì)根據(jù)項(xiàng)目配置 config/http.js 來加載各個(gè) express 中間件,默認(rèn)加載:
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 啟動(dòng) express ... var liftTimeout = sails.config.liftTimeout || 4000; // 超時(shí) sails.hooks.http.server.listen(sails.config.port...) ...
?
.initialize待所有 .load 執(zhí)行完畢之后,開始執(zhí)行 sails.config.bootstrap :
// sails/lib/app/private/bootstrap.js ... // 超時(shí) var timeoutMs = sails.config.bootstrapTimeout || 2000; // run ... // sails/lib/app/private/initialize.js // afterBootstrap ... // 調(diào)用 startServer sails.emit("ready"); ...
如果把 log 級(jí)別設(shè)置到 silly ,啟動(dòng)的時(shí)候就可以看到 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 的啟動(dòng)過程,最終的 http 請求都是通過 express 來處理。
Session看完源碼,來具體看看 session 的部分,定位到 sails/lib/hooks/session/index.js 與 sails/lib/hooks/http/middleware/defaults.js 。
可以看到, Sails 的 session 默認(rèn)使用 express-session 的 MemoryStore 作為默認(rèn) store :
function MemoryStore() { Store.call(this) this.sessions = Object.create(null) }
內(nèi)存妥妥的要爆好嗎!
然而項(xiàng)目大都使用 mysql/redis 作 session 存儲(chǔ),并不存在使用 memory 的情況。
express-sessionexpress-session 改寫了 red.end (http.ServerResponse) ,并根據(jù)條件判斷是否 .touch 和 .save session,memory/mysql/redis 三個(gè) session 中間件有不同的實(shí)現(xiàn):
.touch | .save | |
---|---|---|
MemoryStore | √ | √ |
RedisStore | √ | √ |
MysqlStore | × | √ |
那么問題來了,如果 store.save 排隊(duì)阻塞了,那么大量的 req/res 就會(huì)駐留在內(nèi)存當(dāng)中,當(dāng)流量持續(xù)到來時(shí),node 進(jìn)程占用的內(nèi)存就會(huì)哐哐哐的往上蹭!
垃圾回收session 與 req/res 只是保持的內(nèi)存占用,當(dāng)被垃圾回收處理之后,這部分內(nèi)存就會(huì)回落。
然而 v8 的垃圾回收觸發(fā)存在一個(gè)閾值,并且各個(gè)分代區(qū)都設(shè)置了默認(rèn)大小,直接在 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),對這幾個(gè)幾個(gè)不同的堆區(qū),使用不同的垃圾回收算法:
新生區(qū):大多數(shù)對象被分配在這里。新生區(qū)是一個(gè)很小的區(qū)域,垃圾回收在這個(gè)區(qū)域非常頻繁,與其他區(qū)域相獨(dú)立。
老生指針區(qū):這里包含大多數(shù)可能存在指向其他對象的指針的對象。大多數(shù)在新生區(qū)存活一段時(shí)間之后的對象都會(huì)被挪到這里。
老生數(shù)據(jù)區(qū):這里存放只包含原始數(shù)據(jù)的對象(這些對象沒有指向其他對象的指針)。字符串、封箱的數(shù)字以及未封箱的雙精度數(shù)字?jǐn)?shù)組,在新生區(qū)存活一段時(shí)間后會(huì)被移動(dòng)到這里。
大對象區(qū):這里存放體積超越其他區(qū)大小的對象。每個(gè)對象有自己mmap產(chǎn)生的內(nèi)存。垃圾回收器從不移動(dòng)大對象。
代碼區(qū):代碼對象,也就是包含JIT之后指令的對象,會(huì)被分配到這里。這是唯一擁有執(zhí)行權(quán)限的內(nèi)存區(qū)(不過如果代碼對象因過大而放在大對象區(qū),則該大對象所對應(yīng)的內(nèi)存也是可執(zhí)行的。譯注:但是大對象內(nèi)存區(qū)本身不是可執(zhí)行的內(nèi)存區(qū))。
Cell區(qū)、屬性Cell區(qū)、Map區(qū):這些區(qū)域存放Cell、屬性Cell和Map,每個(gè)區(qū)域因?yàn)槎际谴娣畔嗤笮〉脑?,因此?nèi)存結(jié)構(gòu)很簡單。
對于新生代快速 gc,而老生代則使用 Mark-Sweep(標(biāo)記清除)和 Mark-Compact(標(biāo)記整理),所以老生代的內(nèi)存回收并不實(shí)時(shí),在持續(xù)的訪問壓力下,老生代的占用會(huì)持續(xù)增長,并且垃圾內(nèi)存并沒有立刻回收,所以整個(gè) node 進(jìn)程的內(nèi)存占用也會(huì)蹭蹭的漲。
具體的垃圾回收詳解可以參加 這里 或者是 中文版 。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/79126.html
摘要:特別是內(nèi)存,它將強(qiáng)烈的影響區(qū)塊鏈的運(yùn)行速度,過小會(huì)造成區(qū)塊鏈網(wǎng)絡(luò)的嚴(yán)重?fù)矶?。伴隨著區(qū)塊鏈對當(dāng)今社會(huì)的逐步滲透,當(dāng)達(dá)到一定的臨界點(diǎn)之后,這種影響將會(huì)是驚人的,我們拭目以待。 作者介紹:張其中,中科院碩士,連續(xù)創(chuàng)業(yè)者,樂家app創(chuàng)始人,花貓快問聯(lián)合創(chuàng)始人,鏈寶科技聯(lián)合創(chuàng)始人,關(guān)注EOS公鏈生態(tài)發(fā)展,致力于基于EOS的DAPP應(yīng)用實(shí)踐與產(chǎn)品研究。 最近EOS又刷眼球了。讓EOS刷眼球的是EO...
摘要:一需求場景最近閑來無事,提出了一個(gè)要求,研究相關(guān)代碼并完成一個(gè)關(guān)于編輯圖片功能的性能優(yōu)化,該功能的主要界面展示如下通過了幾分鐘的短暫試用,發(fā)現(xiàn)就是一個(gè)簡單的裁剪并保存用戶選擇并上傳的圖片作為用戶頭像的功能。一、需求場景: 最近閑來無事,boss提出了一個(gè)要求,研究相關(guān)代碼并完成一個(gè)關(guān)于編輯圖片功能的性能優(yōu)化,該功能的主要界面展示如下: 通過了幾分鐘的短暫試用,發(fā)現(xiàn)就是一個(gè)簡單的裁剪并保存用...
摘要:作為語言的核心,存在于源碼目錄中的子目錄。年,和決定重寫代碼以解決這兩個(gè)問題。最終他倆把該項(xiàng)技術(shù)的核心引擎命名為,的意思即為。語法分析語法檢查。執(zhí)行引擎執(zhí)行這些。核心核心由兩部分組成和。 Zend Engine 作為 PHP 語言的核心, Zend Engine 存在于 PHP 源碼目錄中的 Zend 子目錄。 Why Zend Engine ? PHP3 采用的是邊解釋、邊...
摘要:基礎(chǔ)布局的中主要為部分,分別是用于搜索篩選和分頁的表單控件用于排序表格的表頭以及用于展示數(shù)據(jù)的。這也是前瞻發(fā)布之后,提出廢棄部分功能后許多人反應(yīng)較為強(qiáng)烈的原因。 與上周的第一篇實(shí)踐教程一樣,在這篇文章中,我將繼續(xù)從一種常見的功能——表格入手,展示Vue.js中的一些優(yōu)雅特性。同時(shí)也將對filter功能與computed屬性進(jìn)行對比,說明各自的適用場景,也為vue2.0版本中即將刪除的部...
閱讀 2496·2021-11-24 09:39
閱讀 3420·2021-11-15 11:37
閱讀 2270·2021-10-08 10:04
閱讀 3981·2021-09-09 11:54
閱讀 1894·2021-08-18 10:24
閱讀 1064·2019-08-30 11:02
閱讀 1808·2019-08-29 18:45
閱讀 1664·2019-08-29 16:33