[SYN]に[SYN+ACK]ではなく[ACK]が戻ってくる超常現象が発生した

大分暖かくなってきましたね。
花見が待ち遠しい日々です。

そんな楽しみにしている花見を心置きなく(w)楽しむ為に、解決しなければいけない超常現象が発生しました。

今回の問題は新しいプロダクト(サーバ)とSSP(クライアント)という既存のSSPに加えて、新たに構築したサーバで発生しました。
何かしら問題は出るとは思っていましたが、法則通り厄介な現象が発生してしまいました。

症状としてはクライアントアプリケーションから見てサーバとの通信でタイムアウトが発生する現象です。
これがあまりにも頻発するためtcpdumpで確認することになりました。

内容を確認したところ、挙動としてはクライアント側が[SYN]を送信した後に、何故かサーバ側から[SYN+ACK]ではなく、いきなり[ACK]が戻ってくるという現象が確認できました。
これでは正常にTCPの3ウェイハンドシェイクを確立することができません。
クライアント側が[ACK]を単発で受信した結果、クライアント側の意図にそぐわないレスポンスということで[RST]をサーバ側に送信し通信が終了してしまうという流れです。

実際のwireshark画面です。
以下全てサーバ側から見た結果になります。
syn_ack1


常識的に考えて[SYN]に[SYN+ACK]が戻ってこないことなんてないだろjkjkjk(^ρ^)

正常に通信できているパターンとだめなパターンが激しく入り乱れており、発生条件の切り分けもできず2週間くらい毎晩酒を飲んでwiresharkを眺め続けました。

激しく悩まされたそんな問題の原因ですが、例によって広告配信という特殊な環境と、自社内NWの通信事情のせいでした。

原因に気づいたきっかけはwiresharkでcapファイルの末尾のあたりのシーケンスを眺めていた時です。
以下が該当の画面になります。
syn_ack2
RSTが発生するときに[Tcp Port numbers reused]が表示されているのは、キャプチャ開始から時間がたったシーケンスに限定されていました。
reusedと言われてるからには使いまわしているはずなので、クライアントのポート番号をフィルタの上限に追加してみたところ…。

syn_ack3
20秒ちょっと前のシーケンスは正常に終了しています。
その後しばらく時間が経った通信で[Tcp Port numbers reused]が発生して、通信が異常終了しています。

つまり先頭の方のシーケンスでは同じ挙動をしていても、キャプチャが断片的なためwiresharkがPort reusedと識別することができないと判断できます。
このPort reuseが色からして黒なのでほぼ間違いありません。

このPort reuseの悪影響として予想できる事は、ソケットはsrc dst IP,Portのタプルをハッシュ化して処理しているため、ハッシュが衝突するとよくない事が起こるはずです。
とは思ってはみたものの、既に通信は正常に終了しているのになんでなのか分からずここでも結構悩みましたが、犯人はTIME_WAIT状態で残っているソケットでした。

サーバ側に残っているTIME_WAIT状態のソケットに対して新規の通信が発生することにより、クライアント側は新規でコネクション要求をしているにも関わらず、サーバ側は既存コネクション扱いになり、結果的にお互いの意図に反する挙動をしていたようです。

ざっくりまとめると以下の通りです。

  • 1:1の通信であれば、通信終了後にTIME_WAITが残っても、双方でTIME_WAIT状態になるためクライアント側からソケットが衝突する条件で新規の通信を発生させることは不可能
  • NATルータのIPマスカレードを挟むことによって、ソースポートが変換されるのが原因(1)
  • NATルータはソースポートを稼ぐために、TIME_WAITを維持する時間を極端に短くしている。サーバ側とルータでTIME_WAITのタイムアウト時間の差異が原因(2)
  • NATルータはソースポートを意図的に使いまわすことがある(らしい)原因(3?)
  • 特定のホスト間での大量の細かいトラフィックにも起因する
  • 実は以前にサーバのポート枯渇問題がありTIME_WAITの最大値を設定して運用していたけど、サービス本格稼働前にフロント側のnginxの台数を増やしたため、最大値によるTIME_WAITのリフレッシュがほぼされなくなってしまったのも原因(4)

自社の環境ではTIME_WAITは不要の存在どころか、トラブルを招く厄介者でした。

そして、上記の原因の対策をするにあたり、いくつか懸念事項があがりました。

  • LVSをを利用しているため、TIME_WAITの再利用はしない
  • タイムアウトを短くしようにも60秒はカーネルに決め打ちで書いてやがる
  • NATルータでTIME_WAITを保持するのは手数が多すぎて無理
  • NATルータ側でソースポートをランダムに使ってもいつかは衝突する

等あり、運用面を考慮してサーバ側のmax_tw_bacuketを、100と極端に小さい数値に設定することにしました。
これにより、サーバ側のTIME_WAITソケットが短い時間で破棄され、問題の発生を解消することができました。

原因を調べる過程でLinuxのTCPスタックのソース全部読めたのでそこそこ収穫もありました。
普段の暮らしだとTCPなんて高レイヤーな部分なかなかじっくり見れませんからね。

いやあ今回はきつかった…。
こんだけ頑張ってるのにクラウドでコスト圧縮とか何も考えないで言ってくるのが定期的に湧くんですよ、ホント嫌になっちゃう。

コメントを残す

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


*