此文章最初發布在IBM Developer。javascript
在基於容器的Node.js應用程序中管理內存的最佳實踐html
在docker容器中運行Node.js應用程序時,傳統的內存參數調整並不老是按預期工做。本文咱們將闡述在基於容器的Node.js應用程序內存參數調優中並不老是有效的緣由,並提供了在容器環境中使用Node.js應用程序時能夠遵循的建議和最佳實踐。java
當Node.js應用程序運行在設置了內存限制的容器中時(使用docker --memory
選項或者系統中的其餘任意標誌),請使用--max-old-space-size
選項以確保Node.js知道其內存限制而且設置其值小於容器限制。node
當Node.js應用程序在容器內運行時,將Node.js應用程序的峯值內存值設置爲容器的內存容量(假如容器內存能夠調整的話)。git
接下來讓咱們更詳細地探討一下。github
默認狀況下,容器是沒有資源限制的,可使用系統(OS)容許的儘量多的可用內存資源。可是docker 運行命令能夠指定選項,用於設置容器可使用的內存或CPU。docker
該docker-run
命令以下所示:docker run --memory <x><y> --interactive --tty <imagename> bash
shell
參數介紹:api
例如:docker run --memory 1000000b --interactive --tty <imagename> bash
將內存或CPU限制設置爲1,000,000字節安全
要檢查容器內的內存限制(以字節爲單位),請使用如下命令:
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
複製代碼
接下來咱們一塊兒來看下設置了--max_old_space_size
以後容器的各類表現。
「舊生代」是V8內存託管堆的公共堆部分(即JavaScript對象所在的位置),而且該--max-old-space-size
標誌控制其最大大小。有關更多信息,請參閱關於-max-old-space-size。
一般,當應用程序使用的內存多於容器內存時,應用程序將終止。
如下示例應用程序以10毫秒的間隔插入記錄到列表。這個快速的間隔使得堆無限制地增加,模擬內存泄漏。
'use strict';
const list = [];
setInterval(()=> {
const record = new MyRecord();
list.push(record);
},10);
function MyRecord() {
var x='hii';
this.name = x.repeat(10000000);
this.id = x.repeat(10000000);
this.account = x.repeat(10000000);
}
setInterval(()=> {
console.log(process.memoryUsage())
},100);
複製代碼
本文全部的示例程序均可以在我推入Docker Hub的Docker映像中得到。你也能夠拉取docker鏡像並運行程序。使用docker pull ravali1906/dockermemory
來獲取圖像。
或者,你能夠本身構建鏡像,並使用內存限制運行鏡像,以下所示:
docker run --memory 512m --interactive --tty ravali1906/dockermemory bash
複製代碼
ravali1906/dockermemory
是鏡像的名稱
接下來,運行內存大於容器限制的應用程序:
$ node --max_old_space_size=1024 test-fatal-error.js
{ rss: 550498304,
heapTotal: 1090719744,
heapUsed: 1030627104,
external: 8272 }
Killed
複製代碼
PS:
--max_old_space_size
取M爲單位的值process.memoryUsage()
以字節爲單位輸出內存使用狀況當內存使用率超過某個閾值時,應用程序終止。但這些閾值是多少?有什麼限制?咱們來看一下約束。
默認狀況下,Node.js(適用於11.x版本及如下)在32位和64位平臺上使用最大堆大小分別爲700MB和1400MB。對於當前默認值,請參閱博客末尾參考文章。
所以,理論上,當設置--max-old-space-size內存限制大於容器內存時,指望應用程序應直接被OOM(Out Of Memory)終止。
實際上,這可能不會發生。
並不是全部經過--max-old-space-size指定的內存的容量均可以提早分配給應用程序。
相反,爲了響應不斷增加的需求,JavaScript內存堆是逐漸增加的。
應用程序使用的實際內存(以JavaScript堆中的對象的形式)能夠在process.memoryUsage()
API中的heapUsed
字段看到。
所以,如今修改後的指望是,若是實際堆大小(駐留對象大小)超過OOM-KILLER閾值(--memory容器中的標誌),則容器終止應用程序。
實際上,這也可能不會發生。
當我在容器受限的環境下分析內存密集型Node.js應用程序時,我看到兩種狀況:
監控容器中運行應用程序的重要指標是駐留集大小(RSS-resident set size)。
它屬於應用程序虛擬內存的一部分。
或者說,它表明應用程序被分配的內存的一部分。
更進一步說,它表示應用程序分配的內存中當前處於活動狀態的部分。
並不是應用程序中的全部已分配內存都屬於活動狀態,這是由於「分配的內存」只有在進程實際開始使用它時纔會真實分配。另外,爲了響應其餘進程的內存需求,系統可能swap out
當前進程中處於非活動或休眠狀態的內存給其餘進程,後續若是當前進程須要的時候經過swapped in
從新分配回來。
RSS反映了應用程序的可用和活動的內存量。
如下buffer_example.js
爲往內存分配空Buffer對象的實例代碼:
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024)
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
複製代碼
運行docker映像並限制其內存用量:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash
複製代碼
運行該應用程序。你會看到如下內容:
$ node buffer_example 2000
2000
16
複製代碼
即便內存大於容器限制,應用程序也不會終止。這是由於分配的內存還未被徹底訪問。rss值很是低,而且沒有超過容器內存限制。
如下爲往內存分配Buffer對象並填滿值的實例代碼:
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x')
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
複製代碼
運行docker映像並限制其內存用量:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash
複製代碼
運行該應用程序
$ node buffer_example_fill.js 2000
2000
984
複製代碼
即便在這裏應用也沒有被終止!爲何?當活動內存達到容器設置限制時,而且swap space
還有空間時,一些舊內存片斷將被推送到swap space
並可供同一進程使用。默認狀況下,docker分配的交換空間量等於經過--memory標誌設置的內存限制。有了這種機制,這個進程幾乎可使用2GB內存 - 1GB活動內存和1GB交換空間。簡而言之,因爲內存的交換機制,rss仍然在容器強制限制範圍內,而且應用程序可以持續運行。
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x')
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
複製代碼
運行鏡像時限制docker內存,交換空間和關閉匿名頁面交換,以下所示:
docker run --memory 1024m --memory-swap=1024m --memory-swappiness=0 --interactive --tty ravali1906/dockermemory bash
複製代碼
$ node buffer_example_fill.js 2000
Killed
複製代碼
當--memory-swap
的值等於--memory
的值時,它表示容器不使用任何額外的交換空間。此外,默認狀況下,容器的內核能夠交換出必定比例的匿名頁,所以將--memory-swappiness
設置爲0以禁用它。所以,因爲容器內沒有發生交換,rss超出了容器限制,在正確的時間終止了進程。
當您運行Node.js應用程序並將其--max-old-space-size設置爲大於容器限制時,看起來Node.js可能不會「尊重」容器強制限制。但正如您在上面的示例中看到的,緣由是應用程序可能沒法使用標誌訪問JavaScript堆集的全長。
請記住,當您使用的內存多於容器中可用的內存時,沒法保證應用定期望行爲方式運行。爲何?由於進程的活動內存(rss)受到許多因素的影響,這些因素超出了應用程序的控制範圍,而且可能依賴於高負載和環境 - 例如工做負載自己,系統中的併發級別,操做系統調度程序,垃圾收集率等。此外,這些因素能夠在運行之間發生變化。
top
命令和process.memoryUsage()
API獲得最高值。若是在容器環境下運行,Node.js 12.x的堆內存限制根據當前可用內存進行配置,而不是使用默認值。對於設置了max_old_space_size
的場景,上面的建議仍然適用。此外,瞭解相關限制可讓您更好地調整應用併發揮應用的性能,由於默認值是相對保守的。
有關更多信息,請參閱配置默認堆轉儲。