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

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

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

Мы разработали расширение Proxima, которое снимает необходимость в настройке и администрировании дополнительного программного обеспечения. Proxima — масштабируемый, отказоустойчивый прокси-сервер и пулер соединений, способный перенаправлять трафик на пишущий узел, а при работе с BiHA переключение на новый пишущий узел в случае аварии происходит автоматически.

В докладе Тофиг Алиев расскажет, как мы делали Proxima, какие архитектурные решения заложены в расширение, почему мы выбрали именно такой подход. Разберет тонкости реализации, которые позволили нам обрабатывать более 10 тысяч одновременных клиентских сессий. Рассмотрит примеры использования и ответит на вопросы.

Расширение Proxima входит в состав Postgres Pro Enterprise. Вначале поговорим о проблемах, которые призваны решать Proxima, об архитектуре Proxima, из каких компонентов она состоит, как они взаимодействуют друг с другом.

Предлагаю начать с архитектурных особенностей PostgreSQL.

Postgres является многопроцессным приложением. Он запускает ряд фоновых процессов, каждый из которых выполняет какую-то определенную задачу. К примеру, на рисунке можно увидеть процесс postmaster. Это основной процесс Postgres, который запускает все остальные фоновые процессы, в том числе отвечает за прием новых входящих клиентских подключений. Для каждого принятого клиентского подключения создается отдельный процесс под названием бэкенд (backend), в рамках которого происходит обработка данной клиентской сессии.

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

Приходим к тому, что Postgres не может обрабатывать достаточно большое количество клиентских сессий. Чтобы решить эту проблему, существует класс программ — пулеры соединений.

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

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

Есть некоторый primary-узел и некоторое количество реплик. На картинке это два standby-узла. Мы взаимодействуем с нашим primary-узлом через пулер соединений. Primary в реальном времени поддерживает несколько своих реплик. Предположим, что primary по какой-то причине вышел из строя, перестал быть доступен. Чтобы выбрать нового primary среди существующих реплик, нам нужно некоторое стороннее ПО. Также нужно переконфигурировать пулер соединений так, чтобы новые соединения открывались уже с новым primary, который был выбран из действующих реплик.

Давайте рассмотрим другую картину. У нас есть реплики, а реплики доступны для чтения.

Соответственно, мы можем балансировать читающую нагрузку на наши реплики. Давайте поставим перед всем нашим кластером некоторый прокси и поставим перед каждым из узлов пулер соединений. Наш прокси будет направлять пишущие транзакции на primary-узел, а читающие — балансировать между репликами. В этой конфигурации Proxy также становится точкой отказа и дополнительной единицей администрирования, за которой нужно следить, использовать некоторые скрипты автоматизации или опять же какое-то стороннее ПО.

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

Давайте рассмотрим наш отказоустойчивый кластер и изобразим на этой картинке Proxima.

Proxima — это один из фоновых процессов Postgres, то есть порождается основным процессом postmaster. Proxima на узлах кластера связаны друг с другом. На каждом из узлов Proxima выполняет роль пулера соединений: она позволяет исполнять клиентские транзакции с использованием пула бэкенд-процессов, то есть поддерживает ограниченное количество соединений с БД. При необходимости она умеет маршрутизировать ваш запрос на другой узел и исполнять его там, и ответ возвращается на тот узел, куда пришел клиент.

Proxima, в отличие от самого Postgres, является многопоточной, а не многопроцессной. Для разработки Proxima был создан специальный фреймворк — библиотека для написания сетевых приложений с возможностью асинхронного межсетевого взаимодействия. Проще говоря, это библиотека, которая позволяет создавать POSIX threads — потоки операционной системы, и в рамках этих потоков запускать какое-то количество корутин (Coroutine) и выполнять в них какую-то логику.

Компоненты, из которых состоит Proxima:

Основной поток, поток main. Он занимается начальной инициализацией расширения, созданием всех необходимых вспомогательных потоков. Воркеры (Workers) — рабочие потоки Proxima, занимаются непосредственно обработкой клиентских соединений. Работает это все так, что в рамках main-потока запускается какое-то количество служебных корутин. К примеру, корутина, которая отвечает за прием клиентских соединений.

После того, как клиентское соединение было принято, оно передается в один из потоков воркеров, то есть все эти потоки содержатся в так называемом воркер-пуле. В рамках потока воркера создается корутина, в которой будет обрабатываться данное клиентское соединение. Таким образом корутина ассоциирована с каким-то клиентским соединением и называется фронтенд (Frontend).

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

Proximа поддерживает для входящих соединений работу с SSL: можно все сконфигурировать, указав путь до сертификата и включить у Proxima опцию работы с SSL. На данный момент Proxima поддерживает методы аутентификации сlear password, md5, scram-sha-256 и trust.

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

Дополнительный компонент роутер (Router) отвечает за получение бэкенда из пула. Предположим, что приходит транзакция, во фронтенде происходит обращение к компоненту роутер. Компонент роутер получает метаинформацию, в которую входит имя базы данных и имя пользователя, под которыми аутентифицировался клиент.

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

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

Соответственно, если клиент создал временную таблицу, вы не можете в дальнейшем исполнять его запросы, используя произвольное соединение из пула. Поэтому появляется такое понятие как dedicated mode — это режим, который в Postgres Pro Enterprise работает только при интеграции с Proxima.

Давайте рассмотрим, что это такое и как работает. Когда приходит новая транзакция на исполнение, в принципе происходит поначалу то же самое. Мы обращаемся к роутеру с просьбой выдать нам бэкенд, чтобы мы могли исполнить клиентскую транзакцию. Передаем метаинформацию и осуществляем обращение к бэкенд-пулу посредством роутера. Роутер нам выдает бэкенд, и мы в рамках исполнения запроса создаем сессионный объект, например, временную таблицу. Теперь мы не имеем права вернуть его обратно в пул, поскольку сессионный объект живет в рамках этого бэкенда и запрос мы можем исполнять только с использованием данного бэкенда. Однако, как только мы почистим все сессионные объекты, к примеру, удалим созданные нами временные таблички, бэкенд выходит из состояния dedicated, и мы можем снова вернуть его в пул.

Proxima также обладает функциями Proxy, то есть она умеет перенаправлять запросы на другой узел. Для этого у Proxima существует вспомогательный компонент — интерконнектор (Interconnector).

Интерконнектор — это отдельный поток, также со своим набором корутин. Каждый интерконнектор устанавливает друг с другом одно TCP-соединение.

В рамках интерконнектора существует channel — это логический канал. Может создаваться сколько угодно много channel. При создании channel создается аналогичный channel на другой стороне, он подписывается в качестве получателя сообщений, которые передаются по TCP-каналу, открытому между интерконнекторами. По интерконнектору мы можем проксировать запросы на другой узел. Также и роутер теперь может ходить на другой узел.

Интерконнектор поддерживает шифрование, то есть в рамках TCP-соединения можно включить SSL. Также интерконнектор поддерживает аутентификацию. На данный момент это аутентификация с использованием так называемого метода trust (когда соединение между интерконнекторами считается уже доверенным) и аутентификация посредством SSL-сертификата (при использовании аутентификации с методом SSL TCP-соединения аутентифицируются по сути взаимной проверкой сертификатов). Интерконнекторы проверяют сертификаты друг друга.

Proxy-to-leader — это режим работы Proxima, когда независимо от того, на какой узел кластера вы пришли, ваш запрос будет отправлен на лидера, и на лидере исполнен. После чего ответ вернется на тот узел, куда вы пришли, и будет передан уже вам.

Есть три конфигурации:

Первая — это standalone. В данной конфигурации Proxima выполняет исключительно роль пулера: все клиентские подключения будут исполняться с использованием локального пула через локальные бэкенды Postgres, на котором находится Proxima.

А также конфигурации: GUK и BiHA. В этих конфигурациях Proxima выполняет роль как пулера, так и роль Proxy-сервера. Если вы приходите на какую-то реплику, ваш запрос отправляется на primary-узел и там исполняется с использованием пулера.

Конфигурация GUK — на старте Proxy происходит запрос конфигурации кластера Postgres. В данной конфигурации необходимо описать в конфиге полностью состав кластера, ноды и их статус, primary или standby. Если клиентское подключение поступает на узел standby, и выполняется некоторая клиентская транзакция, она направляется на primary-узел и исполняется на primary-узле, так как включен режим proxy-to-leader. Непосредственно ответ возвращается на тот узел, куда изначально пришел клиент, и возвращается напрямую клиенту.

Предположим, что primary-узел по какой-то причине вышел из строя. Тогда в данной конфигурации необходимо с помощью, например, скриптов автоматизации, избрать среди существующих реплик нового primary, и также переконфигурировать Proxima: задать новую конфигурацию кластера. Proxima в данной конфигурации не автоматизирована, и важно понимать, что Proxima в этом случае не контролирует репликацию между узлами. Это уже задача администратора.

Давайте перейдем к более интересной конфигурации — BiHA.

BiHA — это built-in high-availability, встроенный отказоустойчивый кластер Postgres Pro. Задача этого расширения — автоматически контролировать репликацию в кластере: при сбое лидера проводить выборы нового лидера и соответственно перенастраивать автоматическую репликацию в кластере. Proxima интегрируется с BiHA.

На старте Proxima запрашивает у BiHA конфигурацию кластера. Proxima определяет, кто в кластере лидер.

Лидером на рисунке является вторая нода. Соответственно, новое клиентское подключение поступает на follower, а так как включен режим Proxy-to-leader, запрос клиента перенаправляется на лидера и там исполняется. После чего результат возвращается на follower и отправляется к клиенту. Если по какой-то причине из строя выходит узел-лидер, BiHA производит процедуру выбора нового лидера и нотифицирует Proxima о том, что в кластере сменился лидер.

В данном случае лидером стала первая нода. Об этом сообщила BiHA, и теперь все клиентские запросы, которые будут поступать на follower, будут проксироваться на исполнение на нового лидера.

В заключение — немного о планах разработки и о том, что окажется в Proxima в ближайших релизах.

Основной порт Proxima, он показан зеленым цветом, теперь будет именоваться как Proxy-to-Leader-порт. Подключаясь на данный порт, все транзакции, входящие от клиентов, будут направляться на лидера, исполняться на нём. Результат будет возвращаться сначала на тот узел, куда клиент пришел и уже непосредственно самому клиенту.

Кроме этого, появится вспомогательный порт — Proxy-to-Follower-порт. Он будет предназначен для того, чтобы исполнять читающие запросы. Когда клиент будет подключаться на порт Proxy-to-Follower, все его запросы будут балансироваться между всеми существующими репликами.

Таким образом, мы получаем Load Balancer для читающей нагрузки.

Основными алгоритмами балансировки будут round-robin (раскидывание последовательно по всем репликам) и алгоритм random, который будет выбирать произвольно любую из доступных реплик. В дальнейшем будут и другие алгоритмы.

Предполагается, что все пишущие транзакции будут приходить на порт Proxy-to-Leader и непосредственно исполняться на primary. Независимо от того, на какой узел вы придете, ваша транзакция окажется на primary и будет исполнена на primary. Все транзакции, которые непосредственно будут поступать на порт Proxy-to-Follower, будут сбалансированы между существующими репликами в кластере и исполнены на одной из реплик, после чего результат вернется изначально на тот узел, на который вы пришли, и после этого уже отправится вам.

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