体验200万次无头会议

2018年6月4日发布在无浏览器的企业博客上。

我们很高兴地宣布,我们最近突破了200万个会话的界限! 这些是数百万个生成的屏幕截图,打印的PDF和经过测试的网站。 我们已经完成了无头浏览器几乎可以想到的所有事情。

虽然很高兴达到这样的里程碑,但是在路上显然存在很多重叠和问题。 由于收到的大量流量,我想退后一步,并提出在生产中启动无头浏览器(和puppeteer )的一般建议。

这里有一些技巧。

1.完全不要使用无头浏览器




无头的Chrome资源消耗


绝对不要以无头模式启动浏览器 。 特别是在与您的应用程序相同的基础结构上(请参见上文)。 无头浏览器就像Rick and Morty的Misix先生一样,变幻莫测,繁琐繁衍。 浏览器几乎可以做的所有事情(内插和运行JavaScript除外)都可以使用简单的Linux工具完成。 如果您的目标是,Cheerio库和其他库提供了一个优雅的Node API,用于通过HTTP请求和抓取来检索数据。

例如,您可以选择一个页面(假设它是某种HTML),并使用以下简单命令将其抓取:

import cheerio from 'cheerio'; import fetch from 'node-fetch'; async function getPrice(url) { const res = await fetch(url); const html = await res.test(); const $ = cheerio.load(html); return $('buy-now.price').text(); } getPrice('https://my-cool-website.com/'); 

显然,该脚本并不涵盖所有使用情况,如果您阅读了本文,则很可能必须使用无头浏览器。 因此,让我们开始吧。

2.不要不必要地启动无头浏览器


我们遇到了许多用户,即使不使用浏览器(打开连接),它们仍试图保持浏览器运行。 尽管这可能是加快会话速度的好策略,但它会在几个小时后崩溃。 很大程度上是因为浏览器喜欢连续缓存所有内容并逐渐消耗内存。 一旦您停止大量使用浏览器,请立即将其关闭!

 import puppeteer from 'puppeteer'; async function run() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://www.example.com/'); // More stuff ...page.click() page.type() browser.close(); // <- Always do this! } 

在无浏览器中,我们通常为用户自己解决此错误,始终为会话设置某种计时器,并在断开WebSocket的连接时关闭浏览器。 但是,如果您不使用我们的服务Docker备份映像 ,请确保确保自动关闭浏览器,因为当一切在深夜降临时,浏览器会令人不快。

3.您的朋友page.evaluate


请注意诸如babel或typescript之类的编译器,因为它们喜欢创建帮助器函数并假定可以使用闭包对其进行访问。 也就是说,.evaluate回调可能无法正常工作。

Puppeteer有许多不错的方法,例如在Node环境中存储DOM选择器和其他东西。 尽管非常方便,但是如果页面上的某些内容迫使此DOM节点发生突变 ,您可以轻松地将自己击倒 。 这可能不是很酷,但实际上最好在浏览器的上下文中在浏览器端完成所有工作。 通常,这意味着需要加载page.evaulate来完成所有需要完成的工作。

例如,代替这样的事情( 三个异步操作):

 const $anchor = await page.$('a.buy-now'); const link = await $anchor.getProperty('href'); await $anchor.click(); return link; 

最好这样做(一个异步操作):

 await page.evaluate(() => { const $anchor = document.querySelector('a.buy-now'); const text = $anchor.href; $anchor.click(); }); 

将动作包装到evaluate调用中的另一个优点是可移植性:可以在浏览器中运行该代码以进行验证,而不必尝试重写Node代码。 当然,始终建议使用调试器以减少开发时间。

一个简单的经验法则是计算await次数, then计算代码中的await次数。 如果不止一个,那么最好在page.evaluate调用中运行代码。 原因是所有异步操作都在Node运行时和浏览器之间来回移动,这意味着常量JSON序列化和反序列化。 尽管没有那么多的解析(因为所有内容都由WebSockets支持),但它仍然占用时间,最好将其用于其他方面。

4.并行化浏览器,而不是网页


因此,我们意识到启动浏览器并不好,我们仅在紧急情况下才需要这样做。 下一个技巧是每个浏览器仅运行一个会话。 尽管实际上可以通过并行处理pages来节省资源,但是如果一页掉下来,可能会使整个浏览器崩溃。 另外,不能保证每个页面都非常干净( 如我们所见 ,Cookie和存储可能会令人头疼)。

相反:

 import puppeteer from 'puppeteer'; // Launch one browser and capture the promise const launch = puppeteer.launch(); const runJob = async (url) { // Re-use the browser here const browser = await launch; const page = await browser.newPage(); await page.goto(url); const title = await page.title(); browser.close(); return title; }; 

最好这样做:

 import puppeteer from 'puppeteer'; const runJob = async (url) { // Launch a clean browser for every "job" const browser = puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); const title = await page.title(); browser.close(); return title; }; 

每个新的浏览器实例都会获得一个干净的--user-data-dir除非另有说明 )。 也就是说,它被完全处理为新的会话。 如果Chrome由于某种原因崩溃,它将不会拉动其他会话。

5.队列和并发限制


无浏览器的主要功能之一是能够巧妙地限制并行化和排队。 因此,客户端应用程序只运行puppeteer.connect ,但他们不考虑队列实现。 这样可以避免大量问题,主要是与Chrome并发实例吞噬了应用程序的所有可用资源有关。

最好和最简单的方法是获取Docker映像并使用必要的参数运行它:

 # Pull in Puppeteer@1.4.0 support $ docker pull browserless/chrome:release-puppeteer-1.4.0 $ docker run -e "MAX_CONCURRENT_SESSIONS=10" browserless/chrome:release-puppeteer-1.4.0 

这将并发请求的数量限制为十个(包括调试会话等)。 该队列由变量MAX_QUEUE_LENGTH配置。 通常,您每GB内存可以执行大约10个并发请求。 CPU利用率的百分比因不同的任务而异,但是基本上您将需要大量的RAM。

6.不要忘了page.waitForNavigation


我们遇到的最常见的问题之一是开始加载页面并随后突然终止脚本的操作。 这是因为触发pageload的动作通常会导致吞噬后续工作。 要解决该问题,通常需要调用页面加载操作-等待页面加载后立即执行。

例如,这样的console.log不能在一个地方工作( 请参阅demo ):

 await page.goto('https://example.com'); await page.click('a'); const title = await page.title(); console.log(title); 

但是它可以在另一个环境中工作( 请参阅演示 )。

 await page.goto('https://example.com'); page.click('a'); await page.waitForNavigation(); const title = await page.title(); console.log(title); 

您可以在此处阅读有关waitForNavigation的更多信息。 此函数具有与page.goto大致相同的接口参数,但仅具有“ wait”部分。

7.使用Docker来满足您的所有需求。


Chrome需要大量依赖项才能正常运行。 真的很多 即使在安装完所有内容后,您也需要担心字体和幻像处理之类的问题。 因此,理想的是使用某种容器将所有内容放入其中。 Docker几乎是专门为此任务设计的,因为您可以限制可用资源的数量并将其隔离。 如果要创建自己的Dockerfile ,请检查以下所有必需的依赖项:

 # Dependencies needed for packages downstream RUN apt-get update && apt-get install -y \ unzip \ fontconfig \ locales \ gconf-service \ libasound2 \ libatk1.0-0 \ libc6 \ libcairo2 \ libcups2 \ libdbus-1-3 \ libexpat1 \ libfontconfig1 \ libgcc1 \ libgconf-2-4 \ libgdk-pixbuf2.0-0 \ libglib2.0-0 \ libgtk-3-0 \ libnspr4 \ libpango-1.0-0 \ libpangocairo-1.0-0 \ libstdc++6 \ libx11-6 \ libx11-xcb1 \ libxcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxrender1 \ libxss1 \ libxtst6 \ ca-certificates \ fonts-liberation \ libappindicator1 \ libnss3 \ lsb-release \ xdg-utils \ wget 

并且为了避免僵尸进程(在Chrome中很常见),最好使用dumb-init之类的东西来正确运行:

 ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init RUN chmod +x /usr/local/bin/dumb-init 

如果您想了解更多信息,请查看我们的Dockerfile

8.记住两个不同的运行时。


请记住,有两个 JavaScript运行时(节点和浏览器),这很有用。 这对于分离任务非常有用,但是不可避免地会造成混乱,因为某些方法将需要显式的链接传递而不是吊装。

例如,使用page.evaluate 。 在协议的最深处,是对该函数字面意义进行字符串化,并将其转移到Chrome 。 因此,诸如关闭和升降机之类的东西根本无法工作 。 如果需要将一些引用或值传递给评估调用,只需将它们添加为将被正确处理的参数即可。

因此,不是通过闭包引用selector

 const anchor = 'a'; await page.goto('https://example.com/'); // `selector` here is `undefined` since we're in the browser context const clicked = await page.evaluate(() => document.querySelector(anchor).click()); 

更好的传递参数:

 const anchor = 'a'; await page.goto('https://example.com/'); // Here we add a `selector` arg and pass in the reference in `evaluate` const clicked = await page.evaluate((selector) => document.querySelector(selector).click(), anchor); 

page.evaluate可以在page.evaluate函数中添加一个或多个参数,因为它在此处是可变的。 一定要利用这一点!

未来


我们对无头浏览器的未来以及它们可以实现的所有自动化都非常乐观。 我们希望通过使用puppeteer和无浏览器之类的强大工具,可以更轻松,更快地调试和运行生产中的无头自动化。 不久,我们将为帐户功能启动即付即用计费 功能 ,这将帮助您更好地应对无聊的工作!

Source: https://habr.com/ru/post/zh-CN413547/


All Articles