127 lines
19 KiB
Plaintext
127 lines
19 KiB
Plaintext
Формат кодограммы:
|
||
------------------
|
||
пакеты идут по следующему пути:
|
||
пакет кем-то помещается в входную очередь (ETCP input_queue).
|
||
Когда load balancer готов отправить следующий пакет он делает вызов etcp_request_pkt в etcp.c/h и etcp либо формирует следующую кодограмму или встаёт в состояние async ожидания (возвращая null):
|
||
- либо wait ETCP input_queue (если нечего передавать)
|
||
- либо wait SACK + wait timeout (если sack очередь полная и нет необслуженных запросов ретрансмиссий). после истечения wait timeout он помечает используя round-robin очередной пакет как "нужна ретрансмиссия".
|
||
из async ожидания он может вызвать etcp_loadbalancer input_from_etcp и передать пакет на отправку.
|
||
как формируется пакет:
|
||
1. берем пакет из списка неподтвержденных пакетов ожидающих отправку
|
||
2. если есть место в rwin (объём inflight данных) то берем очередной пакет из ETCP input_queue и его отправляем
|
||
если пакет найден:
|
||
1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало
|
||
2. в конец добавляем секцию 0x00 с данными пакета
|
||
при отправке пакета помечаем в inflight списке с какого интерфейса он отправлен
|
||
Функция прикрепления опциональных секций:
|
||
- запрашивает выбор канала передачи.
|
||
- добавляет накопившиеся ACK
|
||
При приёме пакета (от etcp_connections):
|
||
- последовательно сканируем секции и отдаём их на обработку нужным обработчикам:
|
||
- ack: помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также и ставим флаг в конце запустить etcp если он в wait timeout, т.е. wait_timeout!=NULL
|
||
- timestamp: обновляем last RTT, пересчитываем RTT10,RTT100 (плавающим окном за последние 10/100 пакетов), и jitter=max(last 10)-min(last 10 packets)
|
||
- [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем).
|
||
после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть.
|
||
|
||
пересканирование inflight:
|
||
- вызывается по таймеру, таймер заводится при добавлении пакета в пустой inflight.
|
||
- если время последней отправки > RTT10*k1+jitter*k2 (коэффициенты вынести в настройки utun instance) то пакет заново отправляется
|
||
и заводим таймер отправки следующей ретрансмиссии (список отсортирован по времени)
|
||
- если встречаем успешный ack, запоминаем это и его timestamp (назовем lp_ts).
|
||
- если после этого далее в очереди находим пакет который не имеет переповторов и timestamp отправки раньше чем [ИСПРАВЛЕНО: фраза обрезана в оригинале; предположительно, "раньше чем lp_ts" на основе контекста; если нет, пометить как неполное]
|
||
|
||
inflight - два списка (ll_queue):
|
||
- список с неподтвержденными пакетами ожидающими ack (при запросе ретрансмиссии пакет перемещается в ожидающие отправку)
|
||
- список с неподтвержденными пакетами ожидающими отправку (при отправке возвращается в ожидающие ack)
|
||
доп. свойства пакетов:
|
||
- указатель на ETC_LINK последней отправки
|
||
- timestamp последней отправки
|
||
- число отправок
|
||
- число запросов переповтора
|
||
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
|
||
optimal_inflight расчитывается из RTT и bandwidth линка.
|
||
|
||
bandwidth по каждому линку адаптивно подстраиваем следующим алгоритмом:
|
||
периодически инициируем burst_transmission (напоминаем ID пакета на которой она началась).
|
||
после передаём пачку пакетов только по этому линку со следующими таймингами:
|
||
<pkt1> <delay 4x> <pkt2><pkt3><pkt4><pkt5> - т.е. чуть задерживаем канал а потом выплёвываем пачку пакетов.
|
||
и добавляем опцию [meas_ts][n] - где n - номер пакета в пачке
|
||
приёмная сторона видя эту секцию должна отправить секцию [meas_resp] по всем пакетам, которые содержат локальный RECV_Timestamp фиксирующий время приёма каждого пакета с [meas_ts].
|
||
приняв эти response анализируем по разнице между pkt1-pkt2 не забита ли очередь отправки, а по дельте pkt2-3-4-5 оцениваем bandwidth.
|
||
дальше по алгоритму (который потом оттюним) корректируем transmitter speed limit.
|
||
|
||
|
||
**** Формат кодограмм для etcp.c/h ****
|
||
Каждая кодограмма состоит из обязательного заголовка и опциональных секций:
|
||
1. Обязательный заголовок всего пакета (2 байта) добавляется при передаче, есть во всех пакетах:
|
||
[Timestamp high][Timestamp low][flags]
|
||
- Timestamp: время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется
|
||
- flags: bit0 = up/down (recv_keepalive)
|
||
|
||
шифруется строго ВСЁ включая ВСЕ заголовки (кроме INIT+publickey). timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ
|
||
2. Опциональные секции (одна или несколько) добавляются в etcp.c:
|
||
а) Подтверждения (ACK) - заголовок 0x01:
|
||
[0x01][count][(id_hi,id_lo,ts_hi,ts_lo)×count][last_delivered_hi,lo][last_rx_hi,lo]
|
||
- count: количество пар ID+timestamp (1-32)
|
||
- last_delivered_id: последний ID, доставленный получателю
|
||
- last_rx_id: последний полученный ID (для синхронизации прогресса)
|
||
в) channel timestamp (добавляется после выбора канала передачи):
|
||
[0x0F] [RET_Timestamp high][RET_Timestamp low] [RECV_Timestamp high][RECV_Timestamp low]
|
||
- RET_Timestamp: это timestamp последнего принятого пакета по этому каналу ETCP_LINK плюс разница времени между принятием этого пакета и текущим временем. т.е. приёмная сторона по этому timestamp (и зная своё время) может просчитать время в пути туда + обратно
|
||
- RECV_Timestamp: это timestamp времени приёма последнего принятого пакета (т.е. отправляем время "когда мы получили этот пакет по локальном урмени") - по нему другая сторона сможет просчитать относительное время задержки в одну сторону (время туда без обратно).
|
||
если последний пакет по этому каналу принят сильно давно (прошло более 30000 единиц времени) то channel timestamp не добавляем. [СПОРНО: порог 30000 (3 сек) arbitrary; может потребовать настройки или обоснования; также риск clock skew между нодами]
|
||
3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя):
|
||
[0x00] [ID 4 байта] [данные...] - Данные пакета
|
||
- ID: 16-битный циклический номер пакета. [СПОРНО: 16-bit может привести к быстрому overflow в long-lived сессиях; рассмотреть 32-bit]
|
||
Пакет может содержать несколько опциональных секций (например, ACK + данные). [СПОРНО: нет явных length полей для секций; parsing implicit, риск ошибок при variable длине; добавить length?]
|
||
цель протокола:
|
||
1. контролировать состояние очередей (не накапливать данные где этого можно избежать), анализировать RTT и inflight - динамически подстраивать размер окна и ретрансмиссии, утилизируя сеть насколько это возможно но не допуская наполнения буферов в сети (что приводит к бестолковому увеличению RTT)
|
||
Установка подключения для канала (реализуется в etcp_connections.c/h):
|
||
каждое ETCP подключение может содержать несколько линков до endpoint (разные маршруты, каналы связи).
|
||
При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно):
|
||
- RTT последнего пакета и время отправки последнего пакета
|
||
- пакеты делятся на 3 категории: <300 байт, 300-699 байт, 700+ байт. для каждой категории отдельно считаются метрики:
|
||
- количество отправленных в пакетах байтах
|
||
- количество перезапросов (по каждому каналу отдельно) в пакетах и байтах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики.
|
||
- обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент
|
||
Планировщик/балансир каналов (etcp_loadbalancer.c/h):
|
||
для каждого канала ограничение полосы с тестированием отклика:
|
||
- полосу ограничиваем по объему данных - полному числу байт ethernet пакта (добавляем размер заголовков)
|
||
Ограничение реализуем следующим образом:
|
||
- в структуре есть счётчик времени (квантизация стандартная 0.1ms) по которое канал загружен данными. И суб-разряды (nanotime) этого счетчика 0.1ns - 0.1ms (считает по модулю 1000000 и переполнения добавляет в основной счетчик) чтобы при высоких скоростях не было потери точности (т.е. было ненулевое время передачи одного байта).
|
||
Этим счётчиком контролируется полоса: при передаче к нему добавляется расчётное время передачи текущего ethernet пакета (используя текущий bandwidth считаем время передачи одного байта (float) и умножаем на число байт, переводим в таймбазу nanotime). А если при передаче время меньше current_time - delta_time (delta_time - константа в define, x0.1ms) то он устанавливается как current time-delta_time. (при неактивности обновляем время)
|
||
счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи"); [СПОРНО: сложность с float; риск precision issues]
|
||
когда etcp пытается отправить пакет через линк, то если в линке не инициализировано подключение и он клиент - то он отбрасывает этот пакет и запускает процесс установки соединения.
|
||
также процесс установки соединения инициируется при добавлении канала (в etcp_link_new) если это client.
|
||
процесс установки соединения:
|
||
- отправляем init запрос и выставляем таймаут (сохраняем его в struct ETCP_LINK)
|
||
- по таймауту повторяем init запросы, ведем счетчик запросов init.
|
||
- когда получаем init подтверждение (отправляется в etcp_connections_read_callback) снимаем таймаут и выставляем флаг struct ETCP_LINK initialized=1.
|
||
При добавлении нового канала посылаем INIT 0x04 (без сброса), а при первичной инициализации подключения - 0x02 (это нужно, т.к. например перезапуск локального софта должен инициировать перезапуск удаленного соединения).
|
||
|
||
**** Формат кодограмм для etcp_connections.c/h ****
|
||
Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда только одна секция). в обязательном заголовке ID не используется, при передаче для порядка =0: [СПОРНО: заголовок в etcp — только 2 байта TS; здесь подразумевается ID? Уточнить единый формат]
|
||
1) Init запрос - заголовок 0x02 (со сбросом etcp сессии) или 0x04 (без сброса):
|
||
[0x02/0x04] [my_node_id 64bit] [my mtu high] [my mtu low] [keepalive high] [keepalive low] [recovery high] [recovery low] [my link_id 1 байт] [my public key (64 байта, не шифруется)]
|
||
- Инициирует новый connection для tcp instance. если tcp instance нет (первое подключение) - создаёт. Между нодами только одно подключение, но можно добавлять каналы.
|
||
- link_id: локальный идентификатор канала (0-255), назначается отправителем для идентификации канала
|
||
- Публичный ключ отправляется в конце пакета без шифрования, чтобы получатель мог установить его и расшифровать остальную часть пакета (т.к. инициатор соединения всегда имеет оригинальный peer public key в конфиге - по нему исключаем MITM)
|
||
2) Init подтверждение - заголовок 0x03/0x05 (ответы соответственно на коды 0x02 и 0x04):
|
||
[0x03/0x05] [my_node_id 64bit] [my mtu high] [my mtu low] [my link_id 1 байт] [peer ipv4 4 байта] [peer port 2 байта]
|
||
- Подтверждение инициализации (канал успешно создан, можно начинать обмен)
|
||
- link_id: локальный идентификатор канала (0-255) отправителя ответа
|
||
- peer ip.port: возвращаем ip:port с которого пришел init запрос (чтобы клиент узнал свой external ip:port если за nat)
|
||
При получении init получатель пакета должен:
|
||
- reset ETCP_LINK с этим ip_port если он есть
|
||
- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - вызвать etcp_reset (функция сброса окон неподтвержденных данных и нумерации)
|
||
приём кодограмм из UDP выглядит так:
|
||
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
|
||
|
||
Keepalive: пакет без секций (только timestamp+flags)
|
||
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика.
|
||
|
||
Клиент: если все линки =down то начинается процедура восстановления связи:
|
||
- клиент посылает init без сброса
|
||
- сервер как получает init без сброса отправляет init подтверждение: со сбросом или без сброса в зависимости от состояния init его соединения (инициализировано - то без сброса).
|
||
|