Improve failover controls and preserve manual switching state
This commit is contained in:
+209
@@ -0,0 +1,209 @@
|
||||
# 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 настроен с:
|
||||
|
||||
```text
|
||||
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 аудита не запускались.
|
||||
Reference in New Issue
Block a user