俺の報告

RoomClipを運営するエンジニアの日報(多分)です。

TCPシェイクハンドとKeepAliveについて - 日報 #152

3ヶ月間ブログ休んでましたが、
まぁそういうこともあるよね。
大事なことは続けることだと思いますんで、グッと面の皮を厚くして今まで通りいきますね。

さて、

色々な求人媒体様ならびにFBでもさかんに言ってますが、
RoomClipのエンジニア大募集中でございます。
iOSAndroid、サーバサイド、インフラ、UI/UX、
全方位に人材不足なので、どの方面から始めても、どの方面にも成長できます。
そして「俺ならもっとすごいのつくれるのに!」と思っいながらも、 日々決められたフレームワークで開発せざるを得ない敏腕エンジニアの方。
ぜひウチで腕試しにいらっしゃってください。
バチバチしましょう。

とにかく、少しでも興味ある人は、このブログへのコメントでもなんでも結構ですので、
ご連絡下さいませ。「ちょっと話してみたいんだけど」的なノリも大歓迎です。
Wantedlyさまからも募集要項だしております!

www.wantedly.com
www.wantedly.com

----ここまで宣伝。

そんで今日の話。
今日はTCPシェイクハンドとKeepAliveについて。

込み入った話になるので結論めいたところから説明します。
リバースプロキシを使っているとき、 クライアントとの間だけでなく、プロキシの向こう側の、要は本体サーバとの接続にもちゃんとKeepAliveしておかないと、
TIME_WAITでポートが枯渇したり、TCP接続に必要なメモリがオーバーフローして、大変なことになるよ。
という話です。

結論を書くと極めてシンプルなのですが、
これが中々気づきづらい。
だって、CPUもメモリ(全体)もさしたる圧迫がないのに、
なぜか通信が止まる、という奇っ怪な様子にみえてしまうからです。
(俗にいうライブロックというやつですかね)
この状態、色々と疑い先は列挙できますが、 「TCPコネクション周りが怪しい」 という目線があるかないかで、解決にかかる時間も大分変わってきます。
普段使っている「意識しない技術」に対してきちんと理解しておくことは、とっても重要ですね。

さて、今回の事件の中心であるTCP
TCPなんて「そうだね、TCPだね」以上のことを知ってる必要がないと思うのですが、
それがサービス停止の原因になりうる箇所だというのなら、 改めて順を追ってこの「プロトコル」を確認してみましょう。

TCPは3ウェイ・ハンドシェイク方式と言われる方法で接続を確立し、
(多くの場合)4ウェイ・ハンドシェイクという方式で接続を終了させる。
今回のような「ポートとかメモリとかの枯渇事件」の落とし穴は、この確立と終了の両者に1つずつ存在しているようです。

まずコネクション確立、3ウェイ・ハンドシェイクをみてみる。

  1. クライアントがサーバに対してSYNシグナルを送る
  2. サーバがクライアントへSYN ACKを返す
  3. クライアントがACKを返す

これで終わりだ。

  1. 「起きてる?いま電話して大丈夫?」とメッセしたら、
  2. 「起きてるよ。電話いいよ、待ってるね。」と返信がきたので
  3. 電話した

こんなイメージだ。
さて、実はこの2番から3番への状態遷移において、落とし穴が存在する。
サーバはSYNを受け取ったとき、
今後の通信に必要なクライアント情報(最低でも16バイト程度)をメモリに保存する。
この状態のまま、クライアントが3番の行為をしてくれないと、長い間その確保したメモリを開放できなくなる。
つまり、
「起きてるよ。電話いいよ、待ってるね。」
とメッセ返してからずっと電話してこねーなあいつなんなんだよマジで、別れたくせに思わせぶりなことしやがってなめてんのかフジコフジコとなってしまうわけですね。
奇特な性癖の豚野郎なら耐えられるでしょうが、これを短時間に大量にやられたら人はパンクしてしまうわけです。
サーバも同様に瞬殺されてしまい、結果としてライブロックといわれるような、 「動作はしているが返ってこない」という人騒がせな状態に追い込まれてしまうのですね。
これをSYN FLOOD攻撃とよび、これはいわゆるDDoS攻撃の一種です。

もちろんLinuxのサーバアプリケーションは大体それに対するワクチンをもっており、
SYN FLOODを受けた!と判定したら、メモリに情報を書き出すのをやめて、
ステートレスに相手を同定するシステムに移行しようとします。
cookieと同じような技術を使うので、set_cookiesとかと呼ばれてます)

さてもう一つ、コネクションの終了の4ウェイ・シェイクハンドをみてみます。
(クライアントから接続を終わらすストーリーでみてみます)
1. クライアントがFINシグナルを送る
2. サーバがACKを送る
3. サーバがFINを送る
4. クライアントがACKを送る
それで終わり、となるところですが、実はこれには隠された5がありまして、、、
5. 指定された秒数サーバはTIME_WAIT状態で待機
というのがあります。
これは使っていたポートが遅れてきたパケットを受信してしまい、 別のコネクションで利用されてしまう、という危険を回避する大事な機能です。
60秒程度は待つ、というのが一般的のようです。

さて、

もうおわかりだと思いますが、このTIME_WAITが中々やっかいです。
そもそもTCP接続のために利用されるポートはエフェメラルポートが利用されます。
CentOSでは 32,768 から 61,000 の28,233個程度あるようです。
( sudo cat /proc/sys/net/ipv4/ip_local_port_range で確認 )
またTIME_WAITの時間は、60秒。
( sudo cat /proc/sys/net/ipv4/tcp_fin_timeout で確認 )
つまり、28,233/60 = 470 req/sec 程度でポートは枯渇します。
もっというと、
もしこのサーバが画像のCDNサーバとかで、
実体のサービスへの1リクエストごとに10個の画像リクエストが飛ぶとしたら、
47req/sec程度で瞬殺されるわけです。
(まぁ極端な話ですが。TCPも同じリクエスト元ならKeepAliveなくして再利用するので)

これを対策するためには、ポートを増やすとかTIME_WAITを短くするとかがありますが、
そもそもエフェメラルポートやらTIME_WAITやらは「別の事情」できまった数値なわけで、
今回のような理由で下げていいわけではないはずです。
なのでKeepAliveをつかう、という発想がでてくるわけです。
僕はこのKeepAliveの設定をクライアント⇔リバースプロキシのみに設定して満足してしまい、
リバースプロキシ⇔本体サーバにおいて設定しておらず、このような地雷を踏んでしまったわけです。
怖いですね。

以上に長くなりましたが、一言でTCPとかいってますが、
設定や場合によってはreq/secのボトルネックになりうるレベルの「知らねぇなんて許されねぇぞ」レベルの話なんですね。
まぁTCPなんて基礎の基礎の基礎だろ!なんでてめぇ今までよく知らずにやってたんだ!
という声も聞こえてきますが、
そーゆーこと言うような凄腕エンジニアの手助けは大歓迎ですので、
ぜひ一度お会いしてガチで叱って下さい。
よろしくお願いいたします!