Docker を使って開発してみてわかったこと

Ruby on Rails の開発に、Docker を使用してみてわかったこと2点とその対処方法をまとめてみた。

  • boot2docker だとファイル数が多くなると非常に遅くなった
  • Aufs のレイヤー数制限でエラーが出た

boot2docker だとファイル数が多くなると非常に遅くなった

Mac で Docker を使用したい場合は、boot2docker を使わないといけない。
boot2docker は、Dockerコンテナを動作させることを目的に作られた軽量な Linux ディストリビューションであり、VirtualBox を使うことで、Linux 以外の環境でも簡単に Docker を利用できるようになっている。
boot2docker は、共有フォルダとしてデフォルトでは VirtualBox の VirtualBox folder sharing を使用する。
VirtualBox SharedFolders (vboxsf) でもファイル数が少ないうちはそれほど気にならないが、ファイル数が多くなってくると非常に遅く感じた。

これについては、boot2docker の issues として上がっていて、様々な議論が展開されているようである。

nfs share / guest to host · Issue #64 · boot2docker/boot2docker
Volume sharing performance issues · Issue #593 · boot2docker/boot2docker

対応方法に関してはいくつかあるようだが、vboxsf ではなくて、NFS を利用するスクリプトを公開している人がいたのでそれを使ってみた。

元のスクリプトはこちら。
Script to mount /Users with nfs instead of vboxsf in boot2docker

そのまま使ってみたら、下記のエラーが出てうまくいかなかったので、

1
mount: RPC: Authentication error; why = Client credential too weak

ホストOSのIPアドレスを取得する部分(30行目あたり)と、/etc/exportsに記述する内容(44行目あたり)をすこし変更している。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/bin/bash

#
# This script will mount /Users in the boot2docker VM using NFS (instead of the
# default vboxsf). It's probably not a good idea to run it while there are
# Docker containers running in boot2docker.
#
# Usage: sudo ./boot2docker-use-nfs.sh
#

if [ "$USER" != "root" ]
then
echo "This script must be run with sudo: sudo ${0}"
exit -1
fi

# Run command as non root http://stackoverflow.com/a/10220200/96855
B2D_IP=$(sudo -u ${SUDO_USER} boot2docker ip &> /dev/null)

if [ "$?" != "0" ]
then
sudo -u ${SUDO_USER} boot2docker up
$(sudo -u ${SUDO_USER} boot2docker shellinit)
B2D_IP=$(sudo -u ${SUDO_USER} boot2docker ip &> /dev/null)
#echo "You need to start boot2docker first: boot2docker up && \$(boot2docker shellinit) "
#exit -1
fi

#OSX_IP=$(ifconfig en0 | grep --word-regexp inet | awk '{print $2}')
VBOXNET=$(sudo -u ${SUDO_USER} VBoxManage showvminfo boot2docker-vm --machinereadable | grep hostonlyadapter | cut -d '"' -f 2)
OSX_IP=$(ifconfig $VBOXNET | grep --word-regexp inet | awk '{print $2}')
MAP_USER=${SUDO_USER}
MAP_GROUP=$(sudo -u ${SUDO_USER} id -n -g)

# Backup exports file
$(cp -n /etc/exports /etc/exports.bak) && \
echo "Backed up /etc/exports to /etc/exports.bak"

# Delete previously generated line if it exists
grep -v '^/Users ' /etc/exports > /etc/exports

# We are using the OS X IP because the b2d VM is behind NAT
# echo "/Users -mapall=${MAP_USER}:${MAP_GROUP} ${OSX_IP}" \
echo "/Users -mapall=${MAP_USER}:${MAP_GROUP} --alldirs" \
>> /etc/exports

nfsd restart

sudo -u ${SUDO_USER} boot2docker ssh << EOF
echo "Unmounting /Users"
sudo umount /Users 2> /dev/null
echo "Restarting nfs-client"
sudo /usr/local/etc/init.d/nfs-client restart 2> /dev/null
echo "Waiting 10s for nfsd and nfs-client to restart."
sleep 10
echo "Mounting /Users"
sudo mount $OSX_IP:/Users /Users -o rw,async,noatime,rsize=32768,wsize=32768,proto=tcp,nfsvers=3
echo "Mounted /Users:"
ls -al /Users
exit
EOF

実行方法については、スクリプト内に書かれている通り、下記コマンドを実行するだけ。
boot2docker が起動していなければ起動もしてくれる。

1
$ sudo ./boot2docker-use-nfs.sh

この対応で、vboxsf よりは、パフォーマンスについて体感的にはかなり改善できた。


Aufs のレイヤー数制限でエラーが出た

Docker で開発をしていたら、Cannot create container with more than 127 parentsというエラーが出た。
これは、Aufs のレイヤー数制限によるものらしい。

エラーが出た背景として

はじめは、各自がそれぞれの端末で、Docker Hub上のオフィシャルのリポジトリのrailsのイメージをベースにして、docker buildをして開発を行っていた。
ただ、それだとビルドするのに時間がかかる、という不満点が出てきた。

じゃあ、ビルド済みのイメージを Docker Hub に上げて置いて、それを基に各自が開発をする、という運用にしてみよう、ということになった。
何か変更があれば、Docker Hub に上がっているイメージに追加で変更を加えた上で、そのイメージを再度 Docker Hub に上げて共有する。

そういった運用を続けていたら、Cannot create container with more than 127 parents というエラーが出てしまったのだ。
Docker Hub でビルド済みのイメージを、開発メンバー間で共有し始めたことでこの制限に気づいたのである。

Docker のレイヤー数制限

Docker Documentation によると、Docker では Union File System というものが使われており、下記の画像のように、変更を加えるごとに何層も read-only の層を重ねていき、最上位の層のみ read-write にする、ということを行う。

Docker では、127 以上のレイヤーを重ねることができないようになっている。
(もともと 42 だったようだ。それが今は、127 まで拡張されている)

Increase max image depth to 127 by crosbymichael · Pull Request #2897 · docker/docker

対処方法

では、どのような対応をすればこの制限に引っかからないのか、調べてみた。
主な対応として、下記の2つある。

  • Dockerfile の RUN などのコマンドを実行する回数を減らす
  • export & import を実行する

Dockerfile の RUN などで実行する回数を減らす

Docker では、Dockerfile に記述している RUNADD などが実行されるたびに、レイヤーが重なることになる。
そこで、コマンドを && でつないで実行する回数を減らす。
例えば下記のような Dockerfile があったとする。

1
FROM debian:wheezy
WORKDIR /tmp

RUN wget -nv http://www.example.com/foo/someutility-v1.0.0.tar.gz
RUN tar -xvf someutility-v1.0.0.tar.gz
RUN mv /tmp/someutility-v1.0.0/someutil /usr/bin/someutil
RUN rm -rf /tmp/someutility-v1.0.0
RUN rm /tmp/someutility-v1.0.0.tar.gz

これを下記のように、&& でつなげることで、レイヤー数をかせげるとのこと。

1
FROM debian:wheezy
WORKDIR /tmp

RUN wget -nv http://www.example.com/foo/someutility-v1.0.0.tar.gz &&
tar -xvf someutility-v1.0.0.tar.gz &&
mv /tmp/someutility-v1.0.0/someutil /usr/bin/someutil &&
rm -rf /tmp/someutility-v1.0.0 &&
rm /tmp/someutility-v1.0.0.tar.gz

もしくは、コマンドをシェルスクリプトにまとめて実行する。

1
FROM debian:wheezy
WORKDIR /tmp

RUN script.sh

という具合に。

export & import を実行する

もう一つの対応方法として、docker export/import を使う方法がある。
docker export をすると、すべてのファイルシステムの差分の履歴情報を保持せずに、ファイルに出力する。
そのため、履歴情報であるレイヤーを節約できるのである。
下記のように実行すれば docker export/import ができる。

【docker export】

1
$ sudo docker export $container_id > tmp.tar

【docker import】

1
$ cat tmp.tar | sudo docker import - <REPOS:TAG>

ただ、気をつけないといけないのが、export/import をすると、履歴情報が消えてしまうため、環境変数などの設定が消えてしまうということだ。
例えば Dockerfile で、

1
ENV TESTENV aaabbbccc

と設定した TESTENV という環境変数が、

1
2
3
4
5
6
$ docker run -it test env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=d3e4c37dbfa6
TERM=xterm
TESTENV=aaabbbccc #この環境変数を設定
HOME=/root

docker export/import を行うことで、

1
2
3
$ docker export d3e4c37dbfa6 > testenv.tar
$ cat testenv.tar | docker import - mmmsasaki/aufstest:testenv
4d4399606c802c31035eb79b729d3ff99d63edfe08916c5b8312b3bc5619f752

消えてしまう。

1
2
3
4
5
$ docker run -ti mmmsasaki/aufstest:testenv env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=094e28fe25e9
TERM=xterm
HOME=/root

そのため、環境変数が必要な場合は、export/import を行ったイメージを基に再度 Dockerfile で定義してビルドしてやる必要がある。
また、環境変数だけでなく、Dockerfile で設定している、CMDEXPOSE なども保持していないので注意が必要である。

docker import を行う際に、環境変数を設定したい場合は、--change "ENV DEBUG true" というようにコマンドで渡してやると設定が可能。
前述の例で言うと、下記のようになる。

1
$ cat testenv.tar | docker import --change "ENV TESTENV aabbcc" - mmmsasaki/aufstest:testenv

まとめ

今回は、Docker を使ってみて気になったこと、エラーとその対応について、まとめてみた。
export/import の運用については、開発メンバーがもっと簡単に各自で行えるようにスクリプト化したいと思っている。
その方が楽だし、何よりミスがないから。

…そのあたりもここでまとめたかったけど、まだちょっと時間がかかりそうなので、今回は割愛。

Dockerを活用したサービス開発を御希望の企業様は、是非MMMにご相談下さいませ!

参考URL

Optimizing Docker Images | Century Link Labs
Dockerイメージの最適化 - ワザノバ | wazanova
気づいたらガジェ獣: docker exportとdocker importでdockerイメージのサイズを小さくする
Dockerfile の 127 行制限を試した - ようへいの日々精進 XP
Docker 管理コマンド - @//メモ
私の Docker TIPS - Qiita
第3回 コンテナ型仮想化の情報交換会@大阪 (コンテナ型VMや関連するカーネル等の技術が話題の勉強会)に参加した - @znz blog
Docker command line - Docker Documentation

このエントリーをはてなブックマークに追加