perf(traffic): optimize production reconciliation

This commit is contained in:
2026-08-28 02:43:26 +05:00
parent 185991504a
commit 9787cb2d23
20 changed files with 1265 additions and 433 deletions
+22 -11
View File
@@ -243,8 +243,8 @@ struct PeerTrafficStatus {
`discover_target()`:
1. Прочитать упорядоченный `amnezia.container_names`.
2. Найти первый запущенный контейнер с точным совпадением имени.
3. Получить container ID и PID.
2. Для каждого имени по порядку выполнить один `docker inspect`, который сразу возвращает running state, container ID и PID.
3. Выбрать первый запущенный контейнер с точным совпадением имени; отсутствующий или stopped candidate не меняет порядок fallback.
4. Построить `netns_path`.
5. Определить интерфейс в этом namespace.
6. Вернуть `RuntimeTarget`.
@@ -313,7 +313,7 @@ tc class add dev <iface> parent 1: classid 1:1 htb rate <root_rate> ceil <root_r
for each peer:
tc class add dev <iface> parent 1:1 classid 1:<id> htb rate <peer_limit> ceil <peer_limit>
tc qdisc add dev <iface> parent 1:<id> fq_codel
tc filter add dev <iface> parent 1: protocol ip u32 match ip dst <peer_ip> flowid 1:<id>
tc filter add dev <iface> parent 1: protocol ip pref 10 flower dst_ip <peer_ip>/32 classid 1:<id>
```
Ingress через IFB:
@@ -329,9 +329,11 @@ tc class add dev <ifb_name> parent 2: classid 2:1 htb rate <root_rate> ceil <roo
for each peer:
tc class add dev <ifb_name> parent 2:1 classid 2:<id> htb rate <peer_limit> ceil <peer_limit>
tc qdisc add dev <ifb_name> parent 2:<id> fq_codel
tc filter add dev <ifb_name> parent 2: protocol ip u32 match ip src <peer_ip> flowid 2:<id>
tc filter add dev <ifb_name> parent 2: protocol ip pref 10 flower src_ip <peer_ip>/32 classid 2:<id>
```
Все peer-фильтры на каждом parent используют один `pref 10`: для `flower` это даёт один hash table на priority вместо множества priority structures. Маска всегда `/32`, поэтому один priority не смешивает несовместимые masks. Catch-all redirect в IFB остаётся `u32`, поскольку это одно правило, а не per-peer lookup.
## 9. Алгоритм reconcile
Traffic и firewall — два независимых reconcile с отдельными блокировками. Они не объединены в одну транзакцию: сбой одного не должен блокировать другой.
@@ -343,10 +345,12 @@ reconcile_traffic():
target = discover_target()
peers = discover_peers(config, target)
если peers пуст:
залогировать "no peers found, skip tc apply" и выйти
проверить наличие AWG2-owned topology
если она осталась от прежних peers: teardown_owned_topology(target)
иначе выйти с debug-диагностикой
drift = inspect_and_diff(target, peers, config)
если drift отсутствует:
залогировать "traffic rules up-to-date, skip apply" и выйти
выйти с debug-диагностикой
teardown_owned_topology(target)
apply_full_topology(target, peers, config) # внутри — verify-after-apply
```
@@ -365,12 +369,15 @@ apply_firewall():
Требования к устойчивости парсеров:
- `tc filter show` разбирается блоками (конечный автомат), а не наивным сканированием токенов по несвязанным строкам;
- единый parser понимает production `flower` и legacy `u32`, включая counters; точный healthy-контракт требует `flower`, `pref 10`, правильное direction-field и `/32`, а legacy `u32` peer rules считаются migration drift;
- нераспознанный или malformed detailed filter block не игнорируется, а делает инспекцию drifted;
- отображение `peer_ip -> class_id` проверяется для обеих сторон: egress (`1:`) и ingress (`2:`);
- проверка обязана падать, если классы и фильтры существуют, но отображение отличается от детерминированного присвоения.
Политика apply/verify:
- все шаги apply — явные вызовы команд;
- `modprobe ifb`, создание IFB и его перевод в UP выполняются строго и до `tc` apply;
- после teardown вся `tc` topology подаётся одним fail-fast сценарием в stdin команды `tc -batch -`; `-force` не используется, поэтому первая ошибка останавливает batch и сохраняет номер строки в stderr;
- ожидаемые конфликты идемпотентности допускаются выборочно, через отдельную обёртку, и по закрытому списку признаков: `file exists`, `eexist`, `object already exists`, `exclusive flag on`;
- ошибки «объект не найден» (`no such`, `not found`, `cannot find`, `does not exist`, `handle of zero`) допускаются только на шагах удаления — в teardown и при отключении владеемых таблиц;
- любая другая ошибка apply фатальна;
@@ -437,9 +444,9 @@ table inet awg2_harness {
- counter `antitorrent_dropped_v4`;
- meter/set state, создаваемый правилом.
Status обязан показывать наличие таблицы/chain/правила, настроенный порог и drop counter. Также он показывает давление на conntrack хоста через `net.netfilter.nf_conntrack_count` и `net.netfilter.nf_conntrack_max`, когда они доступны.
Status обязан показывать наличие таблицы/chain/правила, настроенный порог и drop counter. Также он один раз читает `net.netfilter.nf_conntrack_count` и `net.netfilter.nf_conntrack_max` и, когда оба значения доступны, печатает `nf_conntrack_utilization` в процентах.
При `anti_torrent.enabled = false` демон удаляет только владеемую anti-torrent таблицу. Глобальный ruleset nftables не сбрасывается никогда. Если контейнер в этот момент отсутствует, удаление в его namespace просто пропускается — это не ошибка.
При `anti_torrent.enabled = false` демон удаляет только владеемую anti-torrent таблицу. Глобальный ruleset nftables не сбрасывается никогда. Если контейнер в этот момент именно не найден, удаление в его namespace пропускается — это не ошибка. Ошибка Docker API/daemon/permissions не маскируется под «контейнер не найден» и возвращается оператору.
## 12. Инспекция фактического состояния
@@ -456,6 +463,8 @@ Status обязан показывать наличие таблицы/chain/п
- классы на `<ifb_name>`;
- фильтры под parent `2:` на `<ifb_name>`.
Один вывод `tc qdisc show dev <iface>` используется одновременно для root, ingress и per-peer `fq_codel`; один вывод `tc class show dev <device>` — для root и per-peer classes. Инспекция не делает отдельный child process на каждый parent.
Дополнительно детектируются дубли: один и тот же peer IP в нескольких фильтрах и один и тот же class id у нескольких IP. Оба случая считаются drift.
`status` показывает компактную сводку и таблицу по peers.
@@ -469,6 +478,8 @@ Status обязан показывать наличие таблицы/chain/п
Таймер обязателен. События могут быть пропущены при рестарте демона, рестарте Docker или транзиентных ошибках.
Внутри одного startup/periodic/event-цикла traffic и anti-torrent получают один и тот же результат discovery `RuntimeTarget`; повторные `docker inspect`, PID/interface discovery и transport construction в этом же цикле не выполняются. Наличие host `nsenter` кэшируется на время жизни процесса.
Проверка готовности не должна хардкодить `awg0`. Она вызывает общий детектор интерфейса с интервалом 2 секунды до истечения `traffic.container_ready_timeout_secs`.
Порядок на старте демона и разница в критичности ошибок:
@@ -487,7 +498,7 @@ Status обязан показывать наличие таблицы/chain/п
Ошибки отслеживаются по подсистемам (`traffic`, `firewall`, `swap`, `sysctl_tuning`, `docker_events`), а не одним общим полем. Общее поле в `last_action.state` показывает ту ошибку, которая ещё не устранена: успешный reconcile одной подсистемы не гасит незакрытую ошибку другой и не объявляет восстановление того, что не падало. Поток `docker events` при успешном переподключении тоже отмечается как восстановленный — иначе один рестарт Docker оставлял бы ошибку в `status` навсегда при исправно работающем новом потоке.
В `events.log` пишутся только **переходы состояния**: первое появление ошибки, изменение текста ошибки и восстановление после неё. Рутинный успех не пишется. Раньше каждый успешный цикл добавлял строку — при интервале 15 секунд это порядка 17 тысяч записей в сутки, каждая с полным чтением, дополнением и перезаписью файла, и на их фоне терялись записи, ради которых журнал и читают. Полная поцикловая картина остаётся в journald через tracing.
В `events.log` и journald на уровне `info`/`error` пишутся только **переходы состояния**: первое появление ошибки, изменение её текста и восстановление. Рутинный healthy-цикл и повтор той же ошибки остаются на `debug`. Это исключае повторяющийся journal-шум на каждом 15-секундном reconcile, но сохраняет детали для отладки при включённом debug-уровне.
## 14. Блокировки
@@ -552,7 +563,7 @@ Swap:
desired_active, exclusive, fstab_exclusive, fstab_swap_entries,
swappiness_current, swappiness_persisted, drift[, drift_reason]
Conntrack:
nf_conntrack_count, nf_conntrack_max
nf_conntrack_count, nf_conntrack_max, nf_conntrack_utilization
Last actions:
содержимое /run/awg2-harness/last_action.state
Recent events: