少し前に ParaTest と Docker を使って PHPUnit を並列実行してみたのですが、
1プロセスで実行したときよりも十倍近く遅くなりました。
改善点として次のようなものを考えていました。
- ParaTest を弄って Docker コンテナが並列数だけしか起動しないようにする
- RAID0 とかでディスクの I/O を良くする
- もう少しマシな性能(CPU やメモリ)のサーバでやる
今のご時世 AWS などの IaaS を使えばそれなりの性能のサーバがすぐに使えるので AWS EC2 で試してみました。
並列にテストを実行するためのスクリプト
前回は phpunit
コマンドのラッパーでコンテナの起動と削除を行ったため、コンテナの起動と削除が何度も繰り返されてとても時間がかかっていました。
これを改善するためには ParaTest を弄る必要があると思っていたのですが、よく考えれば単に ParaTest の実行前に必要数だけコンテナを起動して phpunit
コマンドのラッパーでコンテナに入って PHPUnit を実行すればいいだけなので、次のようなスクリプトを作成しました。
phpunit-docker-start
# !/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
# !/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 やらをインストールします。
$ 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
でまるごと転送します。
$ rsync /path/to/sample/ ec2-user@192.0.2.123:/path/to/sample/ -azv
Docker コンテナの準備
前回の内容と変わりありません。
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
ベースイメージをビルドします。
# docker build -t sample:base .
テストを実行するイメージを作成します。
# 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
で、単一プロセスでテストを実行します。
# 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 でテストを実行してみます。
# 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 のインスタンスを作成します。
まずは1プロセスでテストを実行してみます。
# tests/phpunit-docker
(略)
Time: 1.28 minutes, Memory: 111.50Mb
(略)
t2.micro とあまり変わりませんでした。
m3.2xlarge のコンテナで並列数 10 でテストを実行
次に並列数 10 でテストを実行してみます。
# 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(略)
2倍以上早くなりました。さすが m3.2xlarge は格が違いました。
m3.2xlarge のコンテナで並列数 20 でテストを実行
勢い余って並列数 20 で実行してみました。
# PHPUNIT_DOCKER_PARA=20 tests/phpunit-docker-start
(略)
Time: 36.5 seconds, Memory: 37.25Mb
(略)
並列数 10 よりも遅くなりました。このインスタンスではこれ以上並列数を上げても早くはならなさそうです。
インスタンスストアでストライピング
次に、もっとディスクを早くするために AWS のインスタンスストアを使ってみました。
m3.2xlarge だとインスタンスストアが2つあったので、LVM でストライピングします。
# 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 のインスタンスストアなコンテナでテストを実行
まずは1プロセスでテストを実行してみます。
# tests/phpunit-docker
(略)
Time: 2.73 minutes, Memory: 111.50Mb
(略)
EBS よりも遅くなりました。
m3.2xlarge のインスタンスストアなコンテナで並列数 10 でテストを実行
続いて、並列数 10 でテストを実行してみました。
# PHPUNIT_DOCKER_PARA=10 tests/phpunit-docker-start
(略)
Time: 1.74 minutes, Memory: 36.50Mb
(略)
1プロセスで実行するよりは早いですが、やっぱり EBS よりも遅くなりました。
EBS よりインスタンスストアの方が I/O 性能は良いと思っていたのですが、そういうわけではないのでしょうかね? あるいはストライピングやマウントのやり方がまずかったのかもしれません。
m3.2xlarge の tmpfs なコンテナでテストを実行
並列数を増やしてもディスクがボトルネックになってあまり早くならないのかと思ったので、ダメ元で /var/lib/docker
を tmpfs にしてみました。
まず、現在のサイズを確認します。
# du -sh /var/lib/docker/
1.5G /var/lib/docker/
1.5G しかなかったので、8G ぐらいあれば十分でしょう。
# 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
まずは1プロセスで実行してみます。
# tests/phpunit-docker
(略)
Time: 45.86 seconds, Memory: 111.75Mb
(略)
さすがに速いです、EBS とくらべて倍ぐらい速いです。
次に並列数 10 で実行してみます。
# PHPUNIT_DOCKER_PARA=10 tests/phpunit-docker-start
(略)
Time: 17.48 seconds, Memory: 36.50Mb
(略)
さらに早くなりました、当たり前な気もしますがすごいです。
まとめ
当初の私のローカルの VM で1分半ぐらいかかっていたテストが、最終的に 18 秒ぐらいにまで短縮することができました。もっとも、AWS のそれなりのインスタンスを使ったり tmpfs にしてみたりとかなり無理矢理感はありますが。
さすがにこのためだけに1時間で100円は辛いですが、メモリとCPUコア数さえどうにか出来れば ParaTest+Docker でテストを並列に実行する試みもアリかもしれません。
c3.8xlarge の tmpfs なコンテナでテストを実行
限界が見たくなったので c3.8xlarge で試してみました。
やることは m3.2xlarge のときと変わりませんが、コンテナ数が増えるので tmpfs のサイズを 20G にしています。あと、1時間で200円ぐらいチャリンチャリンします。
# 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
まずは1プロセスで実行してみます。
# tests/phpunit-docker
(略)
Time: 44.55 seconds, Memory: 111.75Mb
(略)
まぁ変わらないですね。
次に並列数 20 で試してみます。
# PHPUNIT_DOCKER_PARA=20 tests/phpunit-docker-start
(略)
Time: 6.36 seconds, Memory: 36.25Mb
(略)
もうここまでくるとコンテナの削除に一番時間がかかっています(上で表示されている時間にはコンテナの起動と削除の時間は含まれていません)。
次に並列数 30 で実行・・・してみたのですが上手く動きませんでした。私の作成したスクリプトに何かしら問題があるか、もしくはなにか別の原因があるのかもしれません。