◆ ForgeVis
Skip to content

Кластеризация и Высокая Доступность ​

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, обычно 9091api.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с):

  1. Лидер обнаруживает таймаут.
  2. Узел помечается как failed.
  3. Камеры, назначенные на этот узел, автоматически перераспределяются на другие живые узлы (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, NotValidForNameHost из rpc_addr отсутствует в SAN сертификата целевого узла
h2 protocol error: FRAME_SIZE_ERROR, GoAwayОдна сторона говорит открытым текстом в TLS-порт: первые байты рукопожатия читаются как заголовок кадра HTTP/2
Узлы числятся офлайн при живых процессахHeartbeat не доходит до лидера — проверьте связность по rpc_addr

Proprietary software.