Puppeteer を用いたアプリケーションから Chrome を切り離す
Puppeteer を用いた web スクレイピングアプリケーションの起動中に CPU/memory 使用量が荒ぶるので何とかしたいなあと思って最近対策を考えています
Puppeteer は内部で Chrome (Chromium) を起動しているので、スクレイピングのロジック部分と Chrome を分離すればリソース割り当てがやりやすくなりそうなので挑戦してみます
書いたコードはこちら↓*1
実験台
ここに Puppeteer を使った適当なアプリケーションがあります(Puppeteer 公式サイトのサンプルをコピペしてちょっと手を加えたもの)
# index.ts import * as puppeteer from 'puppeteer'; import { setTimeout } from 'timers/promises'; const getChromeDevPage = async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://developer.chrome.com/'); // Set screen size await page.setViewport({width: 1080, height: 1024}); // Type into search box await page.type('.search-box__input', 'automate beyond recorder'); // Wait and click on first result const searchResultSelector = '.search-box__link'; await page.waitForSelector(searchResultSelector); await page.click(searchResultSelector); // Locate the full title with a unique string const textSelector = await page.waitForSelector( 'text/Customize and automate' ); const fullTitle = await textSelector!.evaluate(el => el.textContent); // Print the full title console.log('The title of this blog post is "%s".', fullTitle); await browser.close(); } (async () => { while (true) { try { await getChromeDevPage(); } catch (e) { console.error('failue', { e }); break; } await setTimeout(10 * 1000); } })();
「developer.chrome.com にアクセスし、 "Customize and automate ~" というブログ記事のタイトルを抽出して標準出力に吐き出す」という動作を 10秒ごとに行います
今回はこのアプリケーションに手を加えて
- puppeteer-core でリファクタリング
- Node.js 部分と Chrome を別々の Docker image にして docker compose で動かす
- kubernetes 環境で動かす
をやっていきます
Puppeteer-core
puppeteer-core は Chromium が同梱されていない計量版の Puppeteer です
puppeteer-core
の切り替えは細かいところを省くと
1. package.json を書き換える
"dependencies": { - "puppeteer": "19.5.2" + "puppeteer-core": "19.5.2" },
2. import を書き換える
- import * as puppeteer from 'puppeteer'; + import * as puppeteer from 'puppeteer-core';
3. puppeteer.launch
を puppeteer.connect
に変更
- const browser = await puppeteer.launch(someOptions); + const browserURL = ... # リモートブラウザの URL + const browser = await puppeteer.connect({ browserURL, ...someOptions });
の三か所を変えればとりあえず完了です
先のコードを書き換えるとこんな感じ
# index.ts import * as puppeteer from 'puppeteer-core'; import { setTimeout } from 'timers/promises'; const getChromeDevPage = async (browserURL: string) => { const browser = await puppeteer.connect({ browserURL }); const page = await browser.newPage(); <<略>> await browser.close(); }; (async () => { # 環境変数からリモートブラウザの URL を受け取る const browserURL = process.env.BROWSER_ADDR; if (browserURL === undefined) { console.error('Environment variable `BROWSER_ADDR` is not set.'); return; } while (true) { try { await getChromeDevPage(browserURL); } catch (e) { console.error('failue', { e }); break; } await setTimeout(10 * 1000); } })();
Docker image を焼いて動かす
puppeteer-core
で書き換えたコードを実際に動かしてみましょう
リモートブラウザは browserless/chrome の Docker image を使います*2
適当に Dockerfile を用意し、こんな感じに docker-compose.yaml
を書きます。Dockerfile の内容は GitHub のコードを見てください*3
version: '3.7' services: chrome: image: browserless/chrome ports: - 3000:3000 app: build: . environment: - BROWSER_ADDR=http://chrome:3000
browserless/chrome
の起動にすこし時間がかかるため、先に chrome
を起動させてから少し待って app
を動かします
$ docker compose up -d chrome $ docker compose up app [+] Building 70.1s (13/13) FINISHED [+] Running 1/1 Attaching to puppeteer-separate-app-1 puppeteer-separate-app-1 | The title of this blog post is "Customize and automate user flows beyond Chrome DevTools Recorder". puppeteer-separate-app-1 | The title of this blog post is "Customize and automate user flows beyond Chrome DevTools Recorder". puppeteer-separate-app-1 | The title of this blog post is "Customize and automate user flows beyond Chrome DevTools Recorder". ...
10 秒ごとに「The title of this blog post is "Customize and automate user flows beyond Chrome DevTools Recorder".」が出力されます
kubernetes で動かす
実運用を想定して kubernetes で動かしてみます
まず docker image をビルドしておきます。今回は適当に scraping:1.0.0
という名前を付けて
docker build -t scraping:1.0.0 .
kubernetes の cluster は kind を使って作ります
kind で cluster を作成します
$ kind create cluster $ kind load docker-image scraping:1.0.0 # 手元でビルドした image を kind の cluster で使えるようにする
kubernetes の manifest を書きます
# chrome.yaml apiVersion: v1 kind: Namespace metadata: name: chrome-ns --- apiVersion: apps/v1 kind: Deployment metadata: namespace: chrome-ns name: chrome labels: app: chrome spec: selector: matchLabels: app: chrome replicas: 1 template: metadata: labels: app: chrome spec: containers: - name: chrome image: browserless/chrome ports: - name: chrome containerPort: 3000 protocol: TCP restartPolicy: Always --- apiVersion: v1 kind: Service metadata: namespace: chrome-ns name: chrome-svc spec: ports: - name: chrome targetPort: chrome port: 3000 selector: app: chrome
# scraping.yaml apiVersion: v1 kind: Namespace metadata: name: scraping --- apiVersion: apps/v1 kind: Deployment metadata: name: scraping namespace: scraping labels: app: scraping spec: selector: matchLabels: app: scraping replicas: 2 template: metadata: labels: app: scraping spec: containers: - name: scraping image: scraping:1.0.0 env: - name: BROWSER_ADDR value: "http://chrome-svc.chrome-ns.svc.cluster.local:3000" restartPolicy: Always
cluster にデプロイして scraping のログを見ると「The title of this blog post is "Customize and automate user flows beyond Chrome DevTools Recorder".」が出力されています
$ kubectl apply -f chrome.yaml $ kubectl apply -f scraping.yaml # スクレイピングが何回か実行されるまで数十秒待つ $ kubectl logs deploy/scraping -c scraping -n scraping Found 2 pods, using pod/scraping-74b5d47b66-psqbj The title of this blog post is "Customize and automate user flows beyond Chrome DevTools Recorder". The title of this blog post is "Customize and automate user flows beyond Chrome DevTools Recorder". The title of this blog post is "Customize and automate user flows beyond Chrome DevTools Recorder". ...
kubernetes 上で動かせていることが確認できました
上記の例では Chrome の pod 1台に対してスクレイピングアプリの pod を2台動かしているので、ブラウザ同梱の Puppeteer を 2台動かすのと比較して Chrome 1台分のリソースを削減できていそうですね(雑な単純計算ですが……)
まとめ
本稿では puppeteer を使ったアプリケーションから Chromium を分離してみました
Chrome を分離することで以下のような利点がありそうです
- Chrome を分離することで kubernetes の node や CPU/memory の配置に改善できる
- docker build が楽になる
まだ構想段階なので、勤務先*5のアプリケーションで実際に運用してみて使用感を確かめたいです*6
謝辞:Chrome 分離構想を提案してくれた id:hiroqn さん id:taketo957 さんに感謝