この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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 モードでベンチマークテストを試してみたいなと考えています。 ではでは