TLS-рукопожатие из Владивостока стоит 400 мс: чем платит далёкий клиент и как сократить
Клиент в Москве открывает HTTPS-сайт на сервере в том же городе и не замечает рукопожатия — оно укладывается в считаные миллисекунды. Тот же сайт, открытый пользователем во Владивостоке, если origin единственный и стоит в Москве или Европе, тратит на установление защищённого соединения ощутимо больше времени — просто потому что каждый шаг рукопожатия требует полного оборота пакета туда-обратно, а RTT до дальнего сервера может быть в разы больше, чем до ближнего. Разберём, из чего складывается эта цена, почему она бьёт по дальним клиентам сильнее всего, и что можно сделать на сервере, не трогая физику канала.
Содержание
- Сколько round trip'ов нужно TLS-рукопожатию
- Почему RTT умножается для дальнего клиента
- Шаг первый: убедиться, что сервер отдаёт TLS 1.3, а не только 1.2
- Session resumption и session tickets: почти бесплатное повторное подключение
- TCP Fast Open и OCSP stapling: где ещё прячется round trip
- Ближе к клиенту: edge вместо одного далёкого origin
Сколько round trip'ов нужно TLS-рукопожатию
Прежде чем пойдут зашифрованные данные приложения, соединению нужно пройти несколько обменов пакетами, и каждый такой обмен — это минимум один RTT (round-trip time, время оборота пакета от клиента до сервера и обратно).
Первый обязательный round trip — обычное TCP-рукопожатие (SYN → SYN-ACK → ACK). Оно устанавливает сам транспортный канал, ещё до того, как речь зайдёт о шифровании. Формально ACK можно совместить с первым TLS-пакетом, поэтому на практике TCP-хендшейк стоит клиенту один RTT задержки перед тем, как можно отправить хоть что-то по TLS.
Дальше начинается собственно TLS, и здесь число round trip'ов зависит от версии протокола:
- TLS 1.2 — классическая схема в два прохода: ClientHello уходит без ключевого материала, сервер отвечает ServerHello и сертификатом, дальше отдельным обменом стороны согласуют общий ключ через (EC)DHE или RSA, и только после этого можно слать данные приложения. Это два полных round trip'а поверх уже установленного TCP.
- TLS 1.3 — клиент сразу кладёт key share в ClientHello, предполагая группу для обмена ключом (обычно X25519), сервер отвечает своей половиной ключа в том же ServerHello — и после одного round trip'а обе стороны уже обладают общим секретом. Подробно про этот механизм и про то, что именно передаётся на каждом шаге, разобрано в статье про TLS-рукопожатие целиком.
- TLS 1.3 с session resumption (0-RTT) — при повторном подключении к тому же серверу, если у клиента сохранён ключ прошлой сессии, он может отправить данные приложения вместе с самым первым пакетом, не дожидаясь ответа. Формально это ноль дополнительных round trip'ов на сам TLS, но есть оговорки про replay-атаки — вернёмся к ним ниже.
Итого для первого подключения по HTTPS: TCP (1 RTT) + TLS 1.2 (2 RTT) = 3 RTT до первого байта данных, либо TCP (1 RTT) + TLS 1.3 (1 RTT) = 2 RTT. Разница между версиями — ровно один RTT, и именно он превращается в заметное число миллисекунд, когда RTT сам по себе большой.
Почему RTT умножается для дальнего клиента
Ключевой момент, который часто упускают: каждый round trip в этой арифметике — это не константа, а множитель на конкретный RTT конкретного клиента. Для клиента в том же городе, что и сервер, RTT может быть однозначным числом миллисекунд, и три round trip'а погоды не делают. Для клиента, чей путь до сервера идёт через несколько межконтинентальных пролётов, оптоволоконных магистралей и точек обмена трафиком, RTT может исчисляться сотнями миллисекунд — и тогда те же 3 round trip'а превращаются в заметную часть времени загрузки страницы, ещё до того, как ушёл первый HTTP-запрос.
Условный пример: если RTT между клиентом и сервером составляет около 130-140 мс (величина сильно зависит от конкретного маршрута, пиринга и промежуточных сетей — не воспринимайте её как гарантированное число для какого-то конкретного города), то три round trip'а TCP + TLS 1.2 дают порядка 400 мс только на установление соединения, прежде чем начнётся передача полезных данных. Именно из такой прикидки родился заголовок этой статьи — не как измеренный факт для конкретного маршрута, а как иллюстрация того, что происходит, когда RTT большой, а round trip'ов несколько.
Важно, что эта задержка не суммируется с загрузкой страницы — она предшествует ей. Пока рукопожатие не завершено, ни один байт HTML, CSS или JS до браузера не доедет. Поэтому для дальнего клиента цена рукопожатия — это не абстрактная метрика в отчёте, а конкретная пауза перед тем, как страница вообще начнёт что-то отображать. На медленном или нестабильном мобильном канале, где сам RTT ещё и плавает, эта пауза становится ощутимее — плюс к ней добавляется риск потери пакета и повторной передачи, что делает время до первого байта ещё менее предсказуемым.
Причина, по которой один и тот же сервис может быть быстрым для одних пользователей и медленным для других, часто вообще не в вычислительной мощности сервера, а в маршруте: где физически проходит трафик, через какие точки обмена и сколько транзитных сетей он пересекает. Это разобрано отдельно в статье про несовпадение маршрута и географии и в примере, где сервер во Франкфурте оказался быстрее московского для части клиентов — RTT определяется не расстоянием по прямой, а фактической топологией сетей на пути.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSШаг первый: убедиться, что сервер отдаёт TLS 1.3, а не только 1.2
Прежде чем оптимизировать что-то ещё, стоит проверить самое дешёвое улучшение — версию протокола. Экономия одного round trip'а достаётся почти бесплатно, если сервер вообще поддерживает TLS 1.3 и клиент им пользуется.
Проверка с сервера, не полагаясь на браузер:
openssl s_client -connect example.com:443 -servername example.com -tls1_3 2>/dev/null | grep -A1 "Protocol"
Если соединение обрывается с ошибкой согласования версии — сервер TLS 1.3 не отдаёт, и первое, что нужно сделать, это включить его в конфиге. Для Nginx:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_cipher_order off; # для TLS 1.3 выбор набора шифров — за клиентом
TLS 1.2 в списке оставлен намеренно: часть клиентов (старые версии Android, встроенные HTTP-клиенты в legacy-приложениях) TLS 1.3 ещё не умеет, и полное отключение 1.2 отсечёт их совсем. Но приоритет должен быть у 1.3 — современные библиотеки и браузеры выберут именно его, если сервер предлагает.
Стоит проверить и версию OpenSSL или используемого TLS-стека — совсем старые сборки (условно OpenSSL до 1.1.1) TLS 1.3 не поддерживают вовсе, и никакая директива в конфиге веб-сервера этого не изменит, пока не обновится сама библиотека.
Session resumption и session tickets: почти бесплатное повторное подключение
Полное рукопожатие имеет смысл платить один раз. Если клиент возвращается на тот же сервер повторно — новая вкладка, обновление страницы, следующий визит в течение того же дня — правильно настроенный TLS-стек умеет пропустить большую часть переговоров и использовать материал прошлой сессии. Это называется session resumption, и в TLS 1.3 оно реализовано через session tickets: сервер выдаёт клиенту зашифрованный тикет с параметрами сессии, и при следующем подключении клиент предъявляет его вместо полного набора ClientHello-параметров.
Для Nginx это включается session cache и session tickets:
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1h;
ssl_session_tickets on;
shared:SSL:10m — общий кеш сессий между воркер-процессами Nginx размером 10 мегабайт (этого достаточно на несколько десятков тысяч сессий, точный расчёт зависит от вашей нагрузки). ssl_session_timeout задаёт, как долго тикет считается валидным — час является разумным средним значением, но конкретное число стоит подбирать под профиль трафика: сайту с редкими долгими визитами час может быть мало, API с частыми короткими запросами хватит и меньшего окна.
Отдельно стоит 0-RTT (ssl_early_data on в Nginx) — самый агрессивный вариант resumption, когда клиент шлёт данные приложения вместе с самым первым пакетом при повторном подключении. Цена этой скорости — данные 0-RTT технически уязвимы к replay-атаке: злоумышленник, перехвативший зашифрованный пакет, может повторно отправить его серверу, и если запрос неидемпотентный (например, списание средств или создание записи), повтор может быть опасен. Поэтому 0-RTT стоит включать выборочно — для статики и идемпотентных GET-запросов, но не для форм и платёжных операций, либо оставлять выключенным, если нет уверенности, что вся цепочка обработки на бэкенде корректно защищена от повторов.
Важная оговорка: session resumption спасает только повторные подключения к уже знакомому серверу. Если у вас за балансировщиком несколько бэкендов без общего кеша сессий, клиент, которого раскидало на другой инстанс, снова пройдёт полное рукопожатие — тикет от одного сервера другой не примет, если они не настроены на общий ключ шифрования тикетов.
TCP Fast Open и OCSP stapling: где ещё прячется round trip
Кроме версии TLS, есть ещё пара мест, где можно сэкономить round trip или, наоборот, случайно его добавить.
TCP Fast Open (TFO) позволяет клиенту, который уже подключался к серверу раньше, отправить данные вместе с самым первым SYN-пакетом — минуя классическое трёхэтапное рукопожатие TCP для повторных подключений. Технология требует поддержки и на клиенте, и на сервере, и не универсальна: часть промежуточных сетевых устройств (старые прокси, некоторые NAT) обрабатывает TFO некорректно, поэтому эффект заметен не везде и включать его стоит с тестированием на реальном трафике, а не вслепую. Включение на Linux:
sysctl -w net.ipv4.tcp_fastopen=3
Значение 3 включает TFO и для клиентских, и для серверных соединений. Для Nginx дополнительно нужен флаг на слушающем сокете:
listen 443 ssl fastopen=256;
OCSP stapling — механизм, который прямо противоположен по духу: он не ускоряет рукопожатие через новый механизм, а убирает лишний round trip, который иначе делает клиент. Без stapling браузер, проверяя, не отозван ли сертификат, сам обращается к серверу удостоверяющего центра (OCSP responder) — это отдельный DNS-запрос и отдельное TCP+TLS-соединение, причём часто к серверу, который географически далёк от клиента ничуть не меньше основного origin. OCSP stapling переносит эту проверку на сторону сервера: ваш сервер сам периодически запрашивает свежий OCSP-ответ у CA, кеширует его и "пришивает" (staples) к своему сертификату прямо в TLS-рукопожатии. Клиенту дополнительный round trip делать не нужно вовсе — статус отзыва приходит вместе с сертификатом.
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
Компромисс здесь не в риске, а в проверке: без resolver директивы Nginx не сможет резолвить адрес OCSP responder'а самостоятельно, и stapling молча не заработает — а в логах явной ошибки может не быть, только отсутствие поля OCSP response при проверке через openssl s_client -status. Стоит проверить работу stapling после включения:
echo | openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null | grep -A5 "OCSP Response"
Если в выводе есть OCSP Response Status: successful — stapling работает, и клиенту не придётся делать собственный запрос к CA.
Ближе к клиенту: edge вместо одного далёкого origin
Все перечисленные выше меры сокращают число round trip'ов, но ни одна не меняет сам RTT — физическую задержку сигнала на конкретном маршруте. Если у вас один origin-сервер, скажем, в Москве, а часть аудитории физически находится за несколько тысяч километров, RTT для этих пользователей не станет меньше от того, что вы включили TLS 1.3 и resumption — вы просто умножаете этот RTT на меньшее число round trip'ов, а не убираете его.
Единственный способ реально сократить RTT — сократить физическое расстояние до точки, где терминируется TLS. Это то, что делают CDN и edge-сети: TLS-рукопожатие (а иногда и часть логики приложения) обрабатывается на узле, географически близком к клиенту, а не на единственном origin. Дальше запрос до origin, если он всё же нужен, идёт уже по внутренним, как правило более быстрым и предсказуемым каналам между узлами CDN и вашим сервером — а не по публичному интернету от конечного пользователя.
Практически это означает: для аудитории, разбросанной по разным макрорегионам (условно европейская часть России и Дальний Восток, или Россия и Юго-Восточная Азия), один сервер в одной точке — не универсальное решение. Варианта два: либо вынести терминацию TLS на CDN/edge-провайдера перед origin, либо держать несколько серверов в разных регионах и направлять клиента на ближайший через DNS-балансировку (GeoDNS) или anycast. Второй вариант тяжелее в эксплуатации — нужно синхронизировать данные и конфигурацию между инстансами, — но даёт больше контроля, чем сторонний CDN, если требования по хранению данных или задержке до бэкенда жёсткие. Про то, почему близкий по карте сервер не всегда самый быстрый по факту, и на что смотреть при выборе локации, — в статье про пиринг и транзит простыми словами: у хостера с хорошим пирингом сервер в соседней стране иногда даёт RTT меньше, чем сервер "рядом" по карте, но с посредственной связностью.
Важно не путать эту меру с оптимизациями рукопожатия выше — они дополняют друг друга, а не заменяют. Даже с ближайшим к клиенту edge-узлом TLS 1.3 и resumption всё равно экономят round trip'ы; просто база (RTT), которую они умножают, становится меньше, и суммарный эффект от всех мер вместе заметнее, чем от каждой по отдельности.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли вообще убрать TCP-рукопожатие и его RTT?
Полностью — нет, TCP требует установления соединения прежде, чем пойдут данные. TCP Fast Open позволяет отправить данные приложения раньше, вместе с SYN, для клиентов, которые уже подключались к серверу, но само трёхэтапное согласование остаётся.
TLS 1.3 обязательно даёт прирост в один RTT на моём сервере?
Да, если клиент и сервер оба его используют и это первое подключение — с session resumption экономия ещё больше, вплоть до нуля дополнительных round trip'ов на TLS при 0-RTT. Если клиент подключается через прокси или библиотеку, которая всё ещё форсирует TLS 1.2, выигрыша не будет независимо от настроек сервера.
Стоит ли включать 0-RTT везде, раз это самый быстрый вариант?
Нет — 0-RTT уязвим к replay-атакам на неидемпотентных запросах. Разумный подход — включить его выборочно для статики и безопасных GET-запросов, оставив формы и платёжные операции на обычном пути без 0-RTT.
Если у меня один сервер и нет бюджета на CDN, есть смысл вообще что-то делать с рукопожатием?
Да — TLS 1.3, resumption, TCP Fast Open и OCSP stapling ничего не стоят с точки зрения инфраструктуры, только конфигурация, и вместе снимают до пары round trip'ов даже без единого дополнительного сервера. Географическое распределение — следующий шаг, когда этого перестаёт хватать.
Как понять, что именно рукопожатие, а не что-то ещё, тормозит загрузку у дальних пользователей?
Смотреть на вкладку Network в браузере: там отдельно видны фазы DNS, Connecting, TLS Handshake и только потом Waiting (TTFB) и Content Download. Если TLS Handshake занимает заметную долю общего времени именно у удалённых пользователей — это прямое подтверждение описанной здесь арифметики RTT.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →