めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

FlannelのVXLANバックエンドの仕組み

何の話かというと

Kubernetesの環境をセットアップする際は、コンテナ間で通信するための内部ネットワークを用意する必要があり、このためのツールとして、Flannelがよく利用されます。この時、バックエンドにVXLANを指定すると、物理ネットワークの上にVXLANによるOverlay方式で内部ネットワークが構成されます。

ここでは、Flannelが構成する内部ネットワークの仕組みを解説しつつ、VXLANについて学んでみたいと思います。RHEL7.1でKubernetes+Flannelの環境を構築する手順は、下記を参照ください。

Flannelが構成する内部ネットワーク

上記の手順で環境構築すると、下図のように内部ネットワークが用意されます。各ノード(Minion)には、VXLANデバイス「flannel.1」が作成されて、VXLANのトンネルを構成します。「etcd」は、Flannelが各種情報を格納するKeyValueストアです。

ただし、この図は少しばかり簡略化されています。より正確には、下図のような論理ネットワークが構成されます。

各Minionには、コンテナが接続するサブネット「10.1.x.0/24」(xはMinionごとに異なる値)が用意されて、仮想ブリッジ「docker0(10.1.x.1)」が外部通信用のゲートウェイになります。その対向として、サブネット「10.1.0.0/16」のOverlayネットワークが用意されており、「flannel.1(10.1.x.0)」が各Minionにおけるゲートウェイになります。

各MinionのホストLinuxは、「docker0」と「flannel.1」を中継する仮想ルーターとして機能すると考えるとよいでしょう。たとえば、上図の左端のMinion上でルーティングテーブルを見ると下記のようになっています。

10.1.0.0/16 dev flannel.1  proto kernel  scope link  src 10.1.11.0 
10.1.11.0/24 dev docker0  proto kernel  scope link  src 10.1.11.1

それでは、このルーティングテーブルにしたがって、どのようにパケットが転送されるのでしょうか。具体例で見ていくことにします。

パケット転送の例

MinionでFlannelデーモン(flanneld)を起動すると、VXLANデバイス「flannel.1」が作成されて自動的にサブネットが割り当てられます。ここでは、下記のように、minion01とminion02に、それぞれ「10.1.24.0/24」と「10.1.65.0/24」が割り当てられました。

この時、flanneldは、各VXLANデバイスの情報をバックエンドのetcdに保存します。今の例では、次のような情報が格納されています。

# curl -sL http://kubemaster01:4001/v2/keys/coreos.com/network/subnets | python -mjson.tool
{
    "action": "get",
    "node": {
        "createdIndex": 19,
        "dir": true,
        "key": "/coreos.com/network/subnets",
        "modifiedIndex": 19,
        "nodes": [
            {
                "createdIndex": 300,
                "expiration": "2015-04-03T15:24:29.665681809+09:00",
                "key": "/coreos.com/network/subnets/10.1.24.0-24",
                "modifiedIndex": 300,
                "ttl": 61186,
                "value": "{\"PublicIP\":\"192.168.122.101\",\"BackendType\":\"vxlan\",\"BackendData\":{\"VtepMAC\":\"fe:07:49:6a:6f:04\"}}"
            },
            {
                "createdIndex": 301,
                "expiration": "2015-04-03T15:27:13.105093384+09:00",
                "key": "/coreos.com/network/subnets/10.1.65.0-24",
                "modifiedIndex": 301,
                "ttl": 61349,
                "value": "{\"PublicIP\":\"192.168.122.102\",\"BackendType\":\"vxlan\",\"BackendData\":{\"VtepMAC\":\"ee:c6:29:27:73:7a\"}}"
            }
        ]
    }
}

これを整理すると、下表の情報が格納されていることが分かります。「物理NICのIPアドレス」は、VXLANでカプセル化したパケットを物理ネットワークに送出する際に使用する物理NICのIPアドレスです。

flannel.1のMACアドレス サブネット 物理NICのIPアドレス
fe:07:49:6a:6f:04 10.1.24.0/24 192.168.122.101
ee:c6:29:27:73:7a 10.1.65.0/24 192.168.122.102

flanneldは、同時に、ローカルホストのルーティングテーブルに前述のゲートウェイの設定を追加します。今の場合、minion01では次のようになります。

# ip r
default via 192.168.122.1 dev eth0  proto static  metric 100 
10.1.0.0/16 dev flannel.1  proto kernel  scope link  src 10.1.24.0 
10.1.24.0/24 dev docker0  proto kernel  scope link  src 10.1.24.1 
192.168.122.0/24 dev eth0  proto kernel  scope link  src 192.168.122.101  metric 100 

また、この例では、minion02でコンテナを起動して、IPアドレス「10.1.65.39」を割り当てています。この状態で、minion01のホストからコンテナにpingを実行します。

まず、ホストLinuxは、ルーティングテーブルを見て、「flannle.1」からパケットを送出しようと考えます。ただし、宛先の「10.1.65.39」はサブネット「10.1.0.0/16」に含まれるIPアドレスですので、L2レイヤーでの直接転送が可能と考えて、まずはARPを解決しようとします。つまり、「flannel.1」に対して、「10.1.65.39」に対するARP解決要求を送るように指示します。

しかし!

実際には、「flannel.1」からARPパケットは送出されません。flanneldは、「flannel.1」を作成した際に下記のカーネルパラメーターをセットしているからです。

# cat /proc/sys/net/ipv4/neigh/flannel.1/app_solicit
3

これは、このデバイスにおけるARP解決は、ユーザー空間のエージェントに依頼することを指定するもので、ホストLinuxのカーネルは「L3MISS」というイベントを発行して、ユーザー空間のプロセスにARP解決を要求します。(処理的にはL2レイヤーのものですが、「IPアドレス10.1.65.39」に対応する情報がないという意味で「L3MISS」と呼んでいます。)

今の場合は、ユーザー空間で稼働するflanneldがこのイベントを受け取って、ARP解決の処理を行います。ただし、flanneldは、ARPパケットを外部に送信することはしません。先ほどのetcdに格納された情報を見て、

  • 宛先IPアドレス「10.1.65.39」を含むサブネット「10.1.65.0/24」が割り当てられたノードの「flannel.1のMACアドレス」は「ee:c6:29:27:73:7a」である。

という事を確認した上で、このMACアドレスをローカルホストのARPテーブルに記録します。ipコマンドで確認すると次のようになります。

# ip n | grep 10.1.65.39
10.1.65.39 dev flannel.1 lladdr ee:c6:29:27:73:7a REACHABLE

これでARP解決ができたことになります。カーネルは、宛先IPアドレス「10.1.65.39」のL3パケットに対して、宛先MACが「ee:c6:29:27:73:7a」のL2ヘッダを付けて「flannel.1」から送出しようとします。

ただし!

「flannel.1」はVXLANの設定がなされたデバイスですので、ここで、カーネルは、VXLANのカプセル化処理を開始します。ちなみに、VXLANデバイスであることは、次の(-dオプション付きの)ipコマンドから分かります。

# ip -d l show flannel.1
3: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT 
    link/ether fe:07:49:6a:6f:04 brd ff:ff:ff:ff:ff:ff promiscuity 0 
    vxlan id 1 local 192.168.122.101 dev eth0 srcport 0 0 dstport 8472 proxy l2miss ageing 300 

そして、VXLANのカプセル化を行う際は、送信先ノードの物理IPアドレスを決める必要があります。このための情報は、カーネル内部の「FDB(Forwarding Database)」に記録されており、今の場合は、下記の情報が入っています。

# bridge fdb show dev flannel.1
ee:c6:29:27:73:7a dst 192.168.122.102 self 

これから、宛先MACアドレスが「ee:c6:29:27:73:7a」のL2パケットは、物理IPアドレス「192.168.122.102」に転送するべきことが分かります。

おっと!

ここで、疑問がわくはずです。このFDBの情報はどこからやってきたのでしょうか? 

実は、この情報は、flanneldが登録しています。最初、FDBは空っぽのため、カーネルは、XVLANのパケットをどこに送信していいか分からず、「L2MISS」というイベントを発行します。(処理的にはL3レイヤーのものですが、「MACアドレスee:c6:29:27:73:7a」に対応する情報がないという意味で「L2MISS」と呼んでいます。)ユーザー空間のflanneldがこのイベントを受け取ると、先ほどのetcdの情報を参照して、

  • 宛先MACアドレス「ee:c6:29:27:73:7a」を持つ「flannel.1」は、物理NICのIPアドレスが「192.168.122.102」のノードにある。

ということを確認して、これを元に先ほどの情報をFDBに登録します。

これで、めでたくカーネルは、VXLANのヘッダを付けてカプセル化したパケットを「192.168.122.102」宛に送ります。この後、このパケット受け取ったminion02のカーネルは、このパケットがVXLANのパケットであることに気づいて、その中身のL2パケットをVXLANデバイスである「flannel.1」に転送します。これで、L2パケットは無事にVXLANのトンネルを抜けて、minion02に届きました。

この後は、L3レイヤーの処理が始まります。すなわち、minion02のホストLinuxにおける仮想ルーターとしての処理です。L2パケットの中にあるL3パケットは、宛先IPアドレスが「10.1.65.39」ですので、minion02のルーティングテーブルに従って、「docker0」からコンテナに転送されることになります。

flanneldの役割

先ほどのpingパケット転送の流れから、flanneldの役割を整理します。

VXLANは、L2パケットをL3パケット(UDPパケット)にカプセル化して「L2 over L3」のトンネルを実現するプロトコルです。しかしながら、このプロトコルで実際にパケットを転送するには、

  • 論理ネットワーク上でのL2レイヤーのARP解決
  • 物理ネットワーク上でのVXLANパケットの転送先ノードの解決

という2種類の処理が必要になります。単純にカプセル化すればうまくいくというものではありません。Linuxカーネルは、「L3MISS」、および、「L2MISS」というイベントを発行することで、これらの処理をユーザー空間のflanneldに委譲していることになります。先ほどのpingを実行した後に、flanneldのログを見ると、確かに「L3MISS」「L2MISS」の処理を実施していることが分かります。

# journalctl --no-pager -l -u flanneld
...
 4月 02 23:54:29 minion01 flanneld[723]: I0402 23:54:29.795403 00723 vxlan.go:258] L3 miss: 10.1.65.39
 4月 02 23:54:29 minion01 flanneld[723]: I0402 23:54:29.796776 00723 device.go:228] calling NeighSet: 10.1.65.39, ee:c6:29:27:73:7a
 4月 02 23:54:29 minion01 flanneld[723]: I0402 23:54:29.796911 00723 vxlan.go:269] AddL3 succeeded
 4月 02 23:54:29 minion01 flanneld[723]: I0402 23:54:29.797181 00723 vxlan.go:242] L2 miss: ee:c6:29:27:73:7a
 4月 02 23:54:29 minion01 flanneld[723]: I0402 23:54:29.797209 00723 device.go:202] calling NeighAdd: 192.168.122.102, ee:c6:29:27:73:7a
 4月 02 23:54:29 minion01 flanneld[723]: I0402 23:54:29.797324 00723 vxlan.go:253] AddL2 succeeded
...

これらの処理は、バックエンドのetcdに格納された情報を使っており、この部分の処理は、VXLANのプロトコルとして規定されたものでもなんでもなくて、flanneldが独自に実装しているものになります。

一般に、VXLANでは、このような補助的な処理を実施する機構を「VTEP」と呼んでいます。VXLANは「業界標準のプロトコルで相互接続性が高い」と言われていますが、実はこれはちょっとウソです。実際には、VXLANで通信するノード上にはVTEPが必要で、どのようなVTEPを利用するかは利用者の自由です。サーバー同士ではなく、VXLAN対応の物理スイッチ同士をVXLANで接続する際は、物理スイッチ自身にVTEPの機能が入っていますが、当然ながら、互換性のあるVTEPが入ったスイッチ同士しか通信することはできません。

その他のVTEP

Kubernetesでは、Flannelがよく使われますが、たとえば、OpenStackではどうでしょう? NeutronのOVS Pluginでは、VXLANを利用することができますが、この場合は、ML2のMechanism Driverである「L2 population」がVTEP的な仕事をしています。あるコンピュートノードでVMが起動して仮想L2のポートに接続したタイミングで、そのVMのIPアドレスとMACアドレスが分かりますので、この情報を各コンピュートノードで共有することで、Flannelと同様に、各ノードのローカルで「L3MISS」「L2MISS」の処理を行います。

このように、VTEPは、ノード間で情報共有する必要があるため、情報共有の方法をどうするかで実装がいろいろと変わります。

ちなみに、VXLAN対応の物理スイッチ(の中に入っているVTEP機能)は、どうやって情報共有するのでしょうか? これにはいくつかパターンがあります。

  • データベース的なもので情報共有するのでなく、L3マルチキャストでスイッチ間で情報交換する。(VXLANの標準として定められたやり方。)
  • 独自の実装方式でスイッチ間で情報交換する。(対向スイッチのIPアドレスを事前に全部登録して、ユニキャストで情報交換するなど。)
  • 独自の実装方式でバックエンドのデータベースを用意する。(つまりコントローラー的なサーバーをどっかに立てておく。)

本当はマルチキャストを使うのが、VXLANの標準なのですが、いかんせん、マルチキャストは扱いづらくてしかたないので、結局は独自実装でいろいろがんばっているようです。たとえば、「vxlan multicast issue」で検索すると次のような記事が見つかります。

おまけ

LinuxカーネルにL2miss, L3missの機能を追加したパッチはこちらです。