Кластер
По умолчанию Wippy работает как одна нода. Включение кластера превращает набор нод в единую координированную систему, разделяющую membership, кластерные имена процессов, распределённые блокировки и групповой обмен сообщениями поверх ограниченного ядра консенсуса Raft.
Кластеризация отключена до установки cluster.enabled: true. Всё описанное ниже инертно на одной ноде.
Что даёт кластеризация
- Membership — каждая нода знает живой состав пиров через gossip с быстрым обнаружением сбоев.
- Кластерные имена процессов — регистрация процесса под именем, разрешаемым с любой ноды, с выбором гарантий согласованности (см. Именование).
- Распределённые блокировки —
system.lockобеспечивает кластерное взаимное исключение с автоматическим освобождением при гибели держателя (см. Распределённые блокировки). - Группы процессов — публикация сообщений каждому члену именованной группы на всех нодах (см. Группы процессов).
- Ядро консенсуса — небольшой, ограниченный Raft-кластер обеспечивает линеаризуемую основу, на которой строятся примитивы именования и блокировок.
Архитектура: ограниченный Raft
Включение каждой ноды в Raft плохо масштабируется: лидер реплицирует каждую запись лога на каждый пир, поэтому стоимость простаивающего лидера растёт с размером кластера. Wippy ограничивает Raft фиксированным ядром и позволяет остальным нодам работать через gossip. Каждая нода занимает одну из трёх ролей в конфигурации Raft:
| Роль | Количество (по умолчанию) | В конфиг. Raft | Получает репликацию | Голосует |
|---|---|---|---|---|
| Voter | до 5 (max_voters, нечётное) |
да | да | да |
| Standby | до 4 (max_standbys) |
да | да | нет |
| Client | без ограничений | нет | нет | нет |
- Voters формируют кворум. Записи фиксируются, когда большинство voters подтверждает их. Количество voters всегда нечётное, чтобы большинство было однозначным.
- Standbys — невотирующие члены, полностью реплицированные и готовые к работе. Когда voter уходит, лидер повышает наиболее приоритетный standby на освободившееся место, и кворум восстанавливается без ожидания новой ноды.
- Clients — все ноды за пределами
voters + standbys. Они не входят в конфигурацию Raft, поэтому лидер никогда не отправляет им записи лога. Они участвуют в gossip и направляют записи к члену Raft. Это сохраняет стоимость лидера постоянной (O(1)) независимо от размера кластера.
Поскольку standbys и clients могут поглотить оставшийся флот, кластер из сотен нод всё равно имеет ядро из 5 voters. Ограничения max_voters/max_standbys и делают дизайн «ограниченным».
Выбор voters
Лидер запускает reconciler, который при каждом изменении membership (с задержкой raft.reconcile_debounce, по умолчанию 2с) вычисляет, какие ноды должны быть voters, и применяет минимальный набор операций promote/demote. Выбор детерминирован — каждая нода выводит один и тот же порядок из одного gossip-вида — и управляется тремя gossip-подсказками:
raft.eligible— нода сeligible: falseникогда не выбирается voter (используйте для нод, которые должны оставаться client или standby).raft.priority— меньшее значение предпочтительнее при заполнении слотов voter; при равенстве выбирается по ID ноды.failure_domain— voters распределяются по различным доменам (зонам/стойкам) в первую очередь, чтобы потеря одного домена не уничтожила большинство voters.
Операции применяются в порядке, сохраняющем кворум: сначала добавления и повышения, затем понижения, затем удаления.
Membership и gossip
Membership использует SWIM gossip (HashiCorp memberlist). Каждая нода привязывается к gossip-порту (по умолчанию 7946) и непрерывно обменивается небольшими сообщениями с пирами для обнаружения сбоев и распространения метаданных.
Нода присоединяется, указывая на одну или несколько существующих нод:
cluster:
enabled: true
name: node-2
membership:
join_addrs: "node-1:7946"
Первой ноде не нужны join_addrs — она стартует как seed. Присоединения повторяются с отступом, и нода, оказавшаяся изолированной, периодически пытается переприсоединиться, поэтому нода, перезапущенная с новым IP (обычно в Kubernetes), быстро сходится.
Gossip можно зашифровать общим ключом, встроенным или из файла:
cluster:
membership:
secret_file: /etc/wippy/cluster.key
Изменения membership (NodeJoined, NodeLeft, NodeUpdated) — это события, которые запускают Raft bootstrap, reconciliation voters, синхронизацию групп процессов и автоматическую очистку имён, принадлежащих ушедшей ноде.
Bootstrap
Начальный кластер формируется через gossip, а не по статическому списку пиров. Это следует паттерну bootstrap_expect из Consul/Nomad: каждой стартующей ноде сообщается ожидаемое количество нод, и они ждут, пока не увидят друг друга, прежде чем вместе сформировать кворум.
bootstrap_expect |
Поведение |
|---|---|
0 |
Никогда не self-bootstrap; только присоединяться к уже существующему кластеру |
1 |
Одна нода; немедленный bootstrap с собой как единственным voter |
N |
Ждать, пока N eligible пиров стабильно видны в gossip, затем все выводят одинаковый список voters и формируют кластер |
Для N-нодового bootstrap установите одинаковый bootstrap_expect: N на каждой начальной ноде. Каждая объявляет статус «pre-bootstrap» в gossip; как только ровно N таких пиров видны в течение короткого окна стабильности, каждая нода независимо вычисляет идентичный отсортированный набор voters и формирует кластер. Окно стабильности предотвращает преждевременный bootstrap из-за кратковременного частичного вида.
Ноды, запускающиеся позже, видят уже сформированный кластер и полностью пропускают bootstrap — reconciler лидера добавляет их как voters или standbys.
Ядро консенсуса Raft
Ядро консенсуса бездисковое: логи и снимки Raft хранятся только в памяти, поэтому нет каталога данных для подготовки. При перезапуске нода переприсоединяется к gossip и воспроизводит состояние от пиров. Это намеренно устраняет режимы отказа «персистентность против кворума», которые вводит дисковый Raft, и соответствует модели in-memory систем координации (Erlang global, Akka distributed data). Компромисс: долговечность кластера определяется живым кворумом, а не диском — см. Восстановление.
Raft не открывает собственного порта прослушивания. Он работает по internode-мешу — тем же TCP-соединениям, что используются для relay-трафика между нодами — с мультиплексированием через yamux. Internode-порт выбирается автоматически при запуске (диапазон 7950-7959, затем эфемерный), фиксируется и объявляется в gossip для обеспечения доступности от пиров. Единственный порт, который обычно нужно открыть — gossip.
FSM Raft хранит глобальный реестр имён: активные привязки name -> PID и незавершённые strong-резервирования. Именно это читают и пишут описанные ниже примитивы именования.
Именование и области имён
Процесс может быть зарегистрирован под именем и достигаться по этому имени вместо raw PID. Ключевое решение — область (scope), определяющая гарантию согласованности. Доступны четыре области от самой дешёвой/слабой до сильнейшей:
| Область | Основа | Видимость | Гарантия |
|---|---|---|---|
| Local | карта на ноде | только эта нода | Мгновенно, локально; без координации |
| Eventual | gossip CRDT | кластер | Eventually consistent; сходится после раундов gossip |
| Consistent | Raft | кластер | Линеаризуемые записи; уникальный синглтон в кластере |
| Strong | Raft + подтверждение всех нод | кластер | Consistent плюс каждая живая нода подтверждает до активации имени |
Как выбирать:
- Local — имена, значимые только на одной ноде (вспомогательный per-node процесс). Освобождается при выходе процесса. Нулевая стоимость.
- Eventual — общекластерные имена сервисов, групп и присутствия, где кратковременное устаревание допустимо. Набор привязок полностью реплицируется на каждую ноду, поэтому подходит для ограниченного пространства имён, а не для одного имени на сущность с высокой кардинальностью, такую как процесс на сессию (адресуйте такие напрямую по PID). Когда два источника регистрируют одно имя, разрешение конфликта выбирает победителя, а проигравший процесс получает событие отмены (
process.event.CANCEL) с причинойname revoked: <name>; он продолжает работу и может перерегистрироваться. Имена освобождаются при уходе ноды-владельца. - Consistent — стандартный выбор для кластерных именованных синглтонов. Побеждает первый пишущий: повторная регистрация того же имени на другой PID завершится ошибкой «already exists» и вернёт текущего владельца. Запись требует кворума, поэтому при разделении в меньшинстве она зависает. Чтение идёт с локальной реплики Raft и может отставать от записи на несколько миллисекунд.
- Strong — небольшой набор control-plane синглтонов, где даже мгновенное устаревшее чтение опасно. Поверх гарантии Consistent регистрация открывает резервирование, которое должна подтвердить каждая живая нода до того, как имя станет авторитетным; любая нода, уже имеющая конфликтующую привязку, немедленно отклоняет запрос. Если дедлайн истекает до подтверждения всех нод, регистрация истекает и сообщает, какие ноды отсутствовали. Это основа распределённых блокировок.
Имена освобождаются автоматически: Local — при выходе процесса; Consistent и Strong — при выходе процесса (через мониторинг топологии) и при уходе ноды; Eventual — при уходе ноды. Разрешение для обмена сообщениями (process.send, process.terminate и т.д.) просматривает плоскости по порядку — Consistent, затем Eventual, затем Local — поэтому Consistent-имя перекрывает Eventual с той же строкой.
Lua-интерфейс именования находится на process.registry (register/lookup/unregister с областью) — см. справочник Process.
Группы процессов
Группы процессов — кластерный publish/subscribe и средство управления членством по образцу Erlang pg. Процесс вступает в именованную группу; широковещательное сообщение расходится по internode-мешу к членам группы на всех нодах и доставляется по принципу best-effort. Группы eventually consistent и независимы от Raft — для выбора получателей они используют представление членства из gossip — поэтому продолжают работать даже пока ядро консенсуса сходится.
Типичные операции: вступление/выход из группы, broadcast всем членам (или только локальным), список членов и мониторинг группы на события вступления/выхода. При присоединении новой ноды группы согласовывают членство через прямое handshake, а фоновый anti-entropy цикл устраняет расхождения со временем.
См. Группы процессов для Lua API и тип записи pg.scope для конфигурации.
Распределённые блокировки
system.lock — кластерное взаимное исключение, построенное непосредственно на области Strong. Захват блокировки регистрирует её имя в области Strong на вызывающий процесс; освобождение снимает регистрацию. Поскольку Strong требует подтверждения каждой живой ноды, в кластере может существовать не более одного держателя.
local ok, err = system.lock.acquire("orders.migration")
if ok then
-- критическая секция: только один держатель в кластере
system.lock.release("orders.migration")
end
Захват fail-fast (неблокирующий): если блокировка уже занята, возврат немедленно, поэтому вызывающие реализуют собственный retry/backoff. Блокировка автоматически освобождается при выходе держателя или уходе его ноды — зависших блокировок нет. См. справочник System для точных сигнатур.
Конфигурация
Полный справочник по ключам — в разделе Конфигурация. Минимальные формы:
Одна нода (разработка):
cluster:
enabled: true
name: dev
raft:
bootstrap_expect: 1
Трёхнодовый voting-кластер:
cluster:
enabled: true
name: node-1
failure_domain: us-east-1a
membership:
join_addrs: "node-2:7946,node-3:7946"
secret_file: /etc/wippy/cluster.key
raft:
bootstrap_expect: 3
Только gossip-клиент (присоединяется для именования/обмена, никогда не запускает Raft):
cluster:
enabled: true
name: edge-7
membership:
join_addrs: "node-1:7946,node-2:7946"
raft:
role: client
Порты
| Назначение | Порт | Протокол | Ключ конфигурации |
|---|---|---|---|
| Gossip (membership) | 7946 | TCP + UDP | cluster.membership.bind_port |
| Internode-меш (relay + Raft) | авто | TCP | cluster.internode.bind_port |
Отдельного Raft-порта нет — Raft мультиплексируется по internode-мешу. Internode-порт назначается автоматически и объявляется через gossip, поэтому только gossip-порт требует предсказуемого открытия.
Наблюдаемость
Здоровье кластера доступно через стандартный эндпоинт Prometheus и через проверки готовности.
Ключевые метрики:
| Метрика | Значение |
|---|---|
raft_state |
0 = follower, 1 = candidate, 2 = leader |
raft_term |
Текущий term Raft; быстрый рост сигнализирует о чехарде выборов |
raft_voters / raft_non_voters |
Живые voters и standbys в конфигурации |
raft_leader_changes_total |
Смены лидера; должны быть близки к нулю в здоровом кластере |
raft_voter_churn_burst_total |
Всплески операций добавления/удаления voters; устойчивый churn — тревожный сигнал |
gossip_members{state} |
Количество по состоянию (alive/suspect/dead/left) |
gossip_convergence_seconds |
Время между gossip-событиями |
Встроенные проверки готовности (подключены к liveness-эндпоинту):
- gossip — здоров, пока gossip health score ноды остаётся низким, с окном grace при запуске, чтобы переприсоединяющаяся нода не была убита преждевременно.
- raft last-contact — follower-voter сообщает об ошибке, если долго не слышал лидера; standby терпит более долгий разрыв; лидеры всегда проходят.
- broadcast группы процессов — ошибка, если группа не видит broadcast-трафика длительное время, что сигнализирует о заблокированном event loop или устойчивом разделении.
Восстановление и режимы отказа
Поскольку ядро консенсуса бездисковое, долговечность обеспечивается живым кворумом, а не диском. Практические правила:
- Держите большинство voters живыми. При 5 voters вы выдерживаете 2 одновременных отказа; standbys повышаются для заполнения освободившихся слотов. При потере большинства записи (новые Consistent/Strong-регистрации и захваты блокировок) зависают до восстановления кворума. Существующие имена и поиск продолжают обслуживаться с локальных реплик.
- Лидер проактивно исключает voter, который одновременно молчит по heartbeat и мёртв по gossip, чтобы мёртвый voter не блокировал кворум постоянно, пока повышается standby.
- Для восстановления кластера, потерявшего кворум, перезапустите отказавшие ноды. Они переприсоединяются к gossip, и выжившие члены принимают их обратно. Распределение voters по
failure_domain— именно то, что предотвращает потерю кворума из-за отказа одной зоны.
Смотрите также
- Конфигурация — все ключи конфигурации кластера
- Process — регистрация и разрешение процессов по имени
- System —
system.cluster,system.raft,system.node,system.lock - Наблюдаемость — метрики и health-эндпоинты
- Модель процессов — акторы, PID и обмен сообщениями