現代のウェブにおいて、古典的なスクレイピング手法は死んだも同然だ。静的なHTMLは消え去り、クライアントサイドレンダリングが必須となり、ボット検知がデフォルトで備わっている。
ローカルのスクリプトがRAMを食いつぶし、ランダムにクラッシュする現象にパッチを当て続けること数ヶ月。私はついにその「対症療法」をやめることにした。そして、Chromiumを単なるライブラリとしてではなく、独立したコンピュートサービスとして扱う 「Browserless」アーキテクチャ へと移行した。
本稿では、そのアーキテクチャ、具体的な設定、そしてスケーリングから得られた教訓を共有する。
1. そもそも Browserless とは何か
Browserless は、一言で言えば 「Dockerコンテナ内でステートレスなリモートサービスとして稼働するヘッドレスChromium」 だ。WebSocket経由で操作を行う。
APIサーバー上に直接 Chrome をインストールする(これはゾンビプロセスやメモリリークの温床となる)代わりに、Puppeteer や Playwright をリモート接続させ、処理を実行し、切断するという形をとる。
これにより、使い捨て(Ephemeral)なセッション、クラッシュの隔離、そして Docker による自動クリーンアップの恩恵を受けられる。
最も重要な設定項目
Chrome はクラッシュした際、ゾンビ化したサブプロセスを残しやすい。この修正方法は拍子抜けするほど単純だ。Docker の init プロセスを有効にすることで、死んだ Chrome のサブプロセスを OS が自動的に回収(Reap)できるようにすればいい。
services:
browserless:
image: ghcr.io/browserless/chromium:latest
init: true # ← これが重要
shm_size: '1gb'
environment:
- MAX_CONCURRENT_SESSIONS=10
- PREBOOT_CHROME=true
たったこれだけのフラグで、長期稼働時のメモリ肥大化(Memory Creep)は完全に解消された。
2. なぜローカル実行は失敗したのか
問題はスクレイピングのロジックではなく、リソースの競合(Resource Contention) にあった。
ローカルで Puppeteer インスタンスを動かすと、1つあたり約300〜500MBのメモリを消費する。同時実行数が増えると、Chrome が落ちるよりも先にワークフローエンジン本体が OOM (Out of Memory) で死んでしまう。ブラウザのクラッシュが、システム全体のクラッシュを招いていたのだ。
ブラウザはアプリのランタイムの一部ではなく、「使い捨てのワーカー」として切り離す必要があった。
3. アーキテクチャ
責務を以下の3つのコンテナに分離した:
- Nginx – ゲートウェイ、SSL終端、レート制限
- Packer (Node.js) – キュー管理 + ブラウザのライフサイクル制御
- Browserless – 隔離された Chromium ワーカー
ブラウザコンテナは完全に使い捨てとして扱う。メモリがスパイクしたりセッションがハングしたりしても、キューに入っている他のジョブに影響を与えることなく再起動できる。
最小限の docker-compose 構成は以下の通りだ:
services:
nginx:
image: nginx:alpine
depends_on:
- packer
packer:
build: ./browser-packer
environment:
- BROWSERLESS_WS=ws://browserless:3000
- PACKER_MAX_CONCURRENT=10
depends_on:
- browserless
browserless:
image: ghcr.io/browserless/chromium:latest
init: true
4. ミドルウェア:ブラウザのための交通整理
Browserless をそのまま(Rawで)使うのは、トラフィックが急増するまではうまくいく。しかし Chromium はキューイングを行わない。死ぬまでプロセスを生成し続けてしまう。
そこで、「Packer」ミドルウェアによって以下を実装し、この問題を解決した:
- FIFO(先入先出)キュー
- 同時実行数の上限設定(Concurrency caps)
- セッションの強制クリーンアップ
const browser = await puppeteer.connect({
browserWSEndpoint: process.env.BROWSERLESS_WS
});
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
} finally {
// 必ず閉じる
await browser.close();
}
これにより、1 vCPU あたり約5セッションを安全に実行できている。
5. スピードは「インターネットを遮断する」ことから生まれる
スクレイピングにおける無駄の大部分は、実際には使わないアセット(画像など)のロードにある。
これらがダウンロードされる 前 に、ネットワークレベルで遮断(Intercept)する。
await page.setRequestInterception(true);
page.on('request', (request) => {
const type = request.resourceType();
const url = request.url();
const blockedTypes = ['image','media','font','stylesheet'];
const blockedDomains = ['google-analytics','doubleclick','facebook','ads','tracking','pixel'];
if (
blockedTypes.includes(type) ||
blockedDomains.some(d => url.includes(d))
) return request.abort();
request.continue();
});
2 vCPU インスタンスでの効果は以下の通り:
- 平均ロード時間:10秒 → 3〜4秒
- セッションあたりのCPU使用率:約40%削減
- 安定した同時実行数:2倍に増加
6. アンチ検知(Anti-Detection):矛盾をなくす
検知エンジンが見ているのは「ボットかどうか」だけではない。「矛盾(Inconsistencies)」 を見ているのだ。
Stealth プラグインを使って、ブラウザ内部の情報の漏れ(Leaks)を修正する:
- Webdriver フラグの隠蔽
- WebGLベンダーと User-Agent の整合性確保
- Navigator プロパティの正規化
ゴールは「人間を装う」ことではなく、「技術的な整合性を保つ」ことにある。
ミドルウェア層での Stealth 注入
Browserless のデフォルト設定だけに頼るのではなく、ブラウザセッションが作られる前のミドルウェア層で Stealth プラグインを直接注入する。
これにより、上流の設定に関わらず、すべての接続が同じ回避能力を持つことが保証される。
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
Puppeteer 層で Chromium にパッチを当てることで、セッション間のフィンガープリントのズレ(Drift)を排除し、パイプライン全体で一貫した検知回避を実現した。
7. 残されたピース:プロキシ
データセンターのIPアドレスを使っている限り、どんなに完璧なフィンガープリントも無意味だ。
次のステップは、WebSocket 接続時に回転(Rotating)する住宅用プロキシ(Residential Proxies)を直接組み込むことだ。
browserWSEndpoint:
'ws://browserless:3000?--proxy-server=http://user:pass@proxy:port'
これにより、IPバンによってキューが詰まる事態を防ぐことができる。
8. 最後に
最大の転換点は、「ブラウザはインフラである」 と認識したことだった。
Chromium を、キュー、制限、再起動、ネットワーク制御を備えた「サービス」として扱った瞬間、スクレイピングの脆弱さは消え去った。スケーリングは予測可能なものになった。
スクリプトはクライアントになり、ブラウザはコンピュート層になった。
この「逆転の発想」こそが、システムを安定させた最大の要因だ。