tcコマンドとDockerコンテナを用いて遅いネットワークをシミュレートする

「手元で、できるだけ実際の環境に近い環境でプログラムの挙動を確認したい」ということがあるかと思います。そこで今回は、遅い(レイテンシの高い)ネットワークをDockerコンテナを用いてシミュレートする具体的な方法を紹介します。

準備

以下のファイルを作成していきます。

  • docker-compose.yml
  • client/Dockerfile
  • server/Dockerfile
  • server/main.go

docker-compose.yml

ネットワーク周りをいじるため、cap_addNET_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.ymlNET_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の設定をしたということです。