Node.js内存不足以进行pdfMake

时间:2018-12-27 23:21:38

标签: javascript node.js

我有一个Nodejs服务器,用于创建大约1200个pdf表单,以后可以由客户端下载。它们是使用pdfmake创建的,然后输出到服务器文件夹。当我执行大约350个文档中编写的代码时,Nodejs的内存不足。我知道必须有一种更好的保存方法,但是我似乎无法弄清楚。

以下方法被Mongoose查询中的一组数据映射所调用。创建和保存表单的相关代码如下:

const whichForm = certList => {
    certList.map(cert => { 
        if (cert.Cert_Details !== null) {
            switch (cert.GWMA) {
              case 'OA':
              case 'PC':
              // Don't provide reports for Feedlots
                if (cert.Cert_Details.cert_type !== null) {
                   if (cert.Cert_Details.cert_type === 'Irrigation') {
                     createOAReport(cert);
                   }
                }
                break;
             case 'FA':
             // Don't provide reports for Feedlots
               if (cert.Cert_Details.cert_type === 'Irrigation') {
                    createFAReport(cert);
                   }
               break;
               }
         }
    }
}

不同文件:

const PdfPrinter = require('pdfmake/src/printer');
const fs = require('fs');

const createOAReport = data => {
    console.log('PC or OA Cert ', data.Cert_ID);
    // console.log(data);

    let all_meters_maint = [];

    data.Flowmeters.map(flowmeter => {
     // Each Flow meter
     // console.log(`Inside Flowmeter ${flowmeter}`);

      if (flowmeter.Active === true) {
        let fm_maint = [];
        fm_maint.push({
        text: `Meter Serial Number: ${flowmeter.Meter_Details.Serial_num}`
        });
      fm_maint.push({
        text: `Type of Meter: ${flowmeter.Meter_Details.Manufacturer}`
      });
      fm_maint.push({ text: `Units: ${flowmeter.Meter_Details.units}`});
      fm_maint.push({ text: `Factor: ${flowmeter.Meter_Details.factor}`});
      all_meters_maint.push(fm_maint);
    }

    docDefinition.content.push({
      style: 'tableExample',
      table: {
        widths: [200, 200, '*', '*'],
        body: all_meters_maint
      },
      layout: 'noBorders'
    });

    const fonts = {
      Roboto: {
        normal: path.join(__dirname, '../', '/fonts/Roboto- 
        Regular.ttf'),
        bold: path.join(__dirname, '../', '/fonts/Roboto-Medium.ttf'),
        italics: path.join(__dirname, '../', '/fonts/Roboto-Italic.ttf'),
        bolditalics: path.join(__dirname, '../', '/fonts/Roboto- 
        MediumItalic.ttf')
        }
    };

  const printer = new PdfPrinter(fonts);
  const pdfDoc = printer.createPdfKitDocument(docDefinition);

  // Build file path
  const fullfilePath = path.join(
    __dirname,
    '../',
    '/public/pdffiles/',
    `${data.Cert_ID}.pdf`
  );

  pdfDoc.pipe(fs.createWriteStream(fullfilePath));
  pdfDoc.end();
};

是否存在另一种方法来保存文件,这些文件不会强迫它们进入流并且不会保留在内存中?

1 个答案:

答案 0 :(得分:0)

在得出答案之前,我根据问题中的信息做出一个巨大的假设。问题指出create about 1200 pdf forms。这意味着我假设函数whichForm中的参数certList是1200个项目的数组。或者我应该说1200个将调用createOAReport方法的项目。你明白了。我假设问题是我们正在调用该方法在该Array.map方法中创建1200次PDF。考虑到代码的问题和上下文,我认为这很有意义。

继续回答。主要问题是您不只是尝试创建1200个pdf。您正在尝试异步创建1200 pdf,这当然会给试图一次完成所有工作的系统带来压力。在像Node.js这样的单线程系统上,甚至更是如此。

简单易用的解决方案是仅增加Node.js的内存。通过使用--max-old-space-size标志并在运行节点命令时以MB为单位设置内存大小。您可以在this tutorial上找到有关此信息的更多信息。但是简短的版本是像node --max-old-space-size=8192 main.js这样的命令。这样会将Node.js的内存大小增加到8192 MB或8 GB。

该方法的几个问题。主要是它不是超级可扩展的。如果某天您要创建5000个pdf,该怎么办?您必须再次增加该内存大小。也许会增加正在运行的机器的规格。

第二种解决方案(实际上可以与第一种解决方案一起使用)是使该过程不异步。根据许多因素以及当前系统的优化程度,很可能会增加创建所有这些PDF所需的时间。

此过程有点象需要两个步骤才能对其进行编码。首先是设置您的createOAReport函数以返回一个承诺以指示完成时间。第二步是更改whichForm函数,以限制在任何单个时间点可以异步运行多少个项目。

您当然必须在系统上进行操作,以确定一次要运行多少个项目而不会使系统过载。微调这个数字并不是我关注的重点,当然您也可以通过增加为Node.js提供的内存来增加该数字。

当然,有很多方法可以做到这一点。我有一些方法的想法,这些想法比我将在此处展示的方法更好,但要复杂得多。限制一次运行多少个项目的基本思想仍然是相同的。您可以对其进行优化以满足您的需求。

我以前已经开发过像这样的系统,但是我不认为我做这件事的方法是最好或最干净的方法。但是在这个问题的结尾,我为您的示例附加了一些示例代码,试图说明我的观点。


const _ = require('lodash');

const MAX_RUNNING_PROMISES = 10; // You will have to play with this number to get it right for your needs

const whichForm = async certList => {
    // If certList is ["a", "b", "c", "d"]
    // And we run the following function with MAX_RUNNING_PROMISES = 2
    // array would equal [["a", "b"], ["c", "d"]]
    certList = _.chunk(certList, MAX_RUNNING_PROMISES);
    // Of course you can use something other than Lodash here, but I chose it because it's the first thing that came to mind

    for (let i = 0; i < certList.length; i++) {     
        const certArray = certList[i];

        // The following line will wait until all the promises have been resolved or completed before moving on
        await Promise.all(certArray.map(cert => {
            if (cert.Cert_Details !== null) {
                switch (cert.GWMA) {
                    case 'OA':
                    case 'PC':
                        // Don't provide reports for Feedlots
                        if (cert.Cert_Details.cert_type !== null) {
                            if (cert.Cert_Details.cert_type === 'Irrigation') {
                                return createOAReport(cert);
                            }
                        }
                        break;
                    case 'FA':
                        // Don't provide reports for Feedlots
                        if (cert.Cert_Details.cert_type === 'Irrigation') {
                            return createFAReport(cert);
                        }
                        break;
                }
            }
        }));
    }
}

然后输入其他文件。我们只需要转换它即可返回承诺。

const PdfPrinter = require('pdfmake/src/printer');
const fs = require('fs');

const createOAReport = data => {
    return new Promise((resolve, reject) => {
        console.log('PC or OA Cert ', data.Cert_ID);
        // console.log(data);

        let all_meters_maint = [];

        const flowmeter = data.Flowmeters[0];

        if (flowmeter.Active === true) {
            let fm_maint = [];
            fm_maint.push({
                text: `Meter Serial Number: ${flowmeter.Meter_Details.Serial_num}`
            });
            fm_maint.push({
                text: `Type of Meter: ${flowmeter.Meter_Details.Manufacturer}`
            });
            fm_maint.push({
                text: `Units: ${flowmeter.Meter_Details.units}`
            });
            fm_maint.push({
                text: `Factor: ${flowmeter.Meter_Details.factor}`
            });
            all_meters_maint.push(fm_maint);
        }

        docDefinition.content.push({
            style: 'tableExample',
            table: {
                widths: [200, 200, '*', '*'],
                body: all_meters_maint
            },
            layout: 'noBorders'
        });

        const fonts = {
                Roboto: {
                    normal: path.join(__dirname, '../', '/fonts/Roboto-Regular.ttf'),
                    bold: path.join(__dirname, '../', '/fonts/Roboto-Medium.ttf'),
                    italics: path.join(__dirname, '../', '/fonts/Roboto-Italic.ttf'),
                    bolditalics: path.join(__dirname, '../', '/fonts/Roboto-MediumItalic.ttf')
                }
        };

        const printer = new PdfPrinter(fonts);
        const pdfDoc = printer.createPdfKitDocument(docDefinition);

        // Build file path
        const fullfilePath = path.join(
            __dirname,
            '../',
            '/public/pdffiles/',
            `${data.Cert_ID}.pdf`
        );

        pdfDoc.pipe(fs.createWriteStream(fullfilePath));
        pdfDoc.on('finish', resolve); // This is where we tell it to resolve the promise when it's finished
        pdfDoc.end();
    });
};

在真正回答这个问题之后,我才意识到我最初的假设是错误的。由于其中一些pdf可能是在第二个功能和data.Flowmeters.map系统中创建的。因此,尽管我不打算演示它,但是您也必须将我在整个答案中给出的相同想法应用于该系统。现在,我已经删除了该部分,而只是使用该数组中的第一项,因为这只是一个示例。

一旦您对此有所了解,并且只拥有一个处理创建PDF的功能,而在整个地方都没有那么多的.map方法调用,您可能希望重新构建代码。提取.map方法,并将其与PDF创建过程分开。这样,限制一次创建的PDF数量会更容易。

在所有这些过程中添加一些错误处理也是一个好主意。


注意我实际上根本没有测试过此代码,因此可能存在一些错误。但是总体思想和原则仍然适用。