Аналитический отчёт способен поставить на колени всю базу. Один тяжёлый запрос, перемалывающий миллионы строк, забивает диск и память, и в этот момент обычные пользовательские запросы начинают тормозить, хотя данные им нужны копеечные. Очевидное решение это отправить аналитику на реплику, которая только и делает, что повторяет за основным сервером. Но как заставить приложение само понимать, какой запрос куда направить, не переписывая тысячи строк кода. Здесь и выходит на сцену прокси PgCat, который берёт разделение чтения и записи на себя. Разберём, как он устроен, как его настроить и где у этой схемы подводные камни.
Почему вообще стоит выносить чтение на отдельные серверы
Идея проста. Основной сервер принимает запись и рассылает изменения на реплики через потоковую репликацию, а реплики служат копиями, готовыми отвечать на чтение. Если направить тяжёлую аналитику на реплику, она будет молотить свои миллионы строк, никак не мешая основному серверу обслуживать быстрые пользовательские запросы. Запись по-прежнему идёт только на основной сервер, а чтение распределяется по репликам, и нагрузка размазывается.
Само по себе разделение можно сделать двумя путями на стороне приложения. Первый это завести два клиента базы, для записи и для чтения, и таскать их по коду. Путь самый явный и предсказуемый, но и самый дорогой: добавить такое в существующий проект означает перелопатить тысячи строк. У него есть и обратная беда, когда читающий запрос по недосмотру уходит через пишущий клиент. Второй путь это middleware, разбирающий каждый запрос и сам выбирающий пул. Гибче, но тоже требует встраивания в код. PgCat предлагает третий путь: вынести всю логику маршрутизации в прокси перед базой, оставив приложение в неведении.
Что такое PgCat и чем он отличается от старых решений
PgCat это современный прокси для PostgreSQL, написанный на Rust поверх асинхронной среды выполнения, многопоточный и умеющий выжимать многоядерные машины. По сути это пулер соединений вроде привычного PgBouncer, но с дополнительными возможностями: разделением чтения и записи, балансировкой нагрузки между репликами, автоматическим обходом отказавших узлов и шардированием. Традиционные решения служили верой и правдой, но в облачных средах упираются в ограничения.
Ключевая для нашей задачи возможность это встроенный разборщик запросов. Если в конфигурации есть основной сервер и реплики, PgCat сам разбирает каждый запрос и направляет все запросы чтения на реплику, а всё остальное, включая явные транзакции, на основной сервер. Разборщик старается определить место запроса как можно точнее, но иногда однозначно решить не получается, и для таких случаев есть ручное управление, о котором ниже.
Как выглядит боевая конфигурация для разделения нагрузки
Настройка PgCat задаётся в конфигурационном файле и для нашей задачи укладывается в несколько секций. Сначала общие параметры прокси.
[general]
host = "0.0.0.0"
port = 6432
admin_username = "pgcat_admin"
admin_password = "secure_password"
worker_threads = 8 # из 16 ядер, оставляя запас системе
connect_timeout = 5000
healthcheck_timeout = 1000
Дальше описывается сам пул с режимом маршрутизации и поведением разборщика.
[pools.analytics]
pool_mode = "transaction"
default_role = "any"
query_parser_enabled = true # разбирать запросы для маршрутизации
query_parser_read_write_splitting = true # разделять чтение и запись
primary_reads_enabled = false # не слать чтение на основной сервер
И, наконец, перечисляются серверы пула с указанием их роли.
[pools.analytics.shards.0]
servers = [
[ "pg-primary.internal", 5432, "primary" ],
[ "pg-replica-1.internal", 5432, "replica" ],
[ "pg-replica-2.internal", 5432, "replica" ],
]
database = "myapp_production"
[pools.analytics.users.0]
username = "app_user"
password = "app_password"
pool_size = 30
statement_timeout = 30000
Приложение подключается к PgCat ровно так же, как к обычной базе, по тому же протоколу. А прокси уже сам решает, на какой из перечисленных серверов уйдёт конкретный запрос.
Какие настройки маршрутизации решают, куда уйдёт запрос
Поведение маршрутизации определяется несколькими параметрами, и их стоит понимать. Роль по умолчанию задаёт, куда уходит запрос, если клиент явно не указал. Значение, разрешающее любой сервер, гоняет чтение по кругу между основным сервером и репликами. Значение реплики держит чтение только на репликах, не трогая основной сервер. Значение основного сервера отправляет всё туда, если не указано иное. Для аналитики разумно держать роль на репликах, чтобы тяжёлые отчёты гарантированно не задевали основной сервер.
Параметр участия основного сервера в чтении тесно связан с предыдущим. Если он включён, основной сервер входит в пул серверов для балансировки чтения. Если выключен, основной сервер используется только под запись, и для разгрузки именно это и нужно. Сама балансировка между репликами идёт по одному из алгоритмов: случайному выбору или выбору наименее загруженного по числу открытых соединений.
Запросы балансируются между репликами автоматически, а вокруг сломанных реплик PgCat перенаправляет трафик сам, проверяя их регулярными проверками здоровья. Это снимает с приложения и заботу о том, что одна из реплик вдруг отвалилась.
Когда разборщик ошибается и как управлять маршрутом вручную
Разборщик запросов хорош, но не всемогущ. Бывают случаи, когда определить место запроса однозначно невозможно. Для таких ситуаций PgCat даёт ручное управление через специальный синтаксис, которым клиент сам выбирает сервер на время следующей транзакции.
-- Отправить следующую транзакцию на основной сервер
SET SERVER ROLE TO 'primary';
-- Отправить следующую транзакцию на реплику
SET SERVER ROLE TO 'replica';
Это спасательный круг для пограничных случаев. Например, если аналитический запрос обёрнут так, что разборщик принял его за пишущую транзакцию, можно явно загнать его на реплику. Или наоборот, если критично свежее чтение, заставить его пойти на основной сервер.
Почему отставание репликации это главный подвох для аналитики
Тут скрывается самая важная оговорка, о которой нельзя забывать. Реплика повторяет за основным сервером не мгновенно. Между записью на основном сервере и её появлением на реплике есть лаг репликации, и из-за него чтение с реплики может вернуть устаревшие данные. Для многих аналитических задач это терпимо: отчёт за вчерашний день не пострадает от того, что данные отстают на пару секунд. Но если аналитика обязана видеть только что записанные данные, наивная отправка её на реплику даст неверный результат.
PgCat предлагает изящный механизм против этого, маршрутизацию на основе активности базы. Если включить её, прокси будет направлять запрос на основной сервер, если запрашиваемая таблица недавно подвергалась записи. Работает это только при включённом разборщике и разделении чтения и записи.
[pools.analytics]
query_parser_enabled = true
query_parser_read_write_splitting = true
db_activity_based_routing = true
db_activity_ttl = 900 # таблица считается неактивной через 900 секунд
table_mutation_cache_ms_ttl = 50
Логика такая: после записи в таблицу все запросы к ней на короткое время уходят на основной сервер, гарантируя свежее чтение, а потом снова распределяются по репликам. У механизма есть существенное ограничение: он работает не так, как ожидается, если перед базой стоит несколько экземпляров PgCat для отказоустойчивости, и тогда выручают липкие сессии. Это важно учитывать в больших развёртываниях.
Какие ещё ограничения держать в голове
Транзакционный режим пулинга, который обычно и используют, накладывает свои ограничения, унаследованные от подобных пулеров. Подготовленные выражения, установка параметров сессии и рекомендательные блокировки уровня сессии в нём не поддерживаются. Альтернатива это локальная установка параметров и блокировки, ограниченные транзакцией.
Отдельно стоит помнить, что PgCat моложе проверенных временем решений и сообщество вокруг него меньше. Для простого пулинга соединений зрелый PgBouncer остаётся более безопасным выбором. А вот ради разделения чтения и записи, многопоточной производительности и работы со сложными топологиями PgCat по-настоящему привлекателен. Это инструмент под конкретную задачу, а не универсальная замена всему.
Все настройки, кроме адреса и порта, перезагружаются на лету без перезапуска прокси, включая конфигурации шардов и реплик. Есть и любопытная возможность зеркалирования, когда запросы дублируются на несколько баз сразу, что удобно для прогрева реплики перед вводом в строй или для тестирования новой версии PostgreSQL на живом трафике. Статистика доступна через служебные базы и отдельный HTTP-эндпоинт для сбора метрик.
Какой стратегии придерживаться
Вынос аналитики на реплики через PgCat решает реальную боль: тяжёлые отчёты перестают мешать пользовательской нагрузке, и всё это без переписывания приложения. Разумный подход складывается из нескольких решений. Держать роль по умолчанию на репликах и отключить участие основного сервера в чтении, чтобы отчёты гарантированно его не трогали. Включить разборщик запросов и разделение чтения и записи. Для запросов, требующих свежих данных, включить маршрутизацию на основе активности базы или явно загонять их на основной сервер через ручное управление ролью. И всегда помнить про лаг репликации как про источник возможных устаревших чтений.
Главная мысль в том, что реплика это не бесплатная копия с мгновенной согласованностью, а инструмент с компромиссом между разгрузкой и свежестью данных. Тот, кто слепо шлёт всю аналитику на реплику, рано или поздно получает отчёт с устаревшими цифрами в самый ответственный момент. А тот, кто понимает природу лага и настраивает маршрутизацию осознанно, получает и разгруженный основной сервер, и достоверную аналитику там, где она действительно должна быть свежей.