Паттерны управления трафиком к PostgreSQL в production
Один неудачный аналитический запрос способен положить весь checkout. PlanetScale предлагает решение — Database Traffic Control: ресурсные бюджеты для разных «слоёв» трафика к Postgres. Разбираем паттерны на Go, которые можно перенести на любой стек.
Один неудачный аналитический запрос способен положить весь checkout. PlanetScale предлагает решение — Database Traffic Control: ресурсные бюджеты для разных «слоёв» трафика к Postgres. Разбираем паттерны на Go, которые можно перенести на любой стек.
Key Takeaways
- SQLCommenter — формат тегов в SQL-комментариях, по которым Traffic Control различает запросы
- Изоляция микросервисов через Postgres-роли и application_name
- Автоматическая маркировка запросов по HTTP-роутам через middleware
- Контроль новых деплоев и фича-флагов через теги feature
- Tier-based лимиты для мультитенантных SaaS-приложений
- Обработка ошибки SQLSTATE 53000 при блокировке запроса
Перевод статьи Patterns for Postgres Traffic Control Джоша Брауна из блога PlanetScale.
Настройка: SQLCommenter и передача тегов через контекст
Большинство паттернов опираются на пользовательские теги, прикреплённые к запросам. Traffic Control читает их в формате SQLCommenter — это SQL-комментарий с URL-кодированными парами ключ=значение, добавляемый в конец запроса:
Эти теги затем доступны для правил Traffic Control. Вот минимальный хелпер на Go, который добавляет теги в этом формате:
Если вы используете pgx с включённым PrepareThreshold, комментарий SQLCommenter станет частью ключа кэша prepared statement. Это может привести к разрастанию кэша планов. Решение — отключить PrepareThreshold для подключений с тегированием или использовать SimpleProtocol.
Примеры кода рассчитаны на Go 1.22+. В частности, r.Pattern (поле http.Request) и range по целому числу (for attempt := range maxRetries) появились именно в этой версии.
Также понадобится способ пробрасывать теги через стек вызовов без изменения каждой сигнатуры функции. Ключ контекста отлично подходит для этого:
С этими двумя хелперами паттерны ниже просто устанавливают ключи и значения в контексте. Тегирование происходит автоматически при выполнении запроса.
Изоляция сервисов через Postgres-роли
В микросервисной архитектуре один «взбесившийся» сервис не должен деградировать все остальные сервисы, использующие ту же базу данных. Простейший способ изолировать сервис — создать правило Traffic Control на основе уникальной строки подключения или application_name.
Бюджет на username='pscale_api_123abc' изолирует весь трафик от этой роли. Это также помогает при инцидентах — можно мгновенно ограничить долю ресурсов сервиса без передеплоя.
Имя пользователя — это внутреннее Postgres-имя роли, а не имя роли в дашборде. Также можно целиться в кастомные роли, созданные через CREATE ROLE, если ваши микросервисы строго разделяют права на таблицы.
Также можно использовать application_name, добавив его к строке подключения:
Маркировка запросов по HTTP-роутам
Когда вы запускаете монолит или крупный API-сервис, проблема обычно не в целом сервисе — а в конкретных роутах. Эндпоинт /api/export, генерирующий CSV-отчёты, не должен убивать /api/checkout.
HTTP-middleware может инжектировать роут в контекст во время выполнения, до запуска любого обработчика:
Оберните вызовы к базе данных, чтобы они автоматически подхватывали теги:
Теперь каждый запрос несёт информацию о роуте, из которого пришёл. Можно создать бюджет Traffic Control для route='/api-export' и задать ему консервативный лимит CPU.
Это также упрощает управление инцидентами. Если вы видите всплеск и не знаете, какой роут виноват — график нарушений в Traffic Control покажет, какой именно тег route достигает лимитов.
Фича-флаги и новые деплои
Выкатка новой фичи в production всегда несёт риск. Может, новый паттерн запросов нормально работает под тестовой нагрузкой, но становится дорогим на масштабе. Traffic Control позволяет ограничить радиус поражения до того, как ситуация превратится в инцидент.
Простейшая версия устанавливает тег из переменной окружения при старте:
Установите DEPLOYMENT_TAG=new_checkout_v2 при раскатке новых подов и оставьте переменную пустой на старых. Traffic Control может иметь бюджет на feature='new_checkout_v2' в режиме Warn с первого дня — так вы увидите, как новый код себя ведёт, прежде чем он вызовет проблемы.
Для фича-флагов, управляемых в рантайме, подход тот же, но определяется результатом проверки флага:
Tier-based лимиты для мультитенантных приложений
В SaaS-приложении пользователи бесплатного тарифа не должны деградировать работу enterprise-клиентов. Traffic Control позволяет обеспечить это на уровне базы данных, а не только на уровне приложения.
Инжектируйте тариф пользователя в SQL-теги сразу после аутентификации:
В middleware аутентификации:
Теперь создайте два бюджета Traffic Control:
tier='free'— консервативные лимиты на долю сервера и максимальное число параллельных запросовtier='pro'— умеренные лимиты
Enterprise-трафик оставьте без бюджета или задайте высокий потолок. Когда пользователь бесплатного тарифа запускает тяжёлый дашборд или триггерит медленный запрос — бюджет сбросит этот трафик до того, как он затронет enterprise-нагрузки.
Паттерны можно комбинировать. Бюджет tier='free' AND route='api-export' будет строже, чем бюджет только на tier='free'. Enterprise-экспорты получат больше ресурсов, чем бесплатные.
Фоновые задачи и скрипты
Фоновые задачи — частая причина инцидентов с базой данных. Миграция, ночная синхронизация или одноразовый бэкфилл данных могут случайно насытить БД, если выполняются быстрее ожидаемого. Traffic Control — чистый способ задать потолок ресурсов без настройки таймаутов на уровне каждого запроса.
Для долгоживущих фоновых воркеров используйте выделенный пул подключений с отдельным application_name:
Функция newJobDB принимает DSN базы и устанавливает application_name в background-jobs перед подключением. Максимум подключений ограничен четырьмя, чтобы фоновая задача не занимала больше воркеров, чем положено.
Установка application_name на уровне строки подключения в коде гарантирует, что имя всегда задано для этого сервиса — независимо от конкретного запроса или переменных окружения. Это можно комбинировать с SQL-комментариями для ещё более гранулярного контроля.
Для одноразовых скриптов и миграций — аналогичный подход. Здесь идентичность скрипта кодируется прямо в строке подключения:
Создайте бюджет Traffic Control для application_name='background-jobs' в режиме Warn. Понаблюдайте, сколько ресурсов базы обычно потребляет фоновая работа. Затем переключите на Enforce, чтобы ограничить её на уровне, при котором она не сможет вытеснить интерактивный трафик.
Обработка заблокированных запросов
Когда Traffic Control в режиме Enforce и запрос превышает бюджет, Postgres возвращает SQLSTATE 53000 с сообщением, начинающимся с [PGINSIGHTS] Traffic Control:. Приложение должно обрабатывать это без падения.
С pgx/v5:
Правильная реакция зависит от роли запроса в приложении. Для некритичных нагрузок — аналитики, отчётов — возвращайте 503 или закэшированный результат:
Для критичных путей можно реализовать короткий retry с экспоненциальным backoff:
Наблюдение за Warn-режимом
Прежде чем переключить бюджет в Enforce, запустите его в режиме Warn. В этом режиме запросы выполняются успешно, но драйвер получает Postgres-notice. С pgx/v5 можно логировать их, чтобы понять, что именно будет заблокировано:
Соберите логи за несколько часов репрезентативного трафика. Паттерн срабатываний подскажет, нужно ли корректировать лимиты.
Собираем всё вместе
Паттерны компонуются. В реальном приложении можно наслоить несколько из них:
Traffic Control видит всё это как комбинацию тегов. Бюджет на tier='free' покрывает весь free-трафик независимо от роута. Бюджет на route='api-export' AND tier='free' — конкретную комбинацию. Несколько совпадающих бюджетов применяются одновременно, и запрос должен удовлетворять каждому из них.
Начните в режиме Warn, понаблюдайте, какие бюджеты срабатывают при нормальной нагрузке, ужесточайте лимиты, пока не будут триггериться только патологические случаи — затем переключите на Enforce.
Выводы
Разница между падением базы и деградированным опытом часто сводится к тому, решили ли вы заранее, какой трафик сбрасывать. Traffic Control делает это решение явным и конфигурируемым — вместо того чтобы оставлять его на милость гонки за ресурсы.
FAQ
Что такое SQLCommenter и зачем он нужен?
SQLCommenter — это открытый формат от Google для добавления метаданных в SQL-запросы через комментарии. Traffic Control использует эти метаданные (теги) для идентификации и группировки запросов, чтобы применять к ним ресурсные бюджеты.
Что произойдёт, если запрос превысит бюджет в режиме Enforce?
Postgres вернёт ошибку с кодом SQLSTATE 53000 и сообщением, начинающимся с [PGINSIGHTS] Traffic Control. Приложение должно перехватить эту ошибку и вернуть деградированный ответ (503, кэш) или выполнить retry с backoff — в зависимости от критичности запроса.
Можно ли комбинировать несколько бюджетов Traffic Control?
Да. Несколько совпадающих бюджетов применяются одновременно, и запрос должен удовлетворять каждому из них. Например, бюджет на tier='free' и бюджет на route='api-export' будут работать вместе для free-tier запросов к экспорту.
Работает ли Traffic Control только с PlanetScale Postgres?
Database Traffic Control — продукт PlanetScale. Однако паттерны тегирования запросов через SQLCommenter, изоляции через роли и application_name применимы к любому Postgres. Для самостоятельной реализации похожего контроля можно использовать pgbouncer с лимитами пулов или pg_stat_statements для мониторинга.
Полный текст оригинальной статьи — в блоге PlanetScale.