dockerでmysqlをvolumeなしで動かしてみた

dockerでmysqlをvolumeなしで動かしてみた。その実験の記録。

# 環境
- VirtualBox上のUbuntu 18.10
- docker 18.09
- mysql 8.0

# 動機
dockerでmysqlを動かす場合、必ずデータの格納先はホスト側になり、コンテナには残らない。理由はofficialなmysqlイメージにはvolume指定が入っているから。volume指定が入っている場合、実際にvolumeを作るか、hostのディレクトリにbindするかしかない。いずれにせよ通常ホスト側になる(例外はホストの外なので、コンテナ側ではない)。

では、mysqlイメージからvolume指定を抜くと、どうなるのか？これを調査する。

# 方法
dockerの公式ツールだけでは、イメージ内に含まれるvolume指定を簡単に削除することができない。なので、この処理にはdocker-copyedit[^docker-copyedit]を使用する。volume指定のないイメージが出来たら、これを起動して終了を繰り返し、イメージの差分をhistoryで見ていく。比較のために、素のイメージでもbind指定で動作させて同じことを繰り返し、イメージの差分と、bind先のdu結果を合わせてどの程度の差異があるかを検証する。

# 実施
## (1) volume指定を削除したmysqlイメージの作成

docker-copyedit[^docker-copyedit]を取得し、実行権限を付けた状態で以下を実施。

```console
$ docker pull mysql
$ ./docker-copyedit.py FROM mysql INTO mysql:novolumes REMOVE ALL VOLUMES
```
これでnovolumeタグの付いたvolume指定を削除したmysqlイメージが完成する。
ついでに元のイメージにもnormalタグを付けておく(後の区別のため)。

```console
docker tag mysql mysql:normal
```

## (2) テスト作業のスクリプト化
コマンドでもいいけど長いし繰り返すので、一応作っておく。

```bash:test.sh
WAIT_FIRST=60
WAIT_SECOND=20
ORG_IMAGE=mysql
NOVOLUMES=novolumes
NORMAL=normal
COMMIT_IMAGE=my-mysql
CONTAINER=my-mysql
MAX_COUNT=5
MODE=${NOVOLUMES}
DATADIR=$PWD/data
DULOG=du.log

for MODE in ${NOVOLUMES} ${NORMAL}; do
    COUNT=1
    mkdir ${DATADIR}
    if [ "${MODE}" == "${NORMAL}" ]; then
        OPTIONS="--volume ${DATADIR}:/var/lib/mysql"
    else
        OPTIONS=""
    fi
    while [ ${COUNT} -le ${MAX_COUNT} ]; do
        echo "${MODE}: START (${COUNT}/${MAX_COUNT})"
        if [ ${COUNT} -eq 1 ]; then
            IMAGE=${ORG_IMAGE}:${MODE}
            WAIT=${WAIT_FIRST}
        else
            IMAGE=${COMMIT_IMAGE}:${MODE}
            WAIT=${WAIT_SECOND}
        fi
        docker run --name ${CONTAINER} ${OPTIONS} -e MYSQL_ROOT_PASSWORD=my-secret-pw -d ${IMAGE}
        echo "${MODE}: WAIT (${COUNT}/${MAX_COUNT}) - ${WAIT}SEC"
        sleep ${WAIT}
        echo "${MODE}: STOP (${COUNT}/${MAX_COUNT})"
        docker stop ${CONTAINER}
        echo "${MODE}: COMMIT (${COUNT}/${MAX_COUNT})"
        docker commit ${CONTAINER} ${COMMIT_IMAGE}:${MODE}
        echo "${MODE}: RM (${COUNT}/${MAX_COUNT})"
        docker rm -v my-mysql
        echo "${MODE}: TAG (${COUNT}/${MAX_COUNT})"
        docker tag ${COMMIT_IMAGE}:${MODE} ${COMMIT_IMAGE}:${MODE}_${COUNT}
        echo "[${MODE}${COUNT}]" >> "${DULOG}"
        du -sh "$DATADIR" >> "${DULOG}"
        COUNT=`expr ${COUNT} + 1`
    done
done
echo "以下実施しないとゴミが残ります"
echo "docker images --filter reference=\"my-mysql*:novolumes*\" | awk '{print \$1 \":\" \$2;}' | tail -n +2 | while read i;do docker image rm \$i;done"
echo "docker images --filter reference=\"my-mysql*:normal*\" | awk '{print \$1 \":\" \$2;}' | tail -n +2 | while read i;do docker image rm \$i;done"
echo "sudo rm -rf ${DATADIR}"
echo "rm ${DULOG}"
```
ボリュームなしと公式の2パターンで、各5回起動/終了/commitを繰り返してるだけのスクリプト。
公式の場合は、./dataにバインドしており、そのディレクトリのduのログを取っている。
適当なのでエラーが出るが、問題なければ読み飛ばす。以下実行方法。

```console
$ bash test.sh
```
最後に出てくる4行のメッセージはコピペしとくと後でゴミ捨てするときに楽。

# 結果
```console
$ docker image ls --filter "reference=*mysql:novolumes*"
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
my-mysql            novolumes           d46afbc3333a        27 minutes ago      1.34GB
my-mysql            novolumes_5         d46afbc3333a        27 minutes ago      1.34GB
my-mysql            novolumes_4         81c354774d2b        27 minutes ago      1.17GB
my-mysql            novolumes_3         55094b7c7c7a        28 minutes ago      994MB
my-mysql            novolumes_2         19f5e8c34a2a        28 minutes ago      822MB
my-mysql            novolumes_1         fe3d91edd6c0        29 minutes ago      649MB
mysql               novolumes           77ae41e89139        7 days ago          477MB
$ docker image ls --filter "reference=*mysql:normal*"
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
my-mysql            normal              e75ca3a58778        23 minutes ago      477MB
my-mysql            normal_5            e75ca3a58778        23 minutes ago      477MB
my-mysql            normal_4            36baccd83d5d        23 minutes ago      477MB
my-mysql            normal_3            b359772d641f        24 minutes ago      477MB
my-mysql            normal_2            c59b670f1a75        24 minutes ago      477MB
my-mysql            normal_1            102b6079086b        24 minutes ago      477MB
mysql               normal              91dadee7afee        7 days ago          477MB
$ cat du.log 
[novolumes1]
4.0K    /home/user/python/docker/mysql/novolumes/data
[novolumes2]
4.0K    /home/user/python/docker/mysql/novolumes/data
[novolumes3]
4.0K    /home/user/python/docker/mysql/novolumes/data
[novolumes4]
4.0K    /home/user/python/docker/mysql/novolumes/data
[novolumes5]
4.0K    /home/user/python/docker/mysql/novolumes/data
[normal1]
164M    /home/user/python/docker/mysql/novolumes/data
[normal2]
164M    /home/user/python/docker/mysql/novolumes/data
[normal3]
164M    /home/user/python/docker/mysql/novolumes/data
[normal4]
164M    /home/user/python/docker/mysql/novolumes/data
[normal5]
164M    /home/user/python/docker/mysql/novolumes/data
$ sudo du -sh ./data
165M    ./data
```
つまり

|ボリューム|回数|イメージサイズ[MB]|ボリュームサイズ[MB]|合計[MB]|
|:-:|--:|--:|--:|--:|
|なし|0|477|0|477|
|なし|1|649|0|649|
|なし|2|822|0|822|
|なし|3|994|0|994|
|なし|4|1170|0|1170|
|なし|5|1340|0|1340|
|あり|0|477|0|477|
|あり|1|477|165|642|
|あり|2|477|165|642|
|あり|3|477|165|642|
|あり|4|477|165|642|
|あり|5|477|165|642|

ということ。

# 考察
ボリュームなし時には、差分が毎回173MB程度発生し、追加レイヤに記録されている。
ボリュームあり時には、差分が検出されず、ボリュームに165MB程度のデータが格納されている。
ボリュームには差分が格納されないため、ボリュームあり時には1~4回目のデータを見ることができないが、ボリュームなし時には全ての履歴が残るも、毎回ほぼ全量分のデータが差分として抽出されているということになる。
これはレイヤを実装するOverlay2がファイル単位の差分を取っており、これらデータの大半を占めているデータがサイズの大きなファイルであることに起因していると考える。そこで実際にボリュームあり時の./dataでサイズの大きなファイルを抽出しみてた。

```console
$ cd data
$ ls -lAFh ib_logfile* *.ibd undo_*
-rw-r----- 1 vboxadd nobody 48M  3月 13 10:10 ib_logfile0
-rw-r----- 1 vboxadd nobody 48M  3月 13 10:07 ib_logfile1
-rw-r----- 1 vboxadd nobody 30M  3月 13 10:09 mysql.ibd
-rw-r----- 1 vboxadd nobody 12M  3月 13 10:10 undo_001
-rw-r----- 1 vboxadd nobody 10M  3月 13 10:10 undo_002
```
これらのデータだけで142MB、サイズの大半をここで消費していることが分かる。見たところ、UNDO/REDOログとデータ本体ではないかと推測されるようなデータであり、起動終了時にこれらの一部が書き換わることは容易に想像できる。

# 結論
databaseのような大きなファイルを少しずつ変更するようなシステムをコンテナで動かす場合は、Overlay2でのレイヤに入れると、ストレージ効率が著しく低下する。なので、volumeやbind-mountなど、コンテナの外に出すか、レイヤの実装を変更するべきと考える。

[^docker-copyedit]: [docker-copyedit](https://github.com/gdraheim/docker-copyedit)(python2なので注意！。イヤなら自分で属性をいじらないといけない)