LinuxのTCP受信バッファ溢れと輻輳制御の話

年明け早々に新鮮なネタがあがったため、暖かいうちに書いておこうと思います。

今回はサービスのフロント側でトラフィックを受けているnginxサーバのTCP受信バッファが溢れた問題です。
後述しますが厄介なところは、ユーザがOSから得られるすべてのカウンターの値を見ても一切ドロップが発生していることが把握できないことです。(多分)

以前よりサービス品質を保つために自社サブネットを指定してtcpdumpをとり、外部から見た通信に異常がないか定期的に確認していました。
その作業をVCの銭亀おじさんたちに仕事の腰をへし折られてからしばらく忘れて怠っていました。
その間いい加減な仕事をしていたということですね。

年が明けてから思い出して久しぶりにdumpをとってみたところ、下記のようにものすごい数のTCPの再送や、重複ACKも確認できました。(popinが邪魔かも…)rmem1
これは明らかに経路のどこかでブラックホールが存在している症状です。
社内L8層とは関係なくトラフィックも地味に伸び続けていたため、どこかのリミットがはじけたと思いました。
かといってtcpdumpまではいかないものの、スイッチやサーバの各種エラーカウンターは休日も毎日確認していました。
なんでこんなひどい状態になるまで検出することができなかったのか、ちょっと考えましたが答えはcapファイルを見てちょっとまじめに考えたらすぐに予想がつきました。

再送多発のトリガーは下記の画像です。
TCPウィンドウサイズの変更をサーバ側がクライアントにプッシュしています。
増える分にはスループットが向上するので構いませんが、減少している場合はTCPの受信バッファに空きがなく、お腹いっぱいなのでちょっとまって状態という解釈ができます。rmem2
では実際に流れを追って見てみます。

まずサーバはクライアントからSYNを受信し、SYN+ACKを返す段階ではウィンドウサイズを14480に設定しています。
rmem3
次にデータのやりとりを始めた段階ですぐにTCP受信バッファの空きがないため、TCP Window Updateでウィンドウサイズを114に設定しています。
rmem4
通常であればこの流れで輻輳制御が作用し、ネットワークの輻輳が回避できます。
ここまでひどいパケットドロップも発生しないはずです。
しかしここで問題になるのが、例によって広告配信という特殊な環境と、Linux TCPスタックの実装との相性の悪さです。

TCPはそもそもの設計思想がスループットを優先する仕組みになっています。
スループットが優先されるシステムやサービスでは、ウィンドウサイズの操作による輻輳制御はとても有効に作用します。
しかし一つ一つの通信が小さく、短い時間で終了し、手数も馬鹿みたいに多い広告配信においては、ウィンドウサイズによる輻輳制御はまったく有効に機能しません。

クライアントからの再送も重なり、結果的にnginxサーバが慢性的なバッファフルによる輻輳状態に陥り、パケットをぼろぼろ落とすという現象が発生していたようです。

またカーネルソースを読んだところ、バッファフルの時は goto drop; とシンプルに一行書いてあるだけでした。
再送があるしウィンドウサイズで輻輳制御もしてるし、バッファフルになったらふわっと捨てればいいやという感じなんですかね。
せめてなにかのカウンターインクリメントしてほしい…。

と経緯は書いてみると長いものの、対策はLinuxネットワークチューニングのテンプレートによく書いてある内容です。
調査過程で気づいた通信中にふわっといなくなるモバイルネットワーク対策で送信側もついでに増やすことにしました。

net.core.wmem_max = 12582912
net.core.rmem_max = 12582912
net.ipv4.tcp_rmem = 10240 12582912 12582912
net.ipv4.tcp_wmem = 10240 12582912 12582912

どうせフルフルで使うので通常値 = 最大値の設定で運用しています。
前にドキュメント読んだ時「動的だから通常ここは設定変えなくてもおk」みたいなこと書いてあったんですけどね。
自社サービスを外から見てみるという事が改めて重要なことだと認識しました。

広告インフラまじ鬼畜。

コメントを残す

メールアドレスが公開されることはありません。


*