CircleCI

CircleCI 2.0 で PostgreSQL を使う時のチューニング小ネタメモ

More than 1 year has passed since last update.

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.


Dockerfile

FROM 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-alpinecircleci/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.ymljobs.{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 行のレコードを持つテーブルを用意し、行をランダムに更新してみます。


gen.rb

#!/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"

image

image


まとめ

以上、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