【実務・中級編】 無線パケット損失と遅延揺らぎ(ジッタ)がTCP/UDPスループットに与える影響 – 家庭用ネットワーク・IoT・モバイル通信実践ガイド

こんにちは、シニアネットワークエンジニアの私です。

現場でWeb APIのレスポンス遅延や、モバイル回線(4G/LTEや5G)経由のクライアントから「たまにリクエストがタイムアウトする」「データ転送が途中で失速する」といったクレートを受けたことはないでしょうか。サーバー側のCPUもメモリも健康そのもの、DBのクエリも一瞬で終わっている。なのに、なぜかエンドユーザー体感のパフォーマンスが悪い——。

その犯人の多くは、サーバーでもアプリでもなく、「無線区間におけるパケット損失(ロス)」と「遅延揺らぎ(ジッタ)」です。

今回は、パケットが電波の海を渡る際に何が起きているのか、そしてそれがTCPの心臓部である「輻輳制御(Congestion Control)」にどう悪影響を及ぼすのか。標準的なRFCの仕組みから、実務でのデバッグ手法、そしてアプリ層での対策まで、泥臭い知見を交えて徹底解説します。

—

1. 無線区間特有の「バーストエラー」と「ジッタ」の正体

有線LANや光回線(FTTH)の世界では、パケットロスは基本的に「ルーターのバッファ溢れ(混雑)」によって等確率(ランダム)に発生します。しかし、モバイル通信(4G/LTEや5GのSub6/ミリ波)の世界はまったく異なります。

電波は、障害物、干渉、マルチパス(電波がビル等で反射して異なるタイミングで届く現象)、そして基地局と端末の距離の変化によって、品質が劇的に変動します。

バーストエラー(連続的なロス)

無線環境では、一瞬のフェージング(電波の減衰)によって、1パケットだけでなく数個から数十個の連続したパケットがまとめて消滅する「バーストエラー」が高頻度で発生します。

ジッタ(遅延揺らぎ)の発生要因

基地局のスケジューラ(無線リソースをどの端末に割り当てるか決めるアルゴリズム)の挙動や、再送制御(RLC/MAC層のARQやHARQ)が裏で必死にパケットを救出しているため、パケットが届くまでの時間(RTT: Round Trip Time)が一定になりません。
平時は 15ms で返ってきていたパケットが、電波が悪くなった瞬間に 250ms に跳ね上がり、次の瞬間には 20ms に戻る。この激しい揺らぎがジッタです。

—

2. TCPの輻輳制御が無線環境で「急ブレーキ」を踏むメカニズム

Web APIの通信に使われる標準的なプロトコルであるTCPは、パケットロスを「ネットワークが混雑している(輻輳している)」と勘違いするように設計されています(RFC 5681など)。

ここに、無線特有の挙動が致命的なミスマッチを生みます。

1. バーストエラーによるロス発生
無線区間でパケットが消えると、受信側は重複ACK(Duplicate ACK)を返し、送信側は「おっと、回線が混んでるぞ」と誤認します。
2. 輻輳ウィンドウ(cwnd)の縮小
多くのレガシーなTCPアルゴリズム(CUBICの初期設定やRenoなど)は、ロスを検知すると輻輳ウィンドウサイズ(cwnd)を強制的に半分(あるいは最小値付近まで)に絞ります。
3. 帯域が空いているのに加速できない
実際の原因は「電波が一時的に悪かっただけ(物理的なロス)」であり、ネットワークのパイプ自体はガラ空きです。しかしTCPは慎重にスロースタートをやり直すため、スループットが急降下します。

さらに、ジッタが大きいと、TCPの再送タイムアウト(RTO: Retransmission Timeout)の算出基準である平滑化RTT(SRTT)とRTTの分散(RTTVAR)が狂い、「まだ届いている最中なのに、タイムアウトと勘違いして無駄な再送を繰り返す」という最悪の悪循環(スプリアス再送)に陥ります。

—

3. 実務での影響:API設計とクライアント実装へのインパクト

この現象は、特に以下のようなユースケースで致命傷になります。

  • 大容量のJSONレスポンスを返すAPI:ウィンドウサイズが小さく抑え込まれるため、理論上の回線速度(5Gで100Mbpsなど)が出ていても、実際の転送速度が数Mbpsまで落ち込む。
  • モバイルアプリからのバッチ送信:細かなPOSTリクエストを大量に投げるとき、ジッタによるRTTの変動でコネクションがつまり、全体がタイムアウトする。

デバッグ時の確認コマンド(Linux / cURL)

実務で「回線が遅いのか、サーバーが遅いのか」を切り分けるため、curlを使ってTCPハンドシェイクやTLS、そして初弾のTTFB(Time to First Byte)の内訳を計測します。

# 接続先APIに対して、名前解決からTLS確立、TTFBまでの時間を詳細に計測する
curl -w "\n--- 統計情報 ---\n\
DNSルックアップ:     %{time_namelookup} 秒\n\
TCP接続時間:         %{time_connect} 秒\n\
TLSハンドシェイク:   %{time_appconnect} 秒\n\
サーバー処理(TTFB):  %{time_starttransfer} 秒\n\
総合計時間:          %{time_total} 秒\n\
平均ダウンロード速度:  %{speed_download} bytes/sec\n" \
-o /dev/null -s "https://api.example.com/v1/heavy-payload"

もし time_connect や time_starttransfer の値が、同一回線の有線環境に比べて異常に大きい場合、無線区間でのTCPウィンドウ制御やパケットロスが疑われます。

—

4. エンジニアが取るべき実践的な対策と設定例

この「無線特有のパケットロスとジッタ」に対抗するため、インフラ側(OSのTCPスタック)およびアプリケーション層ではどのような対策が必要でしょうか。

対策①:モダンな輻輳制御アルゴリズム(BBR)の採用

Googleが開発した TCP BBR (Bottleneck Bandwidth and RTT) は、パケットロスを「輻輳(混雑)」ではなく「利用可能な帯域の指標」として捉えます。電波状況の悪化によるロスでウィンドウサイズをむやみに縮小させないため、モバイル環境でのスループットが劇的に改善します。

Linux(Ubuntu等)のサーバーでBBRを有効にする設定例です。

# 1. 現在使用中の輻輳制御アルゴリズムを確認
sysctl net.ipv4.tcp_congestion_control

# 2. カーネルがBBRをサポートしているか確認(bbrが表示されればOK)
sysctl net.ipv4.tcp_available_congestion_control

# 3. 一時的にBBRに切り替える場合
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

# 4. 恒久的に設定するため、/etc/sysctl.conf に追記する
echo "net.core.default_qdisc = fq" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control = bbr" | sudo tee -a /etc/sysctl.conf

# 設定を反映
sudo sysctl -p

対策②:HTTP/2 から HTTP/3 (QUIC) への移行

TCPの最大の弱点は「ヘッド・オブ・ライン・ブロック(HoLブロック)」です。1つのパケットがロスすると、同じTCPコネクション上の後続のストリームがすべてブロックされます。

これを解決するのが、UDPベースのトランスポートプロトコルである QUIC(HTTP/3) です。
QUICでは、ストリームごとに独立したロスリカバリを持つため、あるパケットが無線区間で消えても、無関係な他のAPIリクエストや画像データの転送は止まりません。

NginxでHTTP/3を有効にする際の設定例(抜粋):

server {
    listen 443 ssl;
    listen 443 quic reuseport; # UDPでQUIC待ち受けを有効化
    ssl_protocols TLSv1.3;     # HTTP/3にはTLS 1.3が必須

    # ブラウザにHTTP/3の存在を通知するヘッダー
    add_header Alt-Svc 'h3=":443"; ma=86400';

    location / {
        # 通常のWeb/APIルーティング設定
        try_files $uri $uri/ =404;
    }
}

対策③:アプリケーション層でのリトライ制御(Pythonの例)

モバイル環境からのAPIリクエストを受ける、あるいはクライアント側を実装する際は、ジッタと一過性のロスを想定した「ジッタ付き指数バックオフ(Exponential Backoff with Jitter)」によるリトライ実装が不可欠です。

固定間隔でのリトライは、無線が不安定なタイミングで一斉にトラフィックを再送させるため、基地局をさらに高負荷にして自爆します。

import time
import random
import requests
from requests.exceptions import RequestException

def api_request_with_jitter_retry(url, max_retries=3):
    """
    無線環境のジッタとバーストロスを考慮した、
    ジッタ付きエクスポネンシャルバックオフによるリトライ関数
    """
    base_delay = 1.0  # 初期待ち時間(秒)
    
    for attempt in range(max_retries + 1):
        try:
            response = requests.get(url, timeout=5.0)
            # ステータスコードが5xx系の場合は例外扱いにする
            if response.status_code >= 500:
                raise RequestException(f"Server error: {response.status_code}")
                
            return response.json()
            
        except RequestException as e:
            if attempt == max_retries:
                print(f"最大リトライ回数 ({max_retries}回) に達しました。エラー: {e}")
                raise
                
            # バックオフ時間の計算: base_delay * (2^attempt)
            sleep_time = base_delay * (2 ** attempt)
            
            # 【重要】完全な同期リトライを防ぐため、ランダムなジッタ(0〜1秒の揺らぎ)を付与する
            jitter = random.uniform(0, 1.0)
            total_sleep = sleep_time + jitter
            
            print(f"通信エラー発生: {e}. {total_sleep:.2f}秒後にリトライします... (試行 {attempt + 1}/{max_retries})")
            time.sleep(total_sleep)

# 実行例
# data = api_request_with_jitter_retry("https://api.example.com/v1/data")

—

5. まとめ

無線パケット損失とジッタは、単なる「回線の悪さ」にとどまらず、TCPの輻輳制御メカニズムをハックするかのようにスループットを低下させます。

  • インフラ層では、TCP BBRの導入やHTTP/3 (QUIC) へのシフトによって、無線特有のロスに対する耐性を上げる。
  • アプリ・API設計層では、ジッタを意識した適切なタイムアウト設定と、確率的に分散されたリトライロジックを組み込む。

この2つのアプローチを組み合わせることで、地下鉄やビル影といった劣悪なモバイル環境からアクセスしてくるユーザーに対しても、ストレスのない堅牢なシステムを提供できるようになります。現場のチューニングの参考にしていただければ幸いです。

コメント

タイトルとURLをコピーしました