【実務・中級編】 pingの往復遅延時間(RTT)計測アルゴリズムとタイムスタンプの利用 – トラブルシューティング&ネットワーク運用監視実践ガイド

夜の静まり返ったNOCルーム。モニターの青白い光が、疲れたエンジニアたちの顔を照らし出している。
「おい、APIのレイテンシが跳ね上がってるぞ。DBか? それともロードバランサーか?」
こんな時、君たちはどうする?とりあえずブラウザのDevToolsを開いたり、お決まりの ping を打ったりしていないか?

ちょっと待て。その ping、ただ「パケットロスが何%か」を見るためだけに叩いていないとしたら、君はすでに一流のインフラエンジニアへの階段を登っている。だが、もし往復遅延時間(RTT: Round Trip Time)の裏側で何が起きているのか、ICMPのタイムスタンプがどう絡んでいるのかを説明できないなら、もう少し僕の講義に付き合ってもらおう。

数々の修羅場をくぐってきたシニアエンジニアの視点から、ICMPの深淵とRTT計測の真実を紐解いていこう。

—

1. そもそも ping のRTTはどうやって測られているのか?

僕たちが日常的に使っている ping コマンド。これはRFC 792で規定されているICMP(Internet Control Message Protocol)の Echo Request(タイプ8)と Echo Reply(タイプ0)というメッセージのキャッチボールで成り立っている。

極めてシンプルな仕組みに見えるが、ここでエンジニアなら一歩踏み込んで考えてほしい。
「私のPCから送信されたパケットが、地球の裏側のサーバーに届き、そして戻ってくるまでの時間を、一体どうやって正確に測定しているのか?」

答えは、「送信側の時計」にある。

送信から受信までのタイムライン

1. 送信 (Echo Request):
OSのネットワークスタックが ping パケットを送り出す瞬間、送信側ホストは自身の高精度なタイマー(CPUのタイムスタンプカウンターやOSのクロック)を参照し、現在の時刻をメモリ上に記録する。これが T1(送信タイムスタンプ) だ。
2. 折り返し (Echo Reply):
宛先のターゲットホストにパケットが届くと、OSのカーネルレベル(あるいはネットワークスタック)で即座にこれを検知し、中身を書き換えることなく宛先と送信元を入れ替えて Echo Reply を押し返す。このとき、ターゲット側がわざわざ自身のタイムスタンプをICMPペイロードに書き込む実装になっている場合もある(後述するICMP Timestampオプションなど)。
3. 受信 (Echo Reply):
送信側に戻ってきたパケットをキャッチした瞬間、再び送信側ホストは現在の時刻を記録する。これが T2(受信タイムスタンプ) だ。

ここで算出されるRTTは、非常にシンプルにこう表される。

$$RTT = T2 – T1$$

「なんだ、ただの引き算じゃないか」と思ったかい?
そうだ、基本はな。しかし、この「ただの引き算」の中に、ネットワークエンジニアを悩ませる罠がいくつも隠されているのだ。

—

2. ICMPヘッダーのタイムスタンプ機能と実務の罠

実は、ICMPには純粋な Echo Request/Reply とは別に、ICMP Timestampメッセージ(タイプ13)とTimestamp Replyメッセージ(タイプ14)という、歴史ある仕様が存在する(RFC 792)。

この仕様では、ICMPのデータ部(ペイロード)に以下の3つのタイムスタンプをねじ込むことができる。

  • Originate Timestamp: 送信側がパケットを発射した時刻
  • Receive Timestamp: 受信側(ターゲット)がパケットを受け取った時刻
  • Transmit Timestamp: 受信側が返信パケットを送り出した時刻

これらが揃っていれば、ネットワークの「往路」と「復路」の遅延を分離して計算できる。さらに、送信側と受信側のクロックが同期していれば、時計のズレ(クロック・ドリフト)さえも検出できる優れものだ。

しかし、実務では「使えない」ことが多い

「じゃあ、これで正確な片道遅延が測れるじゃないか!」と思ったそこの君、現実はそんなに甘くない。

1. セキュリティ上の理由(ICMPブロック):
現代の堅牢なデータセンターやクラウド(AWS, GCP, Azureなど)のセキュリティグループやファイアウォールでは、不審なICMPパケット(特にタイムスタンプ要求やリダイレクト)は容赦なくドロップ、あるいは無視される設定になっていることが多い。
2. カーネルの省電力機能とタイマー精度:
OSの電源管理機能(Cステートなど)が働いている環境では、CPUクロックの周波数が動的に変動するため、ミリ秒以下の高精度なタイムスタンプが狂うことがある。

そのため、実務のインフラ監視やアプリケーションの死活監視では、ICMP Timestampに頼るのではなく、標準的な Echo Request によるRTT計測と、アプリケーション層(HTTP/gRPCなど)でのタイムスタンプヘッダーの付与を組み合わせるのが定石となっている。

—

3. 実践:Pythonによる高精度RTT・ジッタ計測スクリプト

百聞は一見に如かず。実際にコードを書いて、パケットの往復とタイムスタンプの動きを体感してみよう。
ここでは、Pythonの標準ライブラリとサードパーティ製の scapy、あるいはソケット通信を用いて、RTTとジッタ(遅延の揺らぎ)を正確に計測するスクリプトの概念を実装してみる。

実務のトラブルシューティングにおいて、「単発のpingではなく、連続したRTTの変動(ジッタ)から輻輳(ふくそう)を検知する」ためのベースとなるコードだ。

import socket
import struct
import time
import os
import sys

def checksum(source_string):
    """
    ICMPパケットのチェックサムを計算する関数
    ネットワーク層の整合性を担保するための重要な処理。
    """
    sum = 0
    count_to = (len(source_string) // 2) * 2
    count = 0
    while count < count_to:
        val = source_string[count + 1].encode('latin1')[0] * 256 + source_string[count].encode('latin1')[0]
        sum = sum + val
        sum = sum & 0xffffffff
        count = count + 2

    if count_to < len(source_string):
        sum = sum + source_string[len(source_string) - 1].encode('latin1')[0]
        sum = sum & 0xffffffff

    sum = (sum >> 16) + (sum & 0xffff)
    sum = sum + (sum >> 16)
    answer = ~sum
    answer = answer & 0xffff
    answer = answer >> 8 | (answer << 8 & 0xff00)
    return answer

def ping_once(dest_addr, timeout=2.0):
    """
    指定された宛先に対して1回だけICMP Echoを送信し、RTT(ミリ秒)を返す。
    タイムアウトした場合はNoneを返す。
    """
    # ICMPは raw socket を使うため、root権限が必要になる場合がある
    try:
        icmp = socket.getprotobyname("icmp")
    except socket.error:
        print("ICMPプロトコルの取得に失敗しました。権限を確認してください。")
        return None

    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
    except PermissionError:
        print("エラー: RAWソケットを作成するには管理者権限(root)が必要です。")
        sys.exit(1)

    # ICMP Headerの構築 (Type=8: Echo Request, Code=0, Checksum=0, ID=PID, Seq=1)
    # パイロット版としてシンプルな構造体を作成
    packet_id = os.getpid() & 0xFFFF
    
    # タイムスタンプをペイロードに埋め込む(これがアプリケーションレベルのT1となる)
    t1 = time.time()
    payload = struct.pack("d", t1) # 8バイトの浮動小数点数で時刻を格納
    
    # 仮のチェックサム0でヘッダーを作成
    header = struct.pack("bbHHh", 8, 0, 0, packet_id, 1)
    packet = header + payload

    # チェックサムの計算と詰め替え
    chk = checksum(packet)
    header = struct.pack("bbHHh", 8, 0, socket.htons(chk), packet_id, 1)
    packet = header + payload

    # パケット送信
    sock.settimeout(timeout)
    try:
        sock.sendto(packet, (dest_addr, 1))
    except socket.error as e:
        print(f"送信エラー: {e}")
        sock.close()
        return None

    # レスポンス受信待機
    start_time = time.time()
    while True:
        current_time = time.time()
        if current_time - start_time > timeout:
            sock.close()
            return None # タイムアウト

        try:
            eval_packet, addr = sock.recvfrom(1024)
            t2 = time.time()
            
            # 簡易的にIPヘッダー(通常20バイト)をスキップしてICMP部分を検証
            icmp_header = eval_packet[20:28]
            type, code, _, rcv_id, _ = struct.unpack("bbHHh", icmp_header)
            
            # 自分が送ったリクエストに対するリプライ(Type 0: Echo Reply)か判定
            if type == 0 and rcv_id == packet_id:
                sock.close()
                # RTTをミリ秒単位で返す (T2 - T1)
                rtt = (t2 - t1) * 1000.0
                return rtt
        except socket.timeout:
            sock.close()
            return None

if __name__ == "__main__":
    target = "8.8.8.8" # 例としてGoogle Public DNSを使用
    print(f"Target: {target} へのRTT計測を開始します(Ctrl+Cで中断)...")
    
    rtts = []
    try:
        for i in range(5):
            rtt = ping_once(target)
            if rtt is not None:
                print(f"Seq {i+1}: RTT = {rtt:.2f} ms")
                rtts.append(rtt)
            else:
                print(f"Seq {i+1}: Request timed out.")
            time.sleep(1.0)
            
        # ジッタ(RTTの標準偏差や隣接する差分)の簡易計算
        if len(rtts) > 1:
            jitters = [abs(rtts[i] - rtts[i-1]) for i in range(1, len(rtts))]
            print(f"\n--- 統計情報 ---")
            print(f"平均 RTT: {sum(rtts)/len(rtts):.2f} ms")
            print(f"平均 ジッタ: {sum(jitters)/len(jitters):.2f} ms")
            
    except KeyboardInterrupt:
        print("\n中断されました。")

このコードのポイントは、単にOS任センの ping コマンドを叩くのではなく、「パケットのペイロード(データ部)に送信時の高精度タイムスタンプ(time.time())を埋め込んでいる点」にある。これにより、ネットワークスタックのオーバヘッドを含まない、より純粋な往復遅延の傾向を掴むことができるのだ。

—

4. Web API設計やインフラ運用への実務的フィードバック

さて、ここまで低レイヤーなICMPとRTTの話をしてきたが、これを現代のWebエンジニアやクラウドインフラ運用者がどう活かすべきか?

1. 「遅い」というクレームに対する客観的な切り分け

ユーザーから「APIのレスポンスが遅い」という連絡が入ったとする。
ここでアプリケーションログの処理時間(Server Processing Time)だけを見ていても真相にはたどり着けない。クライアントからロードバランサー、そしてアプリケーションコンテナに至るまでのネットワークパスにおいて、どこでパケットが滞留しているのか。
traceroute やカスタム ping を用いて、各ホップ(ルーター)ごとのRTTの跳ね上がりを監視し、「問題がネットワーク層(レイヤー3/4)にあるのか、アプリケーション層(レイヤー7)にあるのか」を即座に切り分ける必要がある。

2. ジッタ(Jitter)の監視を見逃すな

リアルタイム性の高いWebsocket、gRPC、あるいは音声・映像ストリーミング配信の基盤設計において、平均RTT以上に重要なのが「ジッタ(遅延の変動幅)」だ。
平均RTTが 50ms と低く安定していても、ジッタが激しく 10ms 〜 300ms を行ったり来たりしているような回線は、TCPの輻輳制御ウィンドウを狂わせ、パケットロスを誘発する最悪の環境だ。監視ツール(Prometheus + Grafanaなど)を構築する際は、単なる死活監視(Ping Exporterなど)のレスポンスタイムだけでなく、ジッタの指標も時系列でグラフ化し、閾値アラートを設定しておくのがプロの仕事というものだ。

—

5. まとめ

ネットワークのトラブルシューティングは、医者の診察に似ている。
患者(システム)のどこが痛むのか、脈拍(RTT)はどう乱れているのか、その背後にあるメカニズムを正しく理解していれば、パニックになることなく冷静に患部を特定できる。

今日のまとめだ:

  • RTTの基本: 送信側が記録した T1 と、受信して戻ってきたときの T2 の差(T2 - T1)がすべての基本。
  • ICMPの制限: タイムスタンプ機能は便利だが、セキュリティポリシーやファイアウォールで遮断されることが多いため、過信は禁物。
  • 実務での応用: アプリケーション層や独自スクリプトでのタイムスタンプ利用、そして平均値だけでなく「ジッタ」に目を向けることで、インフラの健康状態を正確に把握できる。

さあ、コーヒーブレイクはここまでだ。アラートの鳴り響くダッシュボードへ戻るとしようか。君たちのネットワークに、常に良好なパケットが流れていることを祈る。

コメント

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