トンネル越しの対話型シェルが、ファイルコピーの実行中に数秒間固まり、その後は何事もなかったかのように再開する。問題があるのはダウンロードだけなのに、シェルも待たされ、両方が同じ瞬間に回復する。このパターンは、多数の会話を運ぶ一本の信頼性のあるストリームの特徴であり、人が最初に試すほぼすべての調整変更を生き延びる。

この症状は、設定ではなく仕組みを指しているので、注意深く読む価値がある:停止が共有されているなら原因も共有であり、その共有されている対象は外側の接続である。その間、ログの他の部分は正常に見える。

症状:すべてが一斉に止まる

内側の二つの接続が同時に止まるのが手がかりである。遅いダウンロードと遅いシェルは、別々のシーケンス番号と別々の輻輳状態を持つ別々の TCP 会話なので、どちらかの障害がもう一方に及ぶはずはない。両者が足並みをそろえて止まるなら、その上にある何かが両者のバイトを順序づけている。

その何かとはトンネルのトランスポートである。TCP モードでは、OpenVPN がカプセル化したパケットをピアへの一本の TCP 接続に書き込み、TCP の契約は単一のバイトストリームを順序どおりに届けることである。後続のバイトは、どの内側の会話に属していても、欠けたバイトより先にアプリケーションへ渡されることはない。

症状が語っていないことに注意してほしい。経路が壊れている、サーバーが過負荷だ、暗号が遅い、と言っているのではない。それらは別の形をとる:一律に低いスループット、タイムアウト、あるいは片方向だけに及ぶ障害である。失われたセグメントが再送されて終わる共有の停止は、まさに順序づけの効果である。

外側のバイトストリームは一本、内側の会話は多数

トンネルがダウンロードとシェルを同時に運んでいると想像してほしい。外側の接続のセグメントが一つ失われる。後続のセグメントは受信ホストに届き、受信側の TCP スタックはそれらを確認応答するが、隙間が埋まるまで上位へ引き渡すことはできない。したがって OpenVPN は、シェル自身のバイトはずっと前に届いていたのに、シェル用に読むものが何もない。

これは一つの接続の内部で起きる先頭ブロッキングであり、二つのアプリケーションが内側の接続を共有する必要はない。共有しているのは外側であり、それで十分である。

二次的な効果が事態を悪くする。確認応答を受け取らない内側の送信側は、外側の層がすでに届けようとしているデータを再送することがある。その再送は、セグメントを失い得るとたった今証明した経路の上で追加のトンネル通信になり、回復の作業が本当に重要な回復と競合する。

とはいえ崩壊が必ず起きるわけではない。往復が短くキューに余裕のあるきれいな経路なら、この構成は何年も十分に使える。損失、遅延、キューの深さがそろったときに障害が現れるため、報告は十分に用意されたオフィスではなく、混雑した上流回線やモバイル回線に集中する。

内側のタイマーを延ばしても助けにならない理由

ルート上で rto_min を使って最小再送タイムアウトを引き上げると、そのルートを使うローカル生成の TCP 通信で、タイマーによる再送が先延ばしになる。それは実際に効果のある本物のつまみであり、一次の問題ではなく二次の問題に働きかける。

ip route show
ip route show cache 2>/dev/null | head
ss -tin dst 10.70.0.1

できないのは、外側のバイトを順序外れに解放すること、欠けたセグメントをより速く回復すること、遠隔のアプリケーションサーバーに同じタイマーを採用させることである。受信側スタックは依然として順序どおりに届け、遠端の送信側は依然として独自の往復時間測定を使う。

他の損失回復機構をすべて無効にするわけでもない。実際の損失にタイマーによる回復が必要な場面では、より大きな最小値はアプリケーションが動くまでの待ちを長くし、遅延に敏感なセッションが必要とするものの逆になる。

十二秒という固定の下限が、TCP トンネルがあるというだけで当然のように、この話題の助言に現れる。RFC 6298 に記述された再送タイマーは、指数バックオフを伴う測定された往復時間から導かれるので、一定の下限は特定の経路に対する方針の選択であり、プロトコルの要件ではない。広く適用すると、症状を覆い隠しながら、そのルートが対象とするすべてのフローの回復を変えてしまう。

tcp-nodelay にできることとできないこと

トランスポートのソケットで Nagle 風のバッファリングを無効にするトンネルオプションには、狭い目的がある:小さな書き込みが送信前に追加のデータを待つのを止めることだ。それは、それ以外は健全な経路で対話型の通信が余分な遅延を避けるのに役立つ。

トランスポートを無順序にするわけでも、セグメントを失った後の回復を短くするわけでもない。再送タイムアウトで止まるシェルは、順序の規則に触れていないそのオプションを有効にしても、やはり止まったままだ。

両者を混同しやすいのは、どちらも遅延の修正として説明されるからだ。一方はバッファリングが持ち込んだ人為的な待ちを取り除き、もう一方はトランスポートが保証する内容を変えることを要するが、それはソケットオプションにはできない。

トランスポートを公平に比較する方法

運用者が許し、両方のサービスが存在するなら、同じ作業負荷を運ぶ UDP サービスと TCP サービスを、同じサーバーへの同じアクセス経路で比較してほしい。二つの試験の間で国、事業者、サーバー世代を変えると、結果を支配する変数を持ち込むことになる。

スループットの数値ではなく、共有される停止に注目してほしい。外側の損失の間に複数の内側アプリケーションが一緒に止まることこそ、先頭ブロッキングを特定する観察であり、スピードテストの数値が一つ低いことではない。

トンネルインターフェースと物理インターフェースでの許可されたキャプチャは、内側と外側の再送が別々のシーケンス空間を持つ別々の接続に属するため、二つの層をきれいに分ける。外側のストリームに隙間があるのに内側の再送が見えることが、直接の証拠である。

UDP に切り替えると余分な信頼性のあるストリームがなくなり、内側の TCP が自前で回復するようになる。損失ゼロや低遅延を約束するものではなく、飽和した上流回線、過大なキュー、MTU の問題は依然として修正が必要である。トンネル外の通信がトンネルの順序の隙間の後ろで止められることはないが、両方の経路が一つの混雑したリンクを共有するときには影響を受け得るため、その共有されたボトルネックは、ここに折り込むのではなく独自に調べる価値がある。

関連記事