TCPの再送制御
欠けた番号の範囲と、再送を始める条件を照合します。
説明
TCPは受信したデータをバイト番号で管理します。途中が欠けたときは、後続が届いても累積ACKが止まり、再送で穴が埋まると先へ進みます。
1000バイト欠けた通信記録を解析し、送信側記録と重複ACKを照合します。後半ではRTTからRTOを計算し、重複ACKが得られない場合のタイマーとバックオフを確認します。
1同じACKが繰り返される通信
loss.pcapは受信側で記録した接続途中の試料です。送信側sequence1001から1000バイトずつ送り、2001からの範囲だけ最初の送信が失われた条件です。
tcpdump -n -tt -S受信順と累積ACK -r loss.pcap欠損と再送の記録1001:2001の後、3001:4001、4001:5001、5001:6001が届いてもACKは2001のままです。2001:3001が届くとACKが6001へ進みます。後続の受信を保持しても、途中の穴が埋まるまで累積ACKは進みません。
2どの範囲が届いていなかったか
再送が届く直前までに受信したバイト番号を集合で計算します。この例は小さな範囲の確認用で、実装方式そのものではありません。
python3 -c受信範囲の穴を計算 'received=set(range(1001,2001))|set(range(3001,6001)); missing=sorted(set(range(1001,6001))-received); print(len(missing),missing[0],missing[-1])'欠けた番号の先頭・末尾・個数2001から3000までの1000バイトが欠けています。次に期待する番号が2001なのはこのためです。seq3001を受け取ったことと、1001から3000まで連続して受け取ったことは異なります。
3送信側の記録と照合する
受信側だけでは、遅延と損失のどちらかを確定できません。送信側の保存記録を照合します。
cat sender.csv初回送信と再送の時刻2001を10msで送った後、80msでもう一度送っています。受信側で80msに現れた2001は、この例では再送です。同じ番号が複数回見えるだけで常に損失と決めず、観測点と送信時刻を確認します。
4重複ACKの回数
受信側から返したACK値を抽出します。
python3 -c確認応答を抽出 'import json; rows=json.load(open("loss.json")); a=[r["ack"] for r in rows if r["direction"]=="S>C"]; print(a); print(a.count(2001)-1)'最初のACKを除く同値応答最初のACK2001に続く同値のACKが3回あります。古典的なfast retransmitではこの条件が再送の手掛かりになります。ただし並べ替えでも同様のACKが生じる場合があり、現代の回復方式はSACK等の情報も使います。
5最後のデータが失われた場合
後続データがなく重複ACKが増えない場合、再送タイマーが重要になります。最初のRTT測定を0.1秒、時計粒度を0.001秒として初期計算します。
python3 -c初回RTTからタイマーを計算 'r=0.1; srtt=r; rttvar=r/2; raw=srtt+max(0.001,4*rttvar); print(srtt,rttvar,round(raw,3),max(1.0,raw))'平均・変動・下限SRTT0.1秒、RTTVAR0.05秒、計算値0.3秒です。RFC 6298の1秒下限を適用するとRTOは1秒になります。
6次のRTTで推定を更新する
次の有効なRTT測定値を0.14秒とします。変動の更新には変更前のSRTTを使います。
python3 -c平均と変動を更新 'srtt=0.1; rttvar=0.05; sample=0.14; rttvar=0.75*rttvar+0.25*abs(srtt-sample); srtt=0.875*srtt+0.125*sample; print(round(srtt,4),round(rttvar,4),round(srtt+4*rttvar,4))'変動を先に更新SRTT0.105秒、RTTVAR0.0475秒、下限前のRTO0.295秒です。測定値をそのままタイマーにせず、過去の平均と変動を反映します。再送したデータのACKはどの送信への応答か曖昧なので、タイムスタンプ等で識別できる場合を除きRTT測定へ使いません。
7タイムアウトが続くときの待ち
最初のRTOを1秒とし、期限切れごとに2倍に延ばすバックオフを確認します。
python3 -c指数バックオフの待ちを計算 'rto=1.0; waits=[rto*(2**i) for i in range(4)]; print(waits); print(sum(waits))'各回と合計の秒数待ち時間は1・2・4・8秒で、合計15秒です。これは上限や接続打切りを含めない最初の4回の計算です。高速再送とタイマー再送は起点が違い、再送があったという事実だけから同じ原因と決められません。
まとめ
累積ACKは連続して受信できた範囲を示し、後続だけ届いても穴を飛び越えません。
再送の判断には重複ACK、タイマー、観測点、RTT測定条件を照合します。
操作履歴と解説を振り返れます。
シェル / PCAPと通信の記録
tcpdump -n -tt -S -r loss.pcapreading from file loss.pcap, link-type EN10MB (Ethernet) 0.000000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 1001:2001, ack 9001, win 8192, length 1000: HTTP 0.010000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 2001, win 8192, length 0 0.020000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 3001:4001, ack 9001, win 8192, length 1000: HTTP 0.030000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 2001, win 8192, length 0 0.040000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 4001:5001, ack 9001, win 8192, length 1000: HTTP 0.050000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 2001, win 8192, length 0 0.060000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 5001:6001, ack 9001, win 8192, length 1000: HTTP 0.070000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 2001, win 8192, length 0 0.080000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 2001:3001, ack 9001, win 8192, length 1000: HTTP 0.090000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 6001, win 8192, length 0
python3 -c 'received=set(range(1001,2001))|set(range(3001,6001)); missing=sorted(set(range(1001,6001))-received); print(len(missing),missing[0],missing[-1])'1000 2001 3000
cat sender.csvMS,SEQ,LENGTH,EVENT 0,1001,1000,original 10,2001,1000,original 20,3001,1000,original 40,4001,1000,original 60,5001,1000,original 80,2001,1000,retransmission
python3 -c 'import json; rows=json.load(open("loss.json")); a=[r["ack"] for r in rows if r["direction"]=="S>C"]; print(a); print(a.count(2001)-1)'[2001, 2001, 2001, 2001, 6001] 3
python3 -c 'r=0.1; srtt=r; rttvar=r/2; raw=srtt+max(0.001,4*rttvar); print(srtt,rttvar,round(raw,3),max(1.0,raw))'0.1 0.05 0.3 1.0
python3 -c 'srtt=0.1; rttvar=0.05; sample=0.14; rttvar=0.75*rttvar+0.25*abs(srtt-sample); srtt=0.875*srtt+0.125*sample; print(round(srtt,4),round(rttvar,4),round(srtt+4*rttvar,4))'0.105 0.0475 0.295
python3 -c 'rto=1.0; waits=[rto*(2**i) for i in range(4)]; print(waits); print(sum(waits))'[1.0, 2.0, 4.0, 8.0] 15.0
演習完了