perf(traffic): optimize production reconciliation
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
- [ ] На AWG/WG-интерфейсе есть root class `1:1` с `traffic.root_rate`.
|
||||
- [ ] Для каждого peer есть class `1:<id>` с `traffic.peer_limit`.
|
||||
- [ ] Для каждого peer есть `fq_codel` под `1:<id>`.
|
||||
- [ ] Для каждого peer есть egress filter `dst peer_ip -> 1:<id>`.
|
||||
- [ ] Для каждо peer есть egress filter `flower pref 10 dst_ip peer_ip/32 -> 1:<id>`.
|
||||
- [ ] Есть ingress qdisc `ffff:` на AWG/WG-интерфейсе.
|
||||
- [ ] Есть ingress redirect через `mirred ... redirect dev ifb0`.
|
||||
- [ ] `ifb0` существует и находится в UP state.
|
||||
@@ -46,7 +46,8 @@
|
||||
- [ ] На `ifb0` есть root class `2:1` с `traffic.root_rate`.
|
||||
- [ ] Для каждого peer есть class `2:<id>` с `traffic.peer_limit`.
|
||||
- [ ] Для каждого peer есть `fq_codel` под `2:<id>`.
|
||||
- [ ] Для каждого peer есть ingress filter `src peer_ip -> 2:<id>`.
|
||||
- [ ] Для каждого peer есть ingress filter `flower pref 10 src_ip peer_ip/32 -> 2:<id>`.
|
||||
- [ ] Все peer-фильтры на каждом parent используют один `pref 10`; `u32` остаётся только у catch-all ingress redirect.
|
||||
- [ ] Class ids начинаются с `traffic.class_start_id`, default `10`.
|
||||
- [ ] Peer IP → class id mapping использует стабильный лексикографический контракт: IP сортируются как строки, не numeric IPv4.
|
||||
|
||||
@@ -63,7 +64,10 @@
|
||||
- [ ] Если изменить `rate/ceil` peer class, drift детектируется и topology восстанавливается.
|
||||
- [ ] Если добавить stale peer class/filter, drift детектируется и topology восстанавливается.
|
||||
- [ ] После удаления peer устаревшие class/filter исчезают через rebuild.
|
||||
- [ ] После удаления последнего peer вся AWG2-owned traffic topology удаляется; повторный reconcile не выполняет teardown снова.
|
||||
- [ ] После добавления peer появляются новые class/filter.
|
||||
- [ ] Legacy peer-фильтры `u32` детектируются как migration drift и заменяются на `flower` через rebuild.
|
||||
- [ ] Ошибка в любой строке `tc -batch -` останавливает apply; после успеха проходит verify-after-apply.
|
||||
|
||||
## E. Самовосстановление в жизненном цикле
|
||||
|
||||
@@ -80,6 +84,7 @@
|
||||
- [ ] На старте daemon сбой `sysctl_tuning` или `swap` завершает процесс, а сбой traffic/firewall только логируется — сервис остаётся живым.
|
||||
- [ ] После обрыва потока `docker events` он перезапускается автоматически (проверяется, например, `systemctl restart docker`).
|
||||
- [ ] После восстановления потока `docker events` ошибка подсистемы снимается: в `events.log` появляется `docker_events=recovered`, а `last_action.state` не остаётся с этой ошибкой навсегда.
|
||||
- [ ] Один startup/periodic/event-цикл использует один discovery `RuntimeTarget` для traffic и anti-torrent, а не повторяет `docker inspect` и interface discovery.
|
||||
- [ ] Успешный reconcile одной подсистемы не гасит незакрытую ошибку другой: при постоянно падающем `traffic` успешный `firewall` не очищает `last_error` и не пишет `firewall=recovered`.
|
||||
- [ ] Параллельный запуск `apply-limits` во время работы daemon не приводит к гонке: второй процесс ждёт блокировку до 10 секунд и затем сообщает `lock busy timeout`.
|
||||
- [ ] После `kill -9` блокировка освобождается ядром автоматически; оставшийся lock-файл не мешает следующему захвату `/run/awg2-harness/traffic.lock`.
|
||||
@@ -93,7 +98,9 @@
|
||||
- [ ] `status` показывает недостающие и устаревшие правила peers: в `Traffic actual` — строка `reason` (например, `missing egress class`, `egress per-peer class set mismatch`, `duplicate egress class flowid mapping detected`), в `peer_table` — колонка `drift` по каждому peer.
|
||||
- [ ] `status` явно показывает частичное состояние и drift, а не общий признак «всё хорошо».
|
||||
- [ ] `status` показывает tc filter counters по peer или `filter statistics: unavailable`.
|
||||
- [ ] Flower counters и legacy u32 counters в `status` разбираются одним parser-ом и привязываются к правильному peer IP.
|
||||
- [ ] Логи доступны через `journalctl -u awg2-harness`.
|
||||
- [ ] Повтор одной и той же reconcile-ошибки не создаёт `error`-запись на каждом цикле; первая ошибка, изменение её текста и recovery видны как отдельные переходы.
|
||||
- [ ] Отсутствие обязательной host-утилиты (`tc`, `ip`, `docker`, `nsenter`) логируется явно с именем утилиты.
|
||||
- [ ] Последний reconcile result виден в status или journal.
|
||||
- [ ] `status` показывает последние события reconcile/firewall из `/run/awg2-harness/events.log`.
|
||||
@@ -143,6 +150,9 @@
|
||||
- [ ] В обоих managed-файлах первая строка содержит marker `Managed by awg2-harness sysctl_tuning`.
|
||||
- [ ] Файл без marker по managed-пути не перезаписывается и не удаляется: команда завершается ошибкой.
|
||||
- [ ] В managed-файле отсутствует `udp_wmem_min`.
|
||||
- [ ] В shipped/default config `netdev_backlog_enabled = false`, `netdev_max_backlog = 10000`, а `net.core.netdev_max_backlog` отсутствует в managed-файле.
|
||||
- [ ] При смене `netdev_backlog_enabled = true` на `false` key удаляется из persistence; оператор учитывает, что текущее runtime-значение до reboot автоматически не сбрасывается.
|
||||
- [ ] До/после ручного включения backlog сняты `/proc/net/softnet_stat`, CPU и latency под реальной нагрузкой.
|
||||
- [ ] `net.ipv4.tcp_congestion_control` и `net.core.default_qdisc` присутствуют в managed-файле только при активном BBR.
|
||||
- [ ] `swap` пишет `vm.swappiness` в `/etc/sysctl.d/90-awg2-harness-swap.conf`, а не в `99-awg2-harness.conf`.
|
||||
- [ ] При обновлении с прежней версии `/etc/sysctl.d/99-awg2-harness.conf` удаляется, если несёт маркер `# Managed by awg2-harness`.
|
||||
@@ -184,7 +194,7 @@
|
||||
- [ ] При превышении порога новые flows дропаются, established flows ниже порога не ломаются.
|
||||
- [ ] После restart контейнера nftables rule пересоздаётся в новом netns.
|
||||
- [ ] При `anti_torrent.enabled = false` удаляется/отключается только AWG2-owned nftables state; Docker/Amnezia rules не flush-ятся.
|
||||
- [ ] `status` показывает `nf_conntrack_count` / `nf_conntrack_max`, если sysctl доступны.
|
||||
- [ ] `status` показывает `nf_conntrack_count`, `nf_conntrack_max` и `nf_conntrack_utilization`, если sysctl доступны.
|
||||
|
||||
## J. Откат
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ sysctl net.netfilter.nf_conntrack_max
|
||||
nft list table inet awg2_harness
|
||||
```
|
||||
|
||||
Если `nf_conntrack_count` приближается к `nf_conntrack_max`, проблема уже системная: надо разбираться с timeout, abuse или capacity. Не надо автоматически увеличивать `nf_conntrack_max` из orchestrator без отдельного решения.
|
||||
Если `nf_conntrack_count` приближается к `nf_conntrack_max`, проблема уже системная: надо разбираться с timeout, abuse или capacity. `awg2-harness status` показывает оба значения и вычисленный `nf_conntrack_utilization`, чтобы давление было видно без ручного деления. Не надо автоматически увеличивать `nf_conntrack_max` из orchestrator без отдельного решения.
|
||||
|
||||
## 7. Критерии приёмки
|
||||
|
||||
@@ -106,6 +106,7 @@ nft list table inet awg2_harness
|
||||
- Правило ограничивает `ip saddr <peer_ipv4_cidr>`.
|
||||
- Лимит применяется per source VPN IP, а не глобально на всю подсеть.
|
||||
- Drop counter растёт при превышении threshold.
|
||||
- `status` показывает count/max/utilization conntrack, если эти sysctl доступны.
|
||||
- Нормальные сценарии с числом flows ниже threshold не затрагиваются.
|
||||
- При restart контейнера правило пересоздаётся в новом netns.
|
||||
- При `anti_torrent.enabled = false` удаляется или отключается только AWG2-owned nftables state.
|
||||
|
||||
@@ -58,8 +58,8 @@ tcp_bbr = "auto" # off | auto | require
|
||||
default_qdisc = "fq" # none | fq | fq_codel
|
||||
rmem_max = 67108864
|
||||
wmem_max = 67108864
|
||||
netdev_backlog_enabled = true
|
||||
netdev_max_backlog = 50000
|
||||
netdev_backlog_enabled = false
|
||||
netdev_max_backlog = 10000
|
||||
udp_rmem_min_enabled = true
|
||||
udp_rmem_min = 8192
|
||||
|
||||
@@ -129,6 +129,10 @@ swappiness = 1
|
||||
- `tcp_bbr = auto` не валит запуск при недоступном BBR (warning), `require` — валит;
|
||||
- для `sysctl_tuning`: `modprobe` обязателен только для `tcp_bbr = "require"`.
|
||||
|
||||
`netdev_backlog_enabled = false` — безопасный production default: harness не управляет `net.core.netdev_max_backlog`, пока оператор не включит это явно после измерений. Значение `netdev_max_backlog = 10000` используется только при `netdev_backlog_enabled = true`.
|
||||
|
||||
При обновлении с конфига, где backlog раньше был включён, переключение флага в `false` удаляет key из managed-файла, но не перезаписывает уже изменённое runtime-значение. До следующей перезагрузки его можно вернуть вручную к baseline дистрибутива. Перед осознанным включением backlog снимите pre/post-метрики `/proc/net/softnet_stat` (в том числе dropped/time_squeeze) и CPU/latency под реальной нагрузкой.
|
||||
|
||||
Отдельно для всего пакета:
|
||||
|
||||
- `traffic`-модуль требует `modprobe` для IFB при `traffic.enabled = true`.
|
||||
|
||||
+22
-11
@@ -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:
|
||||
|
||||
@@ -292,7 +292,7 @@ Swap:
|
||||
drift: no
|
||||
```
|
||||
|
||||
Если peers ещё нет, `apply-limits` напечатает `no peers found, skip tc apply`. Для пустого сервера это здоровое состояние.
|
||||
Если peers ещё нет, это здоровое desired state. `apply-limits` успешно завершится и удалит оставшуюся AWG2-owned traffic topology, если до этого peers были.
|
||||
|
||||
При `swap.enabled = true` AWG2 Harness намеренно делает swap эксклюзивным: один управляемый swapfile, одна swap-запись в `/etc/fstab`, заданный `swappiness`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user