Puppeteer 内存增加问题

Avi*_*Bar 5 memory-leaks node.js puppeteer

我在 Amazon Linux EC2 实例和 Macbook Air (OSX) 上运行 Puppeteer 脚本。该脚本必须保持在一页上,并一遍又一遍地重复执行表单填写任务。

我遇到了将其作为守护进程运行的问题pm2,我可以看到该进程的内存消耗每隔一两分钟就会增加,直到堵塞服务器的内存。

为了克服这个问题,我使用了--max-memory-restartflag of pm2,它可以防止服务器崩溃,但节点进程平均每30分钟重新启动一次。

这是代表我正在运行的代码的示例:

// Require the puppeteer library
const puppeteer = require('puppeteer');

async function scrape() {
    // Create the browser
    const browser = await puppeteer.launch({
        headless: true,
        ignoreHTTPSErrors: true,
        args: [
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '--disable-dev-shm-usage',
            '--disable-accelerated-2d-canvas',
            '--no-first-run',
            '--no-zygote',
            '--single-process',
            '--disable-gpu',
            '--ignore-certificate-errors'
        ]
    });

    // Wrap scraping/testing code in try
    try {
        await initialFlow(browser, null); // Page instance is null for the first time
        // Catch and log errors
    } catch (error) {
        // Your chance to handle errors
        console.error(error);
    } finally {
        // Always close the browser
        await browser.close();
    }
}

const initialFlow = async (browser, page) => { // Save resources by reusing the browser and page instances
    try {

        if (!page) {
            const pages = await browser.pages();
            page = pages[0];
        }

        await page.setRequestInterception(true);

        page.on('request', (req) => {
            if (req.resourceType() == 'font' || req.resourceType() == 'image') { // Save resources by denying images and fonts from being rendered
                req.abort();
            } else {
                req.continue();
            }
        });

        await page.goto('about:blank'); // Go to blank page
        await page.goto('https://www.google.com'); // For the example

        await page.waitForSelector('#Form', { visible: true });
        return await initialFlow(browser, page); // Perform the actions again on the same browser and page instances

    } catch (err) {
        return await initialFlow(browser, page);
    };
}

// Run our function
scrape().catch(console.error);
Run Code Online (Sandbox Code Playgroud)

运行此示例时,命令中显示的内存pm2 ls从 30mb 左右开始,并随着时间的推移不断增加。

有什么建议可以防止这种内存泄漏吗?

ggo*_*len 0

即使您进入无限循环,也能放入块,这browser.close()值得称赞。finally对于该线程的许多其他访问者来说,典型的内存泄漏问题是无法关闭代码中所有路径上的浏览器。

然而,使用递归而不是循环是相当奇怪的。幸运的是,由于它是异步的,因此堆栈不会崩溃,但代码很难理解,因为函数中发生了一些条件初始化。误解控制流程可能会导致泄漏。

一个问题是您在每次调用时不断将请求处理程序添加到页面initialFlow

page.on('request', (req) => {});
Run Code Online (Sandbox Code Playgroud)

这些处理程序相互堆叠,消耗内存。这是一个最小的示例,您可以运行它来使用内存分析器观察问题:

const puppeteer = require("puppeteer"); // ^19.1.0
const {setTimeout} = require("timers/promises");

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();

  for (;;) {
    page.on("request", () => {});
    await setTimeout(10);
  }
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());
Run Code Online (Sandbox Code Playgroud)

只需预先添加一次此处理程序,就应该没问题,假设代码中没有任何其他内容在转换为您在此处共享的代码片段时丢失。

快速重写,未经测试:

const puppeteer = require("puppeteer");

async function scrape() {
  const browser = await puppeteer.launch({
    headless: true,
    ignoreHTTPSErrors: true,
    args: [
      "--no-sandbox",
      "--disable-setuid-sandbox",
      "--disable-dev-shm-usage",
      "--disable-accelerated-2d-canvas",
      "--no-first-run",
      "--no-zygote",
      "--single-process",
      "--disable-gpu",
      "--ignore-certificate-errors",
    ],
  });

  try {
    const [page] = await browser.pages();
    await page.setRequestInterception(true);
    page.on("request", (req) => {
      if (
        req.resourceType() == "font" ||
        req.resourceType() == "image"
      ) {
        return req.abort();
      }

      req.continue();
    });

    for (;;) {
      try {
        await page.goto("about:blank"); // Go to blank page
        await page.goto("https://www.google.com"); // For the example
        await page.waitForSelector("#Form", {visible: true});
      }
      catch (err) {} // FIXME risky -- consider detecting if we hit
                     // this branch repeatedly so we can log it
    }
  }
  catch (error) {
    console.error(error);
  }
  finally {
    await browser.close();
  }
}

scrape().catch(console.error);
Run Code Online (Sandbox Code Playgroud)