Кластеризация

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

Параметры cluster_config, шаблоны развёртывания, API управления и устранение неполадок кластера Meridian.

Enterprise

Ключевые понятия кластера описаны на странице Кластеризация. Здесь — справочник для развёртывания и настройки.

Архитектура

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"
    }
  }
}

Поля верхнего уровня

ПолеТипПо умолчаниюОписание
enabledbooleanfalseВключает кластерный режим.
peersarray of host:port—Список пиров для подключения. Пустой список — single-node.
node_namestring—Имя ноды. Переопределяется MERIDIAN_NODE_ID. См. Идентичность ноды.
regionstringпустоРегиональная метка ноды.
data_dirstring/var/lib/meridian/dataКаталог durable-состояния ноды. Переопределяется MERIDIAN_DATA_DIR.
cluster_tokenstringпустоОбщий токен членства в gossip и Raft. Одинаковый на всех нодах. Пустое значение отключает проверку: любой хост с доступом к gossip-порту попадает в список обнаруженных нод. Задавайте токен, если gossip-порт не изолирован в доверенной сети. Смена токена требует перезапуска. Это защита от случайного подключения, а не граница безопасности.
founding_wait_timeout_secondsinteger-1Сколько секунд нода при первом старте ждёт остальные ноды. -1 — ждать бесконечно. N >= 0 — через N секунд процесс завершается с ошибкой. На перезапуски и присоединение не влияет.
gossipobject—Параметры gossip.
raftobject—Параметры Raft.
pubsub_healthobject—Проба готовности через pub/sub.
auto_repairobject—Авто-восстановление топологии.
event_journalobject—Журнал событий отказоустойчивости.
promotionobject—Аварийный промоушен выжившей ноды.
discoveryobject—Не реализовано. См. Service discovery.

gossip

ПолеТипПо умолчаниюОписание
portinteger7946Порт gossip, UDP и TCP.
config.timeout_secondsinteger10Таймаут проб.
config.success_thresholdinteger3Сколько успешных проб подряд делают ноду здоровой.
config.failure_thresholdinteger3Сколько неуспешных проб подряд делают ноду нездоровой.

Если меняете порт, откройте его в сетевых политиках на UDP и TCP.

Идентичность ноды

node_id определяется по приоритету:

  1. Переменная MERIDIAN_NODE_ID.
  2. Поле cluster_config.node_name.
  3. Hostname операционной системы.
  4. 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_portinteger9090Порт gRPC-транспорта Raft.
reservation_ttlduration"1h"Возраст, после которого резервирование бюджета считается осиротевшим и списывается по максимальной стоимости. Слишком маленькое значение автоматически поднимается до окна повторов расчёта.
migration_retentionduration"720h"Сколько хранится история завершённых миграций заказчиков. Незавершённые миграции не удаляются.
olric_bind_portinteger3320Порт Olric — распределённого кэша. Поле cache.bind_port верхнего уровня имеет приоритет.
olric_memberlist_portinteger3322Порт memberlist Olric. Поле cache.memberlist_port верхнего уровня имеет приоритет.
storageobject{"backend": "wal"}Хранение durable-состояния Raft.
timingobjectсм. нижеТайминги Raft.
admissionobjectсм. нижеСброс нагрузки на шарде.

raft.storage

ПолеТипПо умолчаниюОписание
backendstring"wal"wal — локальные сегменты etcd WAL и снапшоты в <data_dir>/raft/budget/<shard_id>/. postgres — хранение в PostgreSQL, допустимо только с PostgreSQL config store. memory — без durability, только для тестов.
wal_segment_bytesinteger67108864Размер сегмента WAL: 64 МиБ, минимум 1 МиБ. Одинаков для всех шардов ноды.
wal_prewritebooleantrueЗаранее заполнять следующие сегменты нулями и сбрасывать на диск. false — только выделять место.

backend переопределяется переменной MERIDIAN_RAFT_STORAGE_BACKEND. Приоритет: переменная → config.json → wal. Неизвестное имя бэкенда — ошибка старта.

Смена бэкенда на работающей ноде — не миграция. Первый запуск wal один раз копирует состояние из таблиц предыдущего формата и оставляет маркер. После этого новые записи хранятся только локально. Откат возможен только к состоянию на момент копирования. Не удаляйте старые таблицы, пока в кластере остаются ноды предыдущей версии.

raft.timing

Блок необязателен. Длительности задаются в формате Go: "100ms", "5s".

ПолеТипПо умолчаниюОписание
tick_intervalduration"100ms"Логический тик Raft. Таймаут выборов = tick_interval × election_ticks.
election_ticksinteger10Сколько тиков без heartbeat follower ждёт перед выборами. Минимум 2.
heartbeat_ticksinteger1Интервал heartbeat лидера в тиках. Строго меньше election_ticks.
dial_timeoutduration"500ms"Таймаут установки соединения с пиром. Меньше таймаута выборов.
send_timeoutduration"500ms"Бюджет на отправку пачки сообщений, включая установку соединения. Меньше таймаута выборов, не меньше dial_timeout.
peer_retry_initialduration"100ms"Первая пауза перед переподключением к пиру.
peer_retry_maxduration"5s"Максимальная пауза между переподключениями.
wal_write_waitduration"0s"Задержка перед durable-записью, чтобы объединить соседние предложения в одну запись. Максимум 10ms, строго меньше половины таймаута выборов. По умолчанию выключено.

wal_write_wait — компромисс. При высоком ровном потоке запросов он снижает число записей на диск лидера. При низком — только добавляет задержку.

Каждое поле переопределяется переменной окружения. Приоритет: переменная → config.json → умолчание.

ПеременнаяПоле
MERIDIAN_RAFT_TICK_INTERVALtiming.tick_interval
MERIDIAN_RAFT_ELECTION_TICKStiming.election_ticks
MERIDIAN_RAFT_HEARTBEAT_TICKStiming.heartbeat_ticks
MERIDIAN_RAFT_DIAL_TIMEOUTtiming.dial_timeout
MERIDIAN_RAFT_SEND_TIMEOUTtiming.send_timeout
MERIDIAN_RAFT_PEER_RETRY_INITIALtiming.peer_retry_initial
MERIDIAN_RAFT_PEER_RETRY_MAXtiming.peer_retry_max
MERIDIAN_RAFT_WAL_WRITE_WAITtiming.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_reservesinteger1024Сколько RESERVE одновременно ждут коммита на шарде. Минимум 16.
commit_latency_softduration"500ms"Задержка коммита (EWMA), с которой начинается отбрасывание.
commit_latency_hardduration"5s"Задержка, при которой доля отбрасывания максимальна. Больше commit_latency_soft и меньше 30s.
max_shed_ratiofloat0.9Максимальная доля отбрасываемых RESERVE. Строго между 0 и 1.
log_backlog_limitinteger20000На сколько записей журнал может опережать применённый индекс. Значение ниже 12 000 — ошибка старта.
apply_batch_bytesinteger1048576Сколько байт закоммиченных записей применяется за цикл. Минимум 65536.
unwind_queue_sizeinteger65536Глубина очереди отброшенных RESERVE, ждущих компенсации. Минимум 1024.

Максимальная доля строго меньше 1: пропущенные запросы дают измерения задержки, по которым шард видит своё восстановление.

Клиент получает 503 с телом overloaded. Retry-After — задержка коммита в секундах, от 1 до 30.

ПеременнаяПоле
MERIDIAN_RAFT_ADMISSION_MAX_INFLIGHT_RESERVESadmission.max_inflight_reserves
MERIDIAN_RAFT_ADMISSION_COMMIT_LATENCY_SOFTadmission.commit_latency_soft
MERIDIAN_RAFT_ADMISSION_COMMIT_LATENCY_HARDadmission.commit_latency_hard
MERIDIAN_RAFT_ADMISSION_MAX_SHED_RATIOadmission.max_shed_ratio
MERIDIAN_RAFT_ADMISSION_LOG_BACKLOG_LIMITadmission.log_backlog_limit
MERIDIAN_RAFT_ADMISSION_APPLY_BATCH_BYTESadmission.apply_batch_bytes
MERIDIAN_RAFT_ADMISSION_UNWIND_QUEUE_SIZEadmission.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.

ПолеТипПо умолчаниюОписание
enabledbooleantrueЗапускает пробу. Работает, если в peers есть хотя бы одна другая нода и включён распределённый кэш.
interval_secondsinteger5Период публикации HealthPing.
stale_threshold_secondsinteger15Через сколько секунд без HealthPing пир считается устаревшим.

auto_repair

Лидер _meta каждые 30 секунд возвращает кластер к объявленной топологии. Шард без кворума он не трогает. Подробнее — Авто-восстановление.

ПолеТипПо умолчаниюОписание
enabledbooleantrueВключает контур. При false не применяются и служебные версии топологии.
stability_windowduration"60s"Сколько нода должна стабильно жить в gossip, прежде чем контур начнёт с ней работать.
cooldownduration"5m"Минимальный интервал между ремонтами.

event_journal

Журнал событий отказоустойчивости в <data_dir>/node.bolt. См. События отказоустойчивости.

ПолеТипПо умолчаниюОписание
retention_daysinteger30Срок хранения событий, от 1 до 365 дней.
max_size_mbinteger64Максимальный размер журнала, от 8 до 1024 МБ.

promotion

ПолеТипПо умолчаниюОписание
min_dead_secondsinteger60Сколько секунд остальные ноды должны быть мертвы, прежде чем выжившую ноду можно превратить в 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_SYNCER1 отключает фоновую синхронизацию бюджетов.

У кластерных переменных нет псевдонимов 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.target

DNS-имена 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 / Activecluster.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 до votercluster.config.update
POST/api/cluster/v1/groups/{group_id}/replicas/{node_name}/reseatВыдать припаркованной реплике новый IDcluster.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/readyReadiness—
GET/internal/liveLiveness—

Эндпоинты шардов и миграций — на странице Шардинг.

Изменяющие топологию эндпоинты требуют заголовка 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 означает идеальное состояние, больше — хуже.

Устранение неполадок

Сетевые порты

ПортПротоколНазначениеМежду
7946TCP и UDPGossipВсе ноды
9090TCPRaft gRPCВсе ноды
3320TCPOlricВсе ноды
3322TCP и UDPMemberlist OlricВсе ноды
8080TCPHTTP APIБалансировщик → ноды и между нодами

API-порт используется и между нодами: через него пересылаются запросы с ?target= и синхронизация LDAP. Без него страницы Events и Shards при выборе другой ноды отвечают 502. Межнодовый трафик не шифруется и несёт учётные данные вызывающего — он рассчитан на доверенную сеть.

Содержание