Alpaca技術ブログ

AIと超高速データストレージを駆使して新しいトレーディングを創る

Netコンテナを使って1ホスト上で複数のDockerデーモンを走らせる

Alpaca CTOの原田(@umitanuki, github)です。

Dockerは素晴らしいです。クラウド世代のソフトウェア実行環境として世界を変えうる気がします。Alpacaでは、AWS上での仮想マシン、ベアメタルのレンタルサーバ、オフィス内の内製サーバ等を本番や開発用途で何十も走らせています。ウチではほぼすべてDockerで動いていて、土台のホストOSやプロビジョニングを気にすることはほとんどありません。たいていの場合、GitHubのプライベートレポジトリからソースをcloneして、makeを打つと依存関係が入っているDockerイメージをダウンロードしてサービスを数分以内に開始します。特に科学計算なPythonとGPU周りでは、(コミュニティが頑張っていますが)まだまだメンテナンスが難しいしスムーズさに掛けます。Dockerなら一回Dockerfile書いておしまいです。美しいですね。

極小スタートアップ企業として、運用コストの見直しは毎日のことです。が、GPUをクラウドで走らせるのはお金がかかります。ウチのアプリケーションはCPUやメモリがボトルネックではないのですが、GPUはキモになるので、CPUやメモリをふんだんに乗せたサーバでは結構無駄になる部分が増えます。これを各開発者用に用意していては死んでしまいます。どちらかと言うと、CPUとメモリはそこそこで、GPUリソースがガッツリ乗っているマシン1台を共用で使えるとより幸せになれます。さてここで問題はどうやってDocker環境を開発者同士で共用するかです。Dockerの元々の思想としては「完全にポータブル、全部切り離されてるし」のようなところがあってそのような問題はなさそうに聞こえますが、現実的にはDockerもそこまでは素晴らしくないわけです。例えば固定ポートで待ち受けたり、そうでなくてもいろいろなサーバがランダムに待ち受けポートを決めたり、コンテナ名も決まった名前があるのでバッティングします。もっとひどいのはDockerキャッシュで、誰かほかの人がキャッシュをアップデートしてる間にその新しいイメージを使って古いバージョンのアプリを走らせたりすると、もう目も当てられません。

この問題、どうやって解決したらいいか当初はよくわかりませんでした。"Docker in Docker"あるいはDnDというそれはそれはクレイジーなソリューションに手を出したりもしましたが、これ自体はコンテナの中から親のDockerデーモンにアクセスするというだけで上記問題の解決にはなっていませんでした。散々調べた結果の結論としては、Dockerデーモンを複数走らせるしかないということになりました。が、これもただそうすればいいというわけではなく、いくつか課題がありました。まずはネットワークブリッジの問題。docker0というインターフェイスを聞いたことがあるかもしれませんが、これはコンテナとその外とのネットワークを接続するためのブリッジになっていて、通常であればDocker初回起動時に自動的に作成されます。しかし複数デーモンの場合はこれを分けなければなりません。なんとこの場合にはDockerは自動でブリッジを作ってくれないのです。

さらに、ウチではnet=hostモードを使っているので、仮にデーモンとネットワークを分けても、まだポートの問題は解決されていませんでした。いろいろ調べた結果、net=containerというものを発見、これは起動するコンテナを他のコンテナのネットワーク名前空間に置くことができるモードで、ネットワーク用のコンテナを一つ起動して他の関連するコンテナをすべてこの中に放り込むことで、ホスト側を汚さずに今までのnet=hostのようなままの構成で運用できることがわかりました。

f:id:alpacablog:20151025180134p:plain

上の図で、サーバコンテナは同じグループのネットワーク名前空間をホストのように見立て、別空間で動いている同じプログラムとの衝突を気にせずポートを使えます。つまり、デーモンAのサーバはデーモンBのサーバと同じポートで待ち受けすることができるのです。考えてみると簡単なことではありますが、こうすることで一つのDockerデーモンを完全なバーチャルマシンのように扱うことができるようになるわけです。net=hostのように走らせることで特に環境変数によるDockerリンクの仕組みも気にすることなく、普通にネットワークを利用すればNetコンテナの中に閉じ込めて他のプロセスとも自由に通信できるというわけです。バンザイ。

下記は複数Dockerデーモンを立ち上げるための参考例です。

    function start_dockers {
      OFFSET=0
      for u in $USERS
      do
        BRIDGE_NAME=br_${u}
        DOCKER_ROOT=/home/${u}/docker
          echo "Creating the bridge and starting the daemon for ${u}"
          # create a bridge
          brctl addbr ${BRIDGE_NAME}
          SUBNET=$(expr 52 + ${OFFSET})
          ip addr add 172.18.${SUBNET}.1/24 dev ${BRIDGE_NAME}
          ip link set dev ${BRIDGE_NAME} up
          # IP Masquerade, if not set yet
          if ! iptables -t nat -C POSTROUTING -j MASQUERADE -s 172.18.${SUBNET}.0/24 -d 0.0.0.0/0; then
            iptables -t nat -A POSTROUTING -j MASQUERADE -s 172.18.${SUBNET}.0/24 -d 0.0.0.0/0
          fi
          # create a docker working directory
          mkdir -p ${DOCKER_ROOT}
          chown $u:$u ${DOCKER_ROOT}
          # launch a docker daemon
          docker daemon -D \
            -g ${DOCKER_ROOT}/g \
            --exec-root=${DOCKER_ROOT}/e \
            -b ${BRIDGE_NAME} \
            --dns=8.8.8.8 \
            --iptables=false \
            -H unix://${DOCKER_ROOT}/docker.sock \
            -p ${DOCKER_ROOT}/docker.pid > ${DOCKER_ROOT}/docker-${u}.log 2>&1 &
        OFFSET=$(expr ${OFFSET} + 1)
      done
    }

最初に各ユーザのブリッジを作り、サブネット172.18.52.0/24で始まるレンジを割り当てます。IPマスカレードを手動で設定するのはDockerを--iptables=falseで走らせているためです。あとはDockerのディレクトリを作り、exec-rootとコンテナイメージやキャッシュのディレクトリを置いておきます。Unixドメインソケットとpidファイルもここにおきます。DOCKER_HOST環境変数をこのドメインソケットのパスにしておくように。

あとはNetコンテナを一番最初に起動しておくだけです。どんなコンテナプロセスでもいいのですが、終了しないことが大事です。

    FROM ubuntu:14.04
    CMD ["/bin/sh", "-c", "yes | while read line; do sleep 1; done"]
$ docker run -t -d --name net net

試しにRedisをこのネットワーク名前空間で走らせます。

    $ docker run --net=container:net -d --name redis redis redis-server --appendonly yes

最後にDOCKER_HOSTをbash_profileに書いて次回ログイン時も同じようにアクセスできるようにしておきます。これでユーザ毎に専用のDocker環境を作ることができました。


先日アナウンスしたディープラーニングを使ったトレーディングプラットフォームのCapitalicoでも、Dockerをかなり使っています。Capitalicoではベータユーザを大募集中です。もしご興味があれば招待リストに応募していただければこちらからご連絡致します!

http://www.capitalico.co/