Суть Прочитай реальную логику stream-ID QUIC, трассу пакетов, цикл отправки и раскладку фрейма, затем предскажи поведение и выбери фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Проблемы QUIC диагностируются в логике stream-ID, трассах пакетов и пути отправки. Прочитай каждый сниппет, предскажи поведение и выбери фикс, к которому senior-инженер тянется первым.
Цель
Потренируй чтение QUIC на уровне провода и кода: декодируй stream ID, отличи здоровое рукопожатие от патологического в трассе, найди узкое место по syscall’ам на быстром линке и рассуди о нумерации пакетов при ретрансмите.
Сниппет 1 — классификация stream-ID
// Классифицировать QUIC stream ID по инициатору и направленности.func classify(id uint64) (initiator string, bidi bool) { if id&0x1 == 0 { initiator = "client" } else { initiator = "server" } bidi = id&0x2 == 0 return}// Вызывающий передаёт stream ID, увиденный на проводе:i, b := classify(3)
Викторина
Completed
Для stream ID 3 что вернёт classify и какая реальная сущность HTTP/3 использует этот класс stream'а?
Heads-up Client-initiated bidirectional stream'ы — это 0, 4, 8, ... (младшие два бита 00). У ID 3 оба младших бита установлены, значит он server-initiated и unidirectional — не request-stream.
Heads-up Бит 1 у ID 3 установлен, поэтому stream unidirectional, а не bidirectional. Bidi-серверные stream'ы оканчиваются на 01 (1, 5, 9, ...), а не на 11.
Heads-up Нечётные ID полностью валидны: они server-initiated. Два младших бита кодируют инициатора и направленность, поэтому каждое значение 0..N маппится на реальный класс stream'а.
Клиент переотправляет те же байты CRYPTO ClientHello на t=0.045 как новый пакет (pkt=1), до прихода Ack сервера на t=0.051. Что происходит и дефект ли это?
Heads-up Ретрансмит — это pkt=1, свежий packet number — QUIC никогда не переиспользует packet number при ретрансмите. Именно это убирает Karn-неоднозначность TCP; переиспользование 0 было бы багом, но его тут нет.
Heads-up Сервер отвечает на t=0.051 пакетом Initial+Ack. Ретрансмит был единичной ложной PTO-пробой за ~6 мс до прихода ответа — здорово для пути в 50 мс round-trip, а не отказ.
Heads-up Initial и Handshake by design имеют свой packet-number space, поэтому потеря в одном не ужимает окно другого. Трасса показывает корректную пер-space-нумерацию.
Сниппет 3 — цикл отправки
// Горячий путь: сбросить очередь QUIC-датаграмм в сокет.for _, dgram := range batch { // batch может держать 40+ датаграмм _, err := conn.WriteTo(dgram, peer) // один syscall sendmsg на датаграмму if err != nil { return err }}
Викторина
Completed
При 1 Gbps с датаграммами по 1500 байт этот цикл упирает ядро CPU и goodput рушится, а тот же код нормально работает на мобильном линке 10 Mbps. В чём узкое место и фикс с наибольшим рычагом?
Heads-up Доминирующая цена тут — пер-датаграммный syscall, а не аллокация. Масштабируемый фикс — батчинг UDP GSO, схлопывающий ~40 syscall'ов в горстку; пулинг буферов — второстепенная подстройка.
Heads-up peer уже loop-invariant-локальная; повторная передача ничего не стоит. Настоящее узкое место — один syscall на датаграмму, который решается GSO segmentation offload.
Heads-up Добавление ядер откладывает насыщение, но пер-байтовая цена syscall'ов масштабируется с трафиком, поэтому спираль возвращается под нагрузкой. GSO убирает сами syscall'ы — устойчивый фикс.
Сниппет 4 — burst фреймов при миграции
# Взгляд сервера после прихода пакета клиента с НОВОГО source-адреса:recv src=203.0.113.50:55555 dcid=a1b2 bytes=1200send PATH_CHALLENGE token=0x9f3c... # 64-байтный random# у сервера всё ещё стоят в очереди данные stream'а для клиентаsend Stream(0, off=8192, 1500B) # данные приложения в очереди
Викторина
Completed
Сервер получил 1200 байт с нового, невалидированного адреса и хочет сбросить PATH_CHALLENGE плюс 1500 байт стоящих в очереди данных stream'а. Что разрешает anti-amplification-лимит и почему это важно?
Heads-up Установленный Connection ID, приходящий с нового, невалидированного адреса, — это ровно случай миграции/спуфинга, который и охраняет лимит 3x. Сервер обязан дросселировать до 3x полученных байт, пока PATH_RESPONSE не подтвердит путь.
Heads-up PATH_CHALLENGE мал и легко влезает в бюджет 3x; так и бутстрапится валидация. Лимит ограничивает объём, а не запрещает сам challenge.
Heads-up И padding Initial в 1200 байт, и лимит отправки 3x существуют, чтобы ограничить amplification; лимит 3x применяется к любой отправке на невалидированный адрес, включая миграцию посреди соединения.
Итог
QUIC читается на проводе: два младших бита stream ID декодируют инициатора и направленность; трасса рукопожатия показывает ложные PTO-пробы как новые packet number (никогда не переиспользуются, поэтому Karn-неоднозначности нет); пер-датаграммный цикл отправки вскрывает узкое место по syscall’ам, которое схлопывает UDP GSO; а burst миграции ограничен бюджетом 3x anti-amplification, пока PATH_RESPONSE не валидирует новый адрес. Диагностируй по трассе и горячему пути, затем применяй структурный фикс — GSO, а не больше ядер; идемпотентность, а не отключение фич.