Node.js GPS追踪器应用内存泄漏求助:V8堆快照分析
我们开发了一款用于GPS追踪器的Node.js Socket通信应用,核心功能为监听GPS追踪器的TCP连接、读取数据包、解析GPS数据并存储至数据库。应用运行初期内存占用约150MB,但数天后会飙升至10GB以上——并非持续增长,而是触发某事件后以每秒5MB的速度快速上涨。
已完成的排查工作
- 优化过事件处理逻辑
- 通过
snapshot指令生成V8堆快照,GenereFichierConnexionActive指令生成活跃连接文件 - 活跃连接文件显示仅约10个活跃连接,状态正常
- 对比应用启动1小时与内存异常时的堆快照,发现Chrome DevTools中Mapping构造函数实例新增2869个且无回收记录
我们并非堆分析专家,特此求助排查该内存泄漏的原因,或提供其他可能的问题方向。核心SocketServer类代码如下:
import dotenv from "dotenv" import ContextTrame from "../../context/ContextTrame"; import TeltonikaTrame from "../../trames/TeltonikaTrame"; import RedirectionTrame from "../../trames/RedirectionTrame"; import NS10Trame from "../../trames/NS10Trame"; import net, {Socket} from 'net'; import prisma from "../../lib/db"; import * as v8 from "v8"; import fs from 'fs'; dotenv.config(); const args = process.argv.slice(2); const portIndex = args.indexOf("-p"); let port = 3000; const activeConnection = new Map(); const TIMEOUT = 5 * 60 * 1000; // 5 minutes if (portIndex !== -1 && args[portIndex + 1]) { port = parseInt(args[portIndex + 1], 10); } class SocketServer { private static socketServer = net.createServer(SocketServer.onConnect); private PORT: number = port; public constructor() { SocketServer.socketServer.listen(this.PORT, () => { console.log(`socket server bound on port ${this.PORT}`); }); } /** * Méthode appelée lors de la connexion d'un client-backup. * @param socket Socket du client-backup. */ public static onConnect(socket: Socket): void { console.log('a user connected : ' + socket.remoteAddress + ':' + socket.remotePort); socket.setTimeout(TIMEOUT); socket.on('data', (data) => SocketServer.onData(socket, data)); socket.on('error', (err) => SocketServer.onError(socket, err)); socket.on('close', (hadError) => SocketServer.onClose(socket,hadError)); socket.on('end', () => SocketServer.onEnd(socket)); socket.on('timeout', () => SocketServer.onTimeout(socket)); activeConnection.set(socket,{ lastActiveTime: Date.now() }); } /** * Méthode appelée lors de la réception de données. * @param socket Socket du client-backup. * @param data Données reçues. */ private static onData(socket: any, data: Buffer): void { // On maj la dateheure d'activité activeConnection.get(socket).lastActiveTime = Date.now(); // Sanapshot de la mémoire ram avec V8 if(Buffer.from(data).toString() === 'snapshot') { console.log('snapshot received'); // ! Comments v8.writeHeapSnapshot(`./snapshots/heap_snapshot-${Date.now()}.heapsnapshot`); } // Si réception de la commande de snapshot. if(Buffer.from(data).toString() === 'GenereFichierConnexionActive') { this.generateActiveConnectionsFile(); } const contextTrame = new ContextTrame(); // Si la trame est un IMEI. if(Buffer.from(data).toString('hex').substring(0, 4) === '000f') { console.log('IMEI received'); // ! Comments socket.imei = Buffer.from(data).toString('ascii').substring(2); socket.write(Buffer.from('01', 'hex')); } else { // Si la trame est une trame Teltonika. if (socket.imei !== undefined) { console.log('Teltonika trame received'); // ! Comments contextTrame.setStrategy(new TeltonikaTrame(socket, data)); } else { const hexString = Buffer.from(data).toString('hex').toUpperCase(); const last12Characters = hexString.substring(hexString.length - 12); // Si la trame est une trame de redirection. if (last12Characters === '0D0A0D0A0D0A') { console.log('Redirection trame received'); // ! Comments contextTrame.setStrategy(new RedirectionTrame(data)); } else { // Si la trame est une trame NS10. if (parseInt( Buffer.from(data).toString('ascii').substring(0, 1)) == 2) { console.log('NS10 trame received'); // ! Comments contextTrame.setStrategy(new NS10Trame(data)); } } } } contextTrame.isStrategySet() && contextTrame.cut().then((res: boolean) => { if (res) { console.log('trame cuted'); // ! Comments contextTrame.insert().then((res: boolean) => { if (res) { console.log('trame inserted'); // ! Comments contextTrame.redirect(); } else { console.log('trame not inserted'); // ! Comments } }); } else { console.log('trame not cuted'); // ! Comments } }); } /** * Méthode appelée lors d'une erreur. * @param socket Socket du client-backup. * @param err Erreur. */ private static onError(socket: any, err): void { console.log(err); socket.destroy(); // On supprime le socket pour être sur qu'il soit bien fermé } private static onClose(socket: any, hadError: boolean): void { activeConnection.delete(socket); } private static onEnd(socket: any): void { console.log("Client à fermer la connection : "+socket.address()); socket.destroy(); // On supprime le socket pour être sur qu'il soit bien fermé } private static onTimeout(socket: any): void { console.log("Socket tiemout : "+socket.address()); socket.destroy(); // On supprime le socket pour être sur qu'il soit bien fermé } /** * Méthode appelée lors de l'envoi de données. * @param socket Socket du client-backup. * @param message Données à envoyer. */ public write(socket: any, message: Buffer): void { socket.write(message); } // Function to generate text file containing active connections private static generateActiveConnectionsFile(): void { const filename = './snapshots/connexionActive.txt'; let fileContent = ''; activeConnection.forEach((data, socket) => { fileContent += `Socket: ${socket.remoteAddress}:${socket.remotePort}`; fileContent += ` --> Dernière data reçu : ${new Date(data.lastActiveTime).toLocaleString()}\n`; }); fs.writeFile(filename, fileContent, err => { if (err) { console.error('Erreur pendant la création du fichier des connexions actives:', err); } else { console.log('Fichier des connexions actives généré :', filename); } }); } } export { SocketServer };
排查方向建议
追踪
Mapping实例的保留引用
在Chrome DevTools堆快照中,选中这些Mapping实例,查看保留路径(Retainers),定位持有它们引用的对象——通常是全局变量、闭包或长期存在的容器(如Map/Set)阻止了垃圾回收。检查策略类的资源泄漏
每次接收数据都会创建ContextTrame及对应策略类实例,需确认:- 策略类是否持有Socket引用,且Socket关闭后未释放
- 是否创建了定时器、事件监听器但未在资源释放时移除
- 数据库交互(如
insert())是否存在未处理的Promise,导致上下文被长期持有
Socket连接的引用清理
- 虽然
onClose中会从activeConnection删除Socket,但需确认socket.destroy()是否彻底清理了所有事件监听器,可补充socket.removeAllListeners()避免残留监听器持有引用 - 检查策略类或其他模块是否额外持有Socket引用,导致Socket实例无法被GC回收,进而
activeConnection条目无法彻底清理
- 虽然
修复Promise链的异常处理
当前contextTrame.cut().then(...)嵌套Promise未添加catch块,未处理的reject会导致上下文保留。建议添加异常捕获:contextTrame.isStrategySet() && contextTrame.cut() .then((res: boolean) => { // 原有逻辑 }) .catch(err => { console.error('Trame处理异常:', err); });检查Buffer的长期缓存
代码中多次调用Buffer.from(data),若策略类中缓存了这些Buffer且未及时释放,会导致内存增长。需检查各Trame类是否存在不必要的Buffer长期存储。验证Prisma连接池配置
确认Prisma客户端是否存在连接泄漏,比如闲置连接未被回收、批量查询积累了大量未处理实例。可调整连接池的idleTimeout等参数,确保闲置资源被及时释放。
内容的提问来源于stack exchange,提问作者SCR Informatiques

