Кластер расколол мозг: две ноды запустили одну и ту же виртуалку
В понедельник утром мониторинг прислал два алерта подряд: "VM 142 недоступна" и "VM 142 отвечает на пинг" — с разницей в сорок секунд. Через пять минут выяснилось, что виртуалка с базой биллинга крутится сразу на двух нодах кластера, обе пишут в один и тот же диск на общем хранилище, а гостевая файловая система внутри уже успела получить трещину. Это классический сплит-брейн — кластер перестал быть единым целым, но каждая половина была уверена, что она главная. Разбираем, как мы до этого докатились и что заставило кворум и watchdog-fencing не сработать так, как было написано в документации.
Содержание
Что мы увидели
Обстановка на момент инцидента: двухнодовый кластер Proxmox VE (node-a и node-b), HA-группа с несколькими виртуалками, общее хранилище — thick-LVM поверх iSCSI-таргета на отдельном стораджевом сервере. Виртуалка 142 (биллинг) была в списке HA-managed сервисов с политикой max_restart: 1.
Первый сигнал — Zabbix зафиксировал ARP-конфликт: один и тот же MAC-адрес виртуалки светился то на порту коммутатора, где висел node-a, то на порту node-b. Одновременно клиенты биллинга начали жаловаться на рассинхрон сумм — одна и та же операция списания иногда проводилась дважды, иногда терялась.
Зайдя по SSH на обе ноды, мы увидели одно и то же:
root@node-a:~# qm list | grep 142
142 billing-db running 8192 64.00 0
root@node-b:~# qm list | grep 142
142 billing-db running 8192 64.00 0
Обе ноды считали себя владельцем процесса qemu для VM 142. Обе писали в один и тот же LVM-том /dev/vg_storage/vm-142-disk-0 — без кластерной блокировки на уровне тома, потому что storage был подключен как обычный shared LVM без lvmlockd. Proxmox честно предупреждает об этом в документации по типам хранилищ, но при живой миграции между нодами всё работало годами без единого сбоя, и предупреждение стало фоновым шумом.
Мы остановили одну из копий (qm stop 142 на node-b), подняли базу из бэкапа за прошлую ночь и сверили расхождения вручную — это заняло основную часть утра. Только после того как сервис снова заработал, начался разбор причин.
Что показали логи и метрики
Первым делом посмотрели статус кворума на обеих нодах в момент инцидента (по логам, не в реальном времени, инцидент уже был закрыт):
root@node-a:~# journalctl -u corosync --since "05:50" --until "06:10"
...
corosync[1284]: [TOTEM ] Token has not been received in 3225 ms
corosync[1284]: [TOTEM ] A processor failed, forming new configuration
corosync[1284]: [QUORUM] Members[1]: 1
corosync[1284]: [QUORUM] This node is within the primary component and will provide service.
На node-b практически синхронно — своя версия той же истории, только с точностью до наоборот:
root@node-b:~# journalctl -u corosync --since "05:50" --until "06:10"
...
corosync[1190]: [TOTEM ] Token has not been received in 3311 ms
corosync[1190]: [QUORUM] Members[1]: 2
corosync[1190]: [QUORUM] This node is within the primary component and will provide service.
Обе ноды в один и тот же момент решили, что кворум есть именно у них. Это возможно только в одном случае конфигурации — двухнодовый кластер с флагом two_node: 1 в corosync.conf. Этот режим специально существует для кластеров из двух узлов: без него ни одна нода никогда не наберёт большинство (1 из 2 — не большинство), и HA в двухнодовом кластере была бы бессмысленной. Проверили конфиг — так и есть:
root@node-a:~# grep -A3 quorum /etc/pve/corosync.conf
quorum {
provider: corosync_votequorum
two_node: 1
}
Дальше посмотрели логи HA-менеджера на обеих нодах:
root@node-a:~# journalctl -u pve-ha-crm --since "05:55" --until "06:05"
pve-ha-crm[1523]: node 'node-b': state changed from 'online' => 'unknown'
pve-ha-crm[1523]: fencing node 'node-b'
pve-ha-crm[1523]: got lock 'ha_agent_node-b_lock'
pve-ha-crm[1523]: fencing: acknowledged - got agent lock for node 'node-b'
pve-ha-crm[1523]: recover service 'vm:142' from fenced node 'node-b' to node 'node-a'
pve-ha-crm[1523]: service 'vm:142': state changed from 'started' to 'recovery'
pve-ha-crm[1523]: recover service 'vm:142' to node 'node-a'
CRM на node-a честно написал "fencing node 'node-b'" и "acknowledged" — то есть посчитал, что фенсинг подтверждён, и запустил VM 142 у себя. А теперь самое интересное — что было на node-b в это же время:
root@node-b:~# journalctl -u pve-ha-lrm --since "05:55" --until "06:05"
pve-ha-lrm[1401]: lost lock 'ha_agent_node-b_lock - cfs lock update failed - Permission denied
pve-ha-lrm[1401]: status change active => lost_agent_lock
pve-ha-lrm[1401]: watchdog update failed - Broken pipe
LRM на node-b увидел, что потерял блокировку, и по протоколу обязан немедленно перезагрузить ноду через watchdog-таймер — это и есть механизм самофенсинга в Proxmox: нода, которая не может подтвердить, что она в мажоритарной части кластера, должна сама себя "убить" раньше, чем истечёт таймаут, на который полагается CRM другой ноды. Но ребута не произошло. Проверили состояние watchdog-устройства на node-b:
root@node-b:~# lsmod | grep softdog
root@node-b:~# ls -la /dev/watchdog*
ls: cannot access '/dev/watchdog*': No such file or directory
Модуль softdog не был загружен вообще, устройства /dev/watchdog не существовало. LRM пытался писать в watchdog для продления таймера и получал ошибку, но отсутствие watchdog-устройства само по себе не останавливает уже запущенные виртуалки немедленно — LRM переходит в состояние ошибки и перестаёт быть активным менеджером, а процесс qemu, запущенный ранее через qm start, продолжает жить как обычный процесс, пока его кто-то явно не остановит.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверКакие гипотезы мы отбросили
Пока собирали логи, проверили три версии, которые казались правдоподобнее на первый взгляд.
- Баг репликации хранилища. Первая мысль — раз оба видят один диск как "свой", значит что-то сломалось на уровне iSCSI-таргета. Проверили счётчики сессий на сторадж-сервере (
targetcli sessions) — сессии от обеих нод были легитимными и независимыми, хранилище честно отдавало блочное устройство всем, кто подключился. Отбросили. - Забытый клон виртуалки. Предположили, что кто-то клонировал VM 142 для тестов, оставил тот же MAC и ID и забыл выключить. Проверили
pvesh get /cluster/resourcesза неделю до инцидента и историю задач в интерфейсе — операций клонирования не было, ID виртуалки везде один. - Проблема на уровне гостевой ОС — например, systemd-юнит с двойным запуском или залипший DHCP-lease. Отбросили сразу, как только
qm listна обеих нодах синхронно показалrunningдля одного и того же VMID — на уровне гипервизора это не могло быть проблемой гостя.
Все три гипотезы отняли около часа до того, как мы догадались посмотреть на watchdog, хотя стоило начать именно с него — любой инцидент "два хоста одновременно считают себя главными" в кластере с HA стоит в первую очередь проверять именно на предмет фенсинга, а не хранилища.
Что оказалось на самом деле
Причина — стечение двух отдельных недосмотров, ни один из которых сам по себе не привёл бы к инциденту.
Первый: несколькими неделями раньше node-b обновляли до нового ядра (pve-kernel) и перезагружали. Сервер собирали не с нуля, а клонировали с шаблонного образа, в котором ранее — для другого проекта — модуль softdog был явно занесён в blacklist через /etc/modprobe.d/blacklist.conf, чтобы не конфликтовал с аппаратным IPMI-watchdog на другом железе. На node-b аппаратного watchdog не было, а строчку из blacklist никто не убрал. После перезагрузки модуль не подгрузился, /dev/watchdog не появился, а pve-ha-lrm при старте молча перешёл в деградированный режим — это видно в логах, но не блокирующая ошибка, поэтому несколько недель всё работало без нареканий: пока не терялся кворум, watchdog никому не был нужен.
Второй: интерконнект corosync между node-a и node-b шёл по одному линку — тому же самому, по которому по ночам стартовал бэкап через Proxmox Backup Server, гоняющий полные копии дисков по этой же сети. В ночь инцидента бэкап совпал по времени с плановой заменой SFP-модуля на аплинке коммутатора, из-за чего порт на несколько секунд ушёл в STP-пересчёт топологии. Этого хватило, чтобы corosync-токен не дошёл вовремя, и обе ноды одновременно решили, что партнёр пропал.
В штатной ситуации это заканчивается тихо: сторона, теряющая блокировку, обязана сама перезагрузиться через watchdog раньше, чем другая подтвердит фенсинг и запустит сервисы у себя. У нас же нода, которая должна была себя перезагрузить, физически не могла этого сделать — устройства не существовало. CRM на node-a по истечении таймаута посчитал фенсинг подтверждённым (он получил распределённую блокировку в кластерной ФС /etc/pve) и поднял виртуалку у себя, ничего не зная о том, что node-b жив и продолжает работать с тем же диском.
Механика в целом такая: Proxmox HA не делает "настоящий" аппаратный фенсинг из коробки (STONITH через IPMI/iLO) — по умолчанию используется самофенсинг через watchdog, и вся модель безопасности держится на допущении, что нода, потерявшая кворум, гарантированно перезагрузится в известное время. Если это допущение нарушено — конфигурацией, забытым blacklist-правилом, чем угодно — кластер физически не может отличить "нода умерла" от "нода просто не отвечает", и в этом зазоре рождается сплит-брейн.
Как устроены кворум и фенсинг в двухнодовом кластере
Если коротко: в кластере без специального режима нода с одним голосом из двух никогда не наберёт большинство — это осознанно, большинство нужно, чтобы не начать раскол при первой же сетевой аномалии. Флаг two_node: 1 — явное исключение из общего правила именно для пары серверов, и оно снимает защиту голосованием, заменяя её обязательством фенсинга. Мы подробно разбирали, зачем в принципе нужен третий голос в кластере, в отдельном материале про кворум — с qdevice или третьим узлом эта авария была бы физически невозможна: партиция без большинства не смогла бы взять на себя HA-сервисы независимо от состояния watchdog.
Общая логика такая:
| Конфигурация | Что защищает | Что не защищает |
|---|---|---|
2 ноды, two_node: 1, без фенсинга | Ничего от сплит-брейна | Требует безупречно работающего watchdog на 100% нод |
| 2 ноды + qdevice (третий голос) | Настоящее большинство при разрыве линка между нодами | От отказа самого qdevice-хоста |
| 3+ нод, без qdevice | Большинство 2 из 3 при разрыве одной ноды | При разрыве сети на 2 и 1 меньшая часть должна сама себя остановить |
| Любая конфигурация + аппаратный STONITH (IPMI/iLO) | Гарантированную остановку "потерянной" ноды извне, а не самим собой | Требует отдельного управляемого канала до BMC |
Мы изначально выбрали двухнодовую схему, потому что третий сервер под кластер казался лишними расходами для нагрузки, которая на нём и не разместилась бы. Задним числом это была неверная экономия: цена третьего узла (или лёгкого qdevice-хоста) ниже, чем цена одного дня простоя биллинга и ручной сверки транзакций. У нас в блоге есть отдельный разбор кластера Proxmox именно из двух узлов — там мы честно перечисляем эти риски, но до реального инцидента они звучали как теория.
Что мы изменили после инцидента
- Добавили третий голос. Развернули лёгкий qdevice на отдельном сервере в другой стойке, включили
corosync-qdevice, убралиtwo_node: 1из конфигурации. Теперь для кворума нужно реальное большинство голосов, а не допущение о самофенсинге. - Развели трафик corosync и бэкапов по разным физическим линкам. Добавили второе кольцо (
ring1) для corosync на отдельной сетевой карте, не пересекающейся с трафиком Proxmox Backup Server. Мы отдельно разбирали, как устроено общее хранилище для кластера и почему разделение трафика управления и данных — не опция, а обязательное условие. - Написали проверку наличия
/dev/watchdog. Добавили в Zabbix активную проверку присутствия watchdog-устройства и загруженности модуляsoftdogна каждой ноде — с алертом критичного уровня, а не warning: до инцидента это предупреждение терялось среди прочих логов при старте службы. - Провели аудит
blacklist.confи модулей ядра на шаблонных образах. Убрали строчки, специфичные для другого проекта, и завели чек-лист приёмки нового узла в кластер с явным пунктом "watchdog присутствует и отвечает". - Перешли на
lvmlockdдля общего LVM-тома. Даже если бы фенсинг снова не сработал, кластерная блокировка на уровне тома не позволила бы второй ноде смонтировать тот же логический том на запись одновременно с первой — второй, независимый рубеж защиты. - Ужесточили HA-политику для критичных сервисов (
max_restart: 0с ручным подтверждением восстановления вместо автоматического) там, где целостность данных важнее автоматической доступности. Материал про то, как устроена высокая доступность в Proxmox при падении узла, помог сверить терминологию внутри команды перед этим разговором.
Отдельно пересмотрели физическую топологию: node-a и node-b до инцидента стояли в одной стойке за одним аплинком, и его обслуживание одинаково задело обе ноды. Часть нагрузки перенесли на отдельный сервер в другом дата-центре, чтобы единая точка отказа на уровне сети не могла зацепить весь кластер разом.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли вообще делать HA-кластер из двух нод без qdevice?
Технически да, Proxmox это поддерживает через two_node: 1, и для многих сценариев это работает годами без проблем. Но это осознанный компромисс: вся защита от сплит-брейна ложится на надёжность watchdog-фенсинга, а не на голосование. Для сервисов с высокими требованиями к целостности данных (биллинг, платежи) лучше сразу закладывать третий голос.
Чем qdevice отличается от полноценного третьего узла кластера?
Qdevice не хранит конфигурацию кластера и не запускает виртуалки — это лёгкий арбитр, который только участвует в голосовании за кворум. Он может быть небольшим сервером или виртуалкой на независимой инфраструктуре, важно лишь, чтобы у него был собственный сетевой путь до обеих нод.
Как понять, что watchdog в порядке, не дожидаясь инцидента?
Регулярно проверяйте lsmod | grep -E "softdog|ipmi_watchdog" и наличие /dev/watchdog на каждой ноде, а также journalctl -u pve-ha-lrm на предмет ошибок вида "watchdog update failed". Лучше вынести обе проверки в мониторинг с критичным уровнем алерта, а не проверять руками при онбординге ноды.
Почему CRM не мог сам убедиться, что вторая нода правда мертва?
У самофенсинга через watchdog нет обратной связи "точно подтверждаю смерть" — есть только допущение "если нода не продлевает блокировку дольше таймаута, значит она мертва или сама перезагрузится". Настоящую обратную связь даёт только аппаратный фенсинг через управляемый канал вроде IPMI.
Стоило ли просто увеличить таймаут corosync-токена?
Это снизило бы чувствительность к ложным срабатываниям, но не устранило бы саму уязвимость — при достаточно долгом реальном разрыве проблема повторилась бы один в один. Основным решением стало устранение единой точки отказа в сети и восстановление рабочего watchdog, а не увеличение времени ожидания.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →