Node.js 大文件上传到 MongoDB 阻塞事件循环和工作池

Node.js Large File Uploads to MongoDB blocking the Event Loop and Worker Pool

所以我想 使用 Node.js 服务器 使用 Express、Mongoose 和 Multer 的 GridFS 存储引擎将大型 CSV 文件上传到 mongoDB 云数据库,但是 当文件上传开始时,我的数据库变得无法处理任何其他 API 请求。例如,如果在上传文件时不同的客户端请求从数据库中获取用户,服务器将收到请求并尝试从 MongoDB 云端获取用户,但是请求会卡住 因为大文件上传吃光了所有的计算资源。因此,客户端执行的获取请求不会return用户直到正在进行的文件上传完成。

我理解如果一个线程需要很长时间来执行回调(事件循环)或任务(Worker),那么它被认为是“阻塞的”并且 Node.js 运行 JavaScript 事件循环中的代码,同时它提供了一个工作池来处理像文件 I/O 这样的昂贵任务。我在 this blog post by NodeJs.org 上读到,为了让您的 Node.js 服务器保持快速,在任何给定时间与每个客户端相关的工作都必须“小”,我的目标应该是 尽量减少任务时间的变化。这样做的原因是,如果 Worker 的当前任务比其他任务昂贵得多,它将无法处理其他待处理的任务,从而将 Worker Pool 的大小减一,直到任务完成。

换句话说,执行大文件上传的客户端正在执行一个昂贵的任务,降低了工作池的吞吐量,进而降低了服务器的吞吐量。根据上述博客post,当每个子任务完成时,它应该提交下一个子任务,当最后一个子任务完成时,它应该通知提交者。 这样,在长任务的每个子任务之间(大文件上传),Worker 可以处理较短任务的子任务, 从而解决阻塞问题

但是,我不知道如何在实际代码中实现这个解决方案。是否有任何特定的分区功能可以解决此问题?我是否必须使用特定的上传架构或节点包而不是 multer-gridfs-storage 来上传我的文件?请帮助

这是我当前使用 Multer 的 GridFS 存储引擎实现的文件上传:

   // Adjust how files get stored.
   const storage = new GridFsStorage({
       // The DB connection
       db: globalConnection, 
       // The file's storage configurations.
       file: (req, file) => {
           ...
           // Return the file's data to the file property.
           return fileData;
       }
   });

   // Configure a strategy for uploading files.
   const datasetUpload = multer({ 
       // Set the storage strategy.
       storage: storage,

       // Set the size limits for uploading a file to 300MB.
       limits: { fileSize: 1024 * 1024 * 300 },
    
       // Set the file filter.
       fileFilter: fileFilter,
   });


   // Upload a dataset file.
   router.post('/add/dataset', async (req, res)=>{
       // Begin the file upload.
       datasetUpload.single('file')(req, res, function (err) {
           // Get the parsed file from multer.
           const file = req.file;
           // Upload Success. 
           return res.status(200).send(file);
       });
   });

你能做到architecture/infrastructure吗?如果是这样,这个挑战最好通过不同的方法来解决。这实际上是无服务器解决方案的完美候选者,即 Lambda。

Lambda 不会 运行 在一台机器上并行执行任何请求。 Lambda 将一个请求分配给一台机器,在请求完成之前,这台机器将不会接收任何其他流量。因此,您永远不会达到现在遇到的极限。

我认为这个问题源于buffer。因为缓冲区 必须接收所有块,然后整个缓冲区被发送 消费者,所以缓冲 需要很长时间 Streams 可以解决这个问题,因此 streams 允许我们在数据从源到达时立即处理数据,并做一些不可能通过缓冲数据一次性处理所有数据。我在 multer GitHub 页面上找到了 storage.fromStream() 方法,并通过上传 122 MB 文件 对其进行了测试,它对我有用,感谢 Node.js 流, 每一个 数据块一收到就被消耗并保存到云数据库中。上传总时间不到 1 分钟,并且服务器可以轻松响应 上传期间的其他请求。

const {GridFsStorage} = require('multer-gridfs-storage');
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
const express = require('express');
const fs = require('fs');
const connectDb = require('./connect');
const app = express();
 
const storage = new GridFsStorage({db:connectDb()});

app.post('/profile', upload.single('file'), function (req, res, next) {
  const {file} = req;
  const stream = fs.createReadStream(file.path); //creates stream
  storage.fromStream(stream, req, file)
    .then(() => res.send('File uploaded')) //saves data as binary to cloud db
    .catch(() => res.status(500).send('error'));
});
app.get('/profile',(req,res)=>{
    res.send("hello");
})

app.listen(5000);

所以经过几天的研究,我发现问题的根源不是 Node.JS 或者我的文件上传实现。 问题是 MongoDB Atlas 无法在处理其他操作(例如从我的数据库中获取用户)的同时处理文件上传工作负载。正如我在问题 post 中所述,Node.js 收到了来自其他客户的 API 呼叫,但他们没有返回任何结果。我现在意识到那是因为 他们卡在了数据库级别 。一旦我切换到 MongoDB 的本地部署,问题就解决了。

根据 this blog post about MongoDB Best Practices,相对于 CPU 数量的活动线程总数(即并发操作)会影响性能,因此会影响 Node.js 服务器的吞吐量。但是,我已经尝试使用最多 8 个 vCPU 的专用 MongoDB 集群(M50 集群包)并且 MongoDB Atlas 在处理其他客户端请求时仍然无法上传文件.

如果有人让它与 云解决方案一起工作,我想了解更多。谢谢。