Конфигурация
Параметры cluster_config, шаблоны развёртывания, API управления и устранение неполадок кластера Meridian.
Ключевые понятия кластера описаны на странице Кластеризация. Здесь — справочник для развёртывания и настройки.
Архитектура
Gossip и синхронизация
Каждая нода:
- берёт список пиров из
cluster_config.peersи подключается к ним по gossip-протоколу (memberlist); - передаёт метаданные и события по UDP и TCP на gossip-порту;
- участвует в Raft-группах
_metaи шардов.
Если peers пуст или все пиры недоступны, нода работает в single-node mode как один из нормальных режимов.
Через gossip синхронизируются:
- метаданные нод:
node_id, регион; - инвалидации конфигурации (ключи
_invalidate:*) — они запускают hot-reload на других нодах; - записи распределённого KV-хранилища. Конфликты разрешаются по правилу Last-Write-Wins по системным часам.
Last-Write-Wins опирается на системные часы. На всех нодах кластера обязательно синхронизируйте время через NTP.
Условия включения Raft
Raft-координация работает, когда одновременно:
cluster_config.enabled: true;config_storeуказывает на PostgreSQL.
Блок cluster_config.raft необязателен: при включённом кластере недостающие параметры получают значения по умолчанию. В режиме файлового config store Raft не используется.
Каталог данных ноды
Meta-группа топологии и бюджетные шарды хранят durable-состояние в локальном каталоге ноды:
<data_dir>/
node.bolt журнал событий отказоустойчивости
raft/ права 0700
_meta.bolt meta-группа топологии
budget/ бюджетные шарды, по каталогу на шард
shard0/
identity.json привязка ноды, кластера, шарда и реплики
state.bolt applied index, граница компакции, маркер снапшота
wal/ сегменты etcd WAL
snap/ снапшоты с проверкой CRC32CТребования к каталогу:
- Постоянное хранилище на каждой ноде. Каталог должен переживать перезапуск процесса и пода.
emptyDirв Kubernetes не подходит. - Не общий volume. Каждая нода владеет своим каталогом. Общий NFS или один PVC на все реплики недопустим.
- Привязка identity.
raft/_meta.boltзакрепляет{node_name, cluster_id, member_id}, каждыйidentity.json—{node_name, cluster_id, shard_id, replica_id}. Если каталог стёрт или подключён не к той ноде, старт завершается фатальной ошибкой. Это защита от небезопасного возврата в кворум. - Две единицы резервного копирования. Бэкап базы данных не содержит Raft-журнал. Для бэкапа узла нужно копировать всю директории. Размер локальных данных очень компактент, так как содержит только журнал еще не сохраненных в PostgreSQL изменений бюджета и топологию кластера. Журнал операций бюджета сбрасывается каждые 10 секунд при настройки по умолчанию.
Потеря всего raft/ — потеря identity ноды. Нода входит в кластер заново под новыми идентификаторами, реплики шардов реплицируются кворумом. Потеря одного raft/budget/<shard_id> при живом _meta.bolt паркует реплику — см. Локальная реплика на ноде.
Путь каталога определяется так: переменная MERIDIAN_DATA_DIR → поле data_dir → /var/lib/meridian/data. Значение указывает на каталог, а не на файл.
Docker-образ Meridian задаёт MERIDIAN_DATA_DIR=/app/data. Поле data_dir в контейнере игнорируется: монтируйте постоянный volume в /app/data.
Размер кластера
Документ топологии допускает не более 5 нод. Фактор репликации не превышает 5.
| Размер | Переживает отказ | Сценарий |
|---|---|---|
| 3 ноды | 1 ноды | Базовый production |
| 5 нод | 2 нод | Повышенная отказоустойчивость |
Используйте нечётное число нод: чётное не повышает отказоустойчивость.
Первый старт кластера
При первом старте с пустой базой кластер создаёт топологию v1: список нод, группу _meta и шард shard0. Это происходит, только когда каждая нода из peers запустилась и активировалась. До этого нода:
- отвечает
200на/internal/liveи/health, поэтому оркестратор её не перезапускает; - отвечает
503на/internal/readyс телом{"status":"not_ready","reason":"greenfield founding: waiting for all configured cluster members — not yet ready"}; - не запускает плагины и не обслуживает
/api.
Ожидание завершается само, когда запустятся недостающие ноды. Ограничить его можно полем founding_wait_timeout_seconds.
Ноды первого старта запускайте одновременно. Схема «запустить первую, дождаться Ready, запустить вторую» не работает. Одна нода из трёх не набирает кворум _meta и завершается с ошибкой node activation: metaraft: no meta leader for activation. В Kubernetes это означает podManagementPolicy: Parallel.
Обновление работающего кластера, перезапуски и присоединение новой ноды это ожидание не затрагивают.
Параметры cluster_config
Минимальный пример
{
"cluster_config": {
"enabled": true,
"node_name": "node-1.meridian.internal",
"region": "eu-central-1",
"cluster_token": "change_me",
"peers": [
"node-1.meridian.internal:7946",
"node-2.meridian.internal:7946",
"node-3.meridian.internal:7946"
],
"gossip": {
"port": 7946,
"config": {
"timeout_seconds": 10,
"success_threshold": 3,
"failure_threshold": 3
}
},
"raft": {
"grpc_port": 9090,
"reservation_ttl": "1h"
}
}
}Поля верхнего уровня
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
enabled | boolean | false | Включает кластерный режим. |
peers | array of host:port | — | Список пиров для подключения. Пустой список — single-node. |
node_name | string | — | Имя ноды. Переопределяется MERIDIAN_NODE_ID. См. Идентичность ноды. |
region | string | пусто | Региональная метка ноды. |
data_dir | string | /var/lib/meridian/data | Каталог durable-состояния ноды. Переопределяется MERIDIAN_DATA_DIR. |
cluster_token | string | пусто | Общий токен членства в gossip и Raft. Одинаковый на всех нодах. Пустое значение отключает проверку: любой хост с доступом к gossip-порту попадает в список обнаруженных нод. Задавайте токен, если gossip-порт не изолирован в доверенной сети. Смена токена требует перезапуска. Это защита от случайного подключения, а не граница безопасности. |
founding_wait_timeout_seconds | integer | -1 | Сколько секунд нода при первом старте ждёт остальные ноды. -1 — ждать бесконечно. N >= 0 — через N секунд процесс завершается с ошибкой. На перезапуски и присоединение не влияет. |
gossip | object | — | Параметры gossip. |
raft | object | — | Параметры Raft. |
pubsub_health | object | — | Проба готовности через pub/sub. |
auto_repair | object | — | Авто-восстановление топологии. |
event_journal | object | — | Журнал событий отказоустойчивости. |
promotion | object | — | Аварийный промоушен выжившей ноды. |
discovery | object | — | Не реализовано. См. Service discovery. |
gossip
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
port | integer | 7946 | Порт gossip, UDP и TCP. |
config.timeout_seconds | integer | 10 | Таймаут проб. |
config.success_threshold | integer | 3 | Сколько успешных проб подряд делают ноду здоровой. |
config.failure_threshold | integer | 3 | Сколько неуспешных проб подряд делают ноду нездоровой. |
Если меняете порт, откройте его в сетевых политиках на UDP и TCP.
Идентичность ноды
node_id определяется по приоритету:
- Переменная
MERIDIAN_NODE_ID. - Поле
cluster_config.node_name. - Hostname операционной системы.
node1.
В кластерном режиме имя из пунктов 3 и 4 — ошибка старта. Задайте MERIDIAN_NODE_ID или node_name явно.
node_id обязан точно совпадать с хостом одной из записей peers — часть до двоеточия, включая домен. Иначе нода не запустится:
cluster_config: resolved node_id ... does not match any hostname in cluster_config.peers.
Имена clusterLeader и имена, начинающиеся с shardLeader:, зарезервированы под селектор ?target=. В режимах с PostgreSQL нода с таким именем не запускается, а POST /api/cluster/v1/nodes отвечает 400.
raft
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
grpc_port | integer | 9090 | Порт gRPC-транспорта Raft. |
reservation_ttl | duration | "1h" | Возраст, после которого резервирование бюджета считается осиротевшим и списывается по максимальной стоимости. Слишком маленькое значение автоматически поднимается до окна повторов расчёта. |
migration_retention | duration | "720h" | Сколько хранится история завершённых миграций заказчиков. Незавершённые миграции не удаляются. |
olric_bind_port | integer | 3320 | Порт Olric — распределённого кэша. Поле cache.bind_port верхнего уровня имеет приоритет. |
olric_memberlist_port | integer | 3322 | Порт memberlist Olric. Поле cache.memberlist_port верхнего уровня имеет приоритет. |
storage | object | {"backend": "wal"} | Хранение durable-состояния Raft. |
timing | object | см. ниже | Тайминги Raft. |
admission | object | см. ниже | Сброс нагрузки на шарде. |
raft.storage
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
backend | string | "wal" | wal — локальные сегменты etcd WAL и снапшоты в <data_dir>/raft/budget/<shard_id>/. postgres — хранение в PostgreSQL, допустимо только с PostgreSQL config store. memory — без durability, только для тестов. |
wal_segment_bytes | integer | 67108864 | Размер сегмента WAL: 64 МиБ, минимум 1 МиБ. Одинаков для всех шардов ноды. |
wal_prewrite | boolean | true | Заранее заполнять следующие сегменты нулями и сбрасывать на диск. false — только выделять место. |
backend переопределяется переменной MERIDIAN_RAFT_STORAGE_BACKEND. Приоритет: переменная → config.json → wal. Неизвестное имя бэкенда — ошибка старта.
Смена бэкенда на работающей ноде — не миграция. Первый запуск wal один раз копирует состояние из таблиц предыдущего формата и оставляет маркер. После этого новые записи хранятся только локально. Откат возможен только к состоянию на момент копирования. Не удаляйте старые таблицы, пока в кластере остаются ноды предыдущей версии.
raft.timing
Блок необязателен. Длительности задаются в формате Go: "100ms", "5s".
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
tick_interval | duration | "100ms" | Логический тик Raft. Таймаут выборов = tick_interval × election_ticks. |
election_ticks | integer | 10 | Сколько тиков без heartbeat follower ждёт перед выборами. Минимум 2. |
heartbeat_ticks | integer | 1 | Интервал heartbeat лидера в тиках. Строго меньше election_ticks. |
dial_timeout | duration | "500ms" | Таймаут установки соединения с пиром. Меньше таймаута выборов. |
send_timeout | duration | "500ms" | Бюджет на отправку пачки сообщений, включая установку соединения. Меньше таймаута выборов, не меньше dial_timeout. |
peer_retry_initial | duration | "100ms" | Первая пауза перед переподключением к пиру. |
peer_retry_max | duration | "5s" | Максимальная пауза между переподключениями. |
wal_write_wait | duration | "0s" | Задержка перед durable-записью, чтобы объединить соседние предложения в одну запись. Максимум 10ms, строго меньше половины таймаута выборов. По умолчанию выключено. |
wal_write_wait — компромисс. При высоком ровном потоке запросов он снижает число записей на диск лидера. При низком — только добавляет задержку.
Каждое поле переопределяется переменной окружения. Приоритет: переменная → config.json → умолчание.
| Переменная | Поле |
|---|---|
MERIDIAN_RAFT_TICK_INTERVAL | timing.tick_interval |
MERIDIAN_RAFT_ELECTION_TICKS | timing.election_ticks |
MERIDIAN_RAFT_HEARTBEAT_TICKS | timing.heartbeat_ticks |
MERIDIAN_RAFT_DIAL_TIMEOUT | timing.dial_timeout |
MERIDIAN_RAFT_SEND_TIMEOUT | timing.send_timeout |
MERIDIAN_RAFT_PEER_RETRY_INITIAL | timing.peer_retry_initial |
MERIDIAN_RAFT_PEER_RETRY_MAX | timing.peer_retry_max |
MERIDIAN_RAFT_WAL_WRITE_WAIT | timing.wal_write_wait |
Несогласованные значения не дают ноде запуститься. Правила: heartbeat_ticks < election_ticks; dial_timeout и send_timeout строго меньше таймаута выборов; dial_timeout ≤ send_timeout; tick_interval ≥ 10ms; peer_retry_initial ≤ peer_retry_max; wal_write_wait ≤ 10ms и меньше половины таймаута выборов; все длительности положительные, wal_write_wait может быть нулём. Если election_ticks < 10 × heartbeat_ticks, в лог пишется предупреждение.
raft.admission
Блок необязателен. Он задаёт, когда шард начинает отвечать 503 с заголовком Retry-After вместо того, чтобы копить очередь резервирований.
Отбрасываются только RESERVE. FINALIZE, RELEASE, компенсации, конфигурационные и миграционные предложения принимаются всегда: они закрывают уже выполненную работу.
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
max_inflight_reserves | integer | 1024 | Сколько RESERVE одновременно ждут коммита на шарде. Минимум 16. |
commit_latency_soft | duration | "500ms" | Задержка коммита (EWMA), с которой начинается отбрасывание. |
commit_latency_hard | duration | "5s" | Задержка, при которой доля отбрасывания максимальна. Больше commit_latency_soft и меньше 30s. |
max_shed_ratio | float | 0.9 | Максимальная доля отбрасываемых RESERVE. Строго между 0 и 1. |
log_backlog_limit | integer | 20000 | На сколько записей журнал может опережать применённый индекс. Значение ниже 12 000 — ошибка старта. |
apply_batch_bytes | integer | 1048576 | Сколько байт закоммиченных записей применяется за цикл. Минимум 65536. |
unwind_queue_size | integer | 65536 | Глубина очереди отброшенных RESERVE, ждущих компенсации. Минимум 1024. |
Максимальная доля строго меньше 1: пропущенные запросы дают измерения задержки, по которым шард видит своё восстановление.
Клиент получает 503 с телом overloaded. Retry-After — задержка коммита в секундах, от 1 до 30.
| Переменная | Поле |
|---|---|
MERIDIAN_RAFT_ADMISSION_MAX_INFLIGHT_RESERVES | admission.max_inflight_reserves |
MERIDIAN_RAFT_ADMISSION_COMMIT_LATENCY_SOFT | admission.commit_latency_soft |
MERIDIAN_RAFT_ADMISSION_COMMIT_LATENCY_HARD | admission.commit_latency_hard |
MERIDIAN_RAFT_ADMISSION_MAX_SHED_RATIO | admission.max_shed_ratio |
MERIDIAN_RAFT_ADMISSION_LOG_BACKLOG_LIMIT | admission.log_backlog_limit |
MERIDIAN_RAFT_ADMISSION_APPLY_BATCH_BYTES | admission.apply_batch_bytes |
MERIDIAN_RAFT_ADMISSION_UNWIND_QUEUE_SIZE | admission.unwind_queue_size |
Некорректное значение — ошибка старта. Несогласованный набор значений останавливает старт, только если Raft действительно включён. Иначе пишется предупреждение.
Состояние сброса нагрузки на ноде: GET /api/cluster/v1/shards/{shard_id}/admission и GET /api/cluster/v1/node/metrics. В Prometheus — семейство meridian_raft_admission_*.
Гео-распределённый кластер
Значения по умолчанию рассчитаны на локальную сеть с задержкой меньше миллисекунды. Для кластера между регионами поднимайте значения вместе:
- интервал heartbeat ≈ p99 RTT между самыми удалёнными нодами;
- таймаут выборов ≥ 10 × интервал heartbeat;
dial_timeoutиsend_timeoutменьше таймаута выборов, но заметно больше p99 RTT.
Пример для p99 RTT около 50 мс:
{
"cluster_config": {
"raft": {
"timing": {
"tick_interval": "50ms",
"election_ticks": 20,
"heartbeat_ticks": 1,
"dial_timeout": "800ms",
"send_timeout": "800ms"
}
}
}
}Больший таймаут выборов — более долгое переключение лидера. Значения по умолчанию между регионами — постоянные ложные перевыборы. Итоговые значения нода пишет в лог при старте.
pubsub_health
Ноды публикуют сообщения HealthPing в канал _pubsub_health. Проба готовности открывается, когда нода услышала каждого ожидаемого пира. Результат попадает в поле cluster.pubsub ответа /health и в /internal/ready.
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
enabled | boolean | true | Запускает пробу. Работает, если в peers есть хотя бы одна другая нода и включён распределённый кэш. |
interval_seconds | integer | 5 | Период публикации HealthPing. |
stale_threshold_seconds | integer | 15 | Через сколько секунд без HealthPing пир считается устаревшим. |
auto_repair
Лидер _meta каждые 30 секунд возвращает кластер к объявленной топологии. Шард без кворума он не трогает. Подробнее — Авто-восстановление.
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
enabled | boolean | true | Включает контур. При false не применяются и служебные версии топологии. |
stability_window | duration | "60s" | Сколько нода должна стабильно жить в gossip, прежде чем контур начнёт с ней работать. |
cooldown | duration | "5m" | Минимальный интервал между ремонтами. |
event_journal
Журнал событий отказоустойчивости в <data_dir>/node.bolt. См. События отказоустойчивости.
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
retention_days | integer | 30 | Срок хранения событий, от 1 до 365 дней. |
max_size_mb | integer | 64 | Максимальный размер журнала, от 8 до 1024 МБ. |
promotion
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
min_dead_seconds | integer | 60 | Сколько секунд остальные ноды должны быть мертвы, прежде чем выжившую ноду можно превратить в single-node кластер. От 10 до 3600. |
Service discovery
Автоматическое обнаружение нод не реализовано и не запланировано. cluster_config.discovery.enabled: true приводит к ошибке старта:
service discovery is not implemented, use static peers: <type>.
Используйте статический список peers во всех сценариях, включая Kubernetes: StatefulSet даёт стабильные DNS-имена.
Переменные окружения
| Переменная | Назначение |
|---|---|
MERIDIAN_NODE_ID | Имя ноды. |
MERIDIAN_DATA_DIR | Каталог durable-состояния ноды. |
MERIDIAN_RAFT_STORAGE_BACKEND | Бэкенд хранения Raft. |
MERIDIAN_RAFT_*, MERIDIAN_RAFT_ADMISSION_* | Тайминги и сброс нагрузки, см. таблицы выше. |
MERIDIAN_FORCE_NEW_CLUSTER | Офлайн-восстановление выжившей ноды в формате <cluster_id>/<node_name>. |
MERIDIAN_DISABLE_BUDGET_SYNCER | 1 отключает фоновую синхронизацию бюджетов. |
У кластерных переменных нет псевдонимов BIFROST_*.
Шаблоны развёртывания
Все шаблоны используют образ Enterprise ghcr.io/neria-cloud/meridian-ent. Образ:
- слушает порт
8080на0.0.0.0; - читает конфигурацию из
/app/data/config.json; - хранит durable-состояние в
/app/data(MERIDIAN_DATA_DIR), полеdata_dirне нужно; - работает от пользователя
1000:0.
Docker Compose
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: meridian
POSTGRES_USER: meridian
POSTGRES_PASSWORD: change_me
volumes:
- postgres_data:/var/lib/postgresql/data
meridian-1:
image: ghcr.io/neria-cloud/meridian-ent:latest
hostname: meridian-1
environment:
MERIDIAN_NODE_ID: meridian-1
volumes:
- ./data-1:/app/data
ports:
- "8081:8080"
meridian-2:
image: ghcr.io/neria-cloud/meridian-ent:latest
hostname: meridian-2
environment:
MERIDIAN_NODE_ID: meridian-2
volumes:
- ./data-2:/app/data
ports:
- "8082:8080"
meridian-3:
image: ghcr.io/neria-cloud/meridian-ent:latest
hostname: meridian-3
environment:
MERIDIAN_NODE_ID: meridian-3
volumes:
- ./data-3:/app/data
ports:
- "8083:8080"
volumes:
postgres_data:В каждом каталоге ./data-N/ создайте config.json с одинаковым cluster_config:
{
"cluster_config": {
"enabled": true,
"region": "local",
"cluster_token": "change_me",
"peers": [
"meridian-1:7946",
"meridian-2:7946",
"meridian-3:7946"
],
"gossip": {
"port": 7946
},
"raft": {
"grpc_port": 9090
}
},
"config_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "postgres",
"port": "5432",
"user": "meridian",
"password": "change_me",
"db_name": "meridian",
"ssl_mode": "disable"
}
}
}Каталоги ./data-N должны быть доступны на запись пользователю с UID 1000.
Kubernetes (StatefulSet)
StatefulSet с headless-сервисом даёт стабильные DNS-имена meridian-0.meridian-cluster.meridian.svc.cluster.local. Эти же имена используются и в peers, и в MERIDIAN_NODE_ID.
apiVersion: v1
kind: ConfigMap
metadata:
name: meridian-config
namespace: meridian
data:
config.json: |
{
"cluster_config": {
"enabled": true,
"region": "eu-central-1",
"cluster_token": "change_me",
"peers": [
"meridian-0.meridian-cluster.meridian.svc.cluster.local:7946",
"meridian-1.meridian-cluster.meridian.svc.cluster.local:7946",
"meridian-2.meridian-cluster.meridian.svc.cluster.local:7946"
],
"gossip": { "port": 7946 },
"raft": { "grpc_port": 9090 }
},
"config_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "postgres.meridian.svc.cluster.local",
"port": "5432",
"user": "meridian",
"password": "change_me",
"db_name": "meridian",
"ssl_mode": "require"
}
}
}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: meridian
namespace: meridian
spec:
serviceName: meridian-cluster
replicas: 3
# Обязательно для первого старта: при OrderedReady под-1 не создаётся,
# пока под-0 не станет Ready, а под-0 ждёт все ноды.
podManagementPolicy: Parallel
selector:
matchLabels:
app: meridian
template:
metadata:
labels:
app: meridian
spec:
securityContext:
fsGroup: 0
containers:
- name: meridian
image: ghcr.io/neria-cloud/meridian-ent:latest
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: MERIDIAN_NODE_ID
value: "$(POD_NAME).meridian-cluster.meridian.svc.cluster.local"
ports:
- containerPort: 8080
name: http
- containerPort: 7946
name: gossip-tcp
protocol: TCP
- containerPort: 7946
name: gossip-udp
protocol: UDP
- containerPort: 9090
name: raft-grpc
- containerPort: 3320
name: olric
- containerPort: 3322
name: olric-ml
volumeMounts:
- name: data
mountPath: /app/data
- name: config
mountPath: /app/data/config.json
subPath: config.json
livenessProbe:
httpGet:
path: /internal/live
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /internal/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: config
configMap:
name: meridian-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: meridian-cluster
namespace: meridian
spec:
clusterIP: None
publishNotReadyAddresses: true
selector:
app: meridian
ports:
- port: 8080
name: http
- port: 7946
name: gossip-tcp
protocol: TCP
- port: 7946
name: gossip-udp
protocol: UDP
- port: 9090
name: raft-grpc
- port: 3320
name: olric
- port: 3322
name: olric-ml
---
apiVersion: v1
kind: Service
metadata:
name: meridian
namespace: meridian
spec:
type: LoadBalancer
selector:
app: meridian
ports:
- port: 80
targetPort: 8080
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: meridian-pdb
namespace: meridian
spec:
minAvailable: 2
selector:
matchLabels:
app: meridianКаждому поду — свой PVC. Том /app/data хранит топологию и Raft-журнал шардов, его размер и IOPS — часть профиля нагрузки. Общий PVC на все поды не работает, emptyDir не сохраняет данные. Потеря тома вызывает фатальную ошибку identity при старте.
publishNotReadyAddresses: true нужен при первом старте: пока ноды не Ready, их DNS-имена должны разрешаться, иначе они не найдут друг друга.
Виртуальные машины (systemd)
{
"cluster_config": {
"enabled": true,
"region": "eu-central-1",
"data_dir": "/var/lib/meridian/data",
"cluster_token": "change_me",
"peers": [
"meridian-1.internal:7946",
"meridian-2.internal:7946",
"meridian-3.internal:7946"
],
"gossip": { "port": 7946 },
"raft": { "grpc_port": 9090 }
}
}Файл кладите в /var/lib/meridian/config.json. Блок config_store — как в примере для Docker Compose.
# /etc/systemd/system/meridian.service
[Unit]
Description=Meridian Gateway
After=network.target
[Service]
Type=simple
User=meridian
Group=meridian
Environment="MERIDIAN_NODE_ID=meridian-1.internal"
ExecStart=/usr/local/bin/meridian -app-dir /var/lib/meridian -host 0.0.0.0 -port 8080
Restart=always
RestartSec=10
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/meridian
[Install]
WantedBy=multi-user.targetDNS-имена meridian-{1,2,3}.internal должны разрешаться на всех нодах. MERIDIAN_NODE_ID на каждой ноде — её имя из peers.
API управления
| Метод | Путь | Назначение | Право |
|---|---|---|---|
GET | /api/cluster/v1/status | Статус кластера | cluster.config.read |
GET | /api/cluster/v1/topology | Документ Latest и курсоры Latest / Target / Active | cluster.config.read |
GET | /api/cluster/v1/topology/versions | Журнал версий топологии | cluster.config.read |
GET | /api/cluster/v1/topology/versions/{version} | Одна версия | cluster.config.read |
GET | /api/cluster/v1/topology/versions/{version}/plan | План применения версии без изменений | cluster.config.read |
POST | /api/cluster/v1/topology/versions/{version}/apply | Запустить применение версии | cluster.config.update |
POST | /api/cluster/v1/topology/rollback | Отменить сохранённую версию | cluster.config.update |
GET | /api/cluster/v1/topology/executions/{execution_id} | Состояние применения | cluster.config.read |
GET | /api/cluster/v1/nodes | Ноды документа | cluster.config.read |
GET | /api/cluster/v1/nodes/{name} | Одна нода | cluster.config.read |
POST | /api/cluster/v1/nodes | Добавить или повторно допустить ноду: {name, replace_existing} | cluster.config.update |
POST | /api/cluster/v1/nodes/{name}/drain | Дренировать ноду | cluster.config.update |
POST | /api/cluster/v1/nodes/{name}/activate | Активировать ноду | cluster.config.update |
POST | /api/cluster/v1/nodes/{name}/exclude | Исключить ноду | cluster.config.update |
GET, PUT | /api/cluster/v1/placement | Правило размещения реплик | cluster.config.read / cluster.config.update |
POST | /api/cluster/v1/groups/{group_id}/transfer-leader | Передать лидерство группы | cluster.config.update |
PATCH | /api/cluster/v1/groups/{group_id}/replicas/{node_name} | Повысить learner до voter | cluster.config.update |
POST | /api/cluster/v1/groups/{group_id}/replicas/{node_name}/reseat | Выдать припаркованной реплике новый ID | cluster.config.update |
POST | /api/cluster/v1/serve-override | Аварийно разрешить приём трафика без кворума | cluster.config.update |
POST | /api/cluster/v1/force-new | Аварийный промоушен выжившей ноды | cluster.config.update |
GET | /api/cluster/v1/events | События отказоустойчивости ноды | cluster.config.read |
GET | /api/cluster/v1/node/metrics | Нагрузка ноды и её шардов | cluster.config.read |
POST | /api/cluster/v1/budget-syncer/trigger | Внеплановая синхронизация бюджетов | cluster.config.update |
POST | /api/cluster/v1/shards/refresh | Перечитать реестр шардов | cluster.config.update |
GET | /health | Сводный health, включая cluster.pubsub | — |
GET | /internal/ready | Readiness | — |
GET | /internal/live | Liveness | — |
Эндпоинты шардов и миграций — на странице Шардинг.
Изменяющие топологию эндпоинты требуют заголовка If-Match с текущей Latest-версией: без него — 428, с устаревшей — 412. dry_run=true возвращает версию и план с 200 без записи. apply=false сохраняет версию без применения и возвращает 200. По умолчанию сервер сохраняет версию, запускает применение в фоне и возвращает 202 с execution_id.
topology.discovered[] — ноды, которые кластер видит, но документ не размещает. source — канал обнаружения (gossip, parked, gossip+parked). reason — что даст добавление: new — просто добавить, excluded_returning — выдать новую identity и пересобрать реплики. replace_existing передаётся в POST /api/cluster/v1/nodes как есть.
Запрос к конкретной ноде: ?target=
Селектор ?target= выполняет запрос на выбранной ноде, куда бы его ни направил балансировщик. Его принимают 14 маршрутов: topology/versions, topology/versions/{version}, topology/versions/{version}/plan, topology/executions/{id}, events, shards, shards/{id}, shards/{id}/counters, shards/{id}/admission, node/metrics, shards/refresh, shards/{id}/stop, shards/{id}/start и groups/{group_id}/replicas/{node_name}/reseat.
| Значение | Кто отвечает |
|---|---|
| не задано | Нода, принявшая запрос |
<имя ноды> | Указанная нода |
clusterLeader | Лидер _meta |
shardLeader:<shard_id> | Лидер шарда |
Ответ несёт заголовок X-Meridian-Served-By с именем ответившей ноды. Остальные маршруты /api/cluster/v1 с ?target= отвечают 400. Коды ошибок: 400 — shardLeader: без идентификатора; 503 — цель не найдена; 502 — цель недоступна.
Пересылка требует глобального права cluster.config.read. Запрос идёт на API-порт ноды, по умолчанию 8080, — откройте его между нодами.
Ответ /api/cluster/v1/status
{
"enabled": true,
"mode": "cluster",
"active_nodes": 3,
"this_node": {
"node_id": "meridian-1.internal",
"region": "eu-central-1",
"gossip_port": 7946
},
"members": [
{
"node_id": "meridian-1.internal",
"address": "10.0.1.10:7946",
"region": "eu-central-1",
"state": "alive",
"is_self": true
}
],
"config": {
"gossip_port": 7946,
"peers": ["meridian-1.internal:7946", "meridian-2.internal:7946", "meridian-3.internal:7946"],
"region": "eu-central-1",
"timeout_seconds": 10
},
"gossip_health": {
"health_score": 0,
"num_members": 3
}
}Дополнительные поля ответа:
mode—cluster, если активных нод больше одной, иначеsingle-node;serving—{ready, reason}: принимает ли нода трафик;topology— документ, курсоры версий, ноды,_metaи шарды с лидерами и здоровьем, состояние авто-восстановления (reconciliation), обнаруженные ноды (discovered);promotion— взведённый аварийный промоушен;local_shards— локальные реплики шардов на этой ноде;gossip_health.health_score—0означает идеальное состояние, больше — хуже.
Устранение неполадок
Сетевые порты
| Порт | Протокол | Назначение | Между |
|---|---|---|---|
7946 | TCP и UDP | Gossip | Все ноды |
9090 | TCP | Raft gRPC | Все ноды |
3320 | TCP | Olric | Все ноды |
3322 | TCP и UDP | Memberlist Olric | Все ноды |
8080 | TCP | HTTP API | Балансировщик → ноды и между нодами |
API-порт используется и между нодами: через него пересылаются запросы с ?target= и синхронизация LDAP. Без него страницы Events и Shards при выборе другой ноды отвечают 502. Межнодовый трафик не шифруется и несёт учётные данные вызывающего — он рассчитан на доверенную сеть.