いまさらDockerでズンドコキヨシ

ちょっと前にズンドコルータというものが流行っていました(http://qiita.com/kooshin/items/2e00cdeb53cf9cf4c51d).
いいなー私もDockerでやりたいとか思ってましたが,Dockerの機能を使ったいい感じのやり方が思いつかなかったのでやってませんでした.

しかし! Docker 1.12からDockerにもロードバランサが!!これでズンドコキヨシできるやん!!
となりました.
以下思いついてやってみたものの記録です.
※今回結論から言うと成功してません.可能なはずですが何度も何度も繰り返す強い心が必要でした.

Docker ロードバランサ

以前の記事に書いた通りです.内部でIPVSのラウンドロビンで飛ばしてくれます.
今回はIngressのロードバランサのみでやってみます(簡単なので)

設計

まず最も簡単な実装を考えるためDocker LB配下のコンテナがhttpリクエストを受けるとそのコンテナIDによってzun, doko, kyoshiのいずれかを返します.
これでアクセスを繰り返せばいつかzun zun zun doko kiyoshiが完成するはず!という目論見です.
dockerzundoko

サーバ側

Dockerイメージはこんな感じ
https://github.com/YujiOshima/dockerfiles/tree/master/zundoko

Dockerfile

FROM python
ADD files /app
WORKDIR /app
ENTRYPOINT python run.py

run.py

from http.server import HTTPServer, SimpleHTTPRequestHandler
import socket

hostname = socket.gethostname()

class MyHandler(SimpleHTTPRequestHandler):
    def do_GET(self):
        uri = self.path
        body = ""
        if uri == "/zun":
            body = b"zun"
        elif uri == "/doko":
            body = b"doko"
        elif uri == "/kiyoshi":
            body = b"__kiyoshi__"
        else :
            body = judge_reponce(hostname).encode('utf-8')

        self.send_response(200)
        self.end_headers()
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.send_header('Content-length', len(body))
        self.wfile.write(body)

def judge_reponce(cid):
    intid = int(hostname, 16)
    if intid % 5 == 0:
        return "**kiyoshi**"
    elif intid % 5 == 1:
        return "doko"
    else:
        return "zun"

host = '0.0.0.0'
port = 8000
httpd = HTTPServer((host, port), MyHandler)
print('serving at port', port)
httpd.serve_forever()

めっちゃ簡単です.
これをDockerクラスタ上にデプロイします.

docker service create --name=zundokokiyoshi -p 9000:8000 --replicas=30 zundoko

クライアント側

アクセスする側も今回Dockerイメージにします.
https://github.com/YujiOshima/dockerfiles/tree/master/getzundoko

import sys
import http.client
import time

DESIRE = b"zun zun zun doko **kiyoshi** "

def main(server, port, loopnum):
    conn = http.client.HTTPConnection(server, port)
    for i in range(0, loopnum):
        result = b""
        for i in range(0, 5):
            conn.request("GET", "/")
            result += conn.getresponse().read() + b" "
            print(result)
            if result not in DESIRE:
                print("faild...")
                break
            time.sleep(0.01)
        if result == DESIRE:
            print("done!!")
            return 

if __name__ == '__main__':
    args = sys.argv
    server = "localhost"
    port = 80
    loopnum = 10
    if len(args) > 1:
        server = args[1]
    if len(args) > 2:
        port = args[2]
    if len(args) > 3:
        loopnum = int(args[3])
    print("connect "+server+":"+str(port))
    main(server, port, loopnum)

うん,これでアクセスを繰り返してzun zun zun doko kiyoshiが完成すれば終了するはず,

動作!

レッツプレイ!

docker run -it --rm getzundoko 52.196.60.122 9000 100
connect 52.196.60.122:9000
b'**kiyoshi** '
b'**kiyoshi** zun '
faild...
b'zun '
b'zun doko '
b'zun doko zun '
faild...
b'zun '
b'zun zun '
b'zun zun doko '
b'zun zun doko **kiyoshi** '
b'zun zun doko **kiyoshi** doko '
faild...
b'**kiyoshi** '
b'**kiyoshi** zun '
faild...
b'zun '
b'zun doko '
b'zun doko **kiyoshi** '
b'zun doko **kiyoshi** zun '
faild...
b'zun '
b'zun doko '
b'zun doko zun '
faild...
b'zun '
b'zun zun '
b'zun zun doko '
b'zun zun doko **kiyoshi** '
b'zun zun doko **kiyoshi** doko '
faild...
b'**kiyoshi** '
b'**kiyoshi** zun '
faild...
b'zun '
b'zun doko '
b'zun doko **kiyoshi** '
b'zun doko **kiyoshi** zun '
faild...

お!動いてるっぽい...!

結果

ダメでした.
正確には何度繰り返してもzun zun zun doko kiyoshiが完成しませんでした.
理由としてDockerLBの内部ではIPVSのラウンドロビンなのでアクセスが均等になるように割り振られるんですよね.
なので今回私一人しかアクセスしてないので着弾するコンテナの順番が固定になってしまうわけですね...もっと早く気付けよ...

もちろん何度もservice createでコンテナを作りなおす,複数プロセスからアクセスして適当に着弾順序を変えるとかすれば完成すると思いますが.面倒なのでやめました.
コンテナ間アクセスのLBとか使ってもうチョット真面目に考えなおそう.

Docker 1.12のoverlayネットワーク

以前Docker 1.9でのマルチホストネットワークの作成について記事を書きましたが,Docker 1.12になって手順がだいぶ変わったので改めてまとめようと思います.ついでに色々してみました.

結論から言うとDocker 1.12のoverlayネットワークはserviceが定義されたコンテナのみ接続されることが想定されていますが回避も可能です.

まず前の記事を参考にDocker swarmのクラスタを作成します.

  • node1(master) : ip-172-31-1-218

  • node2 : ip-172-31-1-219

  • node3 : ip-172-31-1-217

$ 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

node1でoverlayNWを作成してみます.

$ docker network create -d overlay backend

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
9lapeboarpux        backend             overlay             swarm
af3236af05f2        bridge              bridge              local
1592e8ed2c89        docker_gwbridge     bridge              local
950ee88d8b98        host                host                local
5jq39idymrkq        ingress             overlay             swarm
40051a135237        none                null                local

これでNWは作成されたはずですが,他のホストで確認して見るとこのNWはまだ見えません.

ubuntu@ip-172-31-1-217$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
1d3085970cb3        bridge              bridge              local
db8293d930ca        docker_gwbridge     bridge              local
d13db9be8a4c        host                host                local
5jq39idymrkq        ingress             overlay             swarm
a9f1cbd814fa        none                null                local

さらにネットワークを作成したnode1でこのNWに繋がるコンテナを作成しようとすると失敗します.

$ docker run -itd --net=backend busybox
docker: Error response from daemon: network backend not found.

この状態ではまだNWの実体がないんですね.inspectで確認するとサブネットなども設定されていないことがわかります.

$ docker network inspect backend
[
    {
        "Name": "backend",
        "Id": "10kza2wvjm6dz9nzt25jrdygl",
        "Scope": "swarm",
        "Driver": "overlay",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": []
        },
        "Internal": false,
        "Containers": null,
        "Options": {
            "com.docker.network.driver.overlay.vxlanid_list": "257"
        },
        "Labels": null
    }
]

なのでまずこのNWに紐づくserviceを作成します.これでようやくNWのサブネットやGWが割り当てられ,NWの実体の作成とコンテナのアタッチが行われます.

$ docker service create --name=backend --replicas=5 -p 8000:80/tcp --network=backend redis

$ docker network inspect backend
[
    {
        "Name": "backend",
        "Id": "10kza2wvjm6dz9nzt25jrdygl",
        "Scope": "swarm",
        "Driver": "overlay",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "10.0.0.0/24",
                    "Gateway": "10.0.0.1"
                }
            ]
        },
        "Internal": false,
        "Containers": {
            "53b2c41b5b957f5926b48b9517f67b7af833c2c33d1f91023fd80486f83bcd02": {
                "Name": "anotherbackend.1.caomy5tse6gaow0cv6u6vy6rb",
                "EndpointID": "f09a12d8c937b32e9b8707a7e042168ef806372708c4d03cf26152e55b4f0a83",
                "MacAddress": "02:42:0a:00:00:0b",
                "IPv4Address": "10.0.0.11/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.driver.overlay.vxlanid_list": "257"
        },
        "Labels": {}
    }
]

これで別のホスト上でもNWが作成されます.

ubuntu@ip-172-31-1-217:~$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
1ul9idog376w        backend             overlay             swarm
1d3085970cb3        bridge              bridge              local
db8293d930ca        docker_gwbridge     bridge              local
d13db9be8a4c        host                host                local
5jq39idymrkq        ingress             overlay             swarm
a9f1cbd814fa        none                null                local

しかしこの状態でもこのNWに繋がるコンテナをserviceの定義なしに作ることはできません.

$ docker run -itd --net=backend busybox
docker: Error response from daemon: swarm-scoped network (backend) is not compatible with `docker create` or `docker run`. This network can be only used docker service.
See 'docker run --help'.

あくまでoverlay NWに繋がるコンテナの作成はserviceの作成とセットになります.

$ docker service create --name=anotherbackend --replicas=5 --network=backend postgres

$ docker network inspect backend
[
    {
        "Name": "backend",
        "Id": "10kza2wvjm6dz9nzt25jrdygl",
        "Scope": "swarm",
        "Driver": "overlay",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "10.0.0.0/24",
                    "Gateway": "10.0.0.1"
                }
            ]
        },
        "Internal": false,
        "Containers": {
            "02d3b4a24d99720295539c4c56157e559ec2592c193e1f1ec82c79a265dfd655": {
                "Name": "backend.4.1wbj6bo0ruq87nvdkuswt9xy5",
                "EndpointID": "a248c65b18386464afb2baf4279c6541a9252432b153d7519cdc3fc6abc06654",
                "MacAddress": "02:42:0a:00:00:06",
                "IPv4Address": "10.0.0.6/24",
                "IPv6Address": ""
            },
            "06d638c77e2c3c12178ff6e93a453a3f67f9b7b43ce2d407753acb43e18b5d0e": {
                "Name": "backend.3.9387bpsqnvvyhnugj433ufx32",
                "EndpointID": "47c9969877337f965e742a08809a5065b339f543943cee1241af3f845cf94386",
                "MacAddress": "02:42:0a:00:00:05",
                "IPv4Address": "10.0.0.5/24",
                "IPv6Address": ""
            },
            "53b2c41b5b957f5926b48b9517f67b7af833c2c33d1f91023fd80486f83bcd02": {
                "Name": "anotherbackend.1.caomy5tse6gaow0cv6u6vy6rb",
                "EndpointID": "f09a12d8c937b32e9b8707a7e042168ef806372708c4d03cf26152e55b4f0a83",
                "MacAddress": "02:42:0a:00:00:0b",
                "IPv4Address": "10.0.0.11/24",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.driver.overlay.vxlanid_list": "257"
        },
        "Labels": {}
    }
]

もしもserviceに紐付かないコンテナをNWにどうしても接続したい場合は先にコンテナを作成してからNWにconnectしましょう.

$ docker run -itd busybox
2fda7c0bc0b7f70879c38461b9588f35880be76752ca594a6cd52a220a9ca077

$ docker network connect backend 2fda7c0bc0b7

何故作成時には許されなくて作成後は許されているのかはよく分かりませんが今のところはこれが可能です.
ちなみにdiscponnectも可能です.

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
2fda7c0bc0b7        busybox             "sh"                     4 minutes ago       Up 4 minutes                            angry_roentgen
53b2c41b5b95        postgres:latest     "/docker-entrypoint.s"   About an hour ago   Up About an hour    5432/tcp            anotherbackend.1.caomy5tse6gaow0cv6u6vy6rb
06d638c77e2c        redis:latest        "docker-entrypoint.sh"   About an hour ago   Up About an hour    6379/tcp            backend.3.9387bpsqnvvyhnugj433ufx32
02d3b4a24d99        redis:latest        "docker-entrypoint.sh"   About an hour ago   Up About an hour    6379/tcp            backend.4.1wbj6bo0ruq87nvdkuswt9xy5

$ docker network disconnect backend 06d638c77e2c

これをすると表面上サービスはscale=5で正しく動いているように見えて実はネットワーク不通のコンテナが発生することになります.
おいおいこれはまずいだろと思わなくもないですが,まあ新機能なのでこれから変わるのでしょう.

$ docker service tasks backend
          ID                         NAME       SERVICE  IMAGE  LAST STATE             DESIRED STATE  NODE
          euwtk7ck34ghknvggea6i1fg2  backend.1  backend  redis  Running About an hour  Running        ip-172-31-1-217
          24z71bl2u2fwebaa04dk2o186  backend.2  backend  redis  Running About an hour  Running        ip-172-31-1-219
実は不通-> 9387bpsqnvvyhnugj433ufx32  backend.3  backend  redis  Running About an hour  Running        ip-172-31-1-218 
          1wbj6bo0ruq87nvdkuswt9xy5  backend.4  backend  redis  Running About an hour  Running        ip-172-31-1-218
          1nt960d8v1vqkl69790yrborb  backend.5  backend  redis  Running About an hour  Running        ip-172-31-1-217

AWS上での悲しい出来事

これでできるはずなんですが,今回AWS上でテストしていることが原因(?)でDNSの名前解決したうえでの疎通がうまく行きませんでした.

$ docker exec -it 53b2c41b5b95 ping -c 3 backend
PING backend (10.0.0.2): 56 data bytes
92 bytes from 53b2c41b5b95 (10.0.0.11): Destination Host Unreachable
92 bytes from 53b2c41b5b95 (10.0.0.11): Destination Host Unreachable
92 bytes from 53b2c41b5b95 (10.0.0.11): Destination Host Unreachable
--- backend ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss

$ docker exec -it 53b2c41b5b95 ping -c 3 10.0.0.11
PING 10.0.0.11 (10.0.0.11): 56 data bytes
64 bytes from 10.0.0.11: icmp_seq=0 ttl=64 time=0.060 ms
64 bytes from 10.0.0.11: icmp_seq=1 ttl=64 time=0.076 ms
64 bytes from 10.0.0.11: icmp_seq=2 ttl=64 time=0.057 ms
--- 10.0.0.11 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.057/0.064/0.076/0.000 ms

あれ!?っと思ったけどそういやなんかissue見たことあるなーと思ったら案の定こちらで報告されてました.
まあそのうち治るでしょう.以前もAWS上でNWの問題があったのでなにか相性が悪いことがあるようです.

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 libnetworkデザイン

最近Dockerネットワークの機能がいろいろ増えたりしてるけどあんまり使われてないように思います.

Docker enginのドキュメントだとDockerが叩くAPIからの説明しか無いので,機能の実装側であるlibnetworkについて自分の勉強も兼ねてまとめていこうと思います.まずはlibnetworkについて根本思想となるContainer Network Modelについて理解するために公式のdesign.mdを和訳してみました.
思想とlibnetworkの実装が混ざっていてちょっとわかりづらい部分も有るが,やはり全体を把握するには一番いい資料ですので.今後各ドライバなどについても詳解するつもりで,その中でわかってくることもあるかと思います.

Design

libnetworkのバージョンやゴールはroadmapにハイライトされている.
このドキュメントはlibnetworkが如何にデザインされて今の形になっているかを説明する.
特定のリリースでの情報はProject Pageを参照のこと.

多くのデザインの方向性はDocker v1.6のネットワークデザインを参考にしている.
Docker v1.6のネットワークデザインについてはDocker v1.6 Designを参照のこと.

Goal

libnetworkプロジェクトはdeveloping small, highly modular and composable tools that works well independentlyの哲学に従っている.
libnetworkはコンテナのネットワークに必要な機能を満足させることを目的とする.

The Container Network Model

LibnetworkはContainer Network Model (CNM)の実装という位置付けである.
CNMはコンテナに対し様々なネットワーク機能を提供するための抽象化の枠組みで,主に以下の3コンポーネントから成る.

Sandbox

Sandboxはコンテナのネットワークスタックを定義する.
コンテナのインターフェースやルーティングテーブル,DNSの設定などである.
実装としてはLinuxのネットワークネームスペース,FreeBSDのJailなどがこれに相当する.
Sandboxは複数のendpointを持ち,そのEndpointはそれぞれ異なるnetworkに所属することが可能である.

Endpoint

EndpointはSandboxをネットワークに参加させる役割を持つ.
実装としてはvethやOpen vSwitchのinternal portなどがある.
Endpointは必ず1つのネットワークに所属し,1つの以下のSnadboxに所属することができる.

Network

NetworkはEndpointのグループであり,それぞれのEndpointが直接通信するが可能である.
実装としてはLinux bridgeやVLANなどである.
当然,ネットワークは複数のEndpointから成る.

CNM Objects

NetworkController
NetworkController はDockerにおけるDocker daemonのようにAPIを提供し,Networkを管理する.
libnetworkはinbuilt・remote問わず複数のdriverが同時に動作することをサポートしている.

Driver
Driver は実際にネットワークを作成する実装となる.
libnetworkではDriver特有のoption, labelをDriverへ透過的に渡すためのAPIが提供されている.
DriverにはBridgeやHost, None, overlay といったのinbuiltのものとサードパーティの提供するremoteがある.
Driverが提供するネットワークに対し責任を持ち,管理する.
将来的に多くのネットワーク機能を管理できるように幾つかのDriverを用意する予定である.

Network
NetworkオブジェクトはCNMモデルにおけるNetworkの実装となる.
NetworkControllerNetworkオブジェクトを作成,管理するAPIを提供する.
Networkが作成されたり,情報の更新があった際にDriverへイベントの通知が送られる.
libnetworkでは同一Networkに所属するendpoint間での疎通を可能にし,他のNetworkと隔離するというレベルの抽象化を行っている,
実際にはDriverが疎通と隔離を管理する必要がある.
このNetworkがグローバルスコープである場合には,同一ホスト内及び複数ホスト間の両方で疎通が可能である.

Endpoint
Endpointはサービスのエンドポイントであり,あるコンテナのサービスがネットワーク内の別コンテナのサービスへ疎通可能となる.
NetworkオブジェクトがEndpointの作成及び管理のAPIを提供します.あるEndpointは1つのNetworkにアタッチされる.
Endpointの作成は,対応するSandboxのリソース管理を行うDriverオブジェクトによって行われる.
Endpointはコンテナに特有である必要はなく,クラスタ内でグローバルスコープを持つのが好ましい.
(
補足:クラスタ内で一意であり,クラスタを跨いだ別のSandboxにアタッチなどが可能)

Sandbox
Sandboxオブジェクトはコンテナ内のネットワーク設定,例えばIPアドレスやMACアドレス,ルーティングテーブル,DNSエントリなどを表す.
SandboxオブジェクトはユーザがNetworkに対しEndpointの作成を要求したタイミングで作成される.
そのNetworkを管理するDriverはIPアドレスなどのリソースを割り当て,libnetwrokへSandboxInfoを返す.
libnetworkはネットワーク情報を格納するSandboxの実装としてOSごとに適した構成を用いている(例えばLinuxではnetns).
Sansboxは異なるnetwrokに属する複数のendpointをアタッチすることが可能である.
Sandboxはあるホストの特定のコンテナに紐づくため,そのコンテナが存在するホストに閉じたもの(ローカルスコープ)となる.

CNM Attributes

Options
Options はDriver固有の設定を行う共通的かつ柔軟な枠組みである.
Optionsはkey-valueのペアでkeyは文字列,valueは一般的なオブジェクトである(例えばgo言語のinterface{}など).
libnetworkはkeynet-labelsパッケージに定義されたLabelに一致するときのみOptionsを受け付ける.
Optionsは下記で説明するLabelsの概念を包含する.
エンドユーザーがUIからOptionsを確認することはできないがLabaelsは可能である.

Labels
LabelsOptionsと非常に似ており,実質的にOptionsの一部である.
Labelsはエンドユーザーから確認することが可能であり,--labelsオプションを指定することで明示的にUIで指定可能である.
UIからDriverへと渡され,Driverはこれを使用し,そのDriver固有の操作を行うことができる(例えばネットワークにIPアドレスを割り当てたりなど).

CNM Lifecycle

CNMの利用者,たとえばDockerはCNMオブジェクトおよびそのAPIを介して操作すべきコンテナのネットワークをやり取りを行う.

  1. DriversNetworkControllerを登録する.Build-inのドライバはlibnetworkの内部から,remoteドライバはPluginメカニズムを介して行う(Pluginメカニズムについては今後).各driverは特定のnetworkTypeを持つ.
  2. NetworkControllerオブジェクトはlibnetwork.New()APIを用いてネットワークの割当やDockerへのOptionsを用いてDriverへの設定を行う.
  3. NetworkはコントローラのNewNetwork()APIによってnamenetworkTypeを与えられる.
    networkTypeNetwork作成時にどのDriverを選択すべきかを示す.
    これ以降,Networkに対する操作は全てDriverから行われる.
  4. controller.NewNetwork() APIはoptionsパラメータを取ることができる.
    これはドライバ固有のオプションやLabelsを与えるものである.
  5. network.CreateEndpoint() は新しいEndpointをネットワークが作成する際に呼ばれる.
    このAPIはoptionsパラメータを取ることができる.
    これらのoptionsは定義されたラベルかつドライバ固有のラベルを指定できる.
    そしてドライバがdriver.CreateEndpointによって呼ばれ,NetworkEndpointが作成された時に予約されたIPv4/IPv6アドレス群の中からアドレスが選ばれる.
    このIP/IPv6アドレスはそのエンドポイントが待ち受けるポートと共にサービスのエンドポイントとして定義されなければならない.
    なぜならサービスのエンドポイントの本質はそのコンテナが待ち受けているネットワークアドレスとポート番号に他ならないからである.
  6. endpoint.Join()はコンテナにEndpointをアタッチする際に用いられる.
    このJoinはそのコンテナのためのSandboxが未だ作成されていなければ作成する.
    ドライバはSandboxのIDを複数のエンドポイントが同じコンテナにアタッチされるために利用することができる.
    このAPIはまたoptionsパラメータを指定可能である.
  7. libnetworkの直接的なデザインの問題ではないがDockerのようにendpoint.Join()が呼ばれるのは,コンテナが作成が呼び出されるStart() ライフライクルであることが強く推奨される.
  8. エンドポイントのjoin()APIについてのFAQにおいて,エンドポイントのcreateとjoinを別のAPIにする必要があるのかというものがあった.
    • これに対する答えはエンドポイントはサービスを表現しており,それはコンテナとは独立である.エンドポイントが作成された時,リソースが確保されいかなるコンテナもそのエンドポイントにあとからアタッチすることが可能であり,一貫したネットワークの挙動を示す.
  9. endpoint.Leave() はコンテナがストップした際に呼ばれる.
    DriverJoin()の時に確保したものを削除する.
    libnetworkはSandboxの削除を,それを参照する最後のエンドポイントがleaveした時に行う.
    しかしlibnetworkはエンドポイントが持つIPアドレスを持ち続け,コンテナ(同じか別かに関わらず)再びjoinした時にそのアドレスを使用する.
    これはコンテナのリソースがストップし,再スタートしても変わらないことを保証する.
  10. endpoint.Delete() はエンドポイントをネットワークから削除するときに用いられる.
    エンドポイントが削除されるとsandbox.Infoからも取り除かれる.
  11. network.Delete() はネットワークを削除するときに用いられる.
    libnetworkはネットワークに所属するエンドポイントが存在する場合にはそのネットワークを削除する事はできない.

Implementation Details

Networks & Endpoints

libnetworkのネットワークとエンドポイントのAPIは基本的にCNMで示されるオブジェクトと抽象化のレベルに一致させている.
CNMで示したように本質的な実装はドライバで行われている.参考:the drivers section

Sandbox

libnetworkは複数のOSでSandboxを実装するためのフレームワークを提供している.
現状Linuxではnamespace_linux.goconfigure_linux.gosandboxパッケージじ含まれる.
これはファイルシステムにおいて唯一のパスを持つネットワークネームスペースをそれぞれのサンドボックスごとに持つ.
Netlinkがグローバルネットワークからサンドボックスのネームスペースにインターフェースを移動するために用いられる.
Netlinkはネットワーク内でのルーティングテーブルの操作も行う.

Drivers

API

ドライバはlibnetworkを拡張し,上で述べたlibnetworkのAPIを実装している部分となる.
そのため全てのNetworkEndpoint の以下のAPIに1対1で対応するものを持つ.

  • driver.Config
  • driver.CreateNetwork
  • driver.DeleteNetwork
  • driver.CreateEndpoint
  • driver.DeleteEndpoint
  • driver.Join
  • driver.Leave

ドライバはユニークなID (networkid,endpointid,…) を持つ.
これはユーザがAPIを操作する際に目にする名前に対応している.

APIは未だ過渡的なものであり,特にマルチホストネットワークを実現する際には変わり得るものである.

Driver semantics

  • Driver.CreateEndpoint

このメソッドはInterfaceAddInterfaceメソッドによってEndpointInfoインターフェースを渡す.
Interface の返り値がnon-nilの場合,ドライバはそこに含まれるインターフェースの情報を利用(例えばアドレスは静的に与えられているものとして扱う)し,それが不可能な場合はerrorを返さなくてはならない.
返り値がnilの場合にはドライバが新しいインターフェースを割り当てなければならないを意味し,AddInterfaceによってそれらを登録するか,不可能な場合にはerrorを返す.

AddInterfaceInterfacenon-nilの場合には使用できない.

Implementations

libnetworkは以下のドライバを持つ.

  • null
  • bridge
  • overlay
  • remote

Null

nullドライバはAPIとしてnoopの実装となっており,ネットワーキングを必要としない時にのみ利用される.
これはDockerの --net=noneオプションのバックワードコンパチビリティのために用意されている.

Bridge

bridgeドライバはLinux Bridgeをベースとするブリッジング接続を提供する.
より詳細はブリッジドライバの章で説明する.

Overlay

Overlayドライバの実装はVXLAN等のカプセリングによるオーバレイネットワークによりマルチホストネットワークを実現する.
より詳細はオーバレイドライバの章で説明する.

Remote

remoteパッケージはドライバを提供しておらず,リモート接続のドライバをサポートする方法が示されている.
貴方は好きな言語でドライバを実装することができる.
より詳細はリモートドライバの章で説明する.