Docker ロードバランサ内部実装

先日の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を表示しているのでリロードする度着弾するコンテナが変わるのがわかりやすいです.

LBwebUI

ロードバランサの内部実装

これで使う分には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つ作られています.
のうち,567239b6a8b4df78773b11ecはコンテナの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でカプセリングされてそれぞれのコンテナに飛ばされることがわかりました.
ingress

ちなみに,これは外からのアクセスの場合.
内部でのコンテナ間通信でも実はDNSのラウンドロビンでロードバランサは実装されています.
Dockerコンテナ間はDockerのembededDNSサーバが名前解決することでサービス名で疎通することが可能で,その際にラウンドロビンでロードバランスされます.
inner

Docker 1.12のswarmはサービスのオートヒーリングの機能も実装されて,ようやくDockerクラスタの機能が揃った感じがあります.
kubernetesと比較して簡単にクラスタやロードバランサの設定ができるのは魅力ですが,スケーラビリティとかそれぞれの機能比較は今後したい感じです.

Docker ロードバランサ内部実装」への2件のフィードバック

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中