美文网首页
Puppeteer性能优化与执行速度提升

Puppeteer性能优化与执行速度提升

作者: DC_er | 来源:发表于2019-04-11 18:58 被阅读0次

    Puppeteer自身不会消耗太多资源,耗费资源的大户是Chromium Headless。所以需要理解Chromium运行的原理,才能方便优化。

    Chromium消耗最多的资源是CPU,一是渲染需要大量计算,二是Dom的解析与渲染在不同的进程,进程间切换会给CPU造成压力(进程多了之后特别明显)。其次消耗最多的是内存,Chromium是以多进程的方式运行,一个页面会生成一个进程,一个进程占用30M左右的内存,大致估算1000个请求占用30G内存,在并发高的时候内存瓶颈最先显现。

    优化最终会落在内存和CPU上(所有软件的优化最终都要落到这里),通常来说因为并发造成的瓶颈需要优化内存,计算速度慢的问题要优化CPU。使用Puppeteer的用户多半会更关心计算速度,所以下面我们谈谈如何优化Puppeteer的计算速度。

    优化Chromium启动项

    通过查看Chromium启动时都有哪些参数可以配置,能找到大部分线索,因为Chromium这种顶级的开源产品,文档与接口都是非常清晰的,肯定可以找到相关配置项来定制启动方式。Chromium 启动参数列表

    我们需要找到下面几种配置来提升速度:

    1. 如果将Dom解析和渲染放到同一进程,肯定能提升时间(进程上下文切换的时间)。对应的配置是 ​single-process​
    2. 部分功能disable掉,比如GPU、Sandbox、插件等,减少内存的使用和相关计算。
    3. 如果启动Chromium时能绑定到某个CPU核上也能提升速度(单核上进行进程切换耗费的时间更少)。可惜没有找到对应的配置,官方文档写的是Chromium启动时会自动绑定CPU大核(ARM架构的CPU通常有大小核之分),依此推测Chromium启动时是会绑核的。(此处我并未验证)

    最后配置如下:

    const browser = await puppeteer.launch(
    {
        headless:true,
        args: [
            ‘–disable-gpu’,
            ‘–disable-dev-shm-usage’,
            ‘–disable-setuid-sandbox’,
            ‘–no-first-run’,
            ‘–no-sandbox’,
            ‘–no-zygote’,
            ‘–single-process’
        ]
    });
    

    Chromium 启动参数列表 文档中的配置项都可以尝试看看,我没有对所有选项做测试,但可以肯定存在某些选项能提升Chromium速度。

    优化Chromium执行流程

    接下来我们再单独优化Chromium对应的页面。我之前的文章中提过,如果每次请求都启动Chromium,再打开tab页,请求结束后再关闭tab页与浏览器。流程大致如下:

    请求到达->启动Chromium->打开tab页->运行代码->关闭tab页->关闭Chromium->返回数据

    真正运行代码的只是tab页面,理论上启动一个Chromium程序能运行成千上万的tab页,可不可以复用Chromium每次只打开一个tab页然后关闭呢?当然是可以的,Puppeteer提供了​puppeteer.connect()​ 方法,可以连接到当前打开的浏览器。流程如下:

    请求到达->连接Chromium->打开tab页->运行代码->关闭tab页->返回数据

    代码如下:

    const MAX_WSE = 4;  //启动几个浏览器 
    let WSE_LIST = []; //存储browserWSEndpoint列表
    init();
    app.get('/', function (req, res) {
        let tmp = Math.floor(Math.random()* MAX_WSE);
        (async () => {
            let browserWSEndpoint = WSE_LIST[tmp];
            const browser = await puppeteer.connect({browserWSEndpoint});
            const page = await browser.newPage();
            await page.goto('file://code/screen/index.html');
            await page.setViewport({
                width: 600,
                height: 400
            });                
            await page.screenshot({path: 'example.png'});
            await page.close();
            res.send('Hello World!');
        })();
    });
    
    function init(){
        (async () => {
            for(var i=0;i<MAX_WSE;i++){
                const browser = await puppeteer.launch({headless:true,
                    args: [
                    '--disable-gpu',
                    '--disable-dev-shm-usage',
                    '--disable-setuid-sandbox',
                    '--no-first-run',
                    '--no-sandbox',
                    '--no-zygote',
                    '--single-process'
                ]});
                browserWSEndpoint = await browser.wsEndpoint();
                WSE_LIST[i] = browserWSEndpoint;
            }
            console.log(WSE_LIST);
        })();        
    }
    

    利用cluster优化Puppeteer

    通常情况下我们会使用 ​.map()​ 搭配 ​Promise.all()​ 的方式并行处理异步,但是在使用​Puppeteer​批量截图时发现​Promise.all​会打开多个浏览器,导致机器性能急剧下降。

    ​Promise.all()​ 并行处理

    image

    利用 ​Reduce​ 是多个​Promise​顺序执行

    await tasks.reduce((sequence, url, idx) => {
      return sequence.then(() => {
        // doAnalyze 是个异步函数
        return doAnalyze(url, idx);
      });
    }, Promise.resolve())
    

    场景:有40个URL,需要获取每个博客的首页截图

    • 如果是​Promise.all()​,程序启动会同时打开20+的chromium浏览器,导致机器卡死。
    • 使用​reduce​缓解了压力,但没充分利用多核性能
    • 参入​Cluster​
    // cluster_index.js 入口文件
    const cluster = require('cluster');
    
    (async () => {
      let run;
      if (cluster.isMaster) {
        run = require('./cluster_master');
      } else {
        run = require('./cluster_worker');
      }
      try {
        await run();
      } catch (e) {
        // 追踪函数的调用轨迹
        console.trace(e);
      }
    })();
    
    // cluster_master.js master进程分配任务
    
    const cluster = require('cluster');
    const numCPUs = require('os').cpus().length;
    
    // 处理的任务列表
    let arr = [
      'https://github.com/guoguoya',
      'http://www.52cik.com',
      'http://zhalice.com',
      'https://www.yzqroom.cn',
      'http://zxh.name',
      'https://fogdong.github.io/',
      'http://github.com/elsieyin',
      'https://summer.tlb058.com',
      'https://skymon4.cn',
      'http://www.jiweiqing.cn',
      'http://effect.im',
      'http://dingkewz.com',
      'http://xcdh.me',
      'http://d2g.io',
      'http://codingdemon.com',
      'http://blog.leanote.com/dujuncheng',
      'http://niexiaotao.com',
      'http://zhengchengwen.com',
      'http://blog.tophefei.com',
      'https://zh-rocco.github.io',
      'http://wangyn.net',
      'http://dscdtc.ml',
      'http://jweboy.github.io',
      'http://www.wenghaoping.com',
      'http://zhoujingchao.github.io',
      'http://kyriejoshua.github.io/jo.github.io/',
      'http://www.withyoufriends.com',
      'http://if2er.com',
      'https://github.com/zhou-yg',
      'http://github/suoutsky',
      'http://richardsleet.github.io',
      'http://www.89io.com',
      'https://guoshencheng.com',
      'http://www.landluck.com.cn',
      'http://www.89io.com',
      'http://myoungxue.top',
      'https://github.com/Wangszzju',
      'http://www.hacke2.cn',
      'https://github.com/enochjs',
      'https://i.jakeyu.top',
      'http://muyunyun.cn',
    ];
    
    module.exports = async () => {
      // 每个 CPU 分配 N 个任务
      const n = Math.floor(arr.length / numCPUs);
      // 未分配的余数
      const remainder = arr.length % numCPUs;
    
      for (let i = 1; i <= numCPUs; i += 1) {
        const tasks = arr.splice(0, n + (i > remainder ? 0 : 1));
        // 将任务编号传递到 Cluster 内启动
        cluster.fork({ tasks: JSON.stringify(tasks) });
      }
      cluster.on('exit', (worker) => {
        console.log(`worker #${worker.id} PID:${worker.process.pid} died`);
      });
      cluster.on('error', (err) => {
        console.log(`worker #${worker.id} PID ERROR: `, err);
      });
    };
    
    // cluster_worker.js worker进程 完成任务
    
    const cluster = require('cluster');
    const puppeteer = require('puppeteer');
    
    // 禁止直接启动
    if (cluster.isMaster) {
      console.log('----', cluster.worker.id)
      process.exit(0);
    }
    
    module.exports = async () => {
      const env = process.env.tasks;
      let tasks = [];
      if (/^\[.*\]$/.test(env)) {
        tasks = JSON.parse(env);
      }
      if (tasks.length === 0) {
        console.log('????', tasks)
        // 非法启动, 释放进程资源
        process.exit(0);
      }
      console.log(`worker #${cluster.worker.id} PID:${process.pid} Start`);
      await tasks.reduce((sequence, url, idx) => {
        return sequence.then(() => {
          return doAnalyze(url, idx);
        });
      }, Promise.resolve())
    
      console.log(cluster.worker.id + ' 顺利完成');
      process.exit(0);
    };
    
    async function doAnalyze(url, i) {
      try {
        const browser = await (puppeteer.launch({
          // 若是手动下载的chromium需要指定chromium地址, 默认引用地址为 /项目目录/node_modules/puppeteer/.local-chromium/
          // executablePath: '/Users/huqiyang/Documents/project/z/chromium/Chromium.app/Contents/MacOS/Chromium',
          //设置超时时间
          timeout: 30000,
          //如果是访问https页面 此属性会忽略https错误
          ignoreHTTPSErrors: true,
          // 打开开发者工具, 当此值为true时, headless总为false
          devtools: false,
          // 关闭headless模式, 会打开浏览器
          headless: false
        }));
        const page = await browser.newPage();
        await page.setViewport({width: 1920, height: 1080});
        await page.goto(url);
        await page.waitFor(4000);
        console.log(cluster.worker.id, url, i, '截图中...');
        await page.screenshot({
          path: `./img_cluster/${cluster.worker.id}-${i}.png`,
          // path: '3.png',
          type: 'png',
          // quality: 100, 只对jpg有效
          // fullPage: true,
          // 指定区域截图,clip和fullPage两者只能设置一个
          // clip: {
          //   x: 0,
          //   y: 0,
          //   width: 1920,
          //   height: 600
          // }
        });
        browser.close();
      } catch (error) {
        console.log(cluster.worker.id, url, i)
        console.log(error)
      }
    };
    

    多个page轮询与多个browser轮询

    为了性能,现有解决方案是初始化若干个browser,请求打过来时,直接在browserList中取一个browser实例使用。
    作为对比,可以参考初始化一个browser,预先打开若干个page,请求打过来时,直接在pageList中取一个page实例使用。

    参考文章:

    Puppeteer性能优化与执行速度提升
    利用cluster优化Puppeteer

    相关文章

      网友评论

          本文标题:Puppeteer性能优化与执行速度提升

          本文链接:https://www.haomeiwen.com/subject/svmxwqtx.html