Erleben Sie 2 Millionen kopflose Sitzungen

Veröffentlicht am 4. Juni 2018 im browserlosen Unternehmensblog.

Wir freuen uns, Ihnen mitteilen zu können, dass wir kürzlich die Grenze von zwei Millionen Sitzungen überschritten haben! Dies sind Millionen von generierten Screenshots, gedruckten PDFs und getesteten Websites. Wir haben fast alles getan, was Sie sich mit einem kopflosen Browser vorstellen können.

Es ist zwar schön, einen solchen Meilenstein zu erreichen, aber auf dem Weg gab es eindeutig viele Überschneidungen und Probleme. Aufgrund des enormen Verkehrsaufkommens möchte ich einen Schritt zurücktreten und allgemeine Empfehlungen für den Start von Headless-Browsern (und Puppenspielern ) in der Produktion abgeben .

Hier sind einige Tipps.

1. Verwenden Sie überhaupt keinen kopflosen Browser




Headless Chrome-Ressourcenverbrauch


Starten Sie den Browser nach Möglichkeit keinesfalls im Headless-Modus . Insbesondere auf derselben Infrastruktur wie Ihre Anwendung (siehe oben). Der kopflose Browser ist unvorhersehbar, gefräßig und züchtet wie Mr. Misix von Rick und Morty. Fast alles, was ein Browser tun kann (außer das Interpolieren und Ausführen von JavaScript), kann mit einfachen Linux-Tools ausgeführt werden. Die Cheerio-Bibliotheken und andere bieten eine elegante Node-API zum Abrufen von Daten mit HTTP-Anforderungen und zum Scraping, wenn dies Ihr Ziel ist.

Sie können beispielsweise eine Seite aufnehmen (vorausgesetzt, es handelt sich um eine Art HTML) und sie mit einfachen Befehlen wie den folgenden verschrotten:

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

Offensichtlich deckt das Skript nicht alle Anwendungsfälle ab. Wenn Sie diesen Artikel lesen, müssen Sie höchstwahrscheinlich einen kopflosen Browser verwenden. Also fangen wir an.

2. Starten Sie einen Headless-Browser nicht unnötig


Wir haben zahlreiche Benutzer getroffen, die versuchen, den Browser auch dann am Laufen zu halten, wenn er nicht verwendet wird (mit offenen Verbindungen). Obwohl dies eine gute Strategie ist, um eine Sitzung zu beschleunigen, stürzt sie in wenigen Stunden ab. Vor allem, weil Browser gerne alles hintereinander zwischenspeichern und nach und nach Speicherplatz verbrauchen. Sobald Sie den Browser nicht mehr intensiv nutzen, schließen Sie ihn sofort!

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

In browserless beheben wir diesen Fehler normalerweise für Benutzer selbst, indem wir immer einen Timer für die Sitzung einstellen und den Browser schließen, wenn WebSocket getrennt wird. Wenn Sie jedoch unseren Service oder das Backup-Docker-Image nicht nutzen, stellen Sie sicher, dass Sie den Browser automatisch schließen, da dies unangenehm ist, wenn alles mitten in der Nacht fällt.

3. Ihre Freundeseite


Seien Sie vorsichtig mit Transpilern wie Babel oder Typoskript, da sie gerne Hilfsfunktionen erstellen und davon ausgehen, dass sie mit Verschlüssen zugänglich sind. Das heißt, der .evaluate-Rückruf funktioniert möglicherweise nicht richtig.

Puppenspieler hat viele nette Methoden, wie das Speichern von DOM-Selektoren und anderen Dingen in einer Knotenumgebung. Obwohl dies sehr praktisch ist, können Sie sich leicht in den Fuß schießen, wenn etwas auf der Seite diesen DOM-Knoten zur Mutation zwingt. Das mag nicht so cool sein, aber in Wirklichkeit ist es besser, die gesamte Arbeit auf der Browserseite im Kontext des Browsers zu erledigen. Dies bedeutet normalerweise, dass page.evaulate für alle zu erledigenden Arbeiten page.evaulate .

Zum Beispiel anstelle von so etwas ( drei asynchrone Aktionen):

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

Es ist besser, dies zu tun (eine asynchrone Aktion):

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

Ein weiterer Vorteil beim Umschließen von Aktionen in einen evaluate ist die Portabilität: Dieser Code kann in einem Browser ausgeführt werden, um zu überprüfen, anstatt zu versuchen, den Knotencode neu zu schreiben. Natürlich wird immer empfohlen, einen Debugger zu verwenden, um die Entwicklungszeit zu verkürzen.

Eine einfache Faustregel ist, die Anzahl der await oder then im Code zu zählen. Wenn es mehr als eine gibt, ist es wahrscheinlich am besten, den Code innerhalb des Aufrufs page.evaluate . Der Grund dafür ist, dass alle asynchronen Aktionen zwischen der Node-Laufzeit und dem Browser hin und her gehen, was eine konstante JSON-Serialisierung und -Deserialisierung bedeutet. Obwohl nicht so viel analysiert wird (da alles von WebSockets unterstützt wird), nimmt es dennoch Zeit in Anspruch, die besser für etwas anderes ausgegeben wird.

4. Parallelisieren Sie Browser, nicht Webseiten


Wir haben festgestellt, dass das Starten eines Browsers nicht gut ist und wir dies nur im Notfall tun müssen. Der nächste Tipp ist, nur eine Sitzung pro Browser auszuführen. In der Realität ist es zwar möglich, Ressourcen durch Parallelisierung der Arbeit durch pages zu sparen, aber wenn eine Seite ausfällt, kann der gesamte Browser abstürzen. Darüber hinaus kann nicht garantiert werden, dass jede Seite perfekt sauber ist (Cookies und Speicher können, wie wir sehen, zu Kopfschmerzen führen).

Stattdessen:

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

Mach das besser:

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

Jede neue Browserinstanz erhält ein sauberes --user-data-dir ( sofern nicht anders angegeben ). Das heißt, es wird vollständig als neue Sitzung verarbeitet. Wenn Chrome aus irgendeinem Grund abstürzt, werden keine anderen Sitzungen damit abgerufen.

5. Begrenzung der Warteschlange und der Parallelität


Eines der Hauptmerkmale von browserless ist die Möglichkeit, Parallelisierung und Warteschlange sauber einzuschränken. Client-Anwendungen führen also nur puppeteer.connect , denken jedoch nicht an die Implementierung der Warteschlange. Dies verhindert eine Vielzahl von Problemen, vor allem bei gleichzeitigen Chrome-Instanzen, die alle verfügbaren Ressourcen Ihrer Anwendung verschlingen.

Der beste und einfachste Weg ist, unser Docker-Image mit den erforderlichen Parametern auszuführen:

 # 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 

Dies begrenzt die Anzahl gleichzeitiger Anforderungen auf zehn (einschließlich Debugging-Sitzungen und mehr). Die Warteschlange wird durch die Variable MAX_QUEUE_LENGTH . In der Regel können Sie ungefähr 10 gleichzeitige Anforderungen pro Gigabyte Speicher ausführen. Der Prozentsatz der CPU-Auslastung kann für verschiedene Aufgaben variieren, aber im Grunde benötigen Sie viel und viel RAM.

6. Vergessen Sie nicht page.waitForNavigation


Eines der häufigsten Probleme, auf die wir gestoßen sind, sind die Aktionen, die das Laden von Seiten mit der anschließenden plötzlichen Beendigung von Skripten starten. Dies liegt daran, dass die Aktionen, die das pageload von pageload auslösen, häufig zum Verschlucken nachfolgender Arbeiten führen. Um das Problem zu umgehen, müssen Sie normalerweise die Aktion zum Laden von Seiten aufrufen - und unmittelbar danach auf das Laden warten.

Zum Beispiel funktioniert eine solche console.log nicht an einem Ort ( siehe Demo ):

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

Aber es funktioniert in einem anderen ( siehe Demo ).

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

Weitere Informationen zu waitForNavigation finden Sie hier . Diese Funktion hat ungefähr die gleichen Schnittstellenparameter wie page.goto , jedoch nur mit dem Teil "wait".

7. Verwenden Sie Docker für alles, was Sie brauchen.


Chrome benötigt viele Abhängigkeiten, um ordnungsgemäß zu funktionieren. Wirklich viel. Auch nach der Installation müssen Sie sich um Dinge wie Schriftarten und Phantomprozesse kümmern. Daher ist es ideal, einen Behälter zu verwenden, um alles dort hinein zu stellen. Docker wurde fast speziell für diese Aufgabe entwickelt, da Sie die Menge der verfügbaren Ressourcen begrenzen und isolieren können. Wenn Sie eine eigene Dockerfile erstellen Dockerfile , überprüfen Sie unten alle erforderlichen Abhängigkeiten:

 # 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 

Und um Zombie-Prozesse zu vermeiden (eine häufige Sache in Chrome), ist es besser, etwas wie dummes Init zu verwenden, um richtig zu laufen:

 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 

Wenn Sie mehr wissen möchten, schauen Sie sich unsere Docker-Datei an .

8. Denken Sie an zwei verschiedene Laufzeiten.


Beachten Sie, dass es zwei JavaScript-Laufzeiten gibt (Knoten und Browser). Dies ist ideal für die Trennung von Aufgaben, aber es kommt unweigerlich zu Verwirrung, da einige Methoden eine explizite Verbindungsübergabe anstelle von Hebevorgängen erfordern.

Nehmen page.evaluate zum Beispiel page.evaluate . Tief im Inneren des Protokolls gibt es eine wörtliche Stringifizierung der Funktion und ihrer Übertragung auf Chrome . Daher funktionieren Dinge wie Verschlüsse und Aufzüge überhaupt nicht . Wenn Sie einige Referenzen oder Werte an den Evaluierungsaufruf übergeben müssen, fügen Sie sie einfach als Argumente hinzu, die korrekt verarbeitet werden.

Anstatt den selector über Verschlüsse zu referenzieren:

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

Besserer Parameter übergeben:

 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 können der Funktion page.evaluate ein oder mehrere Argumente page.evaluate , da diese hier variabel ist. Nutzen Sie dies unbedingt!

Die Zukunft


Wir sind unglaublich optimistisch in Bezug auf die Zukunft von Headless-Browsern und die Automatisierung, die sie erreichen können. Mit leistungsstarken Tools wie Puppenspieler und browserlos hoffen wir, dass das Debuggen und Ausführen der kopflosen Automatisierung in der Produktion einfacher und schneller wird. In Kürze werden wir eine Abrechnung für Konten und Funktionen durchführen , mit denen Sie Ihre kopflose Arbeit besser bewältigen können!

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


All Articles