PgCat  —  это open source прокси-сервер Postgresql, в развитии которого мы участвуем и который используем на продакшене. Функционал PgCat для кластеров Postgresql: пул соединений, балансировка нагрузки, отработка отказа реплики.

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

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

Раньше эти проблемы решались на стороне приложения библиотекой Ruby Makara  —  адаптером подключения ActiveRecord для балансировки нагрузки и отработки отказа реплики. Но у Makara имеются недостатки, а главное  —  это решение только для Ruby.

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

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

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

Почему PgCat?

Нам нужен прокси-сервер Postgresql, в котором, помимо пула соединений PgBouncer, имеются балансировка нагрузки, отработка отказа реплики и возможность безопасного расширения функционала в будущем.

Мы оценили варианты, исключив Pgpool из-за отсутствия режима транзакций:

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

Оценка PgCat

PgCat был тогда в бета-версии. Добавив недостающий функционал (несколько пулов на экземпляр и нормальное завершение работы), мы протестировали этот прокси в средах «песочницы» и продакшена. Особенно важным было сравнение PgCat с Pgbouncer по временно́й задержке и корректности.

Временна́я задержка

Чтобы сравнение при тестировании временно́й задержки было сопоставимым и сквозным, мы настроили в службе применение одной реплики PgCat, другой  —  PgBouncer, а клиентской библиотекой балансировали нагрузку между ними.

В рабочих нагрузках продакшена обнаружилось близкое соответствие профилей временно́й задержки PgCat и PgBouncer. Вот процентили задержки сквозной обработки запросов на обоих прокси при размере выборки данных примерно в 1,5 млн запросов продакшена, выполненных за 12 часов:

+------+-----------+---------+------------+
| | PgBouncer | PgCat | Разница |
+======+===========+=========+============+
| p50 | 4,68 мс | 4,69 мс | 10 мкс |
| p75 | 6,72 мс | 6,78 мс | 60 мкс |
| p90 | 10,8 мс | 10,9 мс | 100 мкс |
| p95 | 14,8 мс | 15,0 мс | 200 мкс |
| p99 | 26,8 мс | 27,8 мс | 1 мс |
+------+-----------+---------+------------+
Временна́я задержка сквозной обработки запросов
Разница между средними значениями p50 составляет ~10 мкс
Разница между средними значениями p90 составляет ~100 мкс
Разница между средними значениями p99 составляет ~1 мс

При переходе на PgCat заметного влияния на временну́ю задержку службы в целом не наблюдается.

Корректность

Мы проверили, что прокси корректно обращается с протоколом при взаимодействии с базой данных и клиентами, а поведение PgCat и PgBouncer как пулов соединений аналогично.

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

А еще, чтобы подключиться к уже имеющимся инструментам PgBouncer, протестировали поддержку в PgCat команд управления RELOAD, SHOW POOLS и т. д., применяемых в PgBouncer для мониторинга и контроля соединений.

Мы выявили и исправили проблемы, например с нормальным завершением работы, многопользовательским пулом, и сейчас PgCat без проблем справляется с рабочими нагрузками продакшена.

Отличия PgCat от PgBouncer

PgCat  —  это упрощенный PgBouncer. Помимо команд управления и статистики, нескольких пользователей и пулов PgBouncer, в нем поддерживаются режимы сессии/транзакции. Но чем отличается PgCat от PgBouncer?

Архитектура

PgCat  —  это многопоточный прокси на Rust с асинхронной средой выполнения Tokio. Pgbouncer  —  однопоточный прокси на C и libevent.

Схема развертывания

PgCat размещается перед одним экземпляром БД. Согласно схемам A и B ниже, поведение PgCat аналогично PgBouncer.

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

Если перед каждым экземпляром PgCat лишь один экземпляр БД, балансировка нагрузки или отработка отказа не выполняется.

Проблема решается различными способами:

  • Несколько контейнеризированных экземпляров PgCat развертываются за балансировщиком нагрузки, и каждый подключается к нескольким БД в кластере.
  • Один экземпляр PgCat запускается на многоядерном хосте отдельно от БД.
  • Несколько экземпляров PgCat вместе размещаются на машинах с БД, но взаимодействуют с другими экземплярами БД в кластере.

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

В Instacart, согласно схеме C, применяется контейнеризированная схема с репликами, экземпляры обычно разделяются на два пула: один только для первичной, другой для всех реплик.

Примеры схем развертывания для PgCat

Функционал

Балансировка нагрузки

В случае с PgCat в пуле размещается несколько экземпляров БД, между которыми затем распределяется нагрузка. Так код приложения сильно упрощается: до подключения лишь к одной конечной точке БД.

Сейчас в PgCat поддерживается два алгоритма балансировки нагрузки: случайный выбор и наименьшие соединения.

Согласно первому, из пула просто выбирается случайный экземпляр. Позже покажем, что эта стратегия лучше, чем Round-Robin.

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

Отработка отказа реплики

В настройке с несколькими репликами нужна возможность перенаправлять трафик от отказавшей реплики, пока та не восстановится. Быстро найдя такую реплику, мы уменьшим последствия для приложения. Когда в PgCat обнаруживается отказ реплики, ее использование запрещается по умолчанию на 60 секунд.

В PgCat реплика запрещается в следующих случаях.

  1. Истекло время запроса post-checkout проверки работоспособности или случился его сбой.
  2. Клиент теряет соединение при подключении к реплике.
  3. У клиента истекло время ожидания проверки подключения из пула к этой реплике.
  4. В запросе превышено заданное время ожидания инструкции PgCat, это значение в PgCat по умолчанию отключено.

Запрет реплик в сочетании с Round-Robin чреват перекосом нагрузки, поэтому мы использовали случайную балансировку нагрузки. Рассмотрим систему с четырьмя репликами: клиенты проходят через них по порядку, хотя с разными начальными точками, то есть 3 всегда после 2, а 4 после 3 и т. д.

Проблема появляется при запрете реплики: для всех клиентов, которые переходят через нее к следующей, он разрешится одним и тем же экземпляром. Например, если запретить реплики 2 и 3, весь трафик из них окажется в реплике 4: она следующая в очереди для всех клиентов. Этот эффект должен быть временным, но оказывается постоянным, судя по нашему опыту.

Перейдя в PgCat на случайную балансировку нагрузки, мы устранили перекос:

Балансировка нагрузки Round-Robin с запрещенной репликой. Во время первого события запрета в 06:45 весь трафик с отказавшей реплики пришелся на одну реплику, нагрузка на другие две осталась прежней
Случайная балансировка нагрузки с запрещенными репликами: перекос гораздо меньший

Клиенты с плохим поведением

Клиент с плохим поведением несвоевременно отключается от прокси:

  • открывает транзакцию и, пока она открыта, отключается;
  • отключается во время выполнения запроса.

В PgBouncer в этих случаях соответствующее соединение с сервером закрывается. Это проблематично, если случается слишком часто: чревато сильными перегрузками подключения к серверу. Такие события негативно сказываются на производительности БД.

В PgCat эта проблема решается запросом на откат подключения к серверу и возвращением его обратно в пул.

Поддержка сегментированных баз данных

В PgCat поддерживается первоклассная концепция сегментирования: по ключу сегментирования или номеру сегмента в приложении выбирается сегмент для взаимодействия. Это функционал еще только планируется в рабочих нагрузках продакшена.

Текущее состояние

Последние полгода PgCat применяется для некоторых наших несегментированных рабочих нагрузок продакшена. Пиковая пропускная способность  —  около 105 000 запросов в секунду для нескольких ECS task, для каждой task  —  около 5200. Клиенты различались по языку и шаблонам, включая Ruby, Python и Go: все перешли на PgCat и использовали его без проблем и изменений.

Мы перенести одну из крупнейших наших БД на PgCat, добавив балансировку нагрузки некоторым службам. В итоге обработали несколько сбоев БД, упростили сопровождение ее экземпляров.

Сейчас переносим в PgCat бо́льшую часть рабочих нагрузок продакшена, думаем применить PgCat в кластерах сегментированных БД.

Скоро выходит версия 1.0 PgCat, и ведется активный поиск новых участников для развития проекта.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Mostafa Abdelraouf: Adopting PgCat: A Nextgen Postgres Proxy

Предыдущая статьяСоздание приложения ChatGPT в SwiftUI
Следующая статьяBlaze: ускорение ZK для программируемой пользователем вентильной матрицы