Опубликовано 4 июня 2018 года в корпоративном блоге browserless
Рады сообщить, что недавно мы преодолели рубеж в два миллиона обслуженных сессий! Это миллионы сгенерированных скриншотов, напечатанных PDF и протестированных сайтов. Мы сделали почти всё, что вы можете придумать делать с headless-браузером.
Хотя приятно достичь такой вехи, но на пути оказалось явно много накладок и проблем. В связи с огромным объёмом полученного трафика хотелось бы сделать шаг назад и изложить общие рекомендации для запуска headless-браузеров (и puppeteer) в продакшне.
Вот некоторые советы.

Никоим образом, если это вообще возможно, вообще не запускайте браузер в режиме headless. Особенно на той же инфраструктуре, что и ваше приложение (см. выше). Headless-браузер непредсказуем, прожорлив и размножается как мистер Мисикс из «Рика и Морти». Почти всё, что может сделать браузер (кроме интерполирования и запуска JavaScript), можно сделать с помощью простых инструментов Linux. Библиотеки Cheerio и другие предлагают элегантный Node API для извлечения данных HTTP-запросами и скрапинга, если такова ваша цель.
Например, вы можете забрать страницу (предполагая, что это некий HTML) и произвести скрапинг простыми командами вроде таких:
Очевидно, скрипт не охватывает все случаи использования, и если вы читаете данную статью, то скорее всего вам придётся использовать headless-браузер. Поэтому приступим.
Мы столкнулись с многочисленными пользователями, которые пытаются держать браузер запущенным, даже если он не используется (с открытыми соединениями). Хотя это может быть хорошей стратегией, чтобы ускорить запуск сеанса, но приведёт к краху через несколько часов. Во многом потому что браузеры любят кэшировать всё подряд и постепенно выедают память. Как только вы прекратили интенсивно использовать браузер — сразу закройте его!
В browserless мы обычно сами исправляем эту ошибку за пользователей, всегда устанавливая какой-то таймер на сессию и закрывая браузер при отключении WebSocket. Но если вы не используете наш сервис или резервный образ Docker, то обязательно убедитесь в каком-нибудь автоматическом закрытии браузера, потому что будет неприятно, когда всё упадёт посреди ночи.
3. Ваш друг
В Puppeteer есть много приятных методов вроде сохранения DOM-селекторов и прочего в окружении Node. Хотя это очень удобно, но вы легко можете выстрелить себе в ногу, если что-то на странице заставит мутировать этот узел DOM. Пусть это не так круто, но в реальности лучше всю работу на стороне браузера выполнять в контексте браузера. Обычно это означает загрузку
Например, вместо чего-то подобного (три действия async):
Лучше сделать так (одно действие async):
Другое преимущество обернуть действия в вызов
Простое эмпирическое правило состоит в том, чтобы подсчитать количество
Итак, мы поняли, что браузер запускать нехорошо и нужно делать это только в случае крайней необходимости. Следующий совет — запускать только одну сессию на каждый браузер. Хотя в реальности можно и сэкономить ресурсы, распараллелив работу через
Вместо этого:
Лучше сделайте так:
Каждый новый инстанс браузера получает чистый
Одна из главных фич browserless — способность аккуратно ограничивать распараллеливание и очередь. Так клиентские приложения просто запускают
Лучший и самый простой способ — взять наш образ Docker и запустить его с необходимыми параметрами:
Это ограничивает количество параллельных запросов десятью (включая сессии отладки и многое другое). Очередь настраивается переменной
6. Не забывайте про
Одна из самых распространённых проблем, которая нам встречалась, — это действия, запускающие загрузку страниц с последующим внезапным прекращением работы скриптов. Так происходит потому что действия, которые запускают
Например, такой
Но срабатывает в другом (см. демо).
Больше о waitForNavigation можно прочитать здесь. У этой функции примерно такие же параметры интерфейса, как у
Для корректной работы Chrome нужно много зависимостей. Реально много. Даже после установки всего необходимого придётся беспокоиться о таких вещах как шрифты и фантомные процессы. Поэтому идеально использовать какой-то контейнер, чтобы поместить всё туда. Docker почти специально создан для этой задачи, поскольку вы можете ограничить количество доступных ресурсов и изолировать его. Если хотите создать собственный
А чтобы избежать процессов-зомби (обычное дело в Chrome), то лучше для правильного запуска использовать что-то вроде dumb-init:
Если хотите узнать больше, взгляните на наш Dockerfile.
Полезно помнить, что здесь две среды выполнения JavaScript (Node и браузер). Это отлично для разделения задач, но неизбежно происходит путаница, потому что некоторые методы потребуют явной передачи ссылок вместо замыканий или подъёмов (hoistings).
Для примера возьмём
Таким образом, вместо ссылки на
Лучше передайте параметр:
К функции
Мы с невероятным оптимизмом смотрим на будущее headless-браузеров и всей автоматизации, которую они позволяют достичь. С помощью мощных инструментов вроде puppeteer и browserless мы надеемся, что отладка и запуск headless-автоматизации в продакшне станет проще и быстрее. Скоро мы запустим тарификацию pay-as-you-go для аккаунтов и функций, которые помогут лучше справляться с вашей headless-работой!
Рады сообщить, что недавно мы преодолели рубеж в два миллиона обслуженных сессий! Это миллионы сгенерированных скриншотов, напечатанных PDF и протестированных сайтов. Мы сделали почти всё, что вы можете придумать делать с headless-браузером.
Хотя приятно достичь такой вехи, но на пути оказалось явно много накладок и проблем. В связи с огромным объёмом полученного трафика хотелось бы сделать шаг назад и изложить общие рекомендации для запуска headless-браузеров (и puppeteer) в продакшне.
Вот некоторые советы.
1. Не используйте headless-браузер вообще

Изменчивое потребление ресурсов Headless Chrome
Никоим образом, если это вообще возможно, вообще не запускайте браузер в режиме headless. Особенно на той же инфраструктуре, что и ваше приложение (см. выше). Headless-браузер непредсказуем, прожорлив и размножается как мистер Мисикс из «Рика и Морти». Почти всё, что может сделать браузер (кроме интерполирования и запуска 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/');
Очевидно, скрипт не охватывает все случаи использования, и если вы читаете данную статью, то скорее всего вам придётся использовать headless-браузер. Поэтому приступим.
2. Не запускайте headless-браузер без необходимости
Мы столкнулись с многочисленными пользователями, которые пытаются держать браузер запущенным, даже если он не используется (с открытыми соединениями). Хотя это может быть хорошей стратегией, чтобы ускорить запуск сеанса, но приведёт к краху через несколько часов. Во многом потому что браузеры любят кэшировать всё подряд и постепенно выедают память. Как только вы прекратили интенсивно использовать браузер — сразу закройте его!
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!
}
В browserless мы обычно сами исправляем эту ошибку за пользователей, всегда устанавливая какой-то таймер на сессию и закрывая браузер при отключении WebSocket. Но если вы не используете наш сервис или резервный образ Docker, то обязательно убедитесь в каком-нибудь автоматическом закрытии браузера, потому что будет неприятно, когда всё упадёт посреди ночи.
3. Ваш друг page.evaluate
Будьте осторожны с транспилерами вроде babel или typescript, поскольку они любят создавать функции хелперов и предполагать, что те доступны с замыканиями. То есть обратный вызов .evaluate может работать неправильно.
В Puppeteer есть много приятных методов вроде сохранения DOM-селекторов и прочего в окружении Node. Хотя это очень удобно, но вы легко можете выстрелить себе в ногу, если что-то на странице заставит мутировать этот узел DOM. Пусть это не так круто, но в реальности лучше всю работу на стороне браузера выполнять в контексте браузера. Обычно это означает загрузку
page.evaulate
для всей работы, которую надо сделать.Например, вместо чего-то подобного (три действия async):
const $anchor = await page.$('a.buy-now');
const link = await $anchor.getProperty('href');
await $anchor.click();
return link;
Лучше сделать так (одно действие async):
await page.evaluate(() => {
const $anchor = document.querySelector('a.buy-now');
const text = $anchor.href;
$anchor.click();
});
Другое преимущество обернуть действия в вызов
evaluate
— это переносимость: этот код можно для проверки запустить в браузере вместо того, чтобы пытаться переписать код Node. Конечно, всегда рекомендуется использовать отладчик для сокращения времени разработки.Простое эмпирическое правило состоит в том, чтобы подсчитать количество
await
или then
в коде. Если их больше одного, то вероятно лучше запускать код внутри вызова page.evaluate
. Причина в том, что все действия async ходят туда-сюда между средой выполнения Node и браузером, а это означает постоянные сериализации и десериализации JSON. Хотя здесь не такой огромный объём парсинга (потому что всё поддерживается WebSockets), он всё равно отнимает время, которое лучше потратить на что-то другое.4. Распараллеливайте браузеры, а не веб-страницы
Итак, мы поняли, что браузер запускать нехорошо и нужно делать это только в случае крайней необходимости. Следующий совет — запускать только одну сессию на каждый браузер. Хотя в реальности можно и сэкономить ресурсы, распараллелив работу через
pages
, но если упадёт одна страница, она может повалить весь браузер. К тому же не гарантируется, что каждая страница идеально чистая (куки и хранение могут стать головной болью, как видим).Вместо этого:
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. Очередь и ограничение параллельной работы
Одна из главных фич browserless — способность аккуратно ограничивать распараллеливание и очередь. Так клиентские приложения просто запускают
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
. Как правило, можно выполнять примерно 10 параллельных запросов на каждый гигабайт памяти. Процент загрузки CPU может изменяться для разных задач, но в основном вам понадобится много и много оперативной памяти.6. Не забывайте про page.waitForNavigation
Одна из самых распространённых проблем, которая нам встречалась, — это действия, запускающие загрузку страниц с последующим внезапным прекращением работы скриптов. Так происходит потому что действия, которые запускают
pageload
, часто вызывают «проглатывание» последующей работы. Чтобы обойти проблему обычно нужно вызвать действие загрузки страницы — и сразу за ним ожидание загрузки.Например, такой
console.log
не срабатывает в одном месте (см. демо):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 (Node и браузер). Это отлично для разделения задач, но неизбежно происходит путаница, потому что некоторые методы потребуют явной передачи ссылок вместо замыканий или подъёмов (hoistings).
Для примера возьмём
page.evaluate
. Глубоко в недрах протокола происходит буквальная стрингификация функции и передача её в Chrome. Поэтому вещи вроде замыканий и подъёмов вообще не будут работать. Если вам нужно передать какие-то ссылки или значения в вызов evaluate, просто добавьте их в качестве аргументов, которые будут правильно обработаны.Таким образом, вместо ссылки на
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
можно добавить один или несколько аргументов, поскольку здесь она вариативна. Обязательно используйте это в своих интересах!Будущее
Мы с невероятным оптимизмом смотрим на будущее headless-браузеров и всей автоматизации, которую они позволяют достичь. С помощью мощных инструментов вроде puppeteer и browserless мы надеемся, что отладка и запуск headless-автоматизации в продакшне станет проще и быстрее. Скоро мы запустим тарификацию pay-as-you-go для аккаунтов и функций, которые помогут лучше справляться с вашей headless-работой!