サーバーが遅い理由 #4 帯域は足りているのに遅いとき — レイテンシ・RTT・再送
回線は 1Gbps なのに API の応答が遅く、帯域のグラフを見ると使用量は 10% にも届いていない、という状況があります。帯域は余っているのに、なぜ遅いのでしょうか? ハードウェア基礎 #6 で帯域とレイテンシが別の軸だという概念をつかみましたが、今回はその区別を実際の診断に適用します。結論を先に書くと、小さいリクエストが多いトラフィックの体感速度は、帯域ではなく往復(RTT)回数 × 往復時間で決まります。
体感速度の公式 — RTT × 往復回数 #
ソウルと米国西海岸の間の RTT は、光の速度が決める物理的な限界のため、どんなに良い回線でも 130ms 前後です。この経路で HTTPS リクエスト一つが新しいコネクションで出ていくと、往復はこう積み上がります。
| 段階 | 往復 | 累積時間 (RTT 130ms) |
|---|---|---|
| TCP ハンドシェイク | 1 RTT | 130ms |
| TLS 1.3 ハンドシェイク | 1 RTT | 260ms |
| HTTP リクエスト/レスポンス | 1 RTT | 390ms |
データをほとんど 1 バイトも余計に送っていないのに、400ms 近くを費やしました。帯域を 10Gbps に上げてもこの数字は 1ms も縮みません。往復回数を減らす(コネクション再利用、TLS セッション再開)か、往復距離を縮める(リージョンの移動、CDN、エッジ)ことだけが対処です。
同じ構造はサービスの内部でも繰り返されます。リクエスト一つを処理する間にマイクロサービス呼び出し 5 回、データベースクエリ 10 回が逐次出ていくなら、内部 RTT が 1ms でも往復 15 回が掛け算されます。さらにリージョン配置を誤って内部呼び出しがリージョンをまたぐと、掛け算される単位が数十ミリ秒に跳ねます。逐次呼び出しを並列にまとめたりバッチにまとめたりするほうが、帯域増強よりはるかに大きい効果を出す理由です。
コネクション再利用 — 往復を消す最も安い方法 #
上の表の TCP と TLS の 2 往復は、コネクションを再利用すれば丸ごと消えます。だから点検の順序の先頭は、いつも再利用の有無です。
- HTTP クライアントが keep-alive とコネクションプールを実際に使っているかを確認します。リクエストごとに新しいクライアントオブジェクトを作るコードは、毎回ハンドシェイクをやり直します。
- サーバーやプロキシの idle timeout が短すぎると、プールにあったコネクションが切られ続けて再利用率が落ちます。
- DNS の問い合わせも往復です。キャッシュの TTL が 0 だったりリゾルバが遅かったりすると、リクエストごとに数十ミリ秒が上乗せされます。
再利用が効いているかは、サーバー側で新規コネクションの比率から確認できます。リクエスト数に対して TCP の accept 数が近いなら、再利用はほとんど効いていないという意味です。
大容量転送が帯域を埋められないとき — BDP とウィンドウ #
逆にファイル転送やレプリケーションのように大きいデータが問題なら帯域の軸を見るべきですが、ここにもレイテンシが割り込みます。TCP は確認応答を受け取る前に送れる量(ウィンドウ)が決まっているので、コネクション一本のスループット上限は「ウィンドウサイズ ÷ RTT」です。経路の帯域を埋めるのに必要なウィンドウが帯域 × RTT、つまり BDP(bandwidth-delay product)です。
1Gbps × RTT 130ms なら BDP は約 16MB です。ウィンドウが 4MB にとどまれば、この経路でコネクション一本が出せる最大値は 250Mbps 前後です。回線は余っているのに転送が遅い、典型的な構造です。長距離転送が遅ければ、カーネルの TCP バッファ上限(net.ipv4.tcp_rmem/tcp_wmem)が BDP より小さくないかを確認し、アプリケーションレベルでは並列ストリームに分けるのが実務的な回避策です。
パケットロスと再送 — 少量でも破壊的です #
TCP はロスを輻輳の信号と解釈し、送信速度を即座に落とします。そのため 0.1% のように小さく見えるロス率でも、長距離・高帯域の経路のスループットを数十分の一まで削ることがあり、失われたパケットの再送タイムアウトは個々のリクエストに数百ミリ秒のスパイクを作ります。「普段は速いのに、たまに一度だけ跳ねる」という症状のよくある犯人です。
再送はソケット単位で直接確認できます。
$ ss -ti dst 10.0.3.7
ESTAB 0 0 10.0.1.21:44712 10.0.3.7:5432
cubic rto:204 rtt:1.8/0.4 retrans:0/842 bytes_retrans:1218432retrans の累積値(842)と bytes_retrans が増え続けているなら、この経路はロスを起こしています。経路のどこでロスしているかは mtr でホップごとのロス率を見て絞り込みます。原因は回線品質だけでなく、過負荷のスイッチのキュードロップ、誤った duplex ネゴシエーション、MTU の不一致(経路 MTU 探索の失敗で大きいパケットだけ消えるケース)のように設備側であることも多いのです。「小さい応答は問題ないのに大きい応答だけ止まる」なら MTU を疑います。
測定ツールの整理 — 軸を分けて測ります #
診断が混乱する理由は、レイテンシ、帯域、ロスが一つの塊として「遅い」と体感されるからです。軸を分けてそれぞれ測ります。
- RTT と経路:
pingで往復時間を、mtrでホップごとの遅延とロスを測ります。負荷中と暇なときを比較して、キューイング遅延(バッファブロート)も見ます。 - 帯域:
iperf3で経路の実際に達成可能なスループットを測ります。回線のスペックではなく実測が基準です。 - 再送:
ss -ti、サーバー全体ではnstatの TcpRetransSegs の推移を見ます。 - 往復回数: アプリケーションのトレーシング(リクエスト一つが作る内部呼び出しの数)を見ます。システムツールでは見えない軸です。
診断手順のまとめ #
症状が「帯域は余っているのに遅い」のとき、手順は次のとおりです。
- まず RTT を測る —
pingとmtrを使います。経路の物理的な距離がそのまま下限です。 - 往復回数を数える — 新規コネクションの比率(ハンドシェイクの無駄)とリクエストあたりの内部呼び出し数を見ます。掛け算を減らすことが最優先の対処です。
- 再送の確認 —
ss -tiとmtrのホップごとのロスで確認します。あるなら設備と経路の問題へ方向を変えます。 - 大容量転送が問題なら BDP を計算 — ウィンドウとバッファが BDP に届いているかを確認します。
- それでも残ればサーバー側 — この時点からはネットワークではなく相手サーバーの処理遅延です。1 編の診断へ引き継ぎます。
まとめ #
- 小さいリクエストのトラフィックの体感速度は RTT × 往復回数です。帯域の増強はこの掛け算に何の効果もありません。
- コネクション再利用(keep-alive、プール、TLS セッション再開)は、リクエストあたり 2 往復をただで消す最初の対処です。
- コネクション一本のスループット上限はウィンドウ ÷ RTT です。長距離の大容量転送は BDP に対してバッファを確認します。
- 0.1% のパケットロスでもスループットを数十分の一に落とすことがあります。
ss -tiの retrans とmtrで確認します。 - レイテンシ、帯域、ロス、往復回数は互いに別の軸です。分けて測ってこそ対処が定まります。
次回がシリーズ最終回、データベースです。問題なく動いていたデータベースが遅くなる理由を、インデックス、ロック、コネクションプールで追跡します。