Chrome
docker
puppeteer

Docker上のpuppeteerがPage crashしてしまうときはshmサイズを疑う

概要

Google Chrome に実装された --headless モード の Node.js API を提供してくれる npm パッケージの puppeteer を使ってJSONで指定したウェブサイトのスクリーンショットを撮影するスクリプトを書いた。一通り動きそうなことを確認したので、どうせならとおもってこのスクリプトを Docker 化していると、ある特定のウェブサイトを撮影した時に以下のようなエラーが出るようになった。

(node:16) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Page crashed!
(node:16) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

ちなみにこのエラーは Docker コンテナを動かしているホストマシン上で直に動作させた時は出ておらず、 Docker コンテナ上でスクリプトを動作させた場合にのみ発生するエラーであった。

この記事では原因調査方法とその解決法について記述する。

解決法

先におおまかな原因と解決法を記述すると、原因としてはウェブサイトの取得時に一時ファイル領域の /dev/shm が足りずに Chrome がクラッシュしていたことであった。解決策としては Docker コンテナ立ち上げ時に /dev/shm のサイズを指定するオプションがあるので、あらかじめ大きめのサイズを指定することで回避できることが確認できた。

shm サイズの指定方法は、コマンド上から直接 docker run する場合であれば、 --shm-size オプションで指定する。指定時にはSI単位の m (メガ) 、 g (ギガ) が使える。

% docker run -it --shm-size=256m [image]

docker-compose から起動しているのであれば、コンテナのオプションに shm_size を指定すればよい。

version: '3'
services:
  service_name:
    image: '[image]'
    shm_size: 256m

原因調査と解決

Node.js 側の Stack trace を表示する

puppeteer のスクリーンショット実行部分については非同期処理を行っていた。非同期処理時に出たエラーについては、 Stack trace が表示されないので以下の方法で Stack trace を表示する。

表示された Stack trace の内容は以下の通り。

Error: Page crashed!
    at Page._onTargetCrashed (/usr/src/app/node_modules/puppeteer/lib/Page.js:91:24)
    at Session.Page.client.on.event (/usr/src/app/node_modules/puppeteer/lib/Page.js:87:56)
    at emitOne (events.js:115:13)
    at Session.emit (events.js:210:7)
    at Session._onMessage (/usr/src/app/node_modules/puppeteer/lib/Connection.js:202:12)
    at Connection._onMessage (/usr/src/app/node_modules/puppeteer/lib/Connection.js:98:19)
    at emitOne (events.js:115:13)
    at WebSocket.emit (events.js:210:7)
    at Receiver._receiver.onmessage (/usr/src/app/node_modules/ws/lib/WebSocket.js:143:47)
    at Receiver.dataMessage (/usr/src/app/node_modules/ws/lib/Receiver.js:389:14)
    at Receiver.getData (/usr/src/app/node_modules/ws/lib/Receiver.js:330:12)
    at Receiver.startLoop (/usr/src/app/node_modules/ws/lib/Receiver.js:165:16)
    at Receiver.add (/usr/src/app/node_modules/ws/lib/Receiver.js:139:10)
    at Socket._ultron.on (/usr/src/app/node_modules/ws/lib/WebSocket.js:139:22)
    at emitOne (events.js:115:13)
    at Socket.emit (events.js:210:7)

あまりよくわからないが、見る限りでは headless で立ち上げた Chrome と通信するための Socket 通信部分でなにかエラーが起きているようだということがわかる。

Chrome 側のエラー出力を表示する

とはいえ puppeteer の実装を疑ったりするのは面倒だし、そもそも他のサイトではちゃんと動いているわけで、特定のサイトだけで動かなくなるというのは puppeteer が悪いといよりかは Chrome がなにがしかのトラブルに巻き込まれている可能性が高いと思われる。

とはいえ Chrome の動作については puppeteer によって抽象化されているのでどうにかエラーを知ることができないかと puppeteer のドキュメント を読んでいると .launch を呼び出す時に dumpio オプションを渡すことによって Chrome が吐き出す標準エラー出力を表示できるようなので行ってみることにした。

const browser = await Puppeteer.launch({ dumpio: true });

すると以下の出力を得ることができた。

Received signal 7 BUS_ADRERR 7f4bf7821000
#0 0x00f24244d657 <unknown>
#1 0x00f24244d1cf <unknown>
#2 0x7f4c10c2a890 <unknown>
#3 0x00f240e00a20 <unknown>
#4 0x00f2426e8391 <unknown>
#5 0x00f2425803d9 <unknown>
#6 0x00f24257fcfb <unknown>
#7 0x00f2425379ef <unknown>
#8 0x00f2426e1386 <unknown>
#9 0x00f2425216f3 <unknown>
#10 0x00f24251f61f <unknown>
#11 0x00f2427cc463 <unknown>
#12 0x00f24251f61f <unknown>
#13 0x00f242a7385d <unknown>
#14 0x00f242a72ce5 <unknown>
#15 0x00f242a72ce5 <unknown>
#16 0x00f244da14f9 <unknown>
#17 0x00f242e71732 <unknown>
#18 0x00f242e71653 <unknown>
#19 0x00f242e70ba7 <unknown>
#20 0x00f242e6ed07 <unknown>
#21 0x00f242ea03a3 <unknown>
#22 0x00f244907197 <unknown>
#23 0x00f2449062da <unknown>
#24 0x00f2424a8652 <unknown>
#25 0x00f2424a3d33 <unknown>
#26 0x7f4c10c23064 start_thread
#27 0x7f4c0aa3562d clone
  r8: 00000000000000f8  r9: 00000000000000f8 r10: 0000000000000100 r11: 00000000000003f7
 r12: 0000000000000400 r13: 0000000000000000 r14: 0000000000000100 r15: 00000000ffffffff
  di: 00007f4bf7821000  si: 00000000ffffffff  bp: 00007f4bf7821000  bx: 00000000000000c5
  dx: 0000000000000100  ax: 00007f4bf7821000  cx: 0000000000000140  sp: 00007f4bfecabd98
  ip: 000000f240e00a20 efl: 0000000000010202 cgf: 002b000000000033 erf: 0000000000000006
 trp: 000000000000000e msk: 0000000000000000 cr2: 00007f4bf7821000
[end of stack trace]

どうやらメモリ関連のエラーっぽいことは分かるのだが、何が原因かはあまりよくわからない。唯一の手がかりになりそうな先頭行のエラー文で調べると以下の記事が引っ掛かった。

詳細については元記事を参照してほしいのだが、要約すると Linux 上の Chrome が使用する一時ファイル領域 (/dev/shm) の容量が足りなくてクラッシュしてしまうらしい。

Docker で shm サイズを変更するには

ウェブサイトによってどれぐらい必要になるのかは不明だが、一部の装飾華美なウェブサイトなどでは Docker 側がデフォルトで確保する一時ファイル領域の 64MB を超えてしまうらしく Chrome がクラッシュしてしまうことがわかった。

Docker コンテナ上で shm サイズの変更はイメージのビルド時ではなく、コンテナの起動時に docker run 時のオプションで指定できるという記述もみつけたので それに従って指定し起動すると今までエラーになっていたウェブサイトについても特に問題なくスクリーンショットを撮影できるようになった。

% docker run -it --shm-size=256m [image]