Appearance
Кластеризация и Высокая Доступность
ForgeVis поддерживает режим High Availability (HA) на базе алгоритма консенсуса Raft. Это позволяет объединить несколько серверов в единый кластер, который обеспечивает:
- Отказоустойчивость: При падении узла камеры автоматически переносятся на живые узлы.
- Балансировку нагрузки: Камеры распределяются по кластеру.
- Централизованное управление: API запросы к любому узлу перенаправляются Лидеру.
Архитектура
Мы используем алгоритм Raft для достижения консенсуса и обеспечения согласованности данных в кластере.
Конфигурация
Для включения кластера добавьте секцию cluster в config.yaml:
yaml
cluster:
enabled: true
node_id: 1 # Уникальный ID узла (1, 2, 3...)
rpc_addr: "192.168.1.10:9091" # Raft RPC адрес ЭТОГО узла
dataDir: "/var/lib/forgevis/data" # Где узел хранит состояние RaftКонфиг описывает только сам узел. Состав кластера управляется исключительно через API — узел никогда не создаёт и не присоединяется к кластеру самостоятельно, поэтому запуск узла с включённым кластерным режимом всегда безопасен.
rpc_addr должен быть IP-адресом с портом (это одновременно адрес прослушивания и адрес, по которому узел доступен остальным).
В dataDir лежит всё, что делает узел собой: журнал Raft, отданный голос, снимок состояния и — в node_<node_id>/tls — сертификат, выданный Control Plane. Путь держите абсолютным и на хранилище, переживающем перезапуск. Узел, поднявшийся с пустым каталогом, ведёт себя как впервые запущенный: голосует заново и просится в кластер, где уже состоит. О пустом каталоге он сообщает в журнале при старте.
Выход из кластера сразу, без перезапуска, стирает журнал Raft, голос и снимок, а ключ и сертификат оставляет: вышедший узел можно запускать снова без опасений.
Два канала узла
У узла два независимых канала, и защищены они по-разному.
| Канал между узлами | Управляющий API | |
|---|---|---|
| Кто обращается | узел → узел | оператор или платформа → узел |
| Порт | cluster.rpc_addr, обычно 9091 | api.address, обычно 9997 |
| Чем защищён | взаимный TLS, обязательно | HTTP Basic из security.users |
Разделение не косметическое: узлы доказывают друг другу принадлежность к кластеру сертификатом, а не паролем оператора. Поэтому пароль на управляющем API можно включать и менять, не задев работу кластера.
Сертификаты
Узлы аутентифицируют друг друга сертификатами, и выключить это нельзя. Любой, кто дотянулся до порта консенсуса незащищённого кластера, может голосовать, дописывать журнал и устанавливать снимки состояния.
Отсюда главное свойство запуска: узел без сертификата не открывает порт консенсуса и пишет об этом в журнал. Для только что установленного узла это нормальное состояние, а не неисправность.
Сертификат берётся одним из двух способов.
Свой удостоверяющий центр
Путь для кластера, который поднимают руками. Узел в выпуске не участвует: файлы выдаёте вы, а конфиг указывает, где их взять.
yaml
cluster:
tls:
ca_cert: /etc/forgevis/tls/ca.pem
cert: /etc/forgevis/tls/node-1.pem
key: /etc/forgevis/tls/node-1.keyВсе три поля задаются вместе — частичная настройка отвергается при старте.
Выпуск на openssl. Удостоверяющий центр создаётся один раз, дальше блок узла повторяется для каждого — со своим именем и своим адресом из rpc_addr:
bash
# Удостоверяющий центр кластера
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout ca.key -out ca.pem -subj "/CN=forgevis-ca-north"
# Сертификат узла
openssl req -newkey rsa:2048 -nodes \
-keyout node-1.key -out node-1.csr -subj "/CN=S1"
openssl x509 -req -in node-1.csr \
-CA ca.pem -CAkey ca.key -CAcreateserial \
-days 825 -out node-1.pem -extfile - <<'EXT'
subjectAltName = IP:192.168.1.10
extendedKeyUsage = serverAuth, clientAuth
keyUsage = digitalSignature, keyEncipherment
EXT
chmod 600 node-1.keyНа узел кладутся ca.pem, его собственные node-N.pem и node-N.key. Ключ удостоверяющего центра ca.key на узлы не попадает — он нужен только там, где вы выпускаете сертификаты.
Три правила, каждое из которых при нарушении даёт запутанную ошибку.
serverAuthиclientAuthв одном сертификате. Узел одновременно сервер для того, кто звонит ему, и клиент для того, кому звонит он.- В SAN — тот же host, что в
rpc_addrцелевого узла. Для адреса-литерала это записьIP:, а неDNS:. - Один удостоверяющий центр на кластер. Общий корень позволил бы узлу одного кластера говорить консенсус со вторым.
Узел читает эти файлы при старте, поэтому замена сертификата — раскладка новых файлов и перезапуск. Следите за сроком: с истёкшим сертификатом узел выпадет из кластера, а в журнале будут ошибки рукопожатия.
Выдача платформой
Под управлением платформы выпуск автоматический: узел создаёт приватный ключ и запрос на подпись, платформа подписывает своим центром и возвращает результат. Приватный ключ узел не покидает, перезапуск не нужен, cluster.tls остаётся пустым. Подробности — в разделе Ноды и кластер.
Сборка кластера
Формирование кластера
cluster_id обязателен и должен быть UUID. На него ссылается строка камеры, когда называет владеющий ею кластер (cameras.cluster_id), — кластер читает ровно те строки, где стоит это значение. Без идентичности кластер не инициализируется: он читал бы все включённые камеры базы как свои, а переинициализировать его позже, чтобы выдать идентичность, нельзя.
Под управлением Control Plane идентичность выдаёт он. Для кластера, поднятого руками, сгенерируйте UUID и поставьте то же значение в строки камер.
bash
# 1. Инициализируем кластер на первом узле. Узел регистрирует себя по
# собственному конфигу — в запросе передаётся только идентичность кластера.
curl -X POST http://192.168.1.10:9997/cluster/init \
-H 'Content-Type: application/json' \
-d '{"cluster_id": "3f8c1d2e-7a45-4b91-9c30-5e6f8a1b2c3d", "cluster_name": "north"}'
# 2. Добавляем остальные узлы как learner'ов (сначала догоняют лог)
curl -X POST http://192.168.1.10:9997/cluster/add-learner \
-H 'Content-Type: application/json' \
-d '{"node_id": 2, "node_name": "S2",
"rpc_addr": "192.168.1.11:9091", "api_addr": "192.168.1.11:9997"}'
# 3. Повышаем их до voter'ов
curl -X POST http://192.168.1.10:9997/cluster/change-membership \
-H 'Content-Type: application/json' -d '{"members": [1, 2, 3]}'Удаление узла
Сначала убираем узел из кластера, затем говорим самому узлу сбросить локальное состояние — иначе он продолжит действовать по нему и не сможет позже войти в другой кластер:
bash
curl -X POST http://192.168.1.10:9997/cluster/remove-node \
-H 'Content-Type: application/json' -d '{"node_id": 3}'
curl -X POST http://192.168.1.12:9997/cluster/leave \
-H 'Content-Type: application/json' -d '{"force": true}'Узел сразу останавливает камеры и стирает локальное состояние Raft, без перезапуска, после чего его можно снова добавить в кластер.
remove-node для узла, которого в составе уже нет, отвечает успехом: запрошенное состояние достигнуто. Это позволяет доводить разбор кластера до конца, не разбирая текст ошибок.
Вывод последнего узла — это не изменение состава, а роспуск кластера. Raft не может убрать единственного voter'а, и обновлять больше нечего: остаётся только скомандовать этому узлу выйти.
Запросы идут на лидера
Изменения принимает только лидер. Узел, получивший изменение и лидером не являющийся, отвечает 307 Temporary Redirect с адресом лидера в Location — он не ходит за ответом сам, поэтому узлам не нужны учётные данные друг друга.
По перенаправлению нужно идти явно. httpx и requests выбрасывают заголовок Authorization при переходе на другой хост, так что автоматическое следование даст 401: повторите запрос сами, с учётными данными того узла, на который вас направили.
Роли узлов
В кластере доступны две runtime-роли узла:
recorder— узел может владеть назначениями камер и выполнять запись.streamer— узел может отдавать HLS/RTSP потоки в cluster mode.
По умолчанию узел имеет обе роли. Роли можно менять на лету через cluster API.
Cluster-aware политики стриминга
Планировщик и слой выдачи применяют per-camera политики в кластере:
hls— включение/выключение HLS для камеры.rtsp— включение/выключение RTSP для камеры.alwaysRemux— принудительный always-on HLS remux для выбранных камер.node_id— предпочтительное размещение камеры на узле.
Это позволяет разделять роли записи и выдачи потоков между узлами, сохраняя отказоустойчивость.
Восстановление после сбоев
Когда воркер падает (нет heartbeat > 15с):
- Лидер обнаруживает таймаут.
- Узел помечается как
failed. - Камеры, назначенные на этот узел, автоматически перераспределяются на другие живые узлы (Failover).
API
| Endpoint | Описание |
|---|---|
GET /cluster/metrics | Текущее здоровье кластера, роли узлов и состояние storage |
POST /cluster/node/roles | Установить runtime-роли (recorder / streamer) для узла |
POST /cluster/init | Инициализировать кластер на этом узле (единственный voter) |
POST /cluster/add-learner | Добавить узел в кластер как learner |
POST /cluster/change-membership | Повысить Learners до Voters |
POST /cluster/remove-node | Убрать узел из состава кластера |
POST /cluster/leave | Сбросить локальное кластерное состояние узла (force — не обновлять состав) |
Диагностика
GET /cluster/metrics— лидер, term, состав кластера.GET /api/metrics— CPU, память и диск конкретного узла. Ресурсы берутся именно с узла: через Raft реплицируется только состояние хранилища, потому что на нём основано размещение камер.
| Симптом | Причина |
|---|---|
| Порт консенсуса закрыт, в журнале предупреждение о сертификате | Узел ещё не сертифицирован — ожидаемое состояние до выдачи |
tls-name-mismatch, NotValidForName | Host из rpc_addr отсутствует в SAN сертификата целевого узла |
h2 protocol error: FRAME_SIZE_ERROR, GoAway | Одна сторона говорит открытым текстом в TLS-порт: первые байты рукопожатия читаются как заголовок кадра HTTP/2 |
| Узлы числятся офлайн при живых процессах | Heartbeat не доходит до лидера — проверьте связность по rpc_addr |