Neco プロジェクトの ymmt です。Neco は cybozu.com のインフラを刷新するプロジェクトで、先日は全面的に見直したネットワークのアーキテクチャと設計をご紹介しました。
簡単にまとめると、spine-leaf ネットワークを実現するにあたって、全てのサーバーで BGP + BFD + ECMP を使うことで Layer-2 拡張技術を使うことなく経路の冗長化と障害時の高速な経路収束を達成しましょうという内容でした。
今回は具体的な実装方法について解説します。ただ、かなり前提知識が必要となるため以下二つのチュートリアルを別に用意しました。BGP や BIRD に詳しくない方はまずこちらをご覧ください。
本記事で解説するのは以下の内容です。長文になりますのでお時間のあるときにどうぞ。
- AS 構成の選択
- ラック内の構成
- 代表 IP アドレスの実装と注意点
- ICMPリダイレクトを避ける工夫
- Kubernetes への組込み
AS 構成の選択
Neco では全てのサーバーが BGP ルータとなります。ということは、全てのサーバーに異なる AS 番号を持たせて全て eBGP でピアリングすることが可能です。これを Calico では AS per Compute Server モデルと呼んでいます。
次の選択肢としては、同一ラック内のサーバーと Top-of-Rack (ToR) スイッチ 2 台は全て同じ AS 番号とし、ラック内は iBGP、ToR スイッチと Spine スイッチ間は eBGP という構成が考えられます。これを Calico では AS per Rack モデルと呼んでいます。
他のバリエーションも考えられますが基本的にはこのどちらかの選択となるでしょう。どちらを選んでも構築は可能ですが、違いを簡単に比較してみたのが以下の表となります。
項目 | AS per Compute Server | AS per Rack |
---|---|---|
AS 数 | 概ねサーバー台数と同じ | 概ねラック数と同じ |
BGP設定 | 量は多いが単純 | 量は少ないが iBGP に注意が必要 |
Neco では AS per Rack 構成を採用しました。主な理由は、ToR/Spine スイッチを管理するネットワークチームからの要請です。As per Compute Server モデルではネットワークチームの管理が及ばないサーバーが個別に AS 番号を名乗ることになるので、これを避けました。
以下の図のように、ラック内の ToR スイッチ 2 台とサーバーで一つの AS を構成します。
ラック内の構成
前述のようにラック内は iBGP で ToR スイッチ 2 台と全てのサーバーは同一の AS 番号を持ちます。MC-LAG のような L2 拡張技術は使わないので、ToR スイッチは一体ではなく、それぞれ別の L2 サブネットを有します。
iBGP 構成を簡略化するため、各 ToR スイッチはルートリフレクターとして動作します。そうすることでラック内のサーバーは ToR スイッチ 2 台とのみピアリングすれば良く、ラック内の他サーバーと直接ピアリングする設定を書かなくて済みます。
各サーバーは ToR スイッチ二台と接続するために NIC のポートを二つ持ちます。別々の L2 ドメインに接続するわけですから、ポート 1 とポート 2 で異なるサブネットのアドレスが割り当てられます。
この L2 サブネットの利用範囲は非常に限定的です。一つは、ネットワークブートするために DHCP でブロードキャストするために使われます。ネットワークブート後は、ToR スイッチとのピアリングにのみ使われます。その他の通信の用途には、次に説明する代表 IP アドレスを使用します。
代表 IP アドレスの実装と注意点
二つの NIC ポートが持つアドレスをそのままサーバーのアドレスとするのは、いくつか問題があります。
- ToR スイッチが持つ L2 サブネットのアドレスなので、スイッチ故障時に到達できなくなる
- Kubernetes ではサーバー(Node)のアドレスは原則として一つ
2 の例としては、Pod が動作しているホストの IP アドレスの表現(status.hostIP
)などがあります。
そこで、各サーバーに ToR スイッチが持つサブネットとは無関係な代表 IP アドレスを持たせます。この代表 IP アドレスは /32 のネットマスクをつけて BGP で経路広告することで、他のサーバーがアクセスできるようになります。
dummy
デバイスによる実装
Linux の場合、dummy
デバイスを以下のように作成すれば代表 IP アドレスを持たせられます。
$ sudo ip link add node0 type dummy $ sudo ip link set node0 up $ sudo ip address add 10.1.0.1/32 dev node0
node0
が dummy
デバイスとして作られます。node0
のアドレスを経路広告するには BIRD に以下の設定を入れます。
protocol direct { ipv4; interface "node0"; } protocol kernel { merge paths; ipv4 { export filter { if source = RTS_DEVICE then reject; accept; }; }; }
protocol direct
で node0
のアドレスを BIRD の master4
ルーティングテーブルにインポートしています。
master4
で BGP ピアと経路交換して Linux カーネルに必要な経路を登録するのが protocol kernel
です。この際に node0
のアドレスは自サーバーのものですので、カーネルに再登録しないようフィルターしているのが if source = RTS_DEVICE then reject;
です。
不定送信元アドレス問題
もう一つ、node0
を代表アドレスにするためにやらなければいけないことがあります。それは node0
以外のリンクが持つ IP アドレスのスコープを link local
に制限することです。例えば eno1 のアドレスを systemd-networkd
で設定している場合は以下のように Scope=link
を指定します。
[Match] Name=eno1 [Address] Address=10.1.0.65/26 Scope=link
Linux カーネルは、スコープが global
なアドレスであれば、どのアドレスであっても直接 NIC が所属していない宛先 IP アドレスのパケットの送信元アドレスに利用してしまいます。そのため node0
以外に NIC のポート二つが持つアドレスのスコープを global
のままにしておくと、3 つのアドレスのどれが送信元アドレスに使われるか不定となってしまうのです。
余談ですが、この問題に気付いた当初は Linux カーネルの不具合の可能性もあるかと、再現コードを準備して LKML で質問しました。その結果上記の動作が仕様であることと、アドレスのスコープを制限する対策についてヒントをいただけて無事解決できました。メールアーカイブで一連の投稿が読めますのでご興味ある方はどうぞ。
ICMPリダイレクトを避ける工夫
AS per Rack モデルなのでラック内のサーバーは ToR スイッチ 2 台と iBGP でピアリングします。そしてフルメッシュ構成を避けようと ToR スイッチをルートリフレクターにすると、少し難しい問題が発生する可能性があります。
- ToR スイッチが eBGP で受け取った経路の
NEXT_HOP
アドレスは iBGP であるため各サーバーにそのまま届く - 各サーバーにとってその
NEXT_HOP
は外部のネットワークアドレスであるため未知である場合がある - 未知の
NEXT_HOP
アドレスを持つ経路を BIRD は受け取らず捨ててしまう
よくある対処その 1 は、未知にならないように当該アドレスを含む経路を同時に BGP で広報する方法です。一般的なルータはこの方法で対処できることが多いのですが、残念ながら BIRD は BGP で同時に広報されたとしても受け取りません。
よくある対処その 2 は OSPF 等の IGP で当該アドレスの経路を広報する方法です。これは BIRD でも有効ですが、せっかく BGP にまとめようとしているのに IGP を併用するのは嬉しくありません。
よくある対処その 3 は next-hop-self
を ToR スイッチで設定する方法です。これは eBGP 経路の NEXT_HOP
アドレスを ToR スイッチのアドレスに書き換えて、未知アドレスではなくするものです。
next-hop-self
+ ルートリフレクターで発生する問題
next-hop-self
は通常パケットの送り先となるルータを NEXT_HOP
にするので大きな問題になりにくい設定です。しかしながら、ここまで解説してきた:
- 同一 L2 ドメイン内に多数 BGP ルータが存在して
- ToR スイッチがルートリフレクターである
ケースでは、本来直接パケットを届けることができる同一ラック(L2)内のサーバーへの経路も ToR スイッチのアドレスに書き換えられてしまいます。すると下図のように、ToR スイッチはパケット送信元に ICMP リダイレクトメッセージを返して直接通信するよう伝えます。
ICMP リダイレクトの発生は避けたほうが効率的というばかりでなく、一部のスイッチでは BFD との併用ができないといった問題が発生する場合があります(例)。他にも複数の ICMP リダイレクトを避けたほうがいい理由が存在します(参考)。
next-hop-self
に頼らず BIRD の設定で解決
未知の NEXT_HOP
アドレスを持つ経路を破棄してしまう問題に対するよくある対処は 1, 2, 3 とも欠点があることが分かりました。
( ˘⊖˘) 。o(待てよ?ルートリフレクターやめてフルメッシュにすればいいのでは?)
( ◠‿◠ )☛そこに気づいたか・・・消えてもらう
仕方がないので BIRD の設定を工夫して解決しました。BIRD の使いかたに書いてあるのですが、以下のようにフィルターで NEXT_HOP
(gw) を条件に応じて書き換えて受け取ることで、eBGP 経路のみ ToR を NEXT_HOP
とし、同一 L2 内のルータの NEXT_HOP
は書き換えないということが可能です。
# BIRD が未知の NEXT_HOP を持つ経路を破棄しないように、 # ダミーでどんな経路もとりあえず受け取るテーブルを用意 ipv4 table dummytab; protocol static dummystatic { ipv4 { table dummytab; }; route 0.0.0.0/0 via "lo"; } protocol bgp { ... ipv4 { igp table dummytab; # ダミーテーブルを参照して NEXT_HOP が未知でも受け取り gateway recursive; # direct 接続時も IGP テーブルを参照させる import filter { # ラック内 AS (iBGP ピア)からの経路なら bgp_next_hop を維持 # 以下の式は送信元ルータのアドレスを 26bit サブネットに含まれるかの判定 if bgp_next_hop.mask(26) = from.mask(26) then { gw = bgp_next_hop; accept; } # そうでないなら from をゲートウェイに(next hop self 相当) gw = from; accept; }; }; }
Kubernetes への組込み
Kubernetes のネットワークプラグインには、BGP で経路広告して Pod 間通信を実現するものが多数あります。
- Calico: Pod に /32 な IP を割り当てて組込みの BIRD で経路広告
- Romana: 経路数を減らせるよう Pod アドレスをブロック単位で BGP 経路広告
- kube-router: CNI bridge プラグインと BGP の組み合わせ
Neco では Pod だけでなく、データセンターネットワーク全体を BGP で制御するため、既に全サーバー上で複雑な設定を持つ BIRD が動作しています。Calico は BIRD を組込んでしまっているため、Neco の BIRD と連携させるのが困難で選択肢から外しました。余談ですが Calico は MetalLB との組み合わせも難しいようです(参考)。
kube-router と Romana を比較すると、Romana は BIRD を直接組込まず、BGP による経路広告はオプション機能としている点や、柔軟なアドレス割り当て方式が優れていると判断しました。
このようにネットワーク設計段階では Romana を採用するつもりだったのですが、実装する段階で様々な点を考慮して自社製のネットワークプラグイン Coil を開発する方針に変更しました。設計開始から実装終了まで 2 週間で、すでに完成しています。
Coil の紹介はまた機会を改めて。
まとめ
Neco のデータセンターネットワークの実装にあたり解決が必要だった諸問題と、その解決方法を紹介しました。基本的なところはすでに動作しており、今後は Cilium や Istio, MetalLB と組み合わせて高度な機能を実装していきます。
Neco プロジェクトではネットワークやストレージ、コンテナ技術などを一緒に追求するメンバーを募集しています。Neco チーム専属の採用枠も用意していますので、エントリーをお待ちしています!