295 lines
31 KiB
Plaintext
295 lines
31 KiB
Plaintext
ETCP: независимая обратная связь каналов и доставка при отказах
|
||
Проверка архитектуры и план следующего изменения, 23.10.2026
|
||
|
||
Статус
|
||
|
||
Бюджет очереди BBR уже реализован в fix/etcp-multipath-recovery: 20мс в PROBE_BW,
|
||
все резервы входят в предел max(4*MSS, BDP + BW*20мс). Это мягкая цель модели;
|
||
STARTUP/DRAIN, сериализация и обратный трафик не дают гарантии физической очереди <=20мс.
|
||
В штатном тесте equal получено 14.464/16Мбит/с, bandwidth — 9.060/10Мбит/с;
|
||
очередь медленного канала в bandwidth сократилась примерно с 47 до 21мс.
|
||
Изменения протокола и учёта попыток ниже ещё не реализованы.
|
||
По уточнению пользователя расчётный RTO ограничен снизу 10мс, сверху 1с.
|
||
Текущая общая формула 2*(RTT+jitter) сохранена; нижняя граница уже исправлена.
|
||
test_etcp_windows: 17 сценариев PASS, включая семь проверок RTO на управляемых
|
||
и обычных часах (нижняя граница, RTT20/200мс, jitter, верхняя граница).
|
||
Логи: /tmp/etcp-rto-windows.log, /tmp/etcp-rto-windows-real.log.
|
||
|
||
Что подтверждено кодом и диагностикой
|
||
|
||
1. ACK выбирает канал независимо от DATA: select_link(conn, 0), inflight/RR.
|
||
Одна общая ack_q смешивает подтверждения DATA разных каналов. Поэтому интервал
|
||
delivery sample включает фактический обратный путь ACK, а rtt_us берётся
|
||
из отдельного timestamp-обмена last_link. Для RTT 20/200мс измерены возвраты
|
||
быстрых DATA через медленный канал за >=112мс при RTT модели около 24мс.
|
||
Это несовместимые исходные данные для BDP, pacing и бюджета очереди.
|
||
|
||
2. Общий ACK.till вызывает тот же etcp_ack_recv(), что и явный ACK элемента.
|
||
Даже если явные ACK возвращать своим путём, till подтверждает DATA других
|
||
каналов и обновляет их BBR в чужой момент времени. Перенаправить ACK недостаточно.
|
||
|
||
3. INFLIGHT_PACKET хранит только последнюю попытку: last_link, last_timestamp,
|
||
delivery snapshots. При повторе они заменяются, ACK содержит только общий seq.
|
||
Поздний ACK старой копии может быть приписан новой попытке и другому каналу.
|
||
Неоднозначны и RTT, и скорость, и выбор окна, которое освобождается.
|
||
|
||
4. В начальном pacing есть отдельный дефект. bbr_update_min_rtt() устанавливает
|
||
has_seen_rtt до bbr_set_pacing_rate(), поэтому предусмотренная инициализация
|
||
на первом RTT пропускается. Её аргумент также ошибочен: передаётся B/s вместо
|
||
размера окна в байтах. Затем перевод pacing в целые Kbit/s превращает малую
|
||
ненулевую скорость в 0, который shaper трактует как unlimited. Обновление
|
||
скорости само по себе не пересчитывает уже поставленный shaper-таймер.
|
||
|
||
5. RTO общий, основан на RTT/jitter соединения. Очередь wait_ack просматривается
|
||
как FIFO с единым timeout; индивидуальные дедлайны с такой остановкой обхода
|
||
несовместимы. Повтор может снова уйти в зависший канал. Приоритет следующего
|
||
канала сейчас не реализован: select_link не получает last_link пакета,
|
||
send_hist только записывается, а общий RR относится ко всему соединению.
|
||
Воспроизведение на настоящих очередях/RTO при двух готовых UP-каналах дало
|
||
seq1 A->A и seq2 B->B. Тест late_ack_and_migration этого не покрывает:
|
||
он явно отключает A перед передачей через B.
|
||
|
||
6. Поток собирается по общему seq: при RTT 20/200мс разность времени доставки
|
||
неизбежно создаёт переупорядочивание. Исправление ACK не устраняет ожидание
|
||
пропущенного фрагмента. В диагностике при высокой скорости накоплено порядка
|
||
0.17–0.29MB. Память сборки и пауза приложения требуют отдельных проверок.
|
||
|
||
7. stcp_link_send() подтверждает постановку в tx_queue. Это не означает прогресс
|
||
записи в TCP. Для stalled TCP одной проверки link_status/cwnd/pacer недостаточно.
|
||
|
||
8. В приёмнике ACK ставится в ack_q до выделения payload/ETCP_FRAGMENT и вставки
|
||
в recv_q. При последующем OOM или отказе вставки уже подготовленный ACK может
|
||
подтвердить несохранённые DATA. В новом контракте положительный ACK допустим
|
||
только после сохранения фрагмента либо проверки, что он уже сохранён/доставлен.
|
||
Инъекция отказа data_pool подтвердила: ACK_queued=1, DATA_stored=0,
|
||
delivered_till=0. Это дефект надёжности, а не только неточность статистики BBR.
|
||
|
||
Экспериментальная проверка причин
|
||
|
||
Диагностический бинарник /tmp/etcp-rtt-experiment включает штатный multipath-стенд
|
||
и настоящее ядро. Подмена происходит через linker --wrap, исходники протокола
|
||
не менялись. Используются реальные шифрование, UDP, dummynet и проверка порядка,
|
||
содержимого, полного завершения доставки, таймеров и счётчика выделений.
|
||
|
||
Контроль RTT 20/200мс, 8+8Мбит/с, с начальным pacing от окна/RTT:
|
||
- после завершения STARTUP полезная скорость 8.055/16Мбит/с, быстрый канал 0.633;
|
||
- свой путь ACK, без обновления BBR через till, фактический RTT попытки:
|
||
8.831 и 8.945/16Мбит/с; длинный канал недогружен;
|
||
- дополнительно исключены неоднозначные повторы из оценки скорости:
|
||
14.127 и 14.559/16Мбит/с, проверки установившегося режима прошли.
|
||
|
||
С последним вариантом дополнительно проверены RTT20/200мс при разных BWlim:
|
||
- 2+8Мбит/с (fast-distant): полезная скорость 8.735/10Мбит/с;
|
||
- 8+2Мбит/с (slow-distant): полезная скорость 8.979/10Мбит/с.
|
||
Проверки каждого канала, суммарной скорости, очереди и полного drain прошли.
|
||
В агрегате максимум очереди составил соответственно 24.9/26.4мс и 22.6/27.6мс.
|
||
Для равной BW выполнено два успешных повтора, для разных BW — по одному;
|
||
этого достаточно для выбора направления, но не для заявления о полной устойчивости.
|
||
Контроль последнего варианта по штатному rtt с фиксированным прогревом 3с:
|
||
14.312/16Мбит/с, очереди в агрегате 23.4/24.9мс, failures=0.
|
||
Это один успешный прогон диагностического прототипа, а не всего рабочего ядра.
|
||
|
||
Выборочные логи показывают преждевременный выход длинного канала из STARTUP
|
||
с заниженной оценкой BW. До фильтрации в модель попадал sample от повторной
|
||
передачи: RTT отвергнут, но delivery interval и скорость использованы.
|
||
Это подтверждает необходимость идентификации попытки; фильтр по send_count
|
||
годится для диагностики, но теряет достоверные измерения повторов и не заменяет ID.
|
||
|
||
Успешный единичный запуск без полного разделения учёта давал 14.45Мбит/с,
|
||
но повторные проверки выявили нестабильность. Он не является критерием готовности.
|
||
Отдельный прогрев до выхода из STARTUP использован только для диагностики:
|
||
штатные требования к разгону, очереди и утилизации не ослаблялись.
|
||
Фильтрация till в последнем прототипе сохраняет кумулятивную границу rx_ack_till;
|
||
её удаление из протокола сломало бы ограничение окна приёмника.
|
||
Прототип не проверяет новый wire-формат, владение попытками или отказоустойчивость.
|
||
|
||
Предлагаемый контракт
|
||
|
||
Разделить логическую доставку фрагмента и физическую попытку передачи.
|
||
|
||
- Логический объект: общий seq, payload, признак доставки. Он отвечает за порядок
|
||
потока, unacked_bytes приложения и очередь повторной доставки.
|
||
Подтверждение фрагмента означает принятие его приёмником в надёжную очередь;
|
||
сборка и выдача приложению идут своим порядком. Переход в очередь повтора
|
||
идемпотентен: таймеры нескольких копий не создают несколько записей одного seq.
|
||
Первый положительный ACK или till, покрывающий этот seq, удаляет ожидающий повтор.
|
||
- Попытка: уникальный uint64 attempt_id, ссылка на канал, seq, размер, время
|
||
отправки, delivery snapshots и состояние учёта окна. Последующая отправка
|
||
создаёт новую попытку; предыдущие снимки остаются неизменными.
|
||
Глобальные очереди содержат логические фрагменты: готовые к отправке и ожидающие
|
||
доставки. Отдельные записи попыток принадлежат очередям линков; один ll_entry
|
||
не включается в две очереди. Попытка хранит seq, а не висячую ссылку на уже
|
||
доставленный логический объект.
|
||
- attempt_id монотонен в пределах согласованной ETCP-сессии и не переиспользуется
|
||
при удалении/создании линка. Перезапуск счётчика допустим только с новой эпохой,
|
||
когда DATA/ACK старой сессии уже отклоняются. Переполнение закрывает сессию.
|
||
- DATA содержит seq + attempt_id. ACK элемента сохраняет оба идентификатора:
|
||
seq:u32 + attempt_id:u64 + receiver_delay_us:u32. seq позволяет подтвердить
|
||
логическую доставку даже после удаления метаданных старой попытки, без отдельной
|
||
истории попыток. Для известного attempt_id проверять соответствие seq.
|
||
Задержка насыщается, UINT32_MAX означает недействительный RTT sample.
|
||
Пересчитать DATA MSS, MTU и ACK batching; не использовать modulo-wrap задержки.
|
||
- ACK означает принятие DATA в надёжную очередь приёмника, не приход в сокет.
|
||
Сначала проверить границы, сохранить DATA/вставить в recv_q либо найти дубликат,
|
||
затем поставить ACK и лишь потом вызывать сборку/пользовательские callbacks.
|
||
Это сохраняет порядок при reentrant close/reset. При OOM до сохранения DATA
|
||
положительного ACK нет.
|
||
При OOM самого ACK данные остаются сохранёнными; следующая копия получает ACK.
|
||
- Подтверждение попытки возвращается через тот же ETCP_LINK, где она принята.
|
||
Очередь ACK принадлежит линку; piggyback содержит только ACK этого линка.
|
||
Дубликат логического DATA с новым attempt_id тоже подтверждается.
|
||
- BBR обновляется по реально подтверждённой попытке своего канала. RTT — elapsed
|
||
этой попытки минус допустимый receiver delay. KA RTT остаётся метрикой живости.
|
||
ACK другого канала не формирует sample. ACK/control по-прежнему обходят только
|
||
локальный DATA-pacer; общая физическая очередь коммуникации сохраняется.
|
||
- Общий till остаётся подтверждением логической доставки и места приёмника.
|
||
Он может прийти по любому живому пути, освобождает payload/очередь повторов,
|
||
но не придумывает доставку попытки, RTT/BW или освобождение чужого окна.
|
||
Продвижение till должно пробуждать отправку отдельного ACK с count=0 через
|
||
готовый рабочий канал, даже без DATA и без ACK-элементов на нём. Объединять
|
||
продвижения и ограничивать частоту, не создавать ответ на каждый ACK.
|
||
Потеря последнего till не должна оставлять полное окно приёмника навсегда:
|
||
ожидание места имеет таймер запроса актуального till через рабочий путь,
|
||
получатель отвечает текущей границей. Таймер принадлежит соединению, активен
|
||
только при ожидающих DATA и полном окне, отменяется при прогрессе/close/reset.
|
||
Session-wrapped запрос/ответ не меняет модели BBR; частота ограничена.
|
||
- Доставка приложения учитывается один раз. Физические попытки учитываются
|
||
каждая в своей модели; их суммарная доставка может включать дубликаты DATA.
|
||
- ACK или объявление потери освобождает окно попытки ровно один раз. Поздний ACK
|
||
не дебетует его повторно. После доставки payload можно освободить, метаданные
|
||
других попыток живут до ACK/потери/закрытия, без удержания лишнего payload.
|
||
При потере запись попытки можно удалить: поздний ACK по seq ещё подтверждает
|
||
логическую доставку, но неизвестный attempt_id не обновляет BBR или окно.
|
||
Отдельная история/tombstone для уже потерянных попыток не нужна.
|
||
- Удаление линка/сессии отменяет его таймеры, удаляет ACK и попытки, освобождает
|
||
учёт ровно один раз. Не хранить указатель на уже освобождённый ETCP_LINK.
|
||
|
||
Правило повторной передачи, независимо от определения DOWN
|
||
|
||
Повтор использует историю именно своего логического фрагмента, не last_rr_link
|
||
соединения и не позиции каналов в send_hist. При готовых A/B/C порядок попыток
|
||
одного фрагмента — A->B->C->A; отправки других фрагментов и ACK его не меняют.
|
||
Начать обход после last_link фрагмента, выбрать первый подходящий другой линк.
|
||
Пропускать DOWN, неготовые, не вмещающие DATA и заблокированные pacer/транспортом.
|
||
Если готовых альтернатив нет, разрешён готовый прежний линк. Если не готов никто,
|
||
фрагмент ждёт штатного события ACK/pacer/transport-ready, без busy loop и нового RTO
|
||
для ещё не отправленной копии. Временно занятый следующий линк не блокирует поиск
|
||
остальных; отправлять без cwnd/pacing или ждать неограниченно именно его нельзя.
|
||
|
||
Для первоначальной отправки остаётся обычный выбор по загрузке. Retry — отдельный
|
||
режим той же функции выбора, с едиными проверками готовности, без второго набора
|
||
условий cwnd/pacing. Burst не навязывает DATA свой линк: при конфликте измерение
|
||
идёт с FILLER, повтор отправляется по своему правилу. Старый линк не объявляется
|
||
DOWN только потому, что копия отправлена другим путём.
|
||
|
||
last_link логического фрагмента — только заимствованный курсор выбора. При удалении
|
||
линка очистить такие курсоры до free; при NULL начать с первого рабочего линка.
|
||
Переиспользуемый uint8 local_link_id и порядковый номер списка не являются
|
||
идентичностью попытки. send_count не должен определять retry через оборачиваемый
|
||
uint8: хранить факт состоявшейся отправки отдельно от статистики.
|
||
Курсор меняется после принятия новой попытки транспортом. Ошибка подготовки/OOM
|
||
или отказ отправки не должны создавать фиктивный flight и ждать сетевого RTO.
|
||
|
||
Повтор и объявление потери — разные события. При ранней отправке второй копии
|
||
первая ещё может быть в пути: её окно и метаданные остаются до собственного ACK,
|
||
срока потери или закрытия. Сам по себе повтор не является loss sample для BBR.
|
||
Дополнительные копии ограничены общим бюджетом памяти и окнами их каналов.
|
||
Ротация сокращает число попыток через зависший путь, но не сокращает ожидание
|
||
первого таймера. Нельзя обещать паузу <=100мс только сменой канала при RTO>100мс.
|
||
Срок ранней повторной отправки и допустимую цену дубликатов проверять отдельно;
|
||
сохранять физически достижимые требования конкретных профилей, не обещать один
|
||
предел задержки для произвольных RTT, нагрузки и потерь.
|
||
|
||
Почему не общий самый быстрый обратный канал
|
||
|
||
Такой выбор не согласует интервалы и не устраняет неоднозначность повторов.
|
||
Понадобится учитывать смену обратного маршрута в каждой модели, а отказ общей
|
||
обратной дороги затронет остальные прямые пути. Собственный ACK сохраняет одну
|
||
модель на канал и независимость отказов. Общая логическая граница при этом
|
||
может распространяться любым рабочим каналом.
|
||
|
||
Последовательность реализации и критерии завершения
|
||
|
||
1. Начальный pacing как отдельное исправление без смены wire-формата.
|
||
Инициализировать от настоящего начального окна и первого валидного RTT.
|
||
Сохранить точные B/s до shaper, отделить unlimited от округления. При смене
|
||
ставки пересчитать оставшийся pacing-долг и таймер, не создавая новый burst.
|
||
Проверки: короткий служебный DATA -> пауза -> bulk, RTT20/200мс, скорость ниже
|
||
1Kbit/s, увеличение/уменьшение ставки с активным таймером, полный STARTUP.
|
||
|
||
2. Цельный переход на логические фрагменты и идентифицируемые попытки.
|
||
Добавить индекс attempt_id в очереди попыток каждого линка и отдельную ACK-очередь.
|
||
Общий индекс попыток не нужен: ACK ищется на принявшем его линке.
|
||
Разделить обработчики logical till и ACK попытки; одновременно обновить DATA,
|
||
ACK, декодеры, MTU/MSS, lifecycle и .h. В этом же изменении нужны очередь/таймер
|
||
попыток каждого линка и собственный RTT/RTO: иначе старый глобальный FIFO
|
||
перестаёт соответствовать новому владению. FIFO допустима при одном текущем
|
||
RTO для всех попыток линка; пересчитывать таймер при его смене.
|
||
До первого DATA ACK использовать RTT того же KA-пути как начальную оценку
|
||
либо консервативный начальный RTO. Расчёт по собственным RTT/jitter с запасом
|
||
ограничить диапазоном 10мс..1с; 10мс — нижняя граница, не верхняя.
|
||
Добавить ротацию retry сразу, пока старый канал ещё UP; не откладывать до DOWN.
|
||
Не оставлять промежуточный формат,
|
||
в котором новый ACK продолжает обращаться только к last_link.
|
||
Проверки: первая копия A -> повтор B -> поздний ACK A; обратный порядок ACK;
|
||
ACK дубликат; чужой/неизвестный ID; till с другого пути; link recreation;
|
||
потеря ACK при уже доставленном payload; close/reset с активными попытками;
|
||
отказ принятия пакета транспортом без продвижения курсора и фиктивного flight;
|
||
OOM сохранения DATA, вставки в recv_q и создания ACK. Проверить передачу till
|
||
без DATA по другому пути и восстановление после потери последнего till.
|
||
Для retry: A->B при обоих UP и меньшем inflight у A; A->B->C->A;
|
||
посторонние DATA/ACK между попытками; пропуск заблокированных каналов;
|
||
один доступный путь; все временно заняты; burst старого пути; удаление курсора.
|
||
После этого повторить всю матрицу скоростей. Выбор пути первоначальных DATA менять только
|
||
при сохраняющейся недогрузке: эксперименты уже показывают полную утилизацию
|
||
без смены алгоритма выбора DATA, поэтому дополнительная эвристика пока лишняя.
|
||
|
||
3. Быстрое восстановление и здоровье каналов.
|
||
Собственный RTT/RTO и ротация уже обязательны в этапе 2. Проверить срок первой
|
||
повторной отправки, отдельно от объявления потери/снятия окна старой попытки.
|
||
При потере ограничить новые DATA на проблемном пути, пробовать восстановление
|
||
ограниченными probe, повторять недоставленные DATA через рабочие каналы.
|
||
Не отправлять следующий bulk вслепую в тот же застрявший путь.
|
||
Проверки: DROP/HOLD отдельно по направлениям и вместе, несколько застрявших
|
||
каналов, возврат старых копий после HOLD, повторный отказ, смена RTT/BW на ходу.
|
||
|
||
4. Учёт нижележащей очереди STCP.
|
||
Ограниченный tx_queue и уведомление о реальном прогрессе/готовности должны
|
||
участвовать в выборе пути. Определить время начала попытки и учёт данных,
|
||
принятых STCP, но ещё не записанных в сокет; не смешивать их с wire delivery.
|
||
Проверки: полностью остановленная TCP-запись рядом с рабочим UDP/STCP,
|
||
последующий выпуск очереди, закрытие соединения и отсутствие роста памяти.
|
||
|
||
Общие проверки
|
||
|
||
Матрица: equal, bandwidth, rtt, fast-distant, slow-distant, three, bidirectional.
|
||
Отдельно измерять время STARTUP, steady goodput, очередь каждого направления,
|
||
переупорядочивание, повторы и паузу приложения. Отчёт содержит данные каждого
|
||
повтора и минимум, а не только лучший результат. Не выполнять сетевые замеры
|
||
параллельно. Сохранить исходные пороги штатного test_etcp_multipath.
|
||
|
||
Добавить асимметричные прямой/обратный пути, ACK compression, поздние копии,
|
||
потери ACK и исчерпание лимитов памяти. Проверять счётчики по очередям, однократный
|
||
учёт подтверждения попытки и одну логическую доставку; утечки/UAF — ASAN/UBSan.
|
||
Логи: session/seq/attempt_id, DATA link, ACK link, send/receive/ack times,
|
||
receiver delay, причина retire, inflight до/после и вход/выход BBR.
|
||
|
||
Проверено для текущего бюджета: BBR18/18, windows16 сценариев, шесть транспортных
|
||
ASAN/UBSan тестов с detect_leaks=1 PASS. Общий check.sh: 180/183; FAIL
|
||
test_nat_detection_faults, test_nat_detection, test_encrypted_attachment.
|
||
Эти падения внесены отдельной задачей; связь с BBR не установлена.
|
||
|
||
Логи опытов: /tmp/etcp-rtt-ab-baseline.log,
|
||
/tmp/etcp-rtt-settled-control.log, /tmp/etcp-rtt-settled-candidate-diagnostic.log,
|
||
/tmp/etcp-rtt-settled-karn.log, /tmp/etcp-rtt-settled-karn-repeat.log,
|
||
/tmp/etcp-rtt-settled-fast-distant.log, /tmp/etcp-rtt-settled-slow-distant.log,
|
||
/tmp/etcp-rtt-original-karn.log.
|
||
Проверки отсутствующей ротации и ложного ACK при OOM: /tmp/etcp-retry-review.c,
|
||
/tmp/etcp-retry-review.mk, /tmp/etcp-retry-review.log
|
||
(exit=1: retry_rotation=2, false_ack=1, total=3; учёт/освобождение ресурсов проверены).
|
||
Сборка диагностического бинарника:
|
||
make -C tests -f Makefile -f /tmp/etcp-rtt-experiment.mk /tmp/etcp-rtt-experiment
|
||
Основной повтор:
|
||
RTT_SETTLED=1 RTT_SEED=1 RTT_SAME_ACK=1 RTT_NO_CUM=1 RTT_ACTUAL=1 RTT_KARN_RATE=1 \
|
||
/tmp/etcp-rtt-experiment rtt
|