Experimente 2 milhões de sessões sem cabeça

Publicado em 4 de junho de 2018 no blog corporativo sem navegador.

Temos o prazer de anunciar que recentemente cruzamos a linha de dois milhões de sessões realizadas ! São milhões de capturas de tela geradas, PDFs impressos e sites testados. Fizemos quase tudo o que você pode pensar em fazer com um navegador sem cabeça.

Embora seja bom alcançar esse marco, no caminho havia claramente muitas sobreposições e problemas. Devido à enorme quantidade de tráfego recebido, gostaria de dar um passo atrás e apresentar recomendações gerais para o lançamento de navegadores sem cabeça (e marionetes ) na produção.

Aqui estão algumas dicas.

1. Não use um navegador sem cabeça




Consumo de recursos sem cabeça do Chrome


De nenhuma maneira, se possível, não inicie o navegador no modo decapitado . Especialmente na mesma infraestrutura que seu aplicativo (veja acima). O navegador sem cabeça é imprevisível, guloso e criador como o Sr. Misix, de Rick e Morty. Quase tudo o que um navegador pode fazer (exceto interpolar e executar o JavaScript) pode ser feito usando ferramentas simples do Linux. As bibliotecas Cheerio e outras oferecem uma API Node elegante para recuperar dados com solicitações HTTP e raspagem, se esse for seu objetivo.

Por exemplo, você pode pegar uma página (supondo que seja algum tipo de HTML) e descartá-la com comandos simples como estes:

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/'); 

Obviamente, o script não cobre todos os casos de uso e, se você ler este artigo, provavelmente precisará usar um navegador sem cabeça. Então, vamos começar.

2. Não inicie um navegador sem cabeça desnecessariamente


Encontramos vários usuários que estão tentando manter o navegador funcionando, mesmo que não esteja em uso (com conexões abertas). Embora essa possa ser uma boa estratégia para acelerar uma sessão, ela falhará em algumas horas. Principalmente porque os navegadores gostam de armazenar em cache tudo em uma fileira e gradualmente consomem memória. Assim que você parar de usar o navegador intensivamente, feche-o imediatamente!

 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! } 

No modo sem navegador, geralmente corrigimos esse erro para os usuários, sempre definindo algum tipo de cronômetro para a sessão e fechando o navegador quando o WebSocket é desconectado. Mas se você não usar nosso serviço ou a imagem de backup do Docker , certifique-se de fechar o navegador automaticamente , pois será desagradável quando tudo cair no meio da noite.

3. A sua página de amigo. page.evaluate


Tenha cuidado com transpilers como babel ou typescript, porque eles gostam de criar funções auxiliares e assumem que eles são acessíveis com fechamentos. Ou seja, o retorno de chamada .valuate pode não funcionar corretamente.

O Puppeteer possui muitos métodos interessantes, como armazenar seletores DOM e outras coisas em um ambiente Node. Embora seja muito conveniente, você pode facilmente dar um tiro no pé se algo na página forçar esse nó DOM a sofrer uma mutação . Pode não ser tão legal, mas, na realidade, é melhor fazer todo o trabalho no lado do navegador no contexto do navegador . Isso geralmente significa carregar page.evaulate para todo o trabalho que precisa ser feito.

Por exemplo, em vez de algo como isto ( três ações assíncronas):

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

É melhor fazer isso (uma ação assíncrona):

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

Outra vantagem de agrupar ações em uma chamada de evaluate é a portabilidade: esse código pode ser executado em um navegador para verificar em vez de tentar reescrever o código do Nó. Obviamente, é sempre recomendável usar um depurador para reduzir o tempo de desenvolvimento.

Uma regra simples é contar o número de await ou then no código. Se houver mais de um, provavelmente é melhor executar o código dentro da chamada page.evaluate . O motivo é que todas as ações assíncronas vão e voltam entre o tempo de execução do Node e o navegador, o que significa serialização e desserialização JSON constantes. Embora não exista uma quantidade tão grande de análise (porque tudo é suportado pelo WebSockets), ele ainda leva tempo, o que é melhor gasto em outra coisa.

4. Paralelize navegadores, não páginas da web


Então, percebemos que iniciar um navegador não é bom e precisamos fazer isso apenas em caso de emergência. A próxima dica é executar apenas uma sessão por navegador. Embora, na realidade, seja possível economizar recursos paralelizando o trabalho pelas pages , mas se uma página cair, poderá travar o navegador inteiro. Além disso, não é garantido que cada página esteja perfeitamente limpa (cookies e armazenamento podem se tornar uma dor de cabeça, como vemos ).

Em vez disso:

 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; }; 

Melhor fazer isso:

 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; }; 

Cada nova instância do navegador recebe um --user-data-dir limpo (a menos que seja indicado de outra forma ). Ou seja, é completamente processado como uma nova sessão nova. Se o Chrome travar por algum motivo, ele não fará outras sessões.

5. Limitação de fila e simultaneidade


Um dos principais recursos do sem navegador é a capacidade de limitar ordenadamente a paralelização e a fila. Portanto, os aplicativos clientes executam o puppeteer.connect , mas eles não pensam na implementação da fila. Isso evita uma enorme quantidade de problemas, principalmente nas instâncias simultâneas do Chrome que consomem todos os recursos disponíveis do seu aplicativo.

A melhor e mais fácil maneira é pegar nossa imagem do Docker e executá-la com os parâmetros necessários:

 # 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 

Isso limita o número de solicitações simultâneas a dez (incluindo sessões de depuração e mais). A fila é configurada pela variável MAX_QUEUE_LENGTH . Normalmente, você pode executar aproximadamente 10 solicitações simultâneas por gigabyte de memória. A porcentagem de utilização da CPU pode variar para tarefas diferentes, mas basicamente você precisará de muita e muita RAM.

6. Não se esqueça da page.waitForNavigation


Um dos problemas mais comuns que encontramos são as ações que começam a carregar páginas com o encerramento repentino subsequente de scripts. Isso ocorre porque as ações que acionam o pageload geralmente causam a ingestão de trabalho subsequente. Para contornar o problema, geralmente você precisa chamar a ação de carregamento da página - e imediatamente após aguardar o carregamento.

Por exemplo, esse console.log não funciona em um único local ( consulte a demonstração ):

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

Mas funciona em outro ( veja a demonstração ).

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

Você pode ler mais sobre waitForNavigation aqui . Esta função possui aproximadamente os mesmos parâmetros de interface que page.goto , mas apenas com a parte "wait".

7. Use o Docker para tudo que você precisa.


O Chrome precisa de muitas dependências para funcionar corretamente. Realmente muito. Mesmo depois de instalar tudo, você precisa se preocupar com coisas como fontes e processos fantasmas. Portanto, é ideal usar algum tipo de recipiente para colocar tudo lá. O Docker foi projetado quase especificamente para esta tarefa, porque você pode limitar a quantidade de recursos disponíveis e isolá-lo. Se você deseja criar seu próprio Dockerfile , verifique abaixo todas as dependências necessárias:

 # 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 

E para evitar processos zumbi (algo comum no Chrome), é melhor usar algo como dumb-init para executar corretamente:

 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 

Se você quiser saber mais, dê uma olhada no nosso Dockerfile .

8. Lembre-se de dois tempos de execução diferentes.


É útil lembrar que existem dois tempos de execução do JavaScript (nó e navegador). Isso é ótimo para separar tarefas, mas a confusão inevitavelmente ocorre porque alguns métodos exigirão a passagem explícita de links em vez de elevações.

Por exemplo, considere page.evaluate . Nas entranhas do protocolo, existe uma string literal da função e sua transferência para o Chrome . Portanto, coisas como fechamentos e elevadores não funcionarão . Se você precisar passar algumas referências ou valores para a chamada de avaliação, adicione-os como argumentos que serão processados ​​corretamente.

Assim, em vez de fazer referência ao selector por meio de fechamentos:

 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()); 

Melhor parâmetro de passagem:

 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 pode adicionar um ou mais argumentos à função page.evaluate , pois ela é variável aqui. Certifique-se de aproveitar isso!

O futuro


Estamos incrivelmente otimistas sobre o futuro dos navegadores sem cabeça e toda a automação que eles podem alcançar. Usando ferramentas poderosas, como marionetistas e sem navegador, esperamos que a depuração e execução da automação sem cabeça na produção sejam mais fáceis e rápidas. Em breve, lançaremos o faturamento pré-pago para contas e funções que ajudarão você a lidar melhor com seu trabalho decapitado!

Source: https://habr.com/ru/post/pt413547/


All Articles