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

## 概要

Google Chrome に実装された --headless モード の Node.js API を提供してくれる npm パッケージの [puppeteer](https://github.com/GoogleChrome/puppeteer) を使って[JSONで指定したウェブサイトのスクリーンショットを撮影するスクリプトを書いた](https://github.com/windyakin/kosen-website-crawler)。一通り動きそうなことを確認したので、どうせならとおもってこのスクリプトを 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` (ギガ) が使える。

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

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

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



## 原因調査と解決

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

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

* [Node.jsで捕捉されなかったPromise内の例外をスタックトレースで表示する](http://qiita.com/syuilo/items/0800d7e44e93203c7285)

表示された 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 のドキュメント](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md) を読んでいると `.launch` を呼び出す時に `dumpio` オプションを渡すことによって Chrome が吐き出す標準エラー出力を表示できるようなので行ってみることにした。

```js
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 35以降が安定して使えない場合の対処](http://technology-memo.seesaa.net/article/404969733.html)

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

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

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

Docker コンテナ上で shm サイズの変更はイメージのビルド時**ではなく**、コンテナの起動時に [docker run 時のオプションで指定できるという記述もみつけたので](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) それに従って指定し起動すると今までエラーになっていたウェブサイトについても特に問題なくスクリーンショットを撮影できるようになった。

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