Files
utun2/doc/etcp_multipath_plan.txt
T

295 lines
31 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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