tcコマンドとDockerコンテナを用いて遅いネットワークをシミュレートする
「手元で、できるだけ実際の環境に近い環境でプログラムの挙動を確認したい」ということがあるかと思います。そこで今回は、遅い(レイテンシの高い)ネットワークをDockerコンテナを用いてシミュレートする具体的な方法を紹介します。
準備
以下のファイルを作成していきます。
- docker-compose.yml
- client/Dockerfile
- server/Dockerfile
- server/main.go
docker-compose.yml
ネットワーク周りをいじるため、cap_add
でNET_ADMIN
を指定する必要があります。
version: "3.0" services: client: container_name: client build: ./client tty: true cap_add: - NET_ADMIN server: container_name: server build: ./server cap_add: - NET_ADMIN
client/Dockerfile
クライアント側のコンテナを用意します。
後にtc
コマンドを用いるので、そのためにiproute2
を入れておきます。また、検証用として使うためにping
コマンドも入れておきます。
FROM ubuntu:18.04 WORKDIR /work RUN apt-get update && \ apt-get install iproute2 iputils-ping -y
server/Dockerfile
サーバー側を用意します。Nginxとかでもいいんですが、なんとなくGo言語でサーバープログラムを用意することにします。こちらにおいてもtc
コマンドを使えるようにします。
FROM ubuntu:18.04 WORKDIR /work RUN apt-get update && \ apt-get install software-properties-common -y && \ add-apt-repository ppa:longsleep/golang-backports && \ apt-get update && \ apt-get install golang-go iproute2 -y ADD main.go . CMD ["go", "run", "main.go"]
server/main.go
net/httpパッケージを用いた、単純なGoのhttpサーバーです。
package main import ( "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, World!")) }) log.Fatal(http.ListenAndServe(":80", nil)) }
レイテンシを追加する
準備ができたので、実際にレイテンシを追加し、それを確認します。
まず、以下のようにして2つのコンテナを起動します。
docker-compose up -d
次に、以下のtcコマンドを実行します。これによって、各コンテナのアウトバウンドトラフィックに100msのレイテンシが追加されます。なお、docker-compose.yml
でNET_ADMIN
を指定していないと、ここでRTNETLINK answers: Operation not permitted
と怒られるはずです。
$ docker exec client tc qdisc add dev eth0 root netem delay 100ms $ docker exec server tc qdisc add dev eth0 root netem delay 100ms
実際に確認してみましょう。RTTは200ms+αになるはずです。
今回作られたコンテナでは、server
というホスト名がserver
コンテナのIPアドレスに解決されます。なので以下のようにしてクライアント側からサーバー側に対してping
コマンドが打てます。
$ docker exec client ping server PING server (172.19.0.3) 56(84) bytes of data. 64 bytes from server.tc_default (172.19.0.3): icmp_seq=1 ttl=64 time=202 ms
正しく設定できていることがわかります。
何が起きているのか
Dockerコンテナを立ち上げる際、仮想ネットワークインターフェイスが作成されます。
仮想ネットワークインターフェイスはペアとして作成され、片方はホストに、片方はコンテナに属することになります(ネットワーク名前空間というものが使われます)。また、ホスト側のネットワークインターフェイスは仮想のブリッジに接続されます。これにより、コンテナ間の通信などができるようになっています。Dockerのネットワーク関係の話についてはこちらが詳しいです(英語です)。
ところで、Linuxで外向きにトラフィックを送ろうとするとき、それはキューに入ります。通常では、キューに入った順から可能な限り速いスピードで外部へと送られていきます(FIFO)。しかし、Linuxではそのキューの動作を変更することが可能になっています。例えば、レイテンシを追加したり、ある確率でデータを破棄したりといった具合です。この機構をQueueing Discipline
、略してqdisc
と言います。tc qdisc ...
コマンドは、まさにそのキューを設定するためのものだったわけです(ここで上げたエミュレーション以外にも様々なことが設定できます)。
ネットワークインターフェイス毎にqdiscの設定ができます。上のコマンドでは、eth0ネットワークインターフェイスに対応するqdiscに100msのレイテンシを追加しています。eth0というのは、Dockerにおいてホスト側とつながっている仮想ネットワークインターフェイスです。
ちなみに、今回はクライントとサーバー、双方でtcコマンドを実行しました。これは、qdiscは(原則として)外向きのトラフィックに関係するものだからです。双方向に100msかかるネットワークを作るために、それぞれでqdiscの設定をしたということです。