Docker for Mac の Bridgeモードを考察してみた

Docker for Mac の Bridgeモードを考察してみた

はじめに

【 大阪オフィス開設1周年勉強会 】第6回 本番で使うDocker勉強会 in 大阪 2017/06/09 #cm_osaka | Developers.IO に参加した際、内輪で Docker の Bridge モードと Host モードの違いは何? という話題になりました。その時は、ざっくり言うと Network Namespace の使用有無であり、Network Namespace を利用する場合 iptables や Linux ブリッジ、veth pair (L2 トンネル)を使って、IP を転送しています。と説明した記憶があるのですが、本記事で少し考察してみます。

下準備

なお今回は、手元の Docker for Mac (moby linux)を利用して Bridge モードの考察を行います。Docker for Mac 環境で moby linux にアクセスする方法は、過去の記事に記載しておりますので参照してください。

筆者の検証環境(macOS と Docker for Mac)は、以下のとおりです。

 ~ $ docker version
Client:
 Version:      17.03.1-ce
 API version:  1.27
 Go version:   go1.7.5
 Git commit:   c6d412e
 Built:        Tue Mar 28 00:40:02 2017
 OS/Arch:      darwin/amd64

Server:
 Version:      17.03.1-ce
 API version:  1.27 (minimum version 1.12)
 Go version:   go1.7.5
 Git commit:   c6d412e
 Built:        Fri Mar 24 00:00:50 2017
 OS/Arch:      linux/amd64
 Experimental: true
 ~ $ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.12.5
BuildVersion:   16F73
 ~ $ 

次に、moby linux 側で Network Namespace を確認するために、iproute2 パッケージをインストールしておきます。

/ # apk update
/ # apk add iproute2

Bridgeモード

Bridgeモードは、Hypervisor 側の Linux に構成されている docker0 という Linux Bridge を介してコンテナ起動時に veth pair(Virtual Ethernet Tunnel)が作成され、片方は docker0 側へ接続、もう片方は起動されたコンテナの Network Namespace 内に eth0 として接続されることで仮想的なネットワーク環境を構成します。

まずは、コンテナが起動されていない状態のネットワークを確認してみます。

/ # ip addr show dev docker0
14: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN 
    link/ether 02:42:6d:7e:c4:d2 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 scope global docker0
       valid_lft forever preferred_lft forever
/ # brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02426d7ec4d2       no
/ # docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "0ef4bececf3170d4e2c6822f5e7560d355cc8be84f5988030887237f38438b96",
        "Created": "2017-06-23T01:17:12.801021267Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]
/ # iptables -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION -j RETURN
/ # iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy DROP)
target     prot opt source               destination         
DOCKER-ISOLATION  all  --  anywhere             anywhere            
DOCKER     all  --  anywhere             anywhere            
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
ACCEPT     all  --  anywhere             anywhere            
ACCEPT     all  --  anywhere             anywhere            

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain DOCKER (1 references)
target     prot opt source               destination         

Chain DOCKER-ISOLATION (1 references)
target     prot opt source               destination         
RETURN     all  --  anywhere             anywhere            
/ # 

では、次に ubuntu コンテナを起動した直後の状態を確認します。

/ # docker run -it --name ubuntu bash
bash-4.4# exit
/ # docker start ubuntu
ubuntu

exit でコンテナから一旦抜け、再度 ubuntu コンテナを起動しておき Linux Bridge を確認してみます。

/ # brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02426d7ec4d2       no              vethef1372a
/ # ip addr show dev vethef1372a
27: vethef1372a@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether da:e5:98:49:86:5b brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::d8e5:98ff:fe49:865b/64 scope link 
       valid_lft forever preferred_lft forever

veth pair の片側が、docker0 に接続されています。 もう片側の構成を確認する前に、docker network を確認しておきます。

/ # docker network inspect bridge 
[
    {
        "Name": "bridge",
        "Id": "0ef4bececf3170d4e2c6822f5e7560d355cc8be84f5988030887237f38438b96",
        "Created": "2017-06-23T01:17:12.801021267Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Containers": {
            "be4f826c8090297f8d85d56a4d75e134932f15eb48cdae6dba1378cf8d445669": {
                "Name": "ubuntu",
                "EndpointID": "6c3ff09fa947e17463a05c46590661379216607b6829f0bf47c40e471609f89a",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

MAC アドレス "02:42:ac:11:00:02" が割当てられた仮想 NIC (veth pair のもう片方)が、eth0 として構成されているはずです。では、Network Namespace 内を覗いてみましょう。その前に、少し下準備が必要です。

ubuntu コンテナで動作している bash のプロセスID を確認し、Network Namespace を操作するための FD(File Descripter)を確認し、/var/run/netns 配下にシンボリックリンクを張っておきます。

/ # ps aux | grep bash
 9356 root       0:00 bash
/ # pstree
init-+-acpid
     |-chronyd
     |-containerd---containerd-shim---tini---rngd
     |-containerd-ctr
     |-crond
     |-dhcpcd
     |-diagnostics-ser
     |-klogd
     |-proxy-vsockd
     |-sh-+-dockerd---docker-containe---docker-containe---bash
     |    `-logger
     |-sh---pstree
     |-syslogd
     |-transfused
     `-vsudd
/ # ls -l /proc/9356/ns/net
lrwxrwxrwx    1 root     root             0 Jun 28 01:38 /proc/9356/ns/net -> net:[4026532310]
/ # mkdir /var/run/netns
/ # ln -s /proc/9356/ns/net /var/run/netns/ubuntu
/ # ip netns
ubuntu (id: 0)

準備が整いました。では、確認します。 以下のコマンドにより、ubuntu コンテナの Network Namespace 内で ip コマンドを実行し eth0 の情報を確認します。

/ # ip netns exec ubuntu ip addr show dev eth0
26: eth0@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link 
       valid_lft forever preferred_lft forever
/ # 

docker network inspect bridge で確認したとおりの仮想NIC(eth0)が構成されています。 つまり、このコンテナ内の eth0@if27 は docker0 に構成された vethef1372a@if26 の相棒です。 ここまでで、Linux Bridge(docker0)と、veth pair および Network Namespace について見てきました。

次に、nginx コンテナを起動し -p 8080:80 のようにポートフォーワードを行った場合、どのように変化があるか 見てみます。

まずは、nginx コンテナを起動します。

/ # docker run -d -p 8080:80 --name nginx nginx
2c8e2dcf5a9d834cf957fe703b7f13207840bd6746d95f223d747b75e410bb20
/ # docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
2c8e2dcf5a9d        nginx               "nginx -g 'daemon ..."   6 seconds ago       Up 5 seconds        0.0.0.0:8080->80/tcp   nginx

次に、moby linux 側で 8080 ポートを LISTEN しているプロセスと、iptables を確認します。

/ # netstat -anp | egrep '8080|Local Address'
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 :::8080                 :::*                    LISTEN      10083/slirp-proxy
/ # ps -ef | grep 10083
10083 root       0:00 /usr/bin/slirp-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80

slirp-proxy というプロセスが 8080 を待ち受けていました。 プロセスの引数から、moby linux 宛の TCP/8080 を nginx(172.17.0.2:80) へフォーワードするようです。

/ # iptables -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT ★
-A DOCKER-ISOLATION -j RETURN
/ # iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy DROP)
target     prot opt source               destination         
DOCKER-ISOLATION  all  --  anywhere             anywhere            
DOCKER     all  --  anywhere             anywhere            
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
ACCEPT     all  --  anywhere             anywhere            
ACCEPT     all  --  anywhere             anywhere            

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain DOCKER (1 references)
target     prot opt source               destination         
ACCEPT     tcp  --  anywhere             172.17.0.2           tcp dpt:http ★

Chain DOCKER-ISOLATION (1 references)
target     prot opt source               destination         
RETURN     all  --  anywhere             anywhere  

こちらは、nginx コンテナ起動前と比較して、星印の行が追加されています。 nginx コンテナ(TCP/80)宛てのパケット転送を許可しているものと思われます。(少し自信がない)

駆け足で、Bridge モード時のネットワーク構成について確認しましたが何となくイメージは掴めたでしょうか。 このあたりの docker コンテナと Network Namespace に関するお話は、以下の記事が凄く参考になると思います。 ご一読頂ければ、幸いです。

なお、Host モードの場合、上記に記載された docker0 ブリッジ経由での(veth pair を介する)通信や IP 転送などが不要となります。

/ # docker run -d --name nginx-host-mode --net=host nginx
ad8cf0dd02dad80555cc2705fb87e94ca63fd8b0ffe6855b2400c7e5a990ca08
/ # docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
ad8cf0dd02da        nginx               "nginx -g 'daemon ..."   About a minute ago   Up About a minute                       nginx-host-mode
/ # netstat -anp | egrep '80|Local Address'
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      10255/nginx: master

moby linux 側の TCP/80 で nginx プロセスが待ち受けていますね。

さいごに

Docker for Mac のBridgeモードについて考察してみました。ECS インスタンス(例えば、Amazon Linux 上の Docker)環境と比較した場合、moby linux のBridgeモードとは、少し実装が異なるかもしれません。(まだ、未確認) しかしながら、BridgeモードとHostモードを比較すると少なからずソフトウェアオーバーヘッドの差異が表れるということは、これまでの説明により理解頂けるのではないかと考えております。

機会があれば、Bridge vs Host モードでベンチマークテストを試してみたいなと考えています。 ではでは