Кластер из трёх машин с witness-нодойотдельным узлом, который не хранит данные, а только участвует в голосовании за кворум строится вокруг votequorum — подсистемы Corosync, которая решает, имеет ли группа узлов право продолжать работу. Чтобы понять, почему такая конфигурация переживает отказ одной машины и не допускает split-brainситуацию, когда две части кластера одновременно считают себя рабочими и независимо меняют данные, нужно разобрать механику подсчёта голосов: какие узлы вообще учитываются, как складывается кворум и в какой момент узел получает право работать.
Роли узлов и зачем нужен witness
Подсчёт голосов начинается с обхода списка членов кластера. Для каждого узла берётся его состояние, и учитываются только узлы в состоянии NODESTATE_MEMBER — те, что признаны полноправными членами. Для них обновляется highest_expected — highest_expectedнаибольшее ожидаемое число голосов среди узлов-членов, то есть максимум поля expected_votes этих узлов, к сумме голосов прибавляется голос узла, а счётчик узлов увеличивается на единицу.
Голос кворум-устройства (qdevice) добавляется к сумме голосов и к счётчику узлов только при установленном флаге NODE_FLAGS_QDEVICE_CAST_VOTE у текущего узла. Это и есть механизм witness: qdevice — отдельная сущность, чей голос учитывается лишь тогда, когда локальный узел подтвердил, что устройство активно и голосует. Отдельного признака для узлов pacemaker_remote в функциях подсчёта нет: различие идёт только по состоянию NODESTATE_MEMBER и по флагам, относящимся к кворум-устройству.
Флаг NODE_FLAGS_QDEVICE_MASTER_WINS («побеждает как мастер») означает, что узел входит в ту часть раскола, которая должна получить кворум принудительно: мастерсторона раскола, которой разрешено выиграть и сохранить кворум, когда кластер разделился на части. Он устанавливается или снимается обработчиком запроса от библиотеки: если пришло разрешение allow, флаг добавляется, иначе снимается. После этого значение сохраняется в глобальную переменную qdevice_master_wins и в ключ runtime.votequorum.qdevice_master_wins. Поле qdevice_poll на этот флаг не влияет: qdevice_pollпериодический опрос кворум-устройства, которым узел сообщает, что устройство живо, и передаёт, голосует ли оно сейчас управляет другими флагами — NODE_FLAGS_QDEVICE_ALIVE и NODE_FLAGS_QDEVICE_CAST_VOTE.
Два пути подсчёта голосов расходятся. Обход в calculate_quorum добавляет голос qdevice только при установленном флаге NODE_FLAGS_QDEVICE_CAST_VOTE, а get_total_votes — по ненулевому значению голосов qdevice. Значит, при ненулевом qdevice->votes, но без флага у локального узла, второй путь включит голос устройства, а первый — нет. Результаты по сумме голосов расходятся именно в этом случае.
Регистрация qdevice
Прежде чем голос witness начнёт учитываться, устройство должно зарегистрироваться. Библиотечная функция регистрации сначала проверяет длину имени: пустое имя или имя длиной не меньше VOTEQUORUM_QDEVICE_MAX_NAME_LEN отклоняется с ошибкой неверного параметра. Затем формируется запрос с идентификатором регистрации, имя копируется в него, запрос отправляется, и функция ждёт ответа.
Обработчик на стороне votequorum сначала смотрит глобальный флаг qdevice_can_operate: если он ложен, регистрация запрещена и возвращается ошибка доступа. Если узел уже зарегистрирован и имя совпадает с сохранённым, обработчик просто выходит; при несовпадении имени возвращается ошибка «уже существует». Если регистрация ещё не завершена (соединение регистрации не пусто), возвращается «попробуйте позже». Иначе соединение сохраняется и рассылается exec-сообщение с операцией регистрации и именем; при ошибке отправки соединение сбрасывается и снова возвращается «попробуйте позже».
Регистрационное сообщение содержит только операцию и имя — ни голосов, ни состояния в нём нет. Голоса и ожидаемое число голосов передаются отдельным широковещательным сообщением nodeinfoслужебное сообщение votequorum, в котором узел рассылает свои голоса, ожидаемое число голосов и флаги, куда кладутся голоса, ожидаемое число голосов и флаги узла. После успешной регистрации такие сообщения рассылаются и для qdevice, и для локального узла. А состояние qdevice попадает в уведомление о кворуме напрямую из поля состояния устройства, тогда как голоса и ожидаемое число голосов распространяются через nodeinfo и уже оттуда используются при подсчёте.
Как votequorum понимает, что qdevice жив
Обработчик опроса qdevice сначала проверяет, разрешена ли работа с устройством, и при запрете возвращает ошибку доступа. Затем, если узел зарегистрировал устройство, сравниваются идентификатор и последовательность из пришедшего ring_id с сохранённым quorum_ringid; при несовпадении возвращается ошибка сообщения. Имя из запроса сравнивается с сохранённым именем устройства, и при несовпадении возвращается ошибка неверного параметра.
Если ранее был установлен таймер, он удаляется и флаг его установки сбрасывается. Сохраняются прежние флаги, затем устанавливается NODE_FLAGS_QDEVICE_ALIVE, а NODE_FLAGS_QDEVICE_CAST_VOTE включается или снимается в зависимости от поля cast_vote в запросе. Если флаги изменились, рассылается nodeinfo для собственного узла.
Таймер ставится на qdevice_timeout*1000000 микросекунд, то есть на qdevice_timeout секунд. Когда он срабатывает, функция сначала проверяет, что флаг NODE_FLAGS_QDEVICE_ALIVE установлен и таймер был активен; иначе она просто выходит. Если условия выполнены, снимаются оба флага — ALIVE и CAST_VOTE, в лог пишется сообщение о потере контакта с кворум-устройством с его именем, и рассылается nodeinfo для собственного узла. После этого сбрасываются флаг установки таймера и признак ожидания опроса. Снятие CAST_VOTE означает, что голос устройства перестаёт учитываться: в подсчёте кворума он прибавляется только при этом флаге.
Кворум и split-brain
Порог кворума вычисляется из двух величин. Первая — highest_expected, максимум ожидаемого числа голосов среди членов кластера; вторая — total_votes, сумма фактически присутствующих голосов (плюс голос qdevice при соответствующем флаге). Если переданное max_expected больше нуля, highest_expected принудительно заменяется на него.
q1 = (highest_expected + 2) / 2;
q2 = (total_votes + 2) / 2;
newquorum = max(q1, q2);Итоговый кворум — максимум из двух порогов. Смысл в том, что для кворума нужно одновременно набрать большинство от ожидаемого состава и большинство от фактически присутствующих голосов: побеждает более строгое требование.
Если понижение кворума не разрешено, новый кворум не может быть меньше текущего — он поднимается до максимума из текущего и вычисленного. При числе узлов не больше двух кворум принудительно становится равен единице; счётчик узлов включает членов кластера плюс qdevice, если у нас установлен флаг его голосования. Если узлов больше двух, принудительного кворума в единицу не происходит.
Решение о кворатности принимается сравнением: если кворум больше суммы голосов, узел некворатен, иначе кворатен. Дополнительно кворатным узел может стать по механизму auto_tie_breakerмеханизм, позволяющий одной из половин раскола получить кворум, чтобы кластер не остался полностью некворумным — при выполнении четырёх условий: режим ATB задан, сумма голосов равна половине ожидаемого числа голосов узла, разница между числом прежних и текущих членов кворума меньше кворума, и проверка ATB вернула единицу. Ещё один путь — через qdevice_master_wins: если этот признак истинно, текущий узел некворатен и проверка check_qdevice_master() вернула единицу, кворум принудительно выставляется в единицу с записью в лог о кворатности как части master_wins-партиции. Проверка check_qdevice_master() возвращает единицу, если найдётся член кластера, у которого одновременно стоят флаги NODE_FLAGS_QDEVICE_MASTER_WINS и NODE_FLAGS_QDEVICE_CAST_VOTE.
Проверки ATB различаются по режиму. Для режима «наименьший» возвращается результат проверки присутствия узла с наименьшим идентификатором, для режима «наибольший» — с наибольшим. Для режима со списком узлов проверяется список: если найден узел из текущей партиции, но какой-либо предшествующий узел списка был в прежнем составе кворума, возвращается ноль; если ни один узел списка не найден в текущей партиции, тоже возвращается ноль.
Почему кластер ждёт появления всех узлов
Функция обновления ожидаемого числа голосов проходит по списку узлов и присваивает новое значение только членам кластера, и только если переданное значение не равно нулю. Само значение — это число текущих членов кластера, подсчитанное при сборе голосов. Узлы в других состояниях новое значение не получают.
Режим wait_for_all заставляет узел не получать кворум, пока он не увидит все ожидаемые голоса: при включённом режиме и активном статусе ожидания узел ждёт, пока сумма голосов не сравняется с ожидаемым числом голосов, и только тогда сбрасывает статус.
С этим связана несовместимость ATB и нечётного числа узлов. При нечётном числе узлов без tie-breaker-узла изолированная половина «half+1» не должна получить кворум при перезапуске, поэтому wait_for_all обязателен. Если ATB задан, число узлов нечётное и wait_for_all не задан, то при также заданном last_man_standingрежим, при котором кворум пересчитывается по числу реально оставшихся в кластере узлов, а не по исходному составу corosync отказывается стартовать с ошибкой конфигурации, иначе ATB принудительно отключается. Кворум-устройство несовместимо с ATB: при наличии qdevice и заданном ATB либо возникает ошибка конфигурации, либо отключаются операции qdevice.
Выборы и переключение ролей
votequorum не выбирает DCузел, координирующего работу кластера напрямую. Функции вычисления наименьшего и наибольшего идентификатора лишь находят минимальный и максимальный идентификатор среди членов кластера и публикуют их в icmap. Эти значения используются только при проверке auto_tie_breaker: одна проверка возвращает единицу, если узел с наименьшим идентификатором присутствует в текущей партиции, другая — если присутствует узел с наибольшим.
Решение о кворуме принимает are_we_quorate: если кворум больше суммы голосов, узел некворатен, иначе кворатен — и только тогда вычисляются и публикуются наименьший и наибольший идентификаторы. ATB применяется только при разделении примерно пополам, когда сумма голосов равна половине ожидаемого числа голосов узла и «другая» партиция может иметь кворум.
Пограничные случаи
При сетевом разрыве (network partition)разделении кластера на части, которые перестают видеть друг друга подсчёт кворума идёт по той же формуле, учитывая только членов кластера. Сравнение кворума с суммой голосов определяет, кворатна ли сторона. При потере кворума в лог пишется сообщение о блокировке активности, при восстановлении — о возобновлении, и флаг кворатности снимается или устанавливается. Уведомление клиентам рассылается с полем кворатности и списком всех известных узлов с их состояниями.
Клиент видит голоса qdevice особым образом. Если на узле устройство не активно (нет флага голосования), вместо реального числа голосов выводится ноль, а фактическое значение указывается в скобках рядом; если активно — выводится реальное число. Флаг кворатности определяет, будет ли после числа кворума пусто или написано о блокировке активности, а также печатается строка «Quorate: Yes/No». Уведомления о кворуме сохраняют признак кворатности, ring_id и список узлов, а при включённом режиме наблюдения печатают «Quorate» или «Not quorate». Уведомления votequorum сохраняют только идентификатор узла из ring_id.
При возврате узла после раздела его состояние устанавливается в NODESTATE_MEMBER, а голоса и флаги берутся из полученного сообщения nodeinfo. Если узел помечен как покидающий, состояние становится NODESTATE_LEAVING и разрешается понижение кворума. Ожидаемое число голосов узла берётся из сообщения, если узел кворатен или включено отслеживание, иначе — из ожидаемого числа голосов текущего узла. Пересчёт кворума запускается, если узел новый, это наш узел, у него стоит флаг первого появления, либо изменились голоса, ожидаемое число голосов, флаги или состояние. При пересчёте общее ожидаемое число голосов кластера повышается до суммы голосов, если она больше, и рассылается уведомление; затем вычисляется кворум, обновляется ожидаемое число голосов узлов и принимается решение о кворатности.
Что из этого следует на практике
Кворум в такой конфигурации — это не просто большинство присутствующих, а максимум из большинства от ожидаемого состава и большинства от фактически присутствующих. Поэтому кластер не «схлопывает» требования, когда часть узлов недоступна: пока ожидаемый состав не снижен, порог держится на прежнем уровне.
Голос witness учитывается только при установленном флаге голосования у локального узла. Если устройство перестало отвечать и таймер сработал, флаг снимается, и голос устройства выпадает из подсчёта — это и есть механизм, которым witness влияет на кворум, не храня данных.
Два пути подсчёта голосов могут расходиться: там, где голос qdevice ненулевой, но флаг голосования у узла не стоит, один путь учтёт устройство, а другой — нет. Это стоит держать в голове при диагностике расхождений между разными представлениями о кворуме.
ATB и qdevice несовместимы: при заданном ATB либо конфигурация отвергается, либо операции с устройством отключаются. А при нечётном числе узлов без wait_for_all ATB отключается принудительно — иначе изолированная половина «half+1» могла бы получить кворум при перезапуске.
Возврат узла после раздела не мгновенно восстанавливает его голос: состояние, голоса и флаги приходят из сообщения nodeinfo, ожидаемое число голосов может быть взято из сообщения или из текущего узла, и только после этого запускается пересчёт кворума.
Где смотреть в коде
- votequorum.c: calculate_quorum
- votequorum.c: update_node_expected_votes
- votequorum.c: get_total_votes
- votequorum.c: message_handler_req_lib_votequorum_qdevice_update
- votequorum.c: votequorum_exec_send_nodeinfo
- votequorum.c: votequorum_init
- corosync-quorumtool.c: votequorum_qdevice_extra_info_notification_fn
- votequorum.c: check_high_node_id_partition