DBアクセスで遅くなったテストの実行時間を Docker で 40% 削減した方法

DBのレイヤーを含むエンドツーエンドテストやDBに依存したコンポーネントの自動テストがたくさんあると、全てのテストが終わるまでに長い時間がかかるようになってしまうことがあります。DBのクエリ実行はネットワークIOやディスクIOなどを含んだ高コストな処理だからです。

Docker を少し工夫して使うと、お手軽にテスト中のDBのクエリ実行にかかる時間を削減できます。自動テストが完了するまでの待ち時間を短縮し、開発のフィードバックサイクルをより早く回せるようになります！

MariaDB を用いたプロジェクトの実績では、DBアクセスを伴うテストケースが 153件 ありましたが、この方法によりそのテストスイートのローカル環境での実行時間を約 43% 削減できました（約 145.7s → 約 83.3s）。

## どうやって？
Docker で tmpfs を使います。

## tmpfs
https://docs.docker.com/storage/tmpfs/

tmpfs とは、ディスクの代わりにメモリへデータを書き込むファイルシステムで、下記のような特徴があります。

- **メモリに対してデータを読み書きするためIOが高速**
- **コンテナを停止すると tmpfs 上に保存されたデータは全て消える**
- コンテナ間でのボリュームの共有は不可

つまり、DBのクエリ実行の中でコストが高めのディスクIOを、よりコストが低いメモリIOに置き換えることで高速化するというとても単純な戦略です。

DB のデータが全てメモリに乗ることになるため、コンテナを停止するとデータは全て消えてしまいますが、自動テストで読み書きするデータに耐久性が要求されることはまずないでしょう。手動でテストしたい場合など、データが消えると困る場合は tmpfs ではなくディスクにデータを書き込む DB のコンテナを別に作ったほうが良いです。

## ローカルの開発環境で使う

各プロジェクトメンバーが tmpfs が有効になったコンテナを使ってローカルでテストを実行できるようにするには [docker-compose](https://docs.docker.com/compose/) が便利です。docker-compose は Docker コンテナの構成を管理するためのツールで、docker-compose.yml ファイルひとつで各プロジェクトメンバーが開発に必要なミドルウェアが入った Docker コンテナ群を素早くローカル環境にセットアップできるようになります。

プロジェクトディレクトリの直下に `docker-compose.yml` を置いて、テストを実行する前にすぐコンテナを立ち上げられるようにしておくと良いでしょう。

**プロジェクト構成の例**

```
project/
  ├── src
  ...
  ├── docker-compose.yml
  ...
  └── README.md
```

**実行例**

```bash
$ docker-compose up -d    # コンテナ群を起動
$ sbt test                # テスト実行
```

メジャーな RDBMS のコンテナを docker-compose で定義するサンプルを書いてみました。

RDBMS がデータを保存するディレクトリに tmpfs がマウントされるように指定しています。`tmpfs:`という設定項目で tmpfs をマウントするディレクトリを指定できます。サンプルにはとりあえず起動するための必要最低限の項目しか書いていないため、実際にプロジェクトで利用する場合は、必要に応じてカスタマイズしてください。

### MySQL

- [DockerHub](https://hub.docker.com/_/mysql/)

```yaml:docker-compose.yml
version: "3"

services:
    mysql-for-test:
        image: mysql:8.0.13
        tmpfs:
            - /var/lib/mysql
        ports:
            - 3306:3306
        environment:
            - MYSQL_ROOT_PASSWORD=mysql
```

### MariaDB

- [DockerHub](https://hub.docker.com/_/mariadb/)

```yaml:docker-compose.yml
version: "3"

services:
    mariadb-for-test:
        image: mariadb:10.4.0
        tmpfs:
            - /var/lib/mysql
        ports:
            - 3306:3306
        environment:
            - MYSQL_ROOT_PASSWORD=mysql
```


### PostgreSQL

- [DockerHub](https://hub.docker.com/_/postgres/)

```yaml:docker-compose.yml
version: "3"

services:
    postgres-for-test:
        image: postgres:11.1
        tmpfs:
            - /var/lib/postgresql/data
        ports:
            - 5432:5432

```

### Oracle Database

:warning: Oracle Database の Docker イメージはローカルでビルドする必要があります。

- [Oracle Database on Docker - GitHub](https://github.com/oracle/docker-images/tree/master/OracleDatabase/SingleInstance)
- [DockerでOracle Databaseを使う - Qiita](https://qiita.com/gorilla0513/items/f22e8cce4e08da031abe)

```yaml:docker-compose.yml
version: "3"

services:
    oracledb-for-test:
        image: oracle/database:12.2.0.1-ee
        tmpfs:
            - /opt/oracle/oradata
        ports:
            - 1521:1521

```

## CI で使う

GitLab CI など、ジョブの中で Docker が使える CI 環境があれば、同様の方法を使ってテストを高速化できるはずです。

私が担当しているプロジェクトでは GitLab CI のテスト実行前に（開発環境と同様に）docker-compose を使って tmpfs が有効になったテスト用の DB を起動した後にテストが実行されるようにしています。

（GitLab CI は Docker コンテナの中で CI のジョブを実行するという仕組みのため、Docker コンテナの中で Docker コンテナを立ち上げられるようにする必要があり、セットアップに少し手間がかかりましたが…）

## データ量には注意

tmpfs を使うとDBに書き込んだデータが全てメモリに保存されるため注意が必要です。テストで読み書きするデータのサイズは Docker が使えるメモリの上限を超えないようにする必要があります。

[Docker for Windows](https://docs.docker.com/docker-for-windows/) であれば、「[Settings > Advanced](https://docs.docker.com/docker-for-windows/#advanced)」で Docker へのメモリの割当量を調整できます。

## 同様の対応が組み込まれている Docker イメージもある

@shimma さんに教えていただきました。

RDBMS のデータが保存されるディレクトリにRAMディスク `/dev/shm` をマウントした Docker イメージを CircleCI が公開してくれています。

Tag の末尾に `-ram` が付いたイメージがそれです。

- [circleci/mysql - DockerHub](https://hub.docker.com/r/circleci/mysql/tags)
- [circleci/mariadb - DockerHub](https://hub.docker.com/r/circleci/mariadb/tags)
- [circleci/postgres - DockerHub](https://hub.docker.com/r/circleci/postgres/tags)

性能を計測してみたところ、私の環境では tmpfs を使った場合とほぼ同等の性能が出ました。

※ `mariadb:10.3.2` (tmpfs) と `circleci/mariadb:10.3.2-ram` を比較

:warning: ただし、Docker コンテナの `/dev/shm` はデフォルトで 64MB のサイズしか確保されておらず、MariaDB だと起動すらできないので明示的に指定してやる必要があります。

> `--shm-size=""`
> Size of `/dev/shm`. The format is `<number><unit>`. `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`.
> https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources

**docker-compose での shm-size の指定例**

```yml:docker-compose.yml
version: "3"

services:
    mariadb-for-test:
        image: circleci/mariadb:10.3.2-ram
        shm_size: 256m  # /dev/shm に 256MB 割当
        ports:
            - 3306:3306
        environment:
            - MYSQL_ROOT_PASSWORD=mysql
```

## さいごに

Docker で tmpfs を使うと自動テストに特化した高速な DB を簡単に構築できます。

Docker は Linux をはじめ、Windows や Mac でもほぼ同じように利用できるため、この方法はプロジェクトメンバーにも展開しやすいです。docker-compose.yml ファイルを一つ作ってプロジェクトに入れておけば OK です。

ただし、コンテナをいくつも立ち上げるとそれなりにリソースを必要とするので、えらい人に頼み込んで強いマシンを用意してもらいましょう。