Cet article vous amènera à explorer l'allocation de mémoire du tas dans Node et à acquérir une compréhension approfondie des limitations de mémoire dans Node.js. J'espère qu'il vous sera utile !
Dans cet article, j'explorerai l'allocation de mémoire du tas dans Node, puis j'essaierai d'augmenter la mémoire jusqu'à la limite que le matériel peut supporter. Nous trouverons ensuite quelques moyens pratiques de surveiller les processus de Node afin de déboguer les problèmes liés à la mémoire.
OK, préparez-vous et partez !
Vous pouvez extraire le code correspondant dans l'entrepôt et cloner le code depuis mon GitHub :
https://github.com/beautifulcoder/node-memory-limitations
Premier , une brève introduction Découvrez le garbage collector V8. La méthode d'allocation de stockage de la mémoire est le tas, qui est divisé en plusieurs zones générationnelles. Un objet change d’âge au cours de son cycle de vie, et la génération à laquelle il appartient change également.
Les générations sont divisées en la jeune génération et l'ancienne génération, et la jeune génération est également divisée en la nouvelle génération et la génération intermédiaire. À mesure que les objets survivent au ramassage des déchets, ils rejoignent également l’ancienne génération.
Le principe de base de l'hypothèse générationnelle est que la plupart des sujets sont jeunes. Le garbage collector V8 s'appuie sur cela en promouvant uniquement les objets qui survivent à un garbage collection. À mesure que les objets sont copiés dans des régions adjacentes, ils finissent par se retrouver dans l'ancienne génération.
La consommation de mémoire dans Nodejs est principalement divisée en trois aspects :
La mémoire tas est notre objectif principal aujourd'hui. Maintenant que vous en savez un peu plus sur le garbage collector, il est temps d'allouer de la mémoire sur le tas !
function allocateMemory(size) { // Simulate allocation of bytes const numbers = size / 8; const arr = []; arr.length = numbers; for (let i = 0; i < numbers; i++) { arr[i] = i; } return arr; }
Dans la pile d'appels, les variables locales sont détruites à la fin de l'appel de fonction. Le type sous-jacent number
ne va jamais dans la mémoire tas, mais est alloué dans la pile d'appels. Mais l'objet arr ira dans le tas et pourra survivre au garbage collection. number
永远不会进入堆内存,而是在调用栈中分配。但是对象arr将进入堆中并且可能在垃圾回收中幸存下来。
现在进行勇敢测试——将 Node 进程推到极限看看在哪个地方会耗尽堆内存:
const memoryLeakAllocations = []; const field = "heapUsed"; const allocationStep = 10000 * 1024; // 10MB const TIME_INTERVAL_IN_MSEC = 40; setInterval(() => { const allocation = allocateMemory(allocationStep); memoryLeakAllocations.push(allocation); const mu = process.memoryUsage(); // # bytes / KB / MB / GB const gbNow = mu[field] / 1024 / 1024 / 1024; const gbRounded = Math.round(gbNow * 100) / 100; console.log(`Heap allocated ${gbRounded} GB`); }, TIME_INTERVAL_IN_MSEC);
在上面的代码中,我们以 40 毫秒的间隔分配了大约 10 mb,为垃圾回收提供了足够的时间来将幸存的对象提升到老年代。process.memoryUsage
Maintenant, pour un test courageux : poussez le processus Node jusqu'à la limite et voyez où il manque de mémoire tas :
Heap allocated 4 GB Heap allocated 4.01 GB <--- Last few GCs ---> [18820:000001A45B4680A0] 26146 ms: Mark-sweep (reduce) 4103.7 (4107.3) -> 4103.7 (4108.3) MB, 1196.5 / 0.0 ms (average mu = 0.112, current mu = 0.000) last resort GC in old space requested <--- JS stacktrace ---> FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
Dans le code ci-dessus, nous avons alloué environ 10 Mo à des intervalles de 40 ms pour le garbage collection. Un temps suffisant est fourni promouvoir les objets survivants auprès de l’ancienne génération. process.memoryUsage
est un outil permettant de recycler un indicateur approximatif de l'utilisation du tas. À mesure que l'allocation du tas augmente, le champ heapUsed enregistre la taille du tas. Ce champ enregistre le nombre d'octets dans la RAM et peut être converti en Mo.
Vos résultats peuvent varier. Sur un ordinateur portable Windows 10 avec 32 Go de RAM, vous obtiendrez le résultat suivant :
node index.js --max-old-space-size=8000
Au moment de la rédaction, le code ci-dessus s'exécute sous la dernière version de LTS Node et utilise un exécutable 64 bits. En théorie, un processus 64 bits devrait pouvoir allouer plus de 4 Go d'espace et atteindre facilement 16 To d'espace d'adressage.
Agrandir la limite d'allocation de mémoireHeap allocated 7.8 GB Heap allocated 7.81 GB <--- Last few GCs ---> [16976:000001ACB8FEB330] 45701 ms: Mark-sweep (reduce) 8000.2 (8005.3) -> 8000.2 (8006.3) MB, 1468.4 / 0.0 ms (average mu = 0.211, current mu = 0.000) last resort GC in old space requested <--- JS stacktrace ---> FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
Cela définit la limite maximale à 8 Go. Soyez prudent lorsque vous faites cela. Mon ordinateur portable dispose de 32 Go d'espace. Je recommande de le régler sur la quantité d'espace réellement disponible dans la RAM. Une fois la mémoire physique épuisée, les processus commencent à occuper de l'espace disque via la mémoire virtuelle. Si vous définissez la limite trop élevée, vous obtiendrez une nouvelle raison de changer d'ordinateur. Ici, nous essayons d'éviter que l'ordinateur ne fume ~
Exécutons à nouveau le code avec la limite de 8 Go :
const path = require("path"); const fs = require("fs"); const os = require("os"); const start = Date.now(); const LOG_FILE = path.join(__dirname, "memory-usage.csv"); fs.writeFile(LOG_FILE, "Time Alive (secs),Memory GB" + os.EOL, () => {}); // 请求-确认
process.memoryUsage 的 heapUsed 字段还是有点用的,调试内存泄漏的一个方法是将内存指标放在另一个工具中以进行进一步处理。由于此实现并不复杂,因此主要解析下如何亲自实现。
const path = require("path"); const fs = require("fs"); const os = require("os"); const start = Date.now(); const LOG_FILE = path.join(__dirname, "memory-usage.csv"); fs.writeFile(LOG_FILE, "Time Alive (secs),Memory GB" + os.EOL, () => {}); // 请求-确认
为了避免将堆分配指标放在内存中,我们选择将结果写入 CSV 文件以方便数据消耗。这里使用了 writeFile 带有回调的异步函数。回调为空以写入文件并继续,无需任何进一步处理。 要获取渐进式内存指标,请将其添加到 console.log:
const elapsedTimeInSecs = (Date.now() - start) / 1000; const timeRounded = Math.round(elapsedTimeInSecs * 100) / 100; s.appendFile(LOG_FILE, timeRounded + "," + gbRounded + os.EOL, () => {}); // 请求-确认
上面这段代码可以用来调试内存泄漏的情况下,堆内存随着时间变化而增长。你可以使用一些分析工具来解析原生csv数据以实现一个比较漂亮的可视化。
如果你只是赶着看看数据的情况,直接用excel也可以,如下图:
在限制为4.1GB的情况下,你可以看到内存的使用率在短时间内呈线性增长。内存的消耗在持续的增长并没有变得平缓,这个说明了某个地方存在内存泄漏。在我们调试这类问题的时候,我们要寻找在分配在老世代结束时的那部分代码。
对象如果再在垃圾回收时幸存下来,就可能会一直存在,直到进程终止。
使用这段内存泄漏检测代码更具复用性的一种方法是将其包装在自己的时间间隔内(因为它不必存在于主循环中)。
setInterval(() => { const mu = process.memoryUsage(); // # bytes / KB / MB / GB const gbNow = mu[field] / 1024 / 1024 / 1024; const gbRounded = Math.round(gbNow * 100) / 100; const elapsedTimeInSecs = (Date.now() - start) / 1000; const timeRounded = Math.round(elapsedTimeInSecs * 100) / 100; fs.appendFile(LOG_FILE, timeRounded + "," + gbRounded + os.EOL, () => {}); // fire-and-forget }, TIME_INTERVAL_IN_MSEC);
要注意上面这些方法并不能直接在生产环境中使用,仅仅只是告诉你如何在本地环境调试内存泄漏。在实际实现时还包括了自动显示、警报和轮换日志,这样服务器才不会耗尽磁盘空间。
尽管上面的代码在生产环境中不可行,但我们已经看到了如何去调试内存泄漏。因此,作为替代方案,可以将 Node 进程包裹在 PM2 之类 的 守护进程 中。
当内存消耗达到限制时设置重启策略:
pm2 start index.js --max-memory-restart 8G
单位可以是 K(千字节)、M(兆字节)和 G(千兆字节)。进程重启大约需要 30 秒,因此通过负载均衡器配置多个节点以避免中断。
另一个漂亮的工具是跨平台的原生模块node-memwatch,它在检测到运行代码中的内存泄漏时触发一个事件。
const memwatch = require("memwatch"); memwatch.on("leak", function (info) { // event emitted console.log(info.reason); });复制代码
事件通过leak触发,并且它的回调对象中有一个reason会随着连续垃圾回收的堆增长而增长。
使用 AppSignal 的 Magic Dashboard 诊断内存限制
AppSignal 有一个神奇的仪表板,用于监控堆增长的垃圾收集统计信息。
上图显示请求在 14:25 左右停止了 7 分钟,允许垃圾回收以减少内存压力。当对象在旧的空间中停留太久并导致内存泄漏时,仪表板也会暴露出来。
在这篇文章中,我们首先了解了 V8 垃圾回收器的作用,然后再探讨堆内存是否存在限制以及如何扩展内存分配限制。
最后,我们使用了一些潜在的工具来密切关注 Node.js 中的内存泄漏。我们看到内存分配的监控可以通过使用一些粗略的工具方法来实现,比如memoryUsage一些调试方法。在这里,分析仍然是手动实现的。
另一种选择是使用 AppSignal 等专业工具,它提供监控、警报和漂亮的可视化来实时诊断内存问题。
希望你喜欢这篇关于内存限制和诊断内存泄漏的快速介绍。
更多node相关知识,请访问:nodejs 教程!!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!