Аннотация к докладу:

В прошлом году мы сделали встроенную поддержку отказоустойчивости в Postgres Pro Enterprise — BiHA. Наше решение позволяет разворачивать отказоустойчивый кластер Postgres, в котором в случае сбоя пишущего узла новый пишущий узел (лидер) будет выбран автоматически.

При этом возникает проблема перенаправления трафика на нового лидера. Её можно решить с помощью нашего расширения Proxima или внешнего TCP-прокси сервера. И первое, и второе решение были вынуждены периодически опрашивать кластер BiHA для определения пишущего узла.

Как альтернатива, в новой версии BiHA появилась возможность зарегистрировать пользовательские функции, которые будут вызваны при возникновении таких событий в кластере, как смена лидера, добавление/удаление ноды и других. Этот механизм мы назвали пользовательские колбэки. Наталия Кокунина расскажет, как реализованы колбэки, и обсудит особенности их использования.

Расскажем про отказоустойчивый кластер BiHA, а именно про конкретную задачу уведомления о событиях в кластере, как узнавать, что происходит в процессе работы BiHA и как это делать с помощью механизма колбэков (callbacks). Мы посмотрим, как этот механизм выглядит снаружи и внутри. Также сформулируем некоторые рекомендации по поводу того, как его лучше использовать.

Итак, в прошлом году мы добавили в Postgres Pro Enterprise встроенную отказоустойчивость BiHA. Она работает по модели primary-standby, использует физическую репликацию Postgres. Главная задача BiHA — обеспечить автоматическое переключение на нового primary при сбое и перенастроить репликацию. Встроенность на практике означает, что BiHA работает в отдельном процессе, но внутри экземпляра Postgres, наряду с другими обычными процессами.

Давайте посмотрим на типичный кластер BiHA из трех нод. В терминах BiHA лидер — это primary, follower — это standby. Ноды получают от лидера по потоковой репликации изменения данных в БД в виде WAL, а сами процессы BiHA общаются между собой по протоколу BiHA Сontrol Protocol — BCP.

В связке с BiHA обычно используют внешний балансировщик. Можно использовать встроенный в Postgres Pro компонент Proxima или отдельный прокси, можно также настроить процесс, управляющий виртуальным IP.

Основная задача этого балансировщика — всегда перенаправлять клиентские соединения на пишущего лидера. При сбое лидера произойдут выборы нового лидера, более подходящая нода выдвинет себя в кандидаты, отправит другим нодам запрос на голосование. Если все хорошо, они ответят: «Становись лидером», — и она станет лидером.

Ну и встает закономерный вопрос. Как нашему балансировщику понять, куда теперь перенаправлять клиентские запросы? Самый очевидный вариант — опрашивать каждую ноду. В BiHA есть специальные представления, в которых записаны текущие статусы нод. Можно просто по очереди ходить на каждую ноду и найти таким образом лидера.

Но у этого подхода есть ряд недостатков:

  • Во-первых, сбои происходят в кластере не так часто (по крайней мере, в нормальном случае). И получается, что мы тратим вычислительные ресурсы на опрос.

  • Во-вторых, нужно задать интервал опроса. Слишком часто опрашивать не очень-то хорошо из-за предыдущего пункта, но если мы будем опрашивать слишком редко, мы потеряем время на обнаружение какого-то события.

  • И в-третьих, нужно следить за отказоустойчивостью самих процессов, которые занимаются опросом, чтобы не потерять момент смены лидера.

Альтернативный подход: пусть BiHA сама расскажет о том, что лидер сменился, о том, что что-то произошло. Тогда нам не придется думать о расписании опроса. Скорость реакции на события возрастет.

Если раньше балансировщик ходил к BiHA, а BiHA отвечал балансировщику. Теперь у нас BiHA сразу сообщает балансировщику про все события, которые происходят в кластере. И такая схема, когда BiHA уведомляет, актуальна не только для балансировщиков, как в этом примере, но и для обновления конфигурации, управления некоторыми сервисами на ноде или для интеграции с внешним ПО, как в случае с Proxima.

Для решения поставленной задачи мы в BiHA добавили колбэки. Колбэк BiHA — это SQL-функция. Эту функцию пользователь создает сам и регистрирует на определенное событие. BiHA же в нужный момент эту функцию вызовет, и мы достигаем главной цели — оперативной реакции на события.

Так как BiHA — это решение, которое находится внутри Postgres, логично колбэк сделать тоже внутри Postgres. Что это нам дает? Колбэк SQL-функции хранятся в базе данных, при этом они автоматически распространятся по кластеру по репликации и внутри самой колбэк-функции мы можем запросить всю информацию в базе данных.

Как этим всем пользоваться? В BiHA есть специальная база biha_db. В ней, в схеме biha, есть две функции — register и unregister для создания и удаления колбэков. Функция register возвращает ID, по которому впоследствии можно удалить колбэк-функцию с помощью unregister. Посмотреть все колбэки можно в таблице biha.callbacks. Важный момент: создавать и удалять колбэк нужно только на лидере, только он доступен на запись.

Что нам нужно, чтобы создать свой колбэк?

Обязательно понадобится определиться с тремя вещами. Нужно выбрать:

  • событие, на которое хотим подписаться;

  • имя функции, которую хотим исполнить;

  • базу данных, в которой эта функция будет находиться.

Дополнительно мы можем указать исполнителя и приоритет.

Давайте разберемся с событиями, которые происходят в кластере. Их условно можно поделить на две категории:

  • общекластерные события: обработчики таких событий исполняются на каждой ноде;

  • локальные события, такие как смена состояния ноды: когда они произойдут, то колбэк исполнится на этой ноде.

Какие есть общекластерные события?

  • LEADER_CHANGED — смена лидера;

  • LEADER_CHANGE_STARTED — когда старый лидер недоступен, а новый лидер еще не выбран (можно использовать, чтобы оградить старого лидера);

  • LEADER_STATE_IS_RW — событие на момент, когда лидер стал доступен на запись;

  • изменение конфигурации:

    • TERM_CHANGED — изменение поколения;

    • NODE_ADDED — добавление ноды;

    • NODE_REMOVED — удаление ноды.

Добавление и удаление ноды может быть важно, например, для Proxima. Если добавляются какие-то новые Follower-ноды, то можно на них балансировать читающий трафик.

На смену состояния ноды сейчас доступно три события:

  • CANDIDATE_TO_LEADER — переход в состояние LEADER из CANDIDATE;

  • OFFERED_TO_LEADER — переход в состояние LEADER при ручном назначении лидера, когда мы вызываем функцию biha.set_leader;

  • LEADER_TO_FOLLOWER — выход из состояния LEADER.

Важно отметить, что именно колбэк на событие LEADER_TO_FOLLOWER вызывается в тот момент, когда лидер замечает, что он больше не лидер.

Давайте посмотрим, когда вызываются колбэки. Для этого на рисунке покажем машину состояний одной ноды BiHA.

Когда Postgres Pro запускается, BiHA находится в состоянии Startup и ждет, пока Postgres Pro достигнет точки консистентности. После этого нода переходит в специальное состояние Cstate Forming, в котором нода решает, кем она станет: Follower или Leader RW (репликой или мастером, доступным на запись).

Когда нода находится в состоянии Leader RO, она может принять решение выполнить promote и стать доступной на запись. Именно в этот момент, уже после этого перехода в Leader RW, вызывается колбэк LEADER_STATE_IS_RW.

В случае сбоя лидера нода Follower перейдет в кандидата, далее перейдет в лидера, доступного только на чтение. И здесь вызовется колбэк CANDIDATE_TO_LEADER. Только после этого перехода, только когда колбэк-функция завершится, нода может перейти в лидера, доступного на запись, и в этом случае сработает колбэк LEADER_STATE_IS_RW.

Аналогично работает колбэк на назначение ноды (при вызове администратором функции biha.set_leader). Отработает колбэк OFFERED_TO_LEADER, когда нода будет в состоянии Leader RO.

На рисунке не рассматриваются все состояния BiHA и все возможные переходы, а также не представлены колбэки на смену конфигурации, потому что они вызываются из любого состояния, когда нода заметила, что конфигурация поменялась.

Давайте перейдем к функции, которую мы хотим исполнить. Сигнатура функции зависит от событий, на которые мы хотим подписаться.

Если мы хотим подписаться на локальные события, то ваша функция ничего не принимает, но внутри самой этой функции можно напрямую вытащить всю информацию из Postgres.

В событиях на смену состояния лидера в функцию передается вся информация о лидере, его ID, хост и порт, чтобы можно было в случае необходимости как раз на этого лидера и перенаправить трафик.

Сигнатура на изменение конфигурации будет зависеть от того, что произошло. Если изменилось поколение, то в функцию передаются старое и новое значения term. При добавлении ноды — вся информация о новой ноде, ее ID, хост, порт и режим работы. А при удалении ноды — ID удаляемой ноды.

Вот пример простой функции на PLPGSQL:

CREATE OR REPLACE FUNCTION new_leader(id INTEGER, host TEXT, port INTEGER)
RETURNS VOID AS $$
BEGIN
	RAISE LOG 'Callback: New leader is % %:%', id, host, port’;
	redirect_traffic(host:port);
END;
$$ LANGUAGE plpgsql;

Как видно, функция принимает информацию о новом лидере и использует аргументы по своему усмотрению, например, записывает в журнал сервера сообщение.

Создавать функцию можно в вашей собственной базе данных или в базе данных biha_db. Мы также можем задать исполнителя — executor, от лица которого будет исполняться функция. По умолчанию, это biha_callbacks_user, у него нет никаких прав, и при необходимости можно выдать ему нужные привилегии. Или лучше всего создать собственного пользователя с нужными грантами.

На одно событие можно зарегистрировать несколько колбэков. В каком порядке исполняются функции? Во-первых, функции исполняются последовательно, распараллеливания нет. Чтобы можно было задать порядок выполнения функций, есть параметр приоритета. Чем меньше это число, тем раньше исполнится колбэк. По умолчанию приоритет равен нулю.

BiHA всегда дожидается завершения функции колбэка перед тем, как продолжить работу! Чтобы исполнение функции не заблокировало работу кластера, есть параметр biha.callbacks_timeout. Этот таймаут общий для всех событий и по умолчанию равен 10 секундам.

Теперь давайте посмотрим, как все это на практике работает.

Давайте зарегистрируем наш колбэк на смену лидера на лидере:

register_callback(LEADER_CHANGED, new_leader, mydb)

Колбэк распространится по репликации на другие узлы. Если произошел сбой и BiHA выбрала себе нового лидера, то вызываются колбэк-функции на всех нодах на смену лидера.

Теперь заглянем внутрь инстанса Postgres Pro. У нас запущен процесс BiHA, который, увидев, что произошло событие, увидит, что есть колбэк-функции, и тогда он создаст колбэк-менеджера — отдельный процесс, который и займется исполнением колбэков.

Для каждой функции менеджер создаст еще отдельный процесс, callback executor, который уже исполнит функцию.

С одной стороны, исполнение колбэк-функций производится в отдельных процессах, а в рамках одного процесса Postgres не позволяет переподключиться к базе данных. С другой, это дает нам преимущество — изолированное исполнение функции, так как каждая функция исполняется в отдельном процессе своим адресным пространством.

Что делает Callbacks Manager? Он ждет, пока колбэк-функции завершатся или произойдет таймаут, о котором мы говорили ранее. Если вдруг Callbacks Executor станет нехорошо, то менеджер не растеряется и перейдет к следующему колбэку.

Какие есть ограничения у такой реализации колбэк-функций:

  • Функции регистрации и удаления колбэка не дожидаются, когда все ноды получат колбэк-функции. К чему это может привести? Например, есть лидер и есть очень отставший Follower. Пусть на лидере был колбэк на удаление ноды, и мы решили этот колбэк удалить. Потом мы решили удалить одного из Follower. Как думаете, что произойдет? Follower вызовет колбэк, который мы только что вроде как удалили.

  • Следующее ограничение — это то, что на легковесной ноде рефери колбэков вообще не будет. Рефери — это такая специальная нода в BiHA, которая не принимает WAL с лидера, и нужна, только чтобы достигать кворума при голосовании. В BiHA есть более интересный режим работы рефери — рефери с WAL, на нем будут колбэк-функции, но только те, которые зарегистрированы в базе biha_db.

Дадим теперь общие рекомендации по использованию колбэков:

  • добавляйте и удаляйте их только тогда, когда кластер в стабильном состоянии;

  • колбэк-функция должна завершаться быстро. Быстро — это значит не превышать biha.callbacks_timeout;

  • при регистрации колбэка укажите с помощью параметра executor роль, которая имеет права на вызов функции;

  • не вызывайте функции управления кластером BiHA внутри колбэка, потому что это может привести к неопределенному поведению (смену состояния нод, за которым последует вызов колбэка, который приведёт к новой смене состояний нод и так далее).

Комментарии (0)