Evans本記事は、原著者の許諾のもとに翻訳・掲載しております。Illustrated』(訳注:日本語版:『詳解TCP/IP〈Vol.1〉プロトコル』)を読破しようとか、そういうことではありません。ほんの少しのTCPの知識がどれほど欠かせないものなのかについてお話します。その理由をお話しましょう。Centerで働いているとき、PythonでTCPスタックを書きました(またPythonでTCPスタックを書いたらどうなるかについても書きました)。それはとても楽しく、ためになる経験でした。それでいいと思っていたんです。そこから1年ぐらい経って、仕事で、誰かが「NSQへメッセージを送ったんだが、毎回40ミリ秒かかる」とSlackに投稿しているのを見つけました。私はこの問題についてすでに1週間ほど考え込んでいましたが、さっぱり答えがでませんでした。ここでちょっとした補足を。NSQはメッセージを送るときのキューです。メッセージを送る方法は、HTTPリクエストをローカルホストに送るというものです。本来ならローカルホストへHTTPリクエストを送るのに40ミリ秒もかかりません。何かが完全に間違っていました。NSQデーモンはCPUの読み込みに高い負荷を与えてはいませんし、多くのメモリを使ってもいません。ガベージコレクションによって休止しているようにも見えません。そこで1週間前に、「パフォーマンスを求めて-各POSTリクエストごとに、どうやって200ミリ秒を削ったか」という記事を読んだのを思い出しました。この記事では、なぜPOSTリクエストを送るごとに200ミリ秒余分にかかっているのかについて触れています。以下が、その記事のポイントとなる段落です。遅延ACKとTCP_NODELAYRubyのNet::HTTPはPOSTリクエストを2つのTCPパケットに分け、1つはヘッダーに、もう1つはボディになります。対照的にcurlでは、2つのTCPパケットが単一パケットにフィットするようであれば、これらを統合します。悪いことに、Net::HTTPは通信を行うTCPソケットにTCP_NODELAYを設定することができません。1つ目のパケットのACKを受信しないと2つ目のパケットが送信されません。これはNagleアルゴリズムによって生じる結果です。接続のもう一方のほうに移ると、HAProxyはこれら2つのパケットをどうACKするかを選択しなくてはなりません。バージョン1.4.18(私たちが使用した物)では、TCP遅延ACKを使うことになっていました。遅延ACKはNagleアルゴリズムとの相性が悪く、サーバが遅延ACKのタイムアウトになるまでリクエストがストップしてしまいます。要約すると次のようになります。TCPとはパケットでデータを送信するアルゴリズムである。HTTPライブラリはPOSTリクエストを2つの小さいパケットにして送信していた。その後のTCPのやりとりは、以下のように続きます。やあ、パケット1だよ。HAProxy:<無反応で、2つ目のパケットを待っている>HAProxy:<そのうちACKするけど、まあいいか>アプリケーション:<無反応>アプリケーション:<ACKを待っているんだけど、ネットワークが混雑しているのかな>HAProxy:もう待ち疲れた。2つ目のパケットを送るよ。HAProxy:よかった。アプリケーションとHAProxyが両方、意図的に他方が情報を送ってくるのを待っている期間がありますよね。それが余分の200ミリ秒です。アプリケーションはNagleのアルゴリズムにのっとっているためにそうなっていて、HAProxyは遅延ACKsのせいでそうなっているのです。私の知る限り、全てのLinuxシステムでは初期値で遅延ACKが設定されています。これはエッジケースとか例外ではありません。2つ以上のTCPパケットでデータを送れば起こり得ることです。魔法のように問題が解決した前述の記事を読んだ後、そのことについては忘れてしまっていました。余分な40ミリ秒について思い悩んでいた時、ふと思い出したのです。これは私の問題とは別物なはずだ。私はチームにメールを送りました。「頭が変になったのかもしれないが、これはTCP問題かもしれない」と。そこで私はアプリケーションに対しTCP_NODELAYをオンにして変更をコミットしたのです。なんと全ての40ミリ秒遅延の問題は瞬時に消えました。全てが解決したのです。魔法使いになったような気分でした。Nagle(Nagleアルゴリズム)のコメントがありました。本来の問題はACKの遅延にある。200ミリ秒の”ACK遅延”タイマーは、1985年頃にバークレー校の誰かがBSDに埋め込んだ、ひどいアイデアである。彼らは問題をちゃんと理解していなかったのだ。遅延ACKは、200ミリ秒以内にアプリケーションレベルからの返信があるだろう、という賭けなのだ。毎回この賭けに負けると分かっていても、TCPは遅延ACKを使い続ける。続けて彼は、「ACKは小さくて安価だが、実際に遅延ACKによって起こる問題は恐らくACKが解決している問題よりもひどいと言える」とコメントしています。TCPを理解せずして、TCPの問題は解決できない私はこれまで、TCPはさほど難しいものではないと思っていましたし、理解する必要もないと思っていました。確かにそうかもしれません。現実的には、TCPアルゴリズム内の何かが原因で起こるバグが発生したりします。結局はTCPを理解することは重要なのです(私のブログの中で度々話題にしていますが、これはシステムコールやOSといった多くのことに当てはまります:):))。遅延ACKとTCP_NODELAYの相性は特に悪く、あらゆるプログラミング言語でHTTPリクエストを送信するコードを書く人に影響を与えかねません。このようなことを思いつくために、システムプログラミングの天才である必要はありません。TCPがどう機能するのかを少しでも理解していたおかげで、この問題に対処する手助けになりました。このブログに書かれていたことが、私の問題と原因が同じであるということに気付けたのです。私はstraceも使っていましたけどね。ツイート翻訳に対するフィードバックがございましたら、メールまたはGitHubのIssueよりお寄せください。Ltd.リクルートグループサイトへ。