先日のDockerCon16でDocker 1.12RCが発表されまして,主な機能追加として
- SwarmのDocker engineへの統合とそれに伴うクラスタ構築の簡略化
-
Service機能の追加
-
Load Balancer機能の追加
が発表されました.
今回はSwarmクラスタの構築~Serviceの定義まで行って,ロードバランサの内部実装を詳しく追ってみます.
Docker 1.12RCのインストール
これは既にget dockerで問題なく可能です.
今回はAWS上にクラスタを構築します.
- node1(master) : ip-172-31-1-218
-
node2 : ip-172-31-1-219
-
node3 : ip-172-31-1-217
これら3台のホスト上でそれぞれ以下のコマンドでDocker 1.12をインストールします.
$ wget -qO- https://experimental.docker.com/ | sh
Swarm クラスタの構築
node1上で以下のコマンドを実行し,Swarmクラスタのmasterとして定義します.
$ docker swarm init Swarm initialized: current node (71g9k9xcb78u90r8w6zcer4z0) is now a manager.
次にnode2, node3それぞれで以下のコマンドを実行し,Swarmノードへ追加します.
$ docker swarm join 172.31.1.218:2377 This node joined a Swarm as a worker.
これで3台のSwarmクラスタが構築されました.
node1で以下のコマンドを実行し,クラスタが構築されていることを確認します.
$ docker node ls ID NAME MEMBERSHIP STATUS AVAILABILITY MANAGER STATUS 71g9k9xcb78u90r8w6zcer4z0 * ip-172-31-1-218 Accepted Ready Active Leader 7gg6fa9a7frr3h66widewkjc2 ip-172-31-1-219 Accepted Ready Active em6kz4ijedaj48tfssi7k26l3 ip-172-31-1-217 Accepted Ready Active
これまでのポートを開放したりSwarmコンテナを構築したりといった手順がなくなり手軽に構築できました.
Dockerサービス定義
次は1.12から追加されたサービス機能です.
composeではサービスの定義はコンテナのラベルで管理されていましたが,正式にserviceというリソースが定義されdockerコマンドで管理できるようになりました.
と言っても難しいことは特にないです.docker runの延長の様な感じです.
node1上で以下のコマンドを実行します.
$ docker service create --name vote -p 8080:80 instavote/vote 4g17854r60v68gcti1gjbvjqx
これでvoteサービスが定義され,instavote/voteイメージのコンテナが1台配備されます.
現在定義されているサービスを確認
$ docker service ls ID NAME REPLICAS IMAGE COMMAND 4g17854r60v6 vote 1/1 instavote/vote
更にそのサービス上で動作しているコンテナを確認
$ docker service tasks vote ID NAME SERVICE IMAGE LAST STATE DESIRED STATE NODE c003owls6tcdfnfwpn7o89g32 vote.1 vote instavote/vote Running 57 seconds Running ip-172-31-1-218
node1上でinstavote/voteイメージのコンテナが1台動作していることがわかります.
composeと同様にscaleコマンドも存在します.
$ docker service scale vote=6 vote scaled to 6 $ docker service tasks vote ID NAME SERVICE IMAGE LAST STATE DESIRED STATE NODE c003owls6tcdfnfwpn7o89g32 vote.1 vote instavote/vote Running About a minute Running ip-172-31-1-218 aff60et0v925cylsiw62docss vote.2 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-217 chpnwvc3cv3fuq9yorzfz6wj3 vote.3 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-219 4jsnmbn9fne9pouh8ffxuo6no vote.4 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-219 7safag59ght9u4pobgw47ae04 vote.5 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-218 4yyo054kzasvupckynhi9dx7b vote.6 vote instavote/vote Preparing 1 seconds Running ip-172-31-1-217
それぞれのホストに6台のコンテナが分散されて配備されたのがわかります.
これで実はサービスは8080番のポートで外部に公開されており,どのホストに8080番でアクセスしてもこれら6台のコンテナにロードバランスされます.
このinstavote/voteはアクセスするとコンテナIDを表示しているのでリロードする度着弾するコンテナが変わるのがわかりやすいです.
ロードバランサの内部実装
これで使う分にはOKですが,内部がどうなっているか一応理解しておかないと気持ち悪いのでもう少し深追いしてみます.
まず,8080番のポートはDockerらしくiptablesでコンテナ内の80番にNATされています.
$ sudo iptables-save ... -A DOCKER-INGRESS -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.18.0.2:8080 ...
さて,このIP 172.18.0.2はどのコンテナにも該当しません.
例えばnode1上のコンテナでは
$ docker exec -it vote.1.c003owls6tcdfnfwpn7o89g32 ip address|grep -A 1 -B 1 link/eth 18: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP link/ether 02:42:0a:ff:00:07 brd ff:ff:ff:ff:ff:ff inet 10.255.0.7/16 scope global eth0 -- 20: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff inet 172.18.0.3/16 scope global eth1 docker exec -it vote.5.7safag59ght9u4pobgw47ae04 ip address|grep -A 1 -B 1 link/eth 22: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP link/ether 02:42:0a:ff:00:0b brd ff:ff:ff:ff:ff:ff inet 10.255.0.11/16 scope global eth0 -- 24: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP link/ether 02:42:ac:12:00:04 brd ff:ff:ff:ff:ff:ff inet 172.18.0.4/16 scope global eth1
実は,コンテナに紐付かないingress用のnetwork namespaceが作られています.
今回その中を覗いてみます.
まずnode1上のコンテナを改めて確認.
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a060a2cdfa46 instavote/vote:latest "gunicorn app:app -b " About a minute ago Up About a minute 80/tcp vote.5.7safag59ght9u4pobgw47ae04 c423eeebc7f4 instavote/vote:latest "gunicorn app:app -b " 2 minutes ago Up 2 minutes 80/tcp vote.1.c003owls6tcdfnfwpn7o89g32
これらのコンテナのnetwork namespaceのIDを確認.
$ docker inspect a060a2cdfa46|grep Sandbox "SandboxID": "567239b6a8b4d9116210e16eb6419a140d510296b090667bf2e45a6a9340d4fa", "SandboxKey": "/var/run/docker/netns/567239b6a8b4", $ docker inspect c423eeebc7f4|grep Sandbox "SandboxID": "df78773b11ec2d07feffdc3b557d1971f4955d7bf5fca66cc9eb882e9d6bbd76", "SandboxKey": "/var/run/docker/netns/df78773b11ec",
ここでのSandboxというのがdockerのCNMの用語でLinuxでのnetwork namespaceに相当します.
それらの実体はSandboxKeyに記述されているファイルです.
実際に/var/run/docker/netnsを見てみると
$ sudo ls /var/run/docker/netns 1-5jq39idymr 567239b6a8b4 c47f7eacb1bc df78773b11ec
コンテナは2台しかないはずなのにSandbox (network namespace)は4つ作られています.
のうち,567239b6a8b4とdf78773b11ecはコンテナのSandboxでc47f7eacb1bcはIngress用のSandbox,1-5jq39idymrはコンテナ間をつなぐoverlay networkのvtepが入っているSandboxになります.
これらのSandboxは/var/run/netns以下にシンボリックリンクを貼れば中を除くことが可能です.
$ sudo ln -s /var/run/docker/netns/1-5jq39idymr /var/run/netns/vtep $ sudo ln -s /var/run/docker/netns/c47f7eacb1bc /var/run/netns/lbingress
まずIngress のnetwork namespaceの中は
$ sudo ip netns exec lbingress ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 14: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default link/ether 02:42:0a:ff:00:03 brd ff:ff:ff:ff:ff:ff inet 10.255.0.3/16 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::42:aff:feff:3/64 scope link valid_lft forever preferred_lft forever 16: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff inet 172.18.0.2/16 scope global eth1 valid_lft forever preferred_lft forever inet6 fe80::42:acff:fe12:2/64 scope link valid_lft forever preferred_lft forever
ここに,iptablesのNAT先の172.18.0.2があることがわかります.
そしてこのnetwork namespace内のipvsの設定を確認します.
$ sudo ip netns exec lbingress ipvsadm -L IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn FWM 256 rr -> ip-10-255-0-7.ap-northeast-1 Masq 1 0 0 -> ip-10-255-0-8.ap-northeast-1 Masq 1 0 0 -> ip-10-255-0-9.ap-northeast-1 Masq 1 0 0 -> ip-10-255-0-10.ap-northeast- Masq 1 0 1 -> ip-10-255-0-11.ap-northeast- Masq 1 0 0 -> ip-10-255-0-12.ap-northeast- Masq 1 0 0
これで外からのパケットが10.255.0.0/16のネットワークにラウンドロビンで転送されることがわかります.
次にvtepの存在するnetwork namespaceを確認します.
$ sudo ip netns exec vtep ip -d link 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000 link/ether 06:4b:04:c2:0e:15 brd ff:ff:ff:ff:ff:ff promiscuity 0 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default link/ether 02:42:e7:e2:42:9c brd ff:ff:ff:ff:ff:ff promiscuity 0 bridge 9: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default link/ether 02:42:bd:90:69:18 brd ff:ff:ff:ff:ff:ff promiscuity 0 bridge 12: ov-000100-5jq39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default link/ether ae:17:c3:4a:f7:a7 brd ff:ff:ff:ff:ff:ff promiscuity 0 bridge 13: vx-000100-5jq39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master ov-000100-5jq39 state UNKNOWN mode DEFAULT group default link/ether ce:8d:fa:16:ca:ab brd ff:ff:ff:ff:ff:ff promiscuity 1 vxlan id 256 port 32768 61000 proxy l2miss l3miss ageing 300 15: vethddbc8d5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master ov-000100-5jq39 state UP mode DEFAULT group default link/ether b2:a9:a5:49:f0:b4 brd ff:ff:ff:ff:ff:ff promiscuity 1 veth 17: veth75b6707: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP mode DEFAULT group default link/ether 2e:59:0d:5c:68:27 brd ff:ff:ff:ff:ff:ff promiscuity 1 veth 19: veth8b7c018: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master ov-000100-5jq39 state UP mode DEFAULT group default link/ether d2:61:21:33:e4:b7 brd ff:ff:ff:ff:ff:ff promiscuity 1 veth 21: veth18584b0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP mode DEFAULT group default link/ether 72:4d:09:77:76:41 brd ff:ff:ff:ff:ff:ff promiscuity 1 veth 23: vetha2a1ee3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master ov-000100-5jq39 state UP mode DEFAULT group default link/ether ae:17:c3:4a:f7:a7 brd ff:ff:ff:ff:ff:ff promiscuity 1 veth 25: veth3589589: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP mode DEFAULT group default link/ether a6:a9:a6:c5:ca:da brd ff:ff:ff:ff:ff:ff promiscuity 1 veth 27: veth2d99f87: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default link/ether 66:3e:a3:35:55:f1 brd ff:ff:ff:ff:ff:ff promiscuity 1 veth 29: vethd1e34b5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default link/ether 06:07:8a:d0:d5:df brd ff:ff:ff:ff:ff:ff promiscuity 1 veth
たくさんインターフェースがありますが,これは同一ovelay networkに繋がるホスト上のコンテナ全てがこのnetwork namespace上でブリッジングされているためです.
そしてvx-000100-5jq39がvtepでov-000100-5jq39に接続されてます.
中のfdbとルーティングテーブルはこんな感じ
$ sudo ip netns exec vtep bridge fdb show dev vx-000100-5jq39 ce:8d:fa:16:ca:ab vlan 0 permanent 02:42:0a:ff:00:04 vlan 0 02:42:0a:ff:00:04 dst 172.31.1.219 self permanent 02:42:0a:ff:00:05 dst 172.31.1.217 self permanent 02:42:0a:ff:00:08 dst 172.31.1.217 self permanent 02:42:0a:ff:00:09 dst 172.31.1.219 self permanent 02:42:0a:ff:00:0a dst 172.31.1.219 self permanent 02:42:0a:ff:00:0c dst 172.31.1.217 self permanent $ sudo ip netns exec vtep ip route default via 172.31.0.1 dev eth0 10.255.0.0/16 dev ov-000100-5jq39 proto kernel scope link src 10.255.0.1 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 172.18.0.0/16 dev docker_gwbridge proto kernel scope link src 172.18.0.1 172.31.0.0/20 dev eth0 proto kernel scope link src 172.31.1.218
ingressからこのnetwork namespaceに飛ばされて,vtepでカプセリングされてそれぞれのコンテナに飛ばされることがわかりました.
ちなみに,これは外からのアクセスの場合.
内部でのコンテナ間通信でも実はDNSのラウンドロビンでロードバランサは実装されています.
Dockerコンテナ間はDockerのembededDNSサーバが名前解決することでサービス名で疎通することが可能で,その際にラウンドロビンでロードバランスされます.
Docker 1.12のswarmはサービスのオートヒーリングの機能も実装されて,ようやくDockerクラスタの機能が揃った感じがあります.
kubernetesと比較して簡単にクラスタやロードバランサの設定ができるのは魅力ですが,スケーラビリティとかそれぞれの機能比較は今後したい感じです.