本稿では複数サーバー複数コンテナ環境でOpenmpi を実行する手順について紹介します。
参考:
環境
2つのサーバーA, B を使用し、その中でそれぞれ2つのコンテナを立ち上げます。 合計4つのコンテナを用いてMPI プログラムを実行します。
Docker Swarm
コンテナ間の通信の確立のためにDocker Swarmを利用します。Docker network を利用すれば同一サーバー内のコンテナ間で通信が行えるようになりますが、Docker Swarmを利用すればそれを複数サーバー間に拡張できます。
Docker Swarm は Docker のオーケストレーションツールでKubernetes (K8s) のようなもの。SwarmはManager(クラスタ全体の状態の管理)とWorker Node(コンテナを実行)で構成されます。
swarm作成前のdocker network
$ docker network ls NETWORK ID NAME DRIVER SCOPE 8ba95d06e22b bridge bridge local 1a107fbc2ad7 host host local 79a4843011e6 none null local
swarmの初期化
@Server Aでdocker swarm init
を実行。server A がマネージャーノードになります。
$ docker swarm init Swarm initialized: current node (favqrqpma9leyba34fgkay0uo) is now a manager. To add a worker to this swarm, run the following command: docker swarm join --token <token> <ip::addr> To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
docker network にもoverlayネットワークとしてingress が追加されています。これにより異なるホスト上にあるコンテナ間での通信を可能になり、クラスタ内の任意のノードが、同じサービスの異なるタスク間でトラフィックをルーティングできます。
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
8ba95d06e22b bridge bridge local
1a107fbc2ad7 host host local
+ glztbyqxbm5p ingress overlay swarm
79a4843011e6 none null local
@Server B にて、docker swarm init の実行時に表示されたコマンドを貼り付け。これで server B がworker ノードとして追加される。
$ docker swarm join --token <token> <ip::addr> This node joined a swarm as a worker.
@Server A (swarm manager) でworkerの状態を確認できます。
$ docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION favqrqpma9leyba34fgkay0uo * xxx Ready Active Leader 24.0.1 ama6yi68spcjzo0sx4n8crwu7 yyy Ready Active 24.0.1
docker network の作成
@Server A でオーバーレイネットワーク mpi-network を作成します。Docker networkは、コンテナ間またはコンテナと外部との通信を管理するための仮想ネットワークです。
--driver overlay
: ネットワークのドライバタイプを指定。overlay
ドライバは、異なるDockerホストにまたがるコンテナ間でネットワークを構築するために使われる--attachable
:overlay
ネットワークに任意の単独コンテナのコンテナ(Swarmサービスに属さないコンテナ)も接続することが可能になる。通常、overlay
ネットワークは Swarm モードで動作するサービスのコンテナに限定される
$ docker network create --driver overlay --attachable mpi-network
@Server B でも同様に mpi-network ネットワークを作成。
$ docker network create --driver overlay --attachable mpi-network
コンテナの作成
Dockerfile
FROM ubuntu:20.04 RUN apt update && apt upgrade RUN DEBIAN_FRONTEND=noninteractive TZ=Asia/Tokyo apt install -y tzdata build-essential libopenmpi-dev openssh-server vim RUN mkdir /var/run/sshd RUN echo 'root:tmp' | chpasswd # rootユーザのpasswordを設定 RUN echo 'PermitRootLogin yes\nPasswordAuthentication yes' >> /etc/ssh/sshd_config RUN mkdir /app WORKDIR app CMD ["/usr/sbin/sshd", "-D"]
コンテナの起動
@Server A --net
で先ほど作成したmpi-network を指定します。
$ docker build -t tmp . $ docker run -itd --net=mpi-network --name tmp_a tmp $ docker run -itd --net=mpi-network --name tmp_b tmp
docker ps でコンテナが立ち上がっているのが見えるはず。例えば次のコマンドでtmp_a コンテナ内に入ることができます。
docker exec -it tmp_a /bin/bash
@Server B でも同様
$ docker build -t tmp . $ docker run -itd --net=mpi-network --name tmp_d tmp $ docker run -itd --net=mpi-network --name tmp_e tmp
ssh鍵の共有
MPIを複数ノードや複数コンテナで実行するには認証なしで相互にログインできる状態にしておく必要があります。
この後一番陥りやすいのが相互にsshする環境の構築である。 複数ノードの場合「ノード間はssh-keyでパスワードなしでssh可」「dockerに入るにはパスワードなしで」を実現しなければならず、通信できない場合にOpenMPIが無反応だったりしてわからんってなるのでちゃんとやる。 (引用: https://vaaaaaanquish.hatenablog.com/entry/2018/09/17/231640)
この際やり方は自由ですが、Server Aでrsa鍵を生成しそれを全てのコンテナで共有して持つように作業しました。
@Server A
1 rsa鍵をコンテナ外で生成し、tmp_a コンテナにコピー。
$ ssh-keygen ... $ docker cp ~/.ssh/id_rsa tmp_a:/root/.ssh/ # id_rsa は作成した鍵の名前に置き換え $ docker cp ~/.ssh/id_rsa.pub tmp_a:/root/.ssh/ $ docker exec tmp_a bash -c 'cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys'
2 Server A :: tmp_a → 他のコンテナに同じ鍵を配布。
for name in tmp_b tmp_d tmp_e; do scp ~/.ssh/* $name:/root/.ssh/; done
あとは全てのコンテナペアで認証なしで、コンテナ間でログインできる必要があります。 最初のsshログイン時にyes を打ち込むことを要求されるで、次のように作業。もっと手軽な方法があるかも。。
3 @ServerA::tmp_a コンテナで 自身を含む全てのコンテナに一度ログイン。
4 /root/.ssh/known_hosts を tmp_aから他のコンテナに配布します
for name in tmp_a tmp_b tmp_d tmp_e; do ssh $name; done # 3 for name in tmp_b tmp_d tmp_e; do scp ~/.ssh/known_hosts $name:/root/.ssh/; done # 4
これでok。
MPI プログラムの実行
最後に簡単なMPIプログラムを実行してみます。
テストプログラム
tmp.cpp
#include <mpi.h> #include <iostream> #include <cstdlib> #include <unistd.h> // For gethostname function #include <vector> int main(int argc, char *argv[]) { MPI_Init(&argc, &argv); int num_procs; int my_rank; MPI_Comm_size(MPI_COMM_WORLD, &num_procs); MPI_Comm_rank(MPI_COMM_WORLD, &my_rank); // ホスト名を取得するためのバッファを用意 std::vector<char> hostname(1024); // バッファサイズは1024文字 // gethostname関数を呼び出し、結果をhostnameに格納 gethostname(hostname.data(), hostname.size()); // プロセス数、ランク、ホスト名を出力 std::cout << "Size: " << num_procs << ", Rank: " << my_rank << ", Host: " << hostname.data() << std::endl; MPI_Finalize(); return EXIT_SUCCESS; }
コンパイル
mpicxx tmp.cpp -o tmp
単一コンテナでの実行
まずは、そのまま
$ ./tmp Size: 1, Rank: 0, Host: 2728c86d3bbb
ホスト内に2プロセス並列。
$ mpirun -n 2 --allow-run-as-root ./a.out Size: 2, Rank: 0, Host: 2728c86d3bbb Size: 2, Rank: 1, Host: 2728c86d3bbb
問題なく実行できていますね。
複数コンテナでの実行
ホストファイルの作成
ホストファイルの作成のためにコンテナのidアドレスを取得します。docker inspect で調べることができる。
@ Server A
$ for name in tmp_a tmp_b; do echo $(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $name); done 10.0.1.2 10.0.1.4
@ Server B
$ for name in tmp_d tmp_e; do echo $(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $name); done 10.0.1.10 10.0.1.12
@ServerA::tmp_a /app/hostfile
10.0.1.2 slots=2 10.0.1.4 slots=2 10.0.1.10 slots=2 10.0.1.12 slots=2
バイナリの共有
忘れがちなバイナリの共有。同じ箇所に置く必要がある。
for name in tmp_b tmp_d tmp_e; do scp ./tmp $name:/app/; done
実行
root@2728c86d3bbb:/app# mpirun -n 8 --allow-run-as-root --hostfile ./hostfile ./a.out Size: 8, Rank: 0, Host: 2728c86d3bbb Size: 8, Rank: 1, Host: 2728c86d3bbb Size: 8, Rank: 3, Host: f3f5691e9308 Size: 8, Rank: 6, Host: d2dcc0ada683 Size: 8, Rank: 2, Host: f3f5691e9308 Size: 8, Rank: 4, Host: 9307f761eb47 Size: 8, Rank: 7, Host: d2dcc0ada683 Size: 8, Rank: 5, Host: 9307f761eb47
無事に実行できました。
まとめ
Docker Swarm を用いたマルチサーバ、マルチコンテナ間のMPI 実行を紹介しました。