CircleCI 2.0 のドキュメントに書かれている小ネタですが、つい最近まで知らず、試してみたので紹介します。
TL;DR
- データベースディレクトリに tmpfs を使う Docker イメージもあるよ(
circleci/postgres:*-ram
) - tmpfs を使わない版では書き込み性能のチューニングがされているよ
- 普通の書き込みでは差が出ないみたいだよ
CircleCI 曰く
tmpfs をマウントしたディレクトリを PostgreSQL のデータベースディレクトリとして利用する事で、テスト実行時間を短縮出来る可能性があります、とのこと。
Optimizing Postgres Images
The circleci/postgres Docker image uses regular persistent storage on disk. Using tmpfs may make tests run faster and may use fewer resources. To create a Dockerfile for your own project and potentially reduce the duration of tests, consider adding the following line to the pre-built image.
DockerfileFROM circleci/postgres ENV PGDATA /dev/shm/pgdata/data
https://circleci.com/docs/2.0/postgres-config/#optimizing-postgres-images
CircleCI は PostgreSQL の Docker イメージ circleci/postgres を Docker Hub で公開しています。この Docker イメージでは、データベースディレクトリのパスを環境変数 PGDATA
で設定しています。つまり、この環境変数値を変更して利用すれば良いですよ、と書いているわけですね。
ドキュメントでは Dockerfile の内容が例示されているため、自分でイメージをビルドする必要がある様にも読めますが、実際にはこのバージョンのイメージも提供されています。circleci/postgres の -ram
で終わるタグがこれに該当します。
Docker イメージの違い
一例として、 circleci/postgres:9.6-alpine
と circleci/postgres:9.6-alpine-ram
の違いを簡単に比べてみましょう。Docker の知識が浅いため、ここでは docker image inspect
を使って簡単に比較します。些事を除けば以下の二点が違いの様です。
- 環境変数
PGDATA
が/var/lib/postgresql/data
から/dev/shm/pgdata/data
に変わっている -
CMD
長々としたがシェルスクリプトからPGDATA
の設定程度に変わっている
fish$ diff -u (docker image inspect circleci/postgres:9.6-alpine | psub) (docker image inspect circleci/postgres:9.6-alpine-ram | psub) \
| grep -v '"\(Id\|Created\|Container\|Image\)"' | grep -v '"circleci/postgres'
--- /var/folders/v8/t4kc4n8138b1mzcrgch8bp4m0000gn/T//.psub.D1QQ33vKe3 2017-11-11 20:00:08.000000000 +0900
+++ /var/folders/v8/t4kc4n8138b1mzcrgch8bp4m0000gn/T//.psub.qLdz8EXsAt 2017-11-11 20:00:08.000000000 +0900
@@ -1,16 +1,16 @@
[
{
"RepoTags": [
],
"RepoDigests": [
],
"Parent": "",
"Comment": "",
"ContainerConfig": {
"Hostname": "785fa172cfca",
"Domainname": "",
@@ -30,17 +30,18 @@
"PG_MAJOR=9.6",
"PG_VERSION=9.6.5",
"PG_SHA256=06da12a7e3dddeb803962af8309fa06da9d6989f49e22865335f0a14bad0744c",
- "PGDATA=/var/lib/postgresql/data",
+ "PGDATA=/dev/shm/pgdata/data",
"POSTGRES_USER=root",
"POSTGRES_DB=circle_test"
],
"Cmd": [
"/bin/sh",
"-c",
- "if [ -e /usr/local/share/postgresql/postgresql.conf.sample ]; then postgresfile=/usr/local/share/postgresql/postgresql.conf.sample; else postgresfile=/usr/share/postgresql/postgresql.conf.sample; fi && echo fsync=off >> $postgresfile && echo synchronous_commit=off >> $postgresfile && echo full_page_writes=off >> $postgresfile && echo bgwriter_lru_maxpages=0 >> $postgresfile"
+ "#(nop) ",
+ "ENV PGDATA=/dev/shm/pgdata/data"
],
"ArgsEscaped": true,
"Volumes": {
"/var/lib/postgresql/data": {}
},
@@ -72,7 +73,7 @@
"PG_MAJOR=9.6",
"PG_VERSION=9.6.5",
"PG_SHA256=06da12a7e3dddeb803962af8309fa06da9d6989f49e22865335f0a14bad0744c",
- "PGDATA=/var/lib/postgresql/data",
+ "PGDATA=/dev/shm/pgdata/data",
"POSTGRES_USER=root",
"POSTGRES_DB=circle_test"
],
@@ -80,7 +81,7 @@
"postgres"
],
"ArgsEscaped": true,
"Volumes": {
"/var/lib/postgresql/data": {}
},
環境変数 PGDATA
の違いだけだと思っていたのですが、見逃せない違いもあるようです。以下、該当スクリプトのワンライナーを整形したものです。
if [ -e /usr/local/share/postgresql/postgresql.conf.sample ]
then
postgresfile=/usr/local/share/postgresql/postgresql.conf.sample
else
postgresfile=/usr/share/postgresql/postgresql.conf.sample
fi
echo fsync=off >> $postgresfile
echo synchronous_commit=off >> $postgresfile
echo full_page_writes=off >> $postgresfile
echo bgwriter_lru_maxpages=0 >> $postgresfile
-ram
の方では行っていない、PostgreSQL の性能に関わる設定を加えていますね。これは…。
PostgreSQL の設定項目
詳しい説明は PostgreSQL のドキュメント 19.5. ログ先行書き込み(WAL)を参考にしてください。
fsync=off
synchronous_commit=off
full_page_writes=off
bgwriter_lru_maxpages=0
簡単に言えば、書き込み系の信頼性を割り切り性能を向上させるための設定ですね。
ブレイクタイム
雲行きが怪しくなってきました。通常のイメージがチューニング済みであるため、大抵のケースで tmpfs 版と大差なさそうな気配を感じます。
話を少し変えて、tmpfs について少し触れておきましょう。といっても tmpfs 自体の詳しい説明はしません。
ここで触れたい事は circleci/postgres:9.6-alpine-ram
の /dev/shm
についてです。
$ docker container run --rm circleci/postgres:9.6-alpine-ram df /dev/shm
Filesystem 1K-blocks Used Available Use% Mounted on
shm 65536 0 65536 0% /dev/shm
ご覧いただけたでしょうか。データベースディレクトリとして利用するはずの /dev/shm
は、最大 64 MB としてマウントされているのですね。つまり、もし 64 MB では足りないという場合は、別途 tmpfs でマウントする必要があります。
$ docker container run --rm --tmpfs /dev/shm2:rw,size=536870912,mode=1777 circleci/postgres:9.6-alpine-ram df /dev/shm2
Filesystem 1K-blocks Used Available Use% Mounted on
tmpfs 524288 0 524288 0% /dev/shm2
自分でマウントできるという点は良いのですが、 .circleci/config.yml
の jobs.{name}.docker
のところでどう書くのかはが分かりませんでした。
version: 2
jobs:
build:
docker:
- image: circleci/postgres:9.6-alpine
# ここで tmpfs を定義する方法はないように見える。
steps:
- checkout
...
CircleCI 2.0 では任意の docker コマンドや docker-compose コマンドを実行出来るので、必要に応じてコンテナを作成する事になりそうです。
tips
CircleCI 2.0 で docker
(docker-compose
) コマンドを実行してコンテナを作るためには、 setup_remote_docker
を使って独立したコンテナ環境(リモート環境)を立ち上げる必要がありますが、このリモート環境とプライマリコンテナ(jobs.{name}.docker.image
の一つ目)はネットワーク接続できない関係にあります。つまり、 docker
コマンドを使って立ち上げた PostgreSQL サーバに接続するためには、クライアントとなるコンテナも立ち上げる必要があります。
一方で jobs.{name}.docker
に列挙したコンテナ同士については、プライマリコンテナからポートフォワードされている様な関係になるため、全てが localhost で動いているように扱えます。
試してみる
前置きが長くなりましたが、そろそろ両バージョンの Docker イメージを使ってみましょう。
ひとまず、ベンチマークとして適切なテストケースかどうかは度外視します。単純なデータベースの読み書きの時点で差が出るのかを確かめるため、10,000 行のレコードを持つテーブルを用意し、行をランダムに更新してみます。
#!/usr/bin/env ruby
N = 10_000
open('test.sql', 'w') do |f|
f.puts("CREATE TABLE test (id INT, name text);\n")
f.puts('INSERT INTO test (id, name) VALUES (1, \'1\')')
2.upto(N) do |n|
f.puts(" ,(#{n}, '#{n}')")
end
f.puts(";\n")
(1..N).to_a.shuffle.each do |n|
f.puts "UPDATE test SET name = name || ' changed' WHERE id = #{n};"
end
end
以下、macOS 上で実行した結果ですが、特別な差は見られませんでした。
$ set container_id (docker container run -d -p 55432:5432 --rm circleci/postgres:9.6-alpine)
$ time psql -q -h localhost -p 55432 -U root -d circle_test -c '\timing' -f test.sql
...
10.26 real 0.13 user 0.17 sys
$ docker container stop $container_id
$ set container_id (docker container run -d -p 55432:5432 --rm circleci/postgres:9.6-alpine-ram)
$ time psql -q -h localhost -p 55432 -U root -d circle_test -c '\timing' -f test.sql
...
10.69 real 0.13 user 0.17 sys
$ docker container stop $container_id
以下の様な .circleci/config.yml
を書いて CircleCI 上でも実行しましたが、同様の結果になりました。ジョブごとに 10 秒近い差が出ることもありましたが、CircleCI ではよくある事なので誤差ですね。
version: 2
jobs:
"circleci/postgres:9.6-alpine":
docker:
- image: circleci/golang:latest
- image: circleci/postgres:9.6-alpine
steps:
- checkout
- run:
name: setup
command: sudo apt-get install -y postgresql-client-9.6
- run:
name: wait
command: ./wait-for-it.sh localhost:5432
- run:
name: psql
command: time psql -q -h localhost -p 5432 -U root -d circle_test -c '\timing' -f test.sql > /dev/null
"circleci/postgres:9.6-alpine-ram":
docker:
- image: circleci/golang:latest
- image: circleci/postgres:9.6-alpine-ram
steps:
- checkout
- run:
name: setup
command: sudo apt-get install -y postgresql-client-9.6
- run:
name: wait
command: ./wait-for-it.sh localhost:5432
- run:
name: psql
command: time psql -q -h localhost -p 5432 -U root -d circle_test -c '\timing' -f test.sql > /dev/null
workflows:
version: 2
compare:
jobs:
- "circleci/postgres:9.6-alpine"
- "circleci/postgres:9.6-alpine-ram"
まとめ
以上、CircleCI 2.0 のドキュメントに書かれていた小ネタでした。
Rails(RSpec) を使っている場合、transactional fixtures などの仕組みを利用してそもそも DB にコミットしないケースが多いかと思います。一方、ウェブブラウザを利用した E2E テストなどプロセス(DB セッション)が分かれる様なテストでは、もしかしたら役に立つかも知れません。その場合も、単純な読み書きでおさまるのであれば差はないかもしれません。
テストの際の PostgreSQL のディスク I/O でお困りの方は試してみると良いのかもしれません。
追記: -ram
でなくてよい
circleci/postgres:9.6-alpine
でもデータベースディレクトリを環境変数 PGDATA
で指定する仕組みなので、これを tmpfs をマウントしたディレクトリに変更すれば済む話であったりします。
docker:
- image: circleci/golang:latest
- image: circleci/postgres:9.6-alpine
environment:
PGDATA: /dev/shm/pgdata/data
steps:
- checkout
- run:
name: setup
command: sudo apt-get install -y postgresql-client-9.6
- run:
name: wait
command: ./wait-for-it.sh localhost:5432
- run:
name: psql
command: time psql -q -h localhost -p 5432 -U root -d circle_test -c '\timing' -f test.sql > /dev/null