Files
dokril a84cca0668
Build and Deploy Gateway / build-and-push (push) Failing after 1s
Build and Deploy Gateway / deploy (push) Has been skipped
Improve failover controls and preserve manual switching state
2026-08-28 19:40:53 +03:00

16 KiB
Raw Permalink Blame History

Read-only аудит failover/failback

Короткий вывод

Штатное переключение между primary и reserve меняет маршрут только для новых соединений. Уже установленные TCP/UDP-соединения не переносятся и не закрываются самим Harbor.

Поэтому:

  • здоровая загрузка, игра или поток продолжаются через старый канал;
  • если старый канал действительно умер, существующая сессия может оборваться независимо от переключения;
  • после переключения новые соединения идут через новый канал;
  • бесшовной миграции уже открытого TCP/UDP-сеанса на другой внешний адрес нет.

1. Точный механизм

Конфигурация

Gateway собирает один dual-channel sing-box config:

  • channel-primary;
  • channel-reserve;
  • selector channel-selector;
  • стабильные tproxy-in и mixed-in;
  • отдельные diagnostic inbounds для проверки каждого канала.

Пользовательский трафик направляется в selector, а selector настроен с:

interrupt_exist_connections: false

См. src/server/singbox.ts:204-247.

Роль меняется через localhost Clash API:

  • PUT /proxies/channel-selector;
  • затем Harbor читает selector обратно и подтверждает выбранную роль.

См. src/server/services/singboxSelectorService.ts:29-81.

Failover API доступен только в Gateway:

  • PUT /api/failover;
  • POST /api/failover/pause;
  • POST /api/failover/switch;
  • POST /api/failover/check.

См. src/server/http/routes/failoverRoute.ts:18-39.

Переключение selector не вызывает sing-box restart/apply/stop.

Автоматический failover

FailoverService:

  1. Проверяет оба канала через отдельные diagnostic inbound и выбранные HTTPS-сервисы.
  2. Считает канал healthy только если все проверки успешны; неизвестный результат даёт unknown.
  3. При сбое primary ждёт failureWindowMs.
  4. Переключается на reserve только если reserve healthy.
  5. При включённом traffic guard ждёт свежий quiet-window.
  6. Перед самой сменой повторно проверяет здоровье и активность.
  7. Выполняет selector PUT, read-back и только после этого обновляет canonical applied state.

См. src/server/features/failover/failoverService.ts:225-245, :320-514; state machine — src/shared/failover.ts:278-354.

Дефолты:

  • проверка каждые 60 секунд;
  • сбой primary — 120 секунд;
  • восстановление primary — 15 минут;
  • quiet-window — 30 секунд;
  • активный трафик — выше 32 КБ/с;
  • минимум на reserve — 10 минут;
  • после 3 failover за 24 часа — карантин primary на 6 часов.

См. src/shared/failover.ts:137-148.

Автоматический failback

Отдельной реализации нет: это обратная ветка той же state machine.

Из reserve Harbor возвращается на primary только после:

  • полного recoveryWindowMs;
  • minimumReserveMs;
  • окончания quarantine, если он действует;
  • quiet-window при включённом traffic guard;
  • подтверждения, что primary healthy.

Причина переключения публикуется как primary-recovered. См. src/shared/failover.ts:311-354.

2. Судьба существующих соединений

Событие Уже открытый TCP/UDP flow Новые соединения
Автоматический failover primary → reserve Остаётся на прежнем outbound; Harbor его не закрывает и не мигрирует Идут через reserve
Автоматический failback reserve → primary Остаётся на reserve Идут через primary
Ручной selector switch Не закрывается, если старый outbound ещё работает Сразу идёт через выбранную роль
Pause Ничего не меняет Идут через текущую роль
Disable failover Selector и текущий маршрут не меняются; dual config временно остаётся загруженным Идут через текущую роль
Обычный stop/restart/config replacement Процесс sing-box останавливается, поэтому TCP/UDP-сессии прерываются После запуска — по новой конфигурации
Реальная авария primary Уже существующий flow может оборваться сам; Harbor не может перенести его на другой внешний IP После selector switch новые flow идут через reserve

Проверка interrupt_exist_connections: false непосредственно подтверждена TCP- и UDP-fixture-тестом: существующие TCP socket и UDP association продолжают обмен после switch, а новые идут через reserve. См. test/server/singbox-selector-capability.test.js:141-142, :237-356.

3. Отличия режимов

Ручное переключение

POST /api/failover/switch вызывает selector напрямую:

  • traffic guard не проверяется;
  • состояние здоровья целевого канала backend не проверяет;
  • после успешного ручного переключения failoverPolicy.paused становится true;
  • автоматический failback не произойдёт, пока пользователь не возобновит автоматическое переключение.

См. src/server/features/failover/failoverService.ts:248-317, :614-631.

Это означает, что ручной switch может быть выполнен даже на канал, который сейчас не подтверждён healthy. Это важная оговорка.

Автоматический failover

Автоматическое переключение:

  • ждёт failure window;
  • требует healthy reserve;
  • при включённом guard блокируется активным или неизвестным трафиком;
  • повторно валидирует условия непосредственно перед selector mutation;
  • не перезапускает sing-box.

Восстановление primary

Восстановившийся primary не получает новые соединения сразу. Сначала выдерживаются recovery/hold/quarantine условия и quiet-window. Пока они не выполнены, новые подключения остаются на reserve.

Pause и Disable

pause приостанавливает решения, но dual config и наблюдение остаются активными.

disable останавливает scheduler, probes и failover activity collector, но не переключает selector и не перезапускает процесс. После обычного stop/следующего запуска собирается single-channel config.

См. README.md:78-84, docs/product/application-state.md:102-110.

4. Практические сценарии

  • Загрузка файла: при обычном автоматическом failover активная передача по умолчанию задерживает switch. Если primary всё же упал, текущая TCP-загрузка не переносится на reserve; она может завершиться ошибкой. Возобновление или новый HTTP-запрос после switch пойдёт через reserve.
  • Игровая сессия: существующий TCP-сеанс остаётся на старом канале. TCP-сессия оборвётся, если primary реально недоступен. UDP-flow также не мигрирует и может начать терять пакеты или истечь по timeout; новая сессия после switch пойдёт через reserve.
  • Стрим: активный поток обычно блокирует автоматический switch при включённом guard. Ручной switch может быть выполнен сразу, но существующий TCP/QUIC-поток остаётся на старом outbound. При аварии старого канала плеер должен переподключиться.
  • WebSocket/долгий polling: действующий flow не переносится; новые подключения после switch используют новую роль.
  • Молчащее соединение: наличие открытого socket само по себе не считается активностью. Guard смотрит на дельты переданных байтов.

5. Условия и оговорки

  • Failover реализован только для Gateway, не для локального Connect или gateway-direct.
  • Активность собирается существующим /connections observer каждые 2 секунды, с bounded окном до 10 секунд. См. src/server/index.ts:311-333, src/server/services/domainTrafficService.ts:323-416, :437-495.
  • Короткое соединение, полностью завершившееся между двумя снимками, может не попасть в activity guard.
  • Неизвестная или устаревшая activity-информация блокирует автоматический switch; ручной switch остаётся доступен.
  • Отключение traffic guard разрешает автоматический switch без ожидания тишины, но interrupt_exist_connections: false всё равно защищает уже открытые connections от закрытия самим selector.
  • При изменении policy во время работающего single-channel VPN dual config становится pending; скрытого restart нет. См. src/server/features/connection/connectionService.ts:140-201, :288-361.
  • Явный stop/restart или обычная смена сервера вне активного failover уже является disruptive operation: sing-box получает SIGTERM и текущие сессии прекращаются. См. src/server/singboxRuntime.ts:39-62, :65-122.
  • Оба канала находятся в одном процессе sing-box; process-wide crash не защищён selector-механизмом.

6. Основные файлы

  • src/server/singbox.ts:204-247 — dual config, selector, inbound routing.
  • src/server/services/singboxSelectorService.ts:29-81 — selector PUT/read-back.
  • src/server/features/failover/failoverService.ts:225-245 — проверки каналов.
  • src/server/features/failover/failoverService.ts:248-317 — selector switch, commit и rollback.
  • src/server/features/failover/failoverService.ts:320-514 — автоматический раунд и traffic guard.
  • src/server/features/failover/failoverService.ts:614-631 — ручное переключение.
  • src/shared/failover.ts:137-148, :278-354 — дефолты и state machine.
  • src/server/services/domainTrafficService.ts:129-160, :323-416, :437-495 — классификация и activity.
  • src/server/features/connection/connectionService.ts:140-201, :243-361 — обычный apply/stop/restart.
  • src/server/singboxRuntime.ts:39-122 — фактическая остановка и перезапуск процесса.
  • README.md:78-84 — пользовательская документация.
  • docs/product/application-state.md:102-110 — контракт состояния.
  • workpack/tasks/TASK-021-auto-server-selection-failover.md:93-106, :187-204 — относящийся план и ограничения; задача не выбиралась и не изменялась.

7. Тесты, подтверждающие выводы

  • test/server/singbox-selector-capability.test.js:141-356
    Интеграционная TCP/UDP-проверка: активный трафик задерживает failover, существующие TCP/UDP продолжают работать после switch, новые соединения идут через reserve, PID процесса не меняется. Тест opt-in и пропускается без HARBOR_SINGBOX_IMAGE.
  • test/server/singbox-gateway-mode.test.js:69-110
    Проверяет selector, interrupt_exist_connections: false, отдельные diagnostic routes и primary/reserve outbounds.
  • test/server/failover-service.test.js:83-200
    Failure window, traffic guard, both-unhealthy и failback recovery/hold/quarantine.
  • test/server/failover-service.test.js:255-420
    Disabled zero-work, отмена устаревших наблюдений и непосредственная revalidation активности.
  • test/server/failover-service.test.js:423-520
    Selector rollback, commit ordering и ручной switch с pause.
  • test/server/domain-traffic.test.js:209-239
    Activity считается по пользовательскому VPN traffic, diagnostic connection не блокирует switch.
  • test/server/connection-service.test.js:265-310, :340-390
    Restart dual config и rollback; pending edits для работающего single-channel.
  • test/server/singbox-runtime.test.js:16-50
    При изменении config runtime запускает новый процесс.
  • test/server/failover-route.test.js:10-53
    Gateway-only API и маршруты ручного switch/pause/check.
  • test/web/failover-feature-contract.test.js:19-76
    UI явно сообщает: новые подключения переключаются, открытые остаются на прежнем канале.

Review findings и residual risks

  • medium — src/server/features/failover/failoverService.ts:614-620: ручной switch не проверяет health целевого канала и не применяет traffic guard; он сразу меняет selector и ставит automation на pause.
  • medium — test/server/singbox-selector-capability.test.js:141-356: capability test opt-in и использует deterministic direct outbounds, а не реальный VLESS/Trojan outage.
  • medium — src/server/services/domainTrafficService.ts:414-416, :451-479: activity основана на polling и byte deltas; короткие или очень малые потоки могут не блокировать автоматическое решение.
  • info — src/server/singboxRuntime.ts:39-122: явный stop/restart отличается от selector switch и прерывает существующие соединения.
  • info — архитектура одного процесса: падение всего sing-box не компенсируется selector failover.

Файлы не изменялись. Тесты и live Gateway в рамках read-only аудита не запускались.