続：ParaTest と Docker で PHPUnit を並列実行する試み

少し前に ParaTest と Docker を使って PHPUnit を並列実行してみたのですが、

 - [ParaTest と Docker で PHPUnit を並列実行する試み - Qiita](http://qiita.com/ngyuki/items/c831c7af4a8ac6948c7d "ParaTest と Docker で PHPUnit を並列実行する試み - Qiita")

１プロセスで実行したときよりも十倍近く遅くなりました。

改善点として次のようなものを考えていました。

 - ParaTest を弄って Docker コンテナが並列数だけしか起動しないようにする
 - RAID0 とかでディスクの I/O を良くする
 - もう少しマシな性能(CPU やメモリ)のサーバでやる

今のご時世 AWS などの IaaS を使えばそれなりの性能のサーバがすぐに使えるので AWS EC2 で試してみました。

# 並列にテストを実行するためのスクリプト

前回は `phpunit` コマンドのラッパーでコンテナの起動と削除を行ったため、コンテナの起動と削除が何度も繰り返されてとても時間がかかっていました。

これを改善するためには ParaTest を弄る必要があると思っていたのですが、よく考えれば単に ParaTest の実行前に必要数だけコンテナを起動して `phpunit` コマンドのラッパーでコンテナに入って PHPUnit を実行すればいいだけなので、次のようなスクリプトを作成しました。

*phpunit-docker-start*

```bash
#!/bin/bash

export PHPUNIT_DOCKER_IMAGE=sample
export PHPUNIT_DOCKER_DIR=$(cd "$(dirname "$0")"/..;pwd)
export PHPUNIT_DOCKER_TMP=/tmp/phpunit-docker-$$
export PHPUNIT_DOCKER_PARA=${PHPUNIT_DOCKER_PARA:=4}
export PHPUNIT_DOCKER_ENTER="sudo /usr/local/bin/docker-enter"

function cleanup
{
  echo -e "\nCleanup..."
  for cid in $(ls "$PHPUNIT_DOCKER_TMP"); do
    docker rm -f "$cid"
    rm -f "$PHPUNIT_DOCKER_TMP/$cid"
  done
  rmdir "$PHPUNIT_DOCKER_TMP"
}

trap cleanup 0

[ ! -d "$PHPUNIT_DOCKER_TMP" ] && {
  mkdir -p "$PHPUNIT_DOCKER_TMP"
}

for (( i=0; i<"$PHPUNIT_DOCKER_PARA"; i++ )); do
  cid=$(docker run -d \
    -v /tmp:/tmp:rw \
    -v "$PHPUNIT_DOCKER_DIR:$PHPUNIT_DOCKER_DIR:ro" \
    -w "$PHPUNIT_DOCKER_DIR" "$PHPUNIT_DOCKER_IMAGE" \
    /usr/bin/mysqld_safe
  )
  echo "docker run $cid"
  touch "$PHPUNIT_DOCKER_TMP/$cid"
done

cd "$PHPUNIT_DOCKER_DIR"
vendor/bin/paratest --processes="$PHPUNIT_DOCKER_PARA" --phpunit=$(dirname "$0")/phpunit-docker-enter
```

*phpunit-docker-enter*

```bash
#!/bin/bash

function log
{
  logger -t phpunit-docker "$@"
}

while :; do
  for cid in $(ls "$PHPUNIT_DOCKER_TMP"); do
    {
      flock -n -x $fd && {
        log "enter: $cid"
        $PHPUNIT_DOCKER_ENTER "$cid" /bin/bash -c '
          cd "$1"
          shift
          vendor/bin/phpunit "$@"
        ' -- "$PHPUNIT_DOCKER_DIR" "$@"
        log "leave: $cid"
        exit
      }
    } {fd}< "$PHPUNIT_DOCKER_TMP/$cid"
  done
  sleep 10
done
```

`phpunit-docker-start` が `paratest` コマンドのラッパーで、必要数だけコンテナを起動して `paratest` を実行し、終了時にコンテナを削除します。並列数は環境変数 `PHPUNIT_DOCKER_PARA` で指定できます。

`phpunit-docker-enter` が `phpunit` コマンドのラッパーで、コンテナの中に入って `phpunit` を実行します。

コンテナの中に入るために `docker-enter` というコマンドを使用しています。このコマンドは後述する手順でインストールします。

# EC2 のインスタンスの準備

まずは EC2 のインスタンスを t2.micro で作成します。AMI は `Amazon Linux AMI 2014.09 (HVM)` を使いました。

インスタンスが作成された後に SSH でログインして次のように Docker やら PHP やらをインストールします。

```text
$ sudo su -

# rm -f /etc/localtime && cp -p /usr/share/zoneinfo/Japan /etc/localtime

# yum -y install docker-io
# yum -y install mysql55-server
# yum -y install php55 php55-cli php55-opcache \
  php55-process php55-mysqlnd php55-pdo php55-mbstring php55-xml \
  php55-pecl-xdebug php55-pecl-jsonc

# mkdir -p /path/to/sample
# chown ec2-user. /path/to/sample

# chkconfig docker on
# service docker start

# docker run --rm -v /usr/local/bin:/target jpetazzo/nsenter
```

`docker run --rm -v /usr/local/bin:/target jpetazzo/nsenter` で `docker-enter` という Docker コンテナに入るためのコマンドをインストールしています。Docker をインストーラーとして使うなんて面白いですね。

# プロジェクトのファイルの転送

ローカルのプロジェクトのディレクトリを `rsync` でまるごと転送します。

```text
$ rsync /path/to/sample/ ec2-user@192.0.2.123:/path/to/sample/ -azv
```

# Docker コンテナの準備

前回の内容と変わりありません。

 - [ParaTest と Docker で PHPUnit を並列実行する試み - Qiita](http://qiita.com/ngyuki/items/c831c7af4a8ac6948c7d "ParaTest と Docker で PHPUnit を並列実行する試み - Qiita")

`Dockerfile` でベースイメージを作成し、テストを実行するためにマイグレーションを実行したイメージの作成までを行います。

```
FROM centos:centos6
MAINTAINER ore

RUN yum -y install epel-release rootfiles
RUN rm -f /etc/localtime && cp -p /usr/share/zoneinfo/Japan /etc/localtime

RUN yum -y install http://dev.mysql.com/get/mysql-community-release-el6-5.noarch.rpm
RUN yum -y install mysql-community-server

RUN yum -y install http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
RUN yum -y --enablerepo=remi-php55 install php php-cli php-opcache \
  php-process php-mysqlnd php-pdo php-mbstring php-xml \
  php-pecl-xdebug php-pecl-zip  php-pecl-yaml php-pecl-jsonc

RUN echo -e "[mysqld]\ntmpdir = /var/tmp\n" > /etc/my.cnf
RUN touch /etc/sysconfig/network
RUN mysql_install_db --user=mysql

RUN service mysqld start && mysql -ve " \
  CREATE DATABASE sample_test DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_bin; \
  GRANT ALL ON sample_test.* TO sample@'localhost'; \
  GRANT ALL ON sample_test.* TO sample@'%'; \
" ; service mysqld stop
```

ベースイメージをビルドします。

```text
# docker build -t sample:base .
```

テストを実行するイメージを作成します。

```text
# docker run --name=tmp -v /path/to/sample:/path/to/sample:ro sample:base /bin/bash -c '
  service mysqld start
  cd /path/to/sample
  ./build.sh migrate --test
  service mysqld stop
'
# docker commit tmp sample
# docker rm tmp
```

# t2.micro のコンテナでテストを実行

上手く構築できていることの確認のために t2.micro のままでテストを実行してみます。

まずは前回作成した `phpunit-docker` で、単一プロセスでテストを実行します。

```text
# cd /path/to/sample
# tests/phpunit-docker
```

次の通り結果になりました。

```
Starting mysqld:  [  OK  ]
PHPUnit 3.7.37 by Sebastian Bergmann.

Configuration read from /path/to/sample/phpunit.xml.dist

...............................................................  63 / 720 (  8%)
............................................................... 126 / 720 ( 17%)
............................................................... 189 / 720 ( 26%)
............................................................... 252 / 720 ( 35%)
............................................................... 315 / 720 ( 43%)
............................................................... 378 / 720 ( 52%)
............................................................... 441 / 720 ( 61%)
............................................................... 504 / 720 ( 70%)
............................................................... 567 / 720 ( 78%)
............................................................... 630 / 720 ( 87%)
............................................................... 693 / 720 ( 96%)
.........................

Time: 1.35 minutes, Memory: 111.50Mb

OK (718 tests, 2726 assertions)
```

# t2.micro のコンテナで並列にテストを実行

続いて、新しく作成した `phpunit-docker-start` と `phpunit-docker-enter` で並列数 2 でテストを実行してみます。

```text
# cd /path/to/sample
# PHPUNIT_DOCKER_PARA=2 tests/phpunit-docker-start
```

次の通り結果になりました。

```
docker run 093e(略)
docker run feb6(略)

Running phpunit in 2 processes with tests/phpunit-docker-enter

Configuration read from /path/to/sample/phpunit.xml.dist

...............................................................  63 / 644 (  9%)
............................................................... 126 / 644 ( 19%)
............................................................... 189 / 644 ( 29%)
............................................................... 252 / 644 ( 39%)
............................................................... 315 / 644 ( 48%)
............................................................... 378 / 644 ( 58%)
............................................................... 441 / 644 ( 68%)
........E...................................................... 504 / 644 ( 78%)
..............................................................F 567 / 644 ( 88%)
............................................................... 630 / 644 ( 97%)
............................................................... 693 / 644 (107%)
.........................

Time: 1.71 minutes, Memory: 36.50Mb

There was 1 error:

(略)

FAILURES!
Tests: 718, Assertions: 2720, Failures: 1, Errors: 1.

Cleanup...
093e(略)
feb6(略)
```

普通に実行するより時間がかかりました・・・t2.micro なので仕方ありません。

コケたテストがあるのは、あるテストケースが別のテストケースの実行を前提にしているところがあるためです。並列にテストを実行するようにした弊害ですが、どちらかといえばテストの書き方の問題です。

# t2.micro のコンテナで並列数 10 でテストを実行

「プロセスの作成に失敗した」のようなエラーになりました。t2.micro だと並列数 10 は無理でした。

# m3.2xlarge のコンテナでテストを実行

t2.micro で並列数を上げるのは無理があるので、お財布事情と相談のうえ m3.2xlarge のインスタンスで試しました。

t2.micro のインスタンスから AMI を作成し、その AMI から m3.2xlarge のインスタンスを作成します。

まずは１プロセスでテストを実行してみます。

```text
# tests/phpunit-docker
(略)
Time: 1.28 minutes, Memory: 111.50Mb
(略)
```

t2.micro とあまり変わりませんでした。

# m3.2xlarge のコンテナで並列数 10 でテストを実行

次に並列数 10 でテストを実行してみます。

```text
# PHPUNIT_DOCKER_PARA=10 tests/phpunit-docker-start
docker run e642(略)
docker run 7afc(略)
docker run bd6d(略)
docker run d7af(略)
docker run 599e(略)
docker run 2278(略)
docker run 464b(略)
docker run 960b(略)
docker run 1789(略)
docker run 84d8(略)

Running phpunit in 10 processes with tests/phpunit-docker-enter

Configuration read from /path/to/sample/phpunit.xml.dist

...............................................................  63 / 644 (  9%)
............................................................... 126 / 644 ( 19%)
............................................................... 189 / 644 ( 29%)
............................................................... 252 / 644 ( 39%)
............................................................... 315 / 644 ( 48%)
............................................................... 378 / 644 ( 58%)
............................................................... 441 / 644 ( 68%)
............................................................... 504 / 644 ( 78%)
......E...................................F.................... 567 / 644 ( 88%)
............................................................... 630 / 644 ( 97%)
............................................................... 693 / 644 (107%)
.........................

Time: 28.5 seconds, Memory: 36.50Mb

There was 1 error:

(略)

FAILURES!
Tests: 718, Assertions: 2721, Failures: 1, Errors: 1.

Cleanup...
1789(略)
2278(略)
464b(略)
599e(略)
7afc(略)
84d8(略)
960b(略)
bd6d(略)
d7af(略)
e642(略)
```

２倍以上早くなりました。さすが m3.2xlarge は格が違いました。

# m3.2xlarge のコンテナで並列数 20 でテストを実行

勢い余って並列数 20 で実行してみました。

```text
# PHPUNIT_DOCKER_PARA=20 tests/phpunit-docker-start
(略)
Time: 36.5 seconds, Memory: 37.25Mb
(略)
```

並列数 10 よりも遅くなりました。このインスタンスではこれ以上並列数を上げても早くはならなさそうです。

# インスタンスストアでストライピング

次に、もっとディスクを早くするために AWS のインスタンスストアを使ってみました。

m3.2xlarge だとインスタンスストアが２つあったので、LVM でストライピングします。

```text
# umount /dev/xvdb
# pvcreate /dev/xvdb /dev/xvdc
# vgcreate vg0 /dev/xvdb /dev/xvdc
# lvcreate -L 40G -i 2 -n docker vg0
# mkfs.ext4 /dev/vg0/docker
# service docker stop
# mount -t ext4 /dev/vg0/docker /mnt
# cp -a /var/lib/docker/* /mnt/
# umount /mnt
# mount -t ext4 /dev/vg0/docker /var/lib/docker
# service docker start
```

# m3.2xlarge のインスタンスストアなコンテナでテストを実行

まずは１プロセスでテストを実行してみます。

```text
# tests/phpunit-docker
(略)
Time: 2.73 minutes, Memory: 111.50Mb
(略)
```

EBS よりも遅くなりました。

# m3.2xlarge のインスタンスストアなコンテナで並列数 10 でテストを実行

続いて、並列数 10 でテストを実行してみました。

```text
# PHPUNIT_DOCKER_PARA=10 tests/phpunit-docker-start
(略)
Time: 1.74 minutes, Memory: 36.50Mb
(略)
```

１プロセスで実行するよりは早いですが、やっぱり EBS よりも遅くなりました。

EBS よりインスタンスストアの方が I/O 性能は良いと思っていたのですが、そういうわけではないのでしょうかね？ あるいはストライピングやマウントのやり方がまずかったのかもしれません。

# m3.2xlarge の tmpfs なコンテナでテストを実行

並列数を増やしてもディスクがボトルネックになってあまり早くならないのかと思ったので、ダメ元で `/var/lib/docker` を tmpfs にしてみました。

まず、現在のサイズを確認します。

```text
# du -sh /var/lib/docker/
1.5G    /var/lib/docker/
```

1.5G しかなかったので、8G ぐらいあれば十分でしょう。

```text
# service docker stop
# umount /var/lib/docker
# mkdir /docker
# mount -t tmpfs -o size=8G tmpfs /docker
# cp -a /var/lib/docker/* /docker
# mount --bind /docker /var/lib/docker
# service docker start
```

まずは１プロセスで実行してみます。

```text
# tests/phpunit-docker
(略)
Time: 45.86 seconds, Memory: 111.75Mb
(略)
```

さすがに速いです、EBS とくらべて倍ぐらい速いです。

次に並列数 10 で実行してみます。

```text
# PHPUNIT_DOCKER_PARA=10 tests/phpunit-docker-start
(略)
Time: 17.48 seconds, Memory: 36.50Mb
(略)
```

さらに早くなりました、当たり前な気もしますがすごいです。

# まとめ

当初の私のローカルの VM で１分半ぐらいかかっていたテストが、最終的に 18 秒ぐらいにまで短縮することができました。もっとも、AWS のそれなりのインスタンスを使ったり tmpfs にしてみたりとかなり無理矢理感はありますが。

さすがにこのためだけに１時間で100円は辛いですが、メモリとCPUコア数さえどうにか出来れば ParaTest+Docker でテストを並列に実行する試みもアリかもしれません。

# c3.8xlarge の tmpfs なコンテナでテストを実行

限界が見たくなったので c3.8xlarge で試してみました。

やることは m3.2xlarge のときと変わりませんが、コンテナ数が増えるので tmpfs のサイズを 20G にしています。あと、１時間で200円ぐらいチャリンチャリンします。

```text
# service docker stop
# mkdir /docker
# mount -t tmpfs -o size=20G tmpfs /docker
# cp -a /var/lib/docker/* /docker
# mount --bind /docker /var/lib/docker
# service docker start
```

まずは１プロセスで実行してみます。

```text
# tests/phpunit-docker
(略)
Time: 44.55 seconds, Memory: 111.75Mb
(略)
```

まぁ変わらないですね。

次に並列数 20 で試してみます。

```text
# PHPUNIT_DOCKER_PARA=20 tests/phpunit-docker-start
(略)
Time: 6.36 seconds, Memory: 36.25Mb
(略)
```

もうここまでくるとコンテナの削除に一番時間がかかっています（上で表示されている時間にはコンテナの起動と削除の時間は含まれていません）。

次に並列数 30 で実行・・・してみたのですが上手く動きませんでした。私の作成したスクリプトに何かしら問題があるか、もしくはなにか別の原因があるのかもしれません。