Назад к блогу

HA с witness-нодой: как собрать отказоустойчивую инфраструктуру Totum на трёх машинах

HA с witness-нодой: как собрать отказоустойчивую инфраструктуру Totum на трёх машинах

Кластер Totum можно сделать отказоустойчивым всего на трёх машинах, если одна из них выполняет роль witness-ноды. В статье разбирается, как votequorum считает голоса, зачем нужен qdevice и почему такая схема переживает отказ узла, не скатываясь в split-brain. Полезно тем, кто хочет понять внутреннюю механику HA-кластера, а не просто скопировать конфиг.

Кластер из трёх машин с witness-нодой строится вокруг votequorum — подсистемы Corosync, которая решает, имеет ли группа узлов право продолжать работу. Чтобы понять, почему такая конфигурация переживает отказ одной машины и не допускает split-brain, нужно разобрать механику подсчёта голосов: какие узлы вообще учитываются, как складывается кворум и в какой момент узел получает право работать.

Роли узлов и зачем нужен witness

Подсчёт голосов начинается с обхода списка членов кластера. Для каждого узла берётся его состояние, и учитываются только узлы в состоянии NODESTATE_MEMBER — те, что признаны полноправными членами. Для них обновляется highest_expected — highest_expected, к сумме голосов прибавляется голос узла, а счётчик узлов увеличивается на единицу.

Голос кворум-устройства (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, куда кладутся голоса, ожидаемое число голосов и флаги узла. После успешной регистрации такие сообщения рассылаются и для 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, ожидаемое число голосов может быть взято из сообщения или из текущего узла, и только после этого запускается пересчёт кворума.

Где смотреть в коде

Источники

Похожее