OpenVPN через TCP: rto_min не устраняет блокировку начала очереди
Внешний поток байтов доставляет данные по порядку, поэтому один потерянный сегмент может остановить каждый внутренний поток. Более длинный внутренний таймер меняет часы, а не это ограничение.
Интерактивная оболочка через туннель замирает на несколько секунд, пока идёт копирование файла, а затем продолжает работу так, будто ничего не случилось. Проблема есть только у загрузки, но оболочка тоже ждёт, и обе восстанавливаются в один и тот же момент. Этот шаблон — подпись одного надёжного потока, несущего множество разговоров, и он переживает почти любую настройку, которую люди пробуют в первую очередь.
Симптом стоит читать внимательно, потому что он указывает на механизм, а не на параметр: пауза общая, значит и причина общая, а общий объект — внешнее соединение. Всё остальное в журнале во время этого выглядит нормально.
Симптом: всё замирает одновременно
Две внутренние связи, застревающие вместе, — вот подсказка. Медленная загрузка и медленная оболочка — это отдельные разговоры 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 всё равно потребуют исправления. Трафик вне туннеля никогда не задерживается за разрывом последовательности туннеля, хотя он всё же может страдать, когда оба пути делят один перегруженный канал, а это общее узкое место заслуживает отдельного расследования, а не сведения в это.