0072. HA на одной площадке: active/passive на синхронной реплике тома
Статус: accepted. Owner: Ian Panaev. Дата: 2026-10-10.
Контекст
Владелец выбрал первый профиль отказоустойчивости (обсуждение 2026-10-10):
- цель — быстрое восстановление без потери данных на одной площадке; потеря площадки по-прежнему закрывается зеркалами (ADR 0058) и резервными копиями;
- подтверждение записи — только после двух устойчивых копий; при потере второй копии запись останавливается (RPO = 0 для подтверждённого);
- арбитраж и fencing — доступны управление питанием (IPMI/iDRAC/iLO, PDU), API гипервизора и отдельный узел-свидетель;
- уровень — production: каждое поведение при отказе описано, проверено на стенде и видно оператору.
Сейчас Arkvory — один писатель на одной PostgreSQL. Владение хранилищем подтверждается раз в 2 секунды и теряется необратимо (ADR 0042), службы перезапускаются после падения и зависания (ADR 0067). Блокеры HA из аудита: синхронная копия байтов и каталога, fencing при переключении, HA PostgreSQL, защита от очистки ещё не скопированного. TWO_NODE_PLAN предлагал active/passive на DRBD, но без принятого решения.
Решение
Топология
Только Linux. Узлы с данными держат одинаковую копию одного блочного тома (DRBD 9, protocol C). На томе — весь корень установки: PGDATA, хранилище, конфигурация, релизы, журналы. Каталог и байты физически не расходятся, PostgreSQL и писатель Arkvory остаются в единственном экземпляре: код блокировок, поколений и владения не меняется.
| Профиль | Узлы с данными | Свидетель | Запись при отказе одного узла с данными |
|---|---|---|---|
ha-2 | 2 | обязателен (без данных, corosync-qnetd + DRBD diskless tiebreaker) | останавливается до возврата копии или решения оператора |
ha-3 | 3 | не нужен | продолжается: две копии остаются |
Требуемое число актуальных копий для подтверждения записи — 2 в обоих профилях (required_copies). ha-3 — рекомендуемый production-профиль; ha-2 дешевле и честно останавливает запись.
Решение об активном узле принимает Pacemaker + Corosync с кворумом, а не Arkvory. Своего leader election нет (ADR 0042). Группа ресурсов, по порядку: fencing прежнего активного → DRBD Promoted → файловая система → PostgreSQL → arkvory-api, arkvory-worker, arkvory-backup → плавающий IP. Таймер обновлений и вебхуки работают только на активном узле, потому что их состояние на томе.
Fencing обязателен (stonith-enabled=true, fencing resource-and-stonith в DRBD): агент питания (fence_ipmilan, fence_redfish, PDU) или гипервизора (fence_pve, fence_vmware_rest, fence_xvm/fence_virsh). Без проверенного fencing кластер не запускается: arkvory cluster-check это проверяет. SSH-команда остановки fencing'ом не считается.
Как запись узнаёт о числе копий
Источник правды — состояние ядра DRBD в момент проверки, а не журнал событий: события приходят с задержкой, и проверка по ним могла бы пропустить только что случившуюся потерю копии.
Актуальная копия — локальный диск UpToDate и каждый сосед, у которого соединение установлено (Established), а диск UpToDate. Protocol C завершает запись только после записи на всех подключённых актуальных соседей; сосед, пропустивший хоть одну запись, перестаёт быть UpToDate до окончания ресинхронизации. Отсюда ключевое свойство: если сразу после завершения записи актуальных копий не меньше двух, эта запись есть минимум в двух копиях.
Состояние ядра читает служба arkvory-replica (root, только на узлах кластера): drbdsetup status --json по запросу через Unix-сокет /run/arkvory/replica.sock (группа arkvory, только чтение). Одновременные запросы объединяются, но ответ выдаётся только из чтения, начатого после запроса: устаревший снимок не может подтвердить запись. Служба также применяет разрешение оператора на одну копию (ниже) и отдаёт { copies, required, singleCopyUntil }.
Arkvory применяет правило подтверждения ко всем изменяющим запросам (/api/v1, реестр OCI, Git LFS, npm):
- до обработки — по снимку не старше секунды: если
copies < required, сразу503 unavailable, reasonreplication_degraded,Retry-After; - после фиксации в PostgreSQL и
fsyncбайтов, перед ответом 2xx — новый запрос к службе. Если копий меньше требуемых или служба не ответила за 2 секунды, клиент получает503 replication_degraded, а не 2xx. Операция могла остаться в одной копии, но не подтверждена; изменяющие операции идемпотентны или сверяются (ADR 0051), и повтор после восстановления безопасен. - Статус задачи завершения (
GET /api/v1/jobs/{id}) — единственное чтение, которое по контракту является подтверждением: клиент узнаёт из него, что большая загрузка опубликована. Оно проверяется так же, как ответ на запись.
Без настроенного сокета (standalone) правило не действует: одна копия — это и есть standalone.
Исключения, перечисленные в коде: вход и выход (сессия — не данные), отзыв в хаб (нужен именно во время инцидента), пакетный запрос Git LFS (по нему же идёт скачивание; сама загрузка объекта проверяется).
Подтверждённое (2xx) поэтому всегда лежит минимум в двух копиях: RPO = 0 для того, что клиент получил как успешное. Чтение — скачивание, каталог, консоль — продолжается, пока активный узел жив.
Fencing и кворум остаются за Pacemaker и штатным crm-fence-peer: правило подтверждения защищает от потери данных, fencing — от двух писателей.
Деградация, решения оператора и возврат
ha-2, отказал резервный: активный продолжает отдавать файлы, запись — 503 до окончания ресинхронизации вернувшегося узла, затем возобновляется сама.ha-2, отказал активный: fencing → переход на второй узел → чтение восстанавливается; запись остановлена до возврата второй копии.- Оператор может явно разрешить запись в одну копию:
arkvory cluster-single-copy --until <срок> --reason "<текст>", только пока копий не хватает. Решение (срок, причина, время) хранится на томе вconfig/single-copy.jsonи попадает в журнал systemd; пока оно действует,required= 1, это видно баннером в консоли, метрикой и алертом. По истечении срока или при возврате копии решение снимается само: следующая потеря снова останавливает запись. ha-3: отказ одного узла запись не останавливает. Отказ второго оставляет последний узел без кворума Corosync и DRBD: Arkvory останавливается целиком — писатель без кворума недопустим. Восстановление — вернуть узлы и запустить на них кластер: Pacemaker продвигает самую свежую копию, а узел, который не смог остановиться без кворума (DRBD отвечает ему ошибками ввода-вывода), изолируется ещё раз. То же вha-2при одновременной потере узла с данными и свидетеля.- Сетевой разрыв: кворум решает свидетель или большинство; проигравший узел выключается fencing'ом. Две стороны одновременно не пишут.
- Автоматического возврата активной роли (failback) нет.
Установка, обновление, резервные копии
arkvory cluster-planформирует конфигурацию DRBD и команды Pacemaker (pcs: fencing, кворум-устройство, группа ресурсов) для проверки и применения оператором; параметры агента fencing заполняет площадка.arkvory configure --cluster <ha-2|ha-3>на основном узле: проверяет, что корень — смонтированный том DRBD с двумя полными копиями, включает правило подтверждения и снимает автозапуск юнитов; при сбое возвращает standalone.arkvory cluster-nodeготовит остальные узлы: те же UID/GID служебных пользователей и юниты с тома, без автозапуска. Копия юнитов на томе обновляется после каждой операции установщика на активном узле, а первый ресурс группы после файловой системы,arkvory-units(oneshotarkvory cluster-units), переносит её на узел, который становится активным: настройки, изменённые после присоединения узла (хранилище копий, вебхуки, обновление), действуют и после переключения.arkvory cluster-status|cluster-check|cluster-fence-test|cluster-switchover|cluster-single-copy: состояние копий, проверка кворума, fencing каждого узла и настроек DRBD, fencing резервного узла, плановое переключение (только если у целевого узла есть своя полная копия: в ha-3 двух полных копий на других узлах недостаточно), решение об одной копии.- Обновление идёт на активном узле: релиз и миграция на томе, поэтому бинарные файлы следуют за томом. Перед обновлением ресурсы переводятся в maintenance, после проверки готовности возвращаются под Pacemaker; автоматическое переключение во время обновления запрещено. Миграции и так должны совместимо откатываться (ADR 0059). Если не удались и обновление, и его откат, группа остаётся unmanaged до вмешательства оператора (
pcs resource cleanup,pcs resource manage): вернуть Pacemaker неисправную группу значило бы позволить ему перезапускать и переключать её вслепую. - Агент резервных копий работает только на активном узле; хранилище копий — вне кластерного тома.
- Проверка ресурса для Pacemaker —
/health/readyс ключом (уже есть), с отдельными кодами для «не готов» и «готов, запись остановлена»: остановленная запись не повод для переключения.
Наблюдаемость
Метрики arkvory_replication_copies, arkvory_replication_required_copies, arkvory_replication_writes_refused_total, arkvory_replication_check_failures_total; алерты на деградацию, на действующее разрешение одной копии и на сбой чтения состояния копий; кворум и свидетель проверяет cluster-check. Баннер администратора в консоли и поле replication в /health/ready.
Альтернативы
- PostgreSQL streaming (Patroni) + отдельная репликация хранилища. Две независимые реплики требуют общего барьера публикации и переключения; без него каталог и байты расходятся. Это дольше и рискованнее, чем один том.
- Репликация на уровне приложения (outbox/ACK, REPLICATION). Переносима и работает на Windows, но это месяцы работы и собственная реализация консенсуса. Остаётся направлением для асинхронных зеркал.
- Общее хранилище (SAN/NFS) + один активный узел. Хранилище остаётся единственной точкой отказа; NFS к тому же не даёт нужных гарантий
fsyncдля PostgreSQL. - Ceph/объектное хранилище. Отдельный кластер, который нужно эксплуатировать; для 4 ТБ на одной площадке избыточно. Может стать вторым backend после ADR 0071.
- Своё переключение в Arkvory по heartbeat. Отвергнуто ADR 0042: нет fencing, есть split brain.
Последствия
- Windows и Docker Compose не поддерживаются в HA-профиле; они остаются standalone + зеркала + резервные копии.
- Каждому узлу с данными нужен диск на весь объём, сеть между узлами с измеренной задержкой: protocol C добавляет RTT к каждому
fsync. Цели по задержке записи измеряются на стенде. - Новая поверхность: служба
arkvory-replicaот root и командаarkvory cluster. Сокет службы доступен Arkvory только на чтение состояния; разрешение одной копии меняет толькоarkvory clusterот root. - Публичный контракт расширяется аддитивно: reason
replication_degraded, полеreplicationв готовности, метрики. - Стенд требует Linux-хост с KVM: модуль DRBD и fencing не проверяются в контейнерах Docker Desktop.
Проверка
Отдельный гейт deployment-ha на стенде из виртуальных машин (KVM, fence_virsh), профили ha-2 и ha-3:
- Выключение питания активного узла во время 5 GiB загрузки и потока мелких загрузок: ни одна подтверждённая (2xx) загрузка не потеряна; неподтверждённые завершаются повтором; время до восстановления чтения измерено.
- Отказ резервного:
ha-2— запись 503replication_degraded, чтение работает, после возврата запись возобновляется сама;ha-3— запись продолжается; потеря второго узла останавливает Arkvory до возврата одного из них. - Разрыв сети между узлами с данными: ровно один активный, проигравший выключен fencing'ом, без split brain.
- Сбой fencing: переключение не происходит, состояние видно оператору.
- Отказ во время коммита: соседа выключают между
fsyncи ответом; загрузка, оставшаяся в одной копии, не получает 2xx, а повтор после возврата узла её подтверждает. - Плановое переключение и обновление релиза в кластере, включая миграцию схемы.
cluster-single-copy: запись идёт, баннер и алерт есть, решение снимается по сроку.- Возврат узла и полная ресинхронизация под нагрузкой чтения; затем повтор сценария 1 в обратную сторону.
Результаты стенда (2026-10-10)
Гейты deployment-ha (потеря резервного, одна копия, разрыв сети), deployment-ha-power (питание активного во время 5 GiB загрузки, сбой fencing), deployment-ha-operations (обновление и переключение, полная ресинхронизация, питание активного в обратную сторону) и deployment-ha-three (ha-3) идут в CI на гостевых ВМ KVM: Ubuntu 24.04, DRBD 9.3 (LINBIT), Pacemaker 2.1, fence_virsh; кластер собирается из нативного пакета по процедуре документации. Все сценарии проходят; ни одна подтверждённая загрузка не потеряна, неподтверждённые подтверждаются повтором.
- Восстановление чтения после потери питания активного узла:
ha-3— около 26 с,ha-2— около 50 с (47–49 с), стенд ограничивает его 60 с. Вha-2узел, оставшийся без соседа с данными, при продвижении вызываетcrm-fence-peer.9.sh; его ожидание по умолчанию (90 с) упиралось в таймаут продвижения и давало около 2 мин. План задаёт--timeout 20: Pacemaker продвигает узел только после подтверждённого fencing, а ограничение обработчика и кворум DRBD исключают второго писателя. - Сценарий 5 проверяется потоком загрузок во время отказа соседа: ответы, пришедшие после потери копии, — 503, и все они подтверждаются повтором после возврата узла.
- Сценарий 6 обновляет до того же кода с другим номером версии: проверены путь обновления на активном узле, передача группы Pacemaker и её возврат, пакет на резервных узлах. Миграция схемы выполняется тем же кодом между остановкой и запуском служб и проверяется стендом обновления с опубликованного релиза (
deployment-stand); собирать в HA-стенде релиз с искусственной миграцией не стали. ha-3при потере двух узлов останавливается целиком; восстановление — запуск кластера на вернувшихся узлах (см. «Деградация»).- Найдено стендом и исправлено: порог ресинхронизации DRBD под нагрузкой (по умолчанию 250 KiB/s, в плане 20 MB/s), поиск группы Pacemaker при обновлении, запуск команд кластера с тома, который переключение отмонтирует, особенности пакетов Ubuntu (образец
corosync.conf, предварительно инициализированныйcorosync-qnetd, агентFilesystemвresource-agents-extra).
Дополнение: страница кластера и замена узла (2026-10-12)
- Служба
arkvory-replicaдополнительно отдаёт имя узла, профиль и соседей (peers: имя, связь, состояние диска, полная ли копия, свидетель). API на них не решает; полеreplicationв/health/readyпоказывает их только администраторам, остальные видятcopies,required,singleCopyUntil. Ответ старой службы без этих полей принимается. - Консоль: страница «Кластер» для администраторов (схема, обслуживающий узел, копии, запись, соседи и один следующий шаг с командой: плановое переключение, возврат или замена узла, решение об одной копии, проверка
arkvory-replica). Баннер при остановке записи ведёт на неё; пользователи без администрирования видят только, что загрузка приостановлена. - Сборка короче:
cluster-releaseотдаёт том (останов служб по порядку, отмонтирование, secondary),cluster-nodeсам берёт том, готовит узел и отдаёт его. Оба отказываются, когда группой уже управляет Pacemaker. - Замена узла без захвата тома:
cluster-kitна активном узле пишет файл (ресурс, UID/GID, юниты; секретов нет, права 0600),cluster-node --kitготовит новый узел по нему; дальше DRBD заполняет копию с активного узла, а запись возобновляется сама. Процедура — wiki «Кластер», раздел «Заменить сервер». - Проверки до действия:
cluster-fence-testсначала выполняет проверкиcluster-checkи вha-2требует--yes;cluster-single-copy --untilтребует часовой пояс (Zили смещение); план проверяет уникальность адресов, отличие виртуального адреса от узлов и длину имени ресурса.
Проверки: tests/cluster-plan.test.mjs, tests/cluster-replica.test.mjs, tests/replica.test.mjs, tests/integration/replica.test.mjs (детали только администраторам), tests/browser/cluster.mjs; сборка на стенде deployment-ha идёт через cluster-release и cluster-node.