QUICのAckとロスリカバリについて

QUICのAckとロスリカバリについて自分なりにまとめる。
(トランスポートは詳しくないのでご容赦ください)

あわせて、関連記事もどうぞ

とくに、ストリームやフレームに関しては下記記事を参照のこと
asnokaze.hatenablog.com

はじめに

QUICでは、UDP上で信頼性のあるデータ通信を提供するトランスポートプロトコルです。
(暗号化およびHTTP/3については、関連記事参照)

f:id:ASnoKaze:20190703231338p:plain

今回はdraft-20の主に下記仕様に基づいています

QUICでは内部的に仮想的な通信単位であるストリームを持ち、フレームと呼ばれるメッセージをやりとりします。TCPではパケットロスが発生すると、OSレイヤでパケットロスが回復されるまで後続のパケットを処理できません。しかし、QUICでは同一ストリーム内でのみデータの順番が保証されているため、パケットロスが発生してもデータのロスがなかったストリームは問題なく処理を進めることができます。

このように、UDP上にストリームという通信単位を持つというのは強力ですが、Ackやロスリカバリも多くの工夫がされています。

例えば、QUICではロスしたパケット自体そのものを再送することはなく、必要なデータのみを新しいパケットに格納して送信します。ロスしたパケットの中身のうち不必要なものは再送しませんし、時間が経過し状況が異なっているため新しいACKフレームを送ったり、はたまた優先度の高いアプリケーションデータを先に送るということもできます。(フレームタイプごとに再送処理が異なる)

また、QUICではパケット番号は常に1つずつ増えていきます。先述の通り、パケットロスした場合もそのままパケットを再送するのではなく、新しいパケットとして送信されます。

TCPでは、同一のシーケンス番号で再送を行うため、パケットを受け取った側は再送されたパケットなのかそうでないのかがわかりません。これは、RTTを測定する上での曖昧性となります。RTTをより正確に測定できればより適切なタイムアウト値を設定できるようになります。

その他にも、QUICのACKフレームではロスしたパケットをたくさん示すことができるようになっていたりします。詳しく見ていきましょう

パケットとアプリケーションデータ

TCPではシーケンス番号を使用していました。

QUICではシーケンス番号の持っていた役割をパケット番号とオフセットに分離しています。すべてのQUICパケットにパケット番号がついており明確に1つずつ増えていきます。パケット番号は明確に送信者がパケットを送信した順番を示します。

それとは別にアプリケーションデータを格納するSTREAMフレームにはデータのオフセット, 長さがあり、データ上の順番を特定することができます。

そのため、ロスしたアプリケーションデータを別のパケット番号で送信することができます。
f:id:ASnoKaze:20190704001213p:plain

Ack自体はパケット番号ごとに行うので、パケロスと判断されたパケットに含まれていたデータを送り直してやれば大丈夫です。また「パケットNが届いてないというAck」に対してAckを返してやれば、確かにロスしたことは伝わったから再送がされるだろうということになります。

ACKフレーム

QUICのAckはACKフレームを送信することで行われます。ACKフレームは受信したパケット番号レンジと、受信できていないパケット番号レンジを複数表現することができます。

ACKフレームは下記のような構造をしています。
f:id:ASnoKaze:20190704010950p:plain

  • Largest Acknowledged: ACKフレームを作成するまでに受信した最大のパケット番号
  • ACK Delay: Largest Acknowledgedで示されるパケットを受信してから、このパケットを送信するまでのディレイ(ms)
  • ACK Range Count:このACKフレーム内のGapフィールドとACKレンジの個数
  • First ACK Range: Largest Acknowledgedに続いて受信したパケットの数を示すACK Range(後述)
  • ACK Ranges: 0回以上のGapとACK Range(後述)

ACK Rangesの構造は下記の通りになります。下記のように複数のGapとACK Rangeのフィールドからなります。
f:id:ASnoKaze:20190704012648p:plain

  • Gap: 前のACKレンジに続いて、受信できていないパケットの数
  • ACK Range: 前のGapに続いて、受信したパケットの数

このように一つのACKフレームで、受信できたレンジと、受信できてないレンジを表現することができます。

f:id:ASnoKaze:20190704014232p:plain
例えば、パケット番号1 ~ 110, 120 ~ 130までのパケットを受信したことを相手に伝えるACKフレームは

  • Largest Acknowledged: 130
  • ACK Range Count: 1
  • First ACK Range: 10 (パケット番号120~129)
  • Gap: 8 (パケット番号111~119 から1引いたものを表記する)
  • ACK Range: 109 (パケット番号1~110 から1引いたものを表記する)

ACKフレームをいつ送信するかについて説明していきます。

  • ACKすべきパケット(ACKのみPADDNGのみ以外のパケット)を受信した際に、max_ack_delay(デフォルトで25ms)以内に送信すべきです。
  • 受信したパケット番号の最大値+1以外のパケットを受信した場合はすぐにACKフレームを送信すべきです。
  • ただし、ACKフレームを送信する前に複数のパケットを処理してもよいです(個々別にACKしないにする)

ロス検出

QUICでもAck情報とタイムアウトを使って、TCPのFast Retransmit [RFC 568]、Early Retransmit [RFC 582]、FACK [FACK]、SACK損失回復[RFC 6675]、およびRACK [RACK]に則りパケットロス検出を行います。

基本的には

  • 3パケット以上の順番の入れ替わり (kPacketThreshold == 3 は仕様で推奨されている)
  • 1パケット以上の順番の入れ替わりがあり、9/8 * max(SRTT, latest_RTT) 以上経過している

また、QUICでは、Ackが送られてこない場合のRTO(Retransmission Time Out)と、データ末尾の再送を促すTLP(Tail Loss Probe)のタイマーを一つにまとめたPTO(Probe Timeout)を持つ。PTOの計算式は下記のとおりである。

pto = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay

もしPTOタイマーが切れた場合、もし送信するデータが有ればそれを送信するか、ロスしたと思われるSTREAMのデータをプローブとして送信します。これにより、ACKフレームを送信させることができます。

連続したPTOタイマーは指数関数的増えていき、送信頻度は少なくなってきます。

今回書かなかったこと

結構奥が深い

  • crypto context毎にパケット番号空間が違うので、異なるcrypto contextに対してACKすることはできなくなっています
  • ハンドシェイク時に使用するCRYPTOはより積極的に再送されます
  • RTTの測定アルゴリズム (仕様を参照)
  • QUICはECN (Explicit Congestion Notification)をサポートしてます
  • 輻輳制御について。仕様上は、輻輳制御はTCP NewReno です。Cubic やその他のアルゴリズムを使用しても良いです。