QUIC-миграция: чему мы научились за полгода в проде
Сюрпризы с MTU, неожиданно высокий CPU на серверной стороне и как перестали бояться handshake'а 0-RTT.
Зачем вообще мигрировали
Наш initial набор транспортов был TLS + WebSocket. Этого хватает, пока сеть «дружелюбная». Но как только начинается тротлинг long-lived TCP connection'ов, у обоих транспортов начинаются одинаковые проблемы — латентность растёт, RTT прыгает.
QUIC даёт три вещи, которые нам важны:
- 0-RTT handshake — переподключение после разрыва занимает миллисекунды, а не секунды.
- Multipath-ready (Apple/Google уже делают реальные мульти-путь эксперименты).
- Меньше feature-print в DPI — он все ещё развивающийся, сигнатуры не так отточены.
Первая неожиданность — MTU
В первый же день мы увидели, что на мобильных сетях (особенно LTE) QUIC иногда фрагментируется. DPI некоторых операторов дропает фрагменты UDP — handshake обрывается. Решили принудительным PMTUd + запасным frame-size 1200 bytes.
Вторая — CPU
QUIC требует в разы больше AEAD операций чем TLS (каждый packet отдельно шифруется). На серверах мы увидели +40% CPU по сравнению с TLS-connections. Спасло:
- Batched packet processing (libquic позволяет).
- Хардовая поддержка AES-NI на всех наших x86-нодах.
- Для мобильных клиентов на старых ARM — fallback на TLS.
Третья — 0-RTT paranoia
Поначалу мы боялись включать 0-RTT: реплей атаки, session ticket leak. Оказалось, для VPN-use-case это не страшно — мы уже аутентифицируем пользователя через отдельный handshake после connection'а, и даже реплей 0-RTT пакета не даст атакующему ничего полезного. Включили — получили −180ms на перекoннект.
Итог
QUIC стал транспортом по умолчанию для мобильных. TLS остаётся fallback'ом для десктопа в жёстких сетях, где UDP режется на корню.