You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

高负载tfjs与discord.js应用慢内存泄漏排查及解决方案

背景
  • 为满足业务流量需求,该应用的事件函数(存放于listener.js)每秒执行约14次
  • listener.js:存储事件处理函数的文件
  • handler.js:负责处理listener.js触发事件的文件
  • sharder.js:实现应用分片能力的启动文件
  • index.js:sharder.js启动每个分片时执行的入口文件
  • gc.js:用于手动调用垃圾回收器的文件(该方案经测试无效,为社区推荐方案)
运行环境与依赖版本
  • Node.js v16.13.1
  • discord.js v13.6.0
  • @tensorflow/tfjs v3.14.0
  • @tensorflow/tfjs-node v3.14.0
问题现象

机器人所有分片上线后即可检测到内存泄漏,泄漏速度缓慢但可明确观测:宿主机配备64GB内存的情况下,仍需每日重启Node进程才能维持服务。目前所有张量均已按规范释放(张量数量稳定为263,原因是推理模型在事件监听器外部加载,未做释放处理)。已配置专门的事件监听器用于手动触发垃圾回收,但未产生实际效果;在listener.js中已尝试将所有可手动处理的变量赋值为null,不确定该操作是否能起到内存回收作用。

咨询问题

当前代码实现中是否存在被忽略的内存泄漏诱发点?对应的可行修复方案有哪些?

项目相关代码

listener.js

const { Readable } = require('stream');
const PImage = require('pureimage');
const tf = require(`@tensorflow/tfjs`)
const tfnode = require('@tensorflow/tfjs-node');

let nameArr = [
  // array of names here
]

let bufferToStream = (binary) => {
  let readableInstanceStream = new Readable({
    read() {
      this.push(binary);
      this.push(null);
    }
  });
  return readableInstanceStream;
}

const predict = async (imageUrl, model) => {

  let data = await fetch(imageUrl);
  let fileType = data.headers.get("Content-Type");
  let buffer = await data.buffer();

  let stream = bufferToStream(buffer);
  let image;
  if ((/png/).test(fileType)) {
    image = await PImage.decodePNGFromStream(stream);
  }
  else if ((/jpe?g/).test(fileType)) {
    image = await PImage.decodeJPEGFromStream(stream);
  }
  else {
    return `Error. Invalid file type.`
  }

  let rawTensor;
  rawTensor = tf.tidy(() => {
    let tensorImage;
    tensorImage = tf.browser.fromPixels(image).toFloat();
    tensorImage = tf.image.resizeNearestNeighbor(tensorImage, [model.inputs[0].shape[1], model.inputs[0].shape[2]]);
    let offset = tf.scalar(127.5);
    tensorImage = tensorImage.sub(offset).div(offset);
    offset = null;
    tensorImage = tensorImage.reshape([1, model.inputs[0].shape[1], model.inputs[0].shape[2], model.inputs[0].shape[3]]);

    return model.predict(tensorImage);
  });

  let classes = []
  for (let i = 1; i < 181; i++) {
    classes.push(`${i}`)
  }

  let sorted = tf.topk(rawTensor, classes.length);
  let predictions = [ sorted.values.arraySync(), sorted.indices.arraySync() ];

  let rawArray;
  rawArray = await rawTensor.data();
  rawArray = Array.from(rawArray);

  tf.dispose([rawTensor, sorted])

  let predInd = predictions[1][0][0];
  let predVal = (predictions[0][0][0]*100).toFixed(2);
  let msg = `${classes[predInd]} (${predVal}%) -`;

  data = null;
  fileType = null;
  buffer = null;
  image = null;
  rawTensor = null;
  classes = null;
  sorted = null;
  predictions = null;
  rawArray = null;
  predInd = null;
  predVal = null;
  i = null;

  return msg
};

module.exports = {
  event: 'messageCreate',
  run: async (message, client, Discord, model) => {

    let mb = message.embeds[0];
    if (!mb) return;
    if (mb.title) {

      var link = mb.image[`proxyURL`];
      let first = Date.now()
      let prediction = await predict(`${link}`, model)
      let second = Date.now()

      let pred1 = prediction.split(` `)
      let pred2 = nameArr[((pred1[0]*1)-1)]
      let logPred = `${pred2} ${pred1[1]} ${pred1[2]} ${second-first}ms`
      console.log(logPred)
      message.channel.send(logPred)

      mb = null;
      link = null;
      first = null;
      prediction = null;
      second = null;
      pred1 = null;
      pred2 = null;
      x = null;
      logPred = null;
    }

  },
};

handler.js

if (err) return console.error(err);
    files.forEach(async (file) => {
        const eventFunction = require(`./../events/${folder}${file}`);
        if (eventFunction.disabled) return;

        const event = eventFunction.event || file.split('.')[0];
        const emitter =
            (typeof eventFunction.emitter === 'string'
                ? client[eventFunction.emitter]
                : eventFunction.emitter) || client;
        const once = eventFunction.once;

        try {
            emitter[once ? 'once' : 'on'](event, (...args) =>
                eventFunction.run(...args, client, Discord, model),
            );
        }
        catch (error) {
            console.error(error.stack);
        }
    });

};

sharder.js

const { token } = require('./config.json');

const manager = new ShardingManager('./index.js', { token: `${token}` });

manager.on('shardCreate', async shard => {
  console.log(`Launched shard ${shard.id}`)
});

manager.spawn({ amount: 90 , delay: 10000, timeout: 1 * 1000 * 60 })

index.js

const Discord = require('discord.js');
const { token } = require('./config.json');
const client = new Discord.Client({ intents: [ Discord.Intents.FLAGS.GUILDS, Discord.Intents.FLAGS.GUILD_MESSAGES ] });
const db = require("quick.db");
const eco = { bot: new db.table("bot") };
module.exports = { eco };

const folders = [ "interactionCreate/" ]
for (let i = 0; i < folders.length; i++) {
    const folder = folders[i]
    fs.readdir(`./events/${folder}`, async (err, files) => {
        const eventHandler = require("./data/eventHandler.js");
        const tf = require(`@tensorflow/tfjs-node`);
        let model = await tf.loadLayersModel(`file://./models/model.json`);

        eventHandler(err, files, client, Discord, folder, model);
    });
}

client.login(token);

gc.js

module.exports = {
  event: 'messageCreate',
  run: async (message, client, Discord) => {

    if (!message.content.startsWith(`clear`)) return

    const col = async (client) => {
      try {
        if (global.gc) {global.gc();}
        console.log(`Garbage Collected`)
      } catch (e) {
        console.log(`Unable to collect`)
      }
    }

    const exec = async () => {
      await client.shard.broadcastEval(col)
    }

    await exec();

  },
};
泄漏点排查与修复方案

代码里存在6个明确的内存泄漏/内存占用过高的诱发点,对应修复方式如下:

  • tfjs张量漏释放
    tfjs 3.x版本中,tf.topk()返回的对象包含values、indices两个独立张量,直接tf.dispose(sorted)不会递归释放这两个内部张量。目前代码只释放了rawTensor和sorted本身,每次推理都会漏两个张量的内存,每秒14次调用持续累积就会形成稳定的内存泄漏。
    修复:将释放语句修改为tf.dispose([rawTensor, sorted, sorted.values, sorted.indices])即可,不需要额外处理rawTensor.data()返回的TypedArray,张量释放后这部分内存会被自动回收。

  • 图片解码与流资源未清理
    自定义的bufferToStream方法每次调用都会新建Readable流实例,图片解码完成后既没有销毁流,也没有清理pureimage解码生成的位图对象。pureimage解码出的image对象内部存储了全量像素数组,内存占用不低;未销毁的流会一直持有buffer引用,两者叠加每次请求都会泄漏几百KB到数MB不等的内存。
    修复:图片推理完成后手动将image.data属性置空,给创建的Readable流添加end、error事件监听,流消费完成后直接调用stream.destroy()释放内部持有的资源引用。

  • 事件监听器重复注册
    当前事件注册逻辑没有做去重判断,一旦出现文件重加载、客户端重连等场景,就会给同一个事件重复绑定监听器。每个监听器的闭包都会持有model、client这类大对象引用,重复绑定不仅会导致事件被多次执行,还会让大对象一直被引用无法回收。另外代码在fs.readdir的异步回调里重复加载tfjs、eventHandler依赖,虽然Node.js有模块缓存不会重复加载,但写法不规范,一旦出现路径拼写错误就可能导致模块重复加载占用内存。
    修复:注册事件前先判断当前emitter上是否已经存在同事件的监听器,存在则先移除旧监听器再绑定新的;所有依赖的require语句统一放到文件顶部,不要在异步回调内加载大体积依赖。
    额外说明:index.js中使用了fs.readdir但顶部没有引入fs模块,运行会直接报错,补上const fs = require('fs')即可。

  • 无意义的null赋值干扰GC
    代码在每个函数末尾挨个给局部变量赋值为null的操作完全没有正向作用。函数执行完成后局部执行上下文会被销毁,只要不存在闭包引用,这些变量指向的内存本来就会被GC自动回收,手动赋null反而会干扰V8引擎的逃逸分析优化,可能让本该在年轻代就回收的内存被晋升到老年代,反而拉长回收周期、增加整体内存占用。
    修复:删掉所有函数末尾无意义的xxx = null赋值语句,只需要手动清理被全局对象、事件监听器、长生命周期闭包持有的对象引用即可。

  • quick.db缓存无上限
    quick.db默认会把全量数据库数据加载到内存缓存中,没有设置默认的缓存大小上限,如果存在频繁写入操作,缓存体积会持续膨胀,旧版本quick.db还存在已知的内存泄漏问题,会持续占用内存不释放。
    修复:如果业务中用不上quick.db的功能直接删掉相关初始化代码;如果需要使用则升级到最新版本,配置合理的缓存大小上限,不要往数据库中存储过大的对象。

  • 手动GC逻辑本身不生效
    现有gc.js的逻辑存在两个问题:一是启动Node进程时没有添加--expose-gc启动参数,global.gc本身为undefined,根本无法执行;二是broadcastEval传入的函数是序列化后传输到分片进程的,闭包引用的外部变量不会被一并传递,就算开启了GC权限也容易出现上下文丢失的问题。
    修复:确实需要手动触发GC的话,在ShardingManager初始化时添加execArgv: ['--expose-gc']参数,给所有分片进程开启GC调用权限;日常运行时不要频繁手动调用GC,V8的自动GC调度效率远高于手动触发。


内容的提问来源于stack exchange,提问作者Nic Cheng

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 19:30:10