LinuxのARPとL2スイッチのお話

この記事は、はてなエンジニアアドベントカレンダー2016の12月19日の記事です。

developer.hatenastaff.com

昨日はid:taketo957くんの 10ms以下のレスポンスタイムを支える継続的負荷テスト - taketo957の日記 でした!

Webオペレーションエンジニアのid:masayoshiです。 2016年に入社後、基盤チームとして仮想化、ネットワーク周りを中心に見ています。

さて、この記事ではLinuxARPの挙動とその挙動から起こった問題を紹介しようと思います。

長々と記事を読みたくない人向けに結論をまとめるとLinuxARPTCP通信で使われ続ける限りキャッシュが飛ばないという挙動になるので、複数のL2スイッチにまたがったセグメントや非対称ルーティングをしている場合は気をつけましょうという話が書かれています。

今回紹介する事例自体は別に新規性もなく、私が入社前から社内でも知られていて対策がされている内容なのですが、ネットワークに詳しくないインフラエンジニアやアプリケーションエンジニアはあまりこういう事例や、動作が説明されている記事がなく、検索してたどり着くのが大変そうな気配がしたので、自分の勉強ついでに書いてみることにしました。

はじめに

はてなでは、ロードバランサーとしてLVS(Linux Virtual Server) + keepalivedを利用しています。 LVSではNAT,DR,TUN形式が利用できますが、リアルサーバ(振り分け先のサーバ)のセグメントを選ばないかつLVSに負荷がかかりにくいTUN形式を用いています。 TUN形式はIP in IPトンネリングでパケットをカプセル化して送ることで、送信元IPアドレスをリアルサーバに渡すことが出来ます。 そのため、リアルサーバからの戻りパケットがLVSを経由することなく、送信元ホストに直接パケットを送ることが出来ます。(直接返せるのはDR方式と同じですね) EC2などとは違い、リソースマネージメントがめんどくさいDC環境ではネットワークを意識しなくて良いのは非常にありがたいことです。 ここらへんのお話は先月OSCTokyoFallでの発表資料も参照していただければと思います。

speakerdeck.com

今回の記事はLVS/TUN形式で送信元ホストとLVS、リアルサーバが同じセグメントに存在し、複数のL2スイッチで構築されている環境というかなり限定された状況下で、戻りパケットがブロードキャストされるという問題です。 正確に言えばLVSは直接関係ないのですが、今回のような状況が起こるのはLVSのようなロードバランサなどを利用している場合で起きやすいと感じた(実際に発生した)のでその事例を例に説明したいと思います。

発生した問題

f:id:masayoshi:20161219103137j:plain 図1-1: 想定している正常な通信

上記の図1-1のように、送信元ホストであるホストAがVIP宛にパケットを送信し(①)、LVSがリアルサーバA,Bに振り分け(②)、リアルサーバA,BはホストAに直接返信する(③)と環境です。

ただ、実際には通信開始から5分程度経つと、下記の図1-2のように、リアルサーバBの戻りパケットがその他のホストBにも届いてしまうようになります。

f:id:masayoshi:20161219103359j:plain 図1-2: 5分後の想定しいない通信

ホストBでtcpdumpをした際に全く関係ない通信であるはずのホストAの通信が見えてしまったことで発覚しました。また、ホストB以外でも関係ないホストでホストAの通信が見えており、ブロードキャストされているようでした。

今回発生した問題はあるホスティングサービス上で構築した際に発覚したので、L2スイッチの詳細なつなぎ方などはわからないため、図は簡略表示をしています。 なぜブロードキャストになったのかを理解するにはL2スイッチの挙動を理解しなければいけないので、まず先にその話をします。

L2スイッチの挙動について

L2スイッチ(普通のLANハブでもよい)では、MACアドレステーブルというものを持っています。これはMACアドレスと物理ポート(L4のポートではない)の対応表であり、対象のMACアドレス宛てのパケットをどの物理ポートから出せばいいのか調べる際に利用します。 MACアドレステーブルに対象のMACアドレスがない場合は送出先ポートがわからないため、フラッディング(受信したポート以外にすべて送る)します。また、当然ですがブロードキャストアドレス(FF:FF:FF:FF:FF:FF)は常にフラッディングします。

このMACアドレステーブルは受信したポートとパケットの送信元MACアドレスからひも付けを行います。先ほどの図1の通信では図2-1,2-2,2-3のようにMACアドレスと、ポート番号を覚えていきます。はじめはフラッディングしていたL2スイッチも図2-3の時点でフラッディングせずに通信が可能になります。

f:id:masayoshi:20161219103611j:plain 図2-1: ホストA->LVSの通信時

f:id:masayoshi:20161219103631j:plain 図2-2: LVS->サーバAの通信時

f:id:masayoshi:20161219103652j:plain 図2-3: サーバA->ホストAの通信時

以降の通信はMACアドレステーブルを参照し、対象のポートのみに通信していきますが、MACアドレステーブルにはエージングタイム(IEEE802.1Dでは300秒とされている)が設けられており、エージングタイム以降は再学習することになっています。細かい仕様は機器毎に違ったりしますが、大体300秒程たった後に再学習出来ないとMACアドレステーブルが更新されないため、対象のMACアドレスへの通信はフラッディングになります。

MACアドレステーブルと非対称ルーティング

これまでの説明で、通信開始から5分程度でブロードキャストになるというのがMACアドレステーブルのエージングタイム(300秒)が切れたタイミングであることから、MACアドレステーブルが更新されなかったことが原因ということが予想できるのですが、なぜそうなるのか見ていきましょう。

f:id:masayoshi:20161219175714j:plain 図3-1: ARP Request時にMACアドレステーブルにエントリを追加

図2にL2スイッチβを追加したのが図3-1です。先程は省略しましたが、一番最初のホストAからの通信はLVS/TUNのMACアドレスを調べるためにARP Requestから通信が始まります。このとき宛先MACアドレスはFF:FF:FF:FF:FF:FFでブロードキャストを使います。

ブロードキャストなので当然L2スイッチβにもARPパケットが届きます。このとき送信元MACアドレス(ホストAのMACアドレス)と受信ポートからMACアドレステーブルが作成されます。これで最低でも5分程度はホストA宛のパケットはフラッディングではなく、対象のポートのみに送られます(図3-2)。

f:id:masayoshi:20161219104041j:plain 図3-2: MACアドレステーブルにエントリがある時の挙動

図3-2の状態ですが、重要なのはホストAからの送信パケット(①)です。ホストAはVIP宛のパケットを送ります。VIPのMACアドレスLVSMACアドレスなのでL2スイッチαはMACアドレステーブルに従ってLVSのつながっているポートにのみパケットを送ります。LVSはサーバBにパケットを送りますが、このパケットの送信元MACアドレスLVSMACアドレスであり、ホストAのMACアドレスでありません(当然ですが)。

つまり最初のARPによるブロードキャスト以外はL2スイッチβにホストAからのパケットは来ないことになります。サーバBからホストA宛のパケットはMACアドレステーブルに従い、L2スイッチαにのみ送られ、その後、ホストAに届きます。ただしそのACKパケットなどもLVSに行ってから送られるのでL2スイッチβはホストAのMACアドレスをブロードキャスト以外で知ることはできません。

なので、そのままエージングタイムが過ぎてしまうと、MACアドレステーブルからエントリが消えてフラッディングになってしまいます(図3-3)。 LVS/TUNのような行きと帰りのパケットが違うルートを通るような非対称ルーティングな通信を行うときには、こういうことが発生しやすいので特に注意する必要があります。

f:id:masayoshi:20161219112355j:plain 図3-3: MACアドレステーブルからエントリが消えた時の挙動

さて、原因っぽいのがわかったのはいいのですが、MACアドレステーブルが消えるまでにホストAがブロードキャストしてくれれば再学習ができるのでなんの問題もありません。サーバ側でそもそもARPテーブルのキャッシュがきれて更新するためにARP Requestが飛ぶので問題ないような気がします。

しかし、LinuxではARPテーブルがTCP通信で使用され続けていると、ARPテーブルのキャッシュは基本的に切れないという挙動になっているようなので、ある程度の頻度でTCP通信が定期的に発生しているとARP Requestが飛ばず、MACアドレステーブルが更新できないという状況になります。

LinuxARPの挙動

カーネルを読み込みが甘く、自分でも追いきれていない部分もありますので、下記内容には誤り、情報が古いなどあるかも知れません。

ARPテーブルのキャッシュに関する状態遷移の簡易図が図4です。

f:id:masayoshi:20161219112417j:plain 図4: ARPの状態遷移の簡易図

実際にはNUD_INCOMPLETE,NUD_FAILEDなど、他にも状態がありますが簡略化しています。ARP Requestが成功するとエントリが登録されNUD_REACHABLEになります。この状態では通信の際に普通にARPテーブルから対象のMACアドレスを得ることが出来ます。一定時間経つとNUD_STALEになります。NUD_STALEのエントリは参照されるとNUD_DELAYに遷移します。NUD_DELAYは一定時間内にTCP通信の場合は対象のMACアドレスからのACKが返ってきた場合ARP Requestは送らずNUD_REACHABLEに遷移します。つまり、ACKが返ってきたということはIPアドレスMACアドレスのひも付けは正しく、再解決する必要がないとカーネルが判断します。 なので、TCPで通信し続けている限りその対象のIPアドレスに対してARP Requestを投げることはしないという動作になります。

ちなみにUDPなどではsendmsgにMSG_CONFIRMフラグをつけることによって対象がまだ生きていることを通知してNUD_REACHABLEにすることが出来るらしいです。

NUD_PROBEに遷移するとNUD_DELAYと同じくTCP ACKを受け取るか、ARP Requestをユニキャストで送り、成功した場合はNUD_REACHABLEに遷移します。 tcpdumpで覗いているとユニキャストのARP Requestが来ることがありますが、そのパケットはNUD_PROBE状態から送られています。

そのユニキャストでの解決に失敗し、送信キューがある場合はARP Requestをブロードキャストで実行します。送信キューがない場合や応答が一定時間内に返ってこない場合はARPテーブルからエントリを削除します。 疑問点としてはNUD_PROBEが行うユニキャストARPは送信キューに入ってなくても行うかどうかですね。送信キューがなくても解決する場合は応答ある限りARPテーブルからエントリは消えないし、ARP Requestのブロードキャストが発生しないことになりそうです。 なお、NUD_REACHABLEなどの情報はip neighbor listなどで確認が可能です

▶ ip -s neighbor list
10.13.0.254 dev wlp4s0 lladdr 2c:6b:f5:3a:48:35 ref 1 used 78630/0/224 probes 4 REACHABLE

対策

対策としては送信元ホスト(ホストAなど)がブロードキャストをしてくれればよいということになります。

簡単なのは送信元ホストでarpingなどをcronでまわしたりするのが良いかなと思います。

はてなでは入れ替えの激しい送信元ホストやリアルサーバに仕込むのではなく、LVSから/proc/net/ip_vs_connをパースして送信元IPアドレスを偽装したICMP echo Requestパケットを送信元ホストに送信し、ICMP echo Replyを行う際にARP Requestを誘発させてブロードキャストを行う方法をとっています。この方法ではLVSにcronを仕込むだけで良いので、サーバの引っ越しや設定変更で書きなおし等があまり発生しません。(LVSを構築し直すときは別ですが頻繁には行わないです。)

まとめ

実際におきた事例の紹介とL2スイッチの基本的な挙動、LinuxARPの挙動について紹介しました。

今回の事例ではサーバからみるといきなりブロードキャストし始めたようにみえるので、L2スイッチの挙動を知らないとサーバ側の調査ばかりしてなかなか答えにたどり着かないみたいなことになりがちです。 ネットワークの問題かな?と思ったら丁寧にパケットの気持ちになってどうやって書き換えられていくのか、書き換える情報はどこから得ているのか、どこに送られていくのかを追ってみるのが大切だと改めて実感させられる事例でした。

また、Linuxではブロードキャストしないようにかなり気を使って実装されているなぁという印象を受けました。 なんかARPテーブルに登録しておいて時間がたったらもう1回解決するんでしょ?ぐらいにしか考えていなかったので勉強になりました。

このエントリで少しでもネットワークに興味を持ってくれる方が増えてくれると嬉しいです。

明日のアドベントカレンダーid:hakobe932さんです!