PostgreSQL под enterprise-нагрузкой. Часть 2: отказы из практики и чек-лист для DBA

PostgreSQL под enterprise-нагрузкой. Часть 2: отказы из практики и чек-лист для DBA

В первой части мы настраивали систему: считали память и соединения, укрощали autovacuum, разбирались с WAL и восстановлением, поднимали мониторинг. Теперь — про то, что происходит, когда все это где-то недокрутили. Четыре истории из эксплуатации, каждая по одной схеме: симптом, который видит заказчик, диагностика, настоящая причина, чем чинили, что сделали, чтобы не повторилось.

Если вы не читали первую часть — в ней остались конфигурации, пороги алертов и оперативные SQL-запросы, на которые я ниже ссылаюсь. Здесь они не дублируются.

Четыре отказа из практики

Инцидент 1. «Задачи зависли», хотя система работает

Симптом
Пользователи сообщают, что задачи не продвигаются по маршруту, но в пример указывают только одну задачу. Мониторинг зеленый: CPU в норме, дисков хватает, запросы выполняются штатно.

Диагностика
pg_stat_activity
показывает сессию в состоянии idle in transaction с возрастом транзакции более 15 минут. pg_blocking_pids() подтверждает, что она держит блокировку на объекте, которого ждут остальные. Параллельно в pg_stat_user_tables виден рост n_dead_tup по всей базе — autovacuum не может почистить ничего новее удерживаемого xmin.

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

Лечение
Если невозможно попросить пользователя снять блокировку (закрыть документ), администратор может принудительно снять блокировку, перейдя в карточку документа. Надо понимать, что несохраненные изменения пользователя при этом останутся только в его локальной сессии.

Профилактика

  • настроить автоматическую разблокировку карточек для веб-сервера через DirectumLauncher или напрямую в config.yml, параметр UNCHANGED_CARD_AUTO_UNLOCK_TIMEOUT;
  • алерт на транзакции старше 15 минут — раньше, чем пользователи начнут писать в поддержку;
  • организационная мера: до пользователей доводится рекомендация закрывать документы и карточки, если работа с ними не планируется прямо сейчас или человек уходит на совещание. Не технологично, но работает лучше любого таймаута.

Инцидент 2. Переполнение pg_wal и остановка базы

Симптом
База встает намертво. В логе:

PANIC: could not write to file "pg_wal/xlogtemp...": No space left on device

или другая ошибка записи WAL. Место кончилось, а раз PostgreSQL не может писать WAL — он не может и фиксировать транзакции. Документооборот лежит целиком.

Диагностика
Первым делом смотрю размер pg_wal и как быстро он растет. Сам по себе распухший каталог — не болезнь, а симптом: база по какой-то причине не может удалить уже отработанные журналы. Дальше иду по списку — кто их держит:

  • реплики (pg_stat_replication) — нет ли отстающих;
  • слоты репликации (pg_replication_slots) — в первую очередь брошенные неактивные слоты, которые держат старые WAL;
  • архивация (pg_stat_archiver) — сколько ошибок archive_command и уходит ли WAL в архив;
  • длинные транзакции (pg_stat_activity) — не мешают ли очистке;
  • wal_keep_size и max_slot_wal_keep_size — сколько журналов база держит принудительно;
  • если стоит Patroni — состояние всех узлов кластера и лаг репликации.

Обычные виновники — один из этой пятерки:

  • неактивный слот, забытый после вывода реплики;
  • отвалившаяся архивация: WAL не считается обработанным, пока archive_command не отработал;
  • реплика, которая давно отстала или вообще недоступна;
  • зависшая длинная транзакция;
  • криво выставленные параметры хранения WAL.

Корень
За всем этим стоит дырявый мониторинг. На проде никто не следил за:

  • размером каталога pg_wal;
  • объемом WAL, который держит каждый слот;
  • состоянием репликации и лагом;
  • тем, отрабатывает ли archive_command;
  • появлением длинных транзакций.

Поэтому и узнали о проблеме только когда диск встал колом.

Лечение
Найти, кто держит WAL, и убрать причину — поднять реплику, снести ненужный слот, починить архивацию, прибить зависшую транзакцию. Дальше PostgreSQL сам подчистит лишние журналы. Если файловая система уже забита под ноль, временно расширить раздел или освободить место вне каталога данных, чтобы поднять базу и добраться до первопричины.

Руками из pg_wal ничего не удалять!
Это гарантированно ломает кластер и хоронит саму возможность восстановления — соблазн «просто освободить место» здесь стоит базы целиком.

Профилактика

  • выставить max_slot_wal_keep_size — потолок на объем WAL, который вправе держать слот;
  • вынести pg_wal на отдельный раздел или отдельный быстрый том;
  • следить за размером pg_wal и скоростью его роста;
  • мониторить слоты: сколько WAL держат и нет ли неактивных;
  • держать под наблюдением репликацию и лаг;
  • повесить алерт на ошибки archive_command и смотреть pg_stat_archiver;
  • в регламент вывода реплики внести обязательное удаление ее слота;
  • регулярно проверять длинные транзакции и их влияние на очистку WAL.

Инцидент 3. «Входящие» открываются минуту и отваливаются по таймауту

Симптом
Часть пользователей не может открыть «Входящие» или другую папку, где много документов. Крутится индикатор загрузки, потом ошибка или таймаут. В логах Directum RX при этом:

  • сообщения Large Fetches;
  • разбухший sqlTimeMs;
  • Remote-функции выполняются все дольше;
  • периодические таймауты веб-сервера и сервисов RX.

На стороне PostgreSQL никакой аварии, но отдельные запросы тянутся десятки секунд, а то и минуты.

Диагностика
Копать начинаю не с PostgreSQL, а с логов Directum RX. Large Fetches означает ровно одно: прикладной код тащит из базы намного больше, чем нужно показать на экране. Именно по этим сообщениям и ищутся неоптимальные Remote-функции и LINQ-запросы, которые выгребают лишнее. Дальше разбираю сами SQL-запросы, из которых собирается папка. Нужно понять:

  • какой запрос самый долгий;
  • сколько строк он возвращает;
  • идет по индексу или сваливается в Sequential Scan;
  • совпадает ли план с ожидаемым.

Затем в статистику PostgreSQL: pg_stat_statements по самым тяжелым запросам, EXPLAIN (ANALYZE, BUFFERS) для фактического плана, использование индексов через pg_stat_user_indexes, мертвые строки, свежесть VACUUM и ANALYZE. На крупных инсталляциях заодно проверяю размеры основных таблиц и не протухла ли статистика оптимизатора. Обычные виновники:

  • пропал или деградировал индекс после того, как база подросла;
  • устарела статистика PostgreSQL;
  • накопились мертвые строки от интенсивных обновлений;
  • заказная доработка выгребает десятки тысяч объектов вместо постраничной выборки;
  • на стороне приложения нет фильтрации, грузится лишнее.

Корень
Почти всегда дело не в PostgreSQL как таковом, а в том, что данных стало на порядок больше, а логика осталась прежней. Запрос, который на базе в 5-10 миллионов записей отрабатывал за пару сотен миллисекунд, через несколько лет перелопачивает десятки, а то и сотни миллионов строк. Нет подходящего индекса, статистика устарела, код написан неоптимально и время растет в разы. Сверху ложится фрагментация таблиц и индексов, если базу вовремя не обслуживали.

Лечение
Нашли проблемный запрос — разбираемся, почему он тормозит, и дальше по ситуации:

  • обновить статистику (ANALYZE);
  • прогнать VACUUM или VACUUM ANALYZE;
  • при необходимости REINDEX самых нагруженных индексов;
  • создать недостающие индексы или поправить существующие;
  • переписать прикладной код Directum RX, чтобы не тянул лишнее;
  • добавить постраничную обработку вместо загрузки всего набора разом;
  • пройтись по пользовательским папкам и представлениям, где документы валят без ограничивающих условий.

Профилактика

  • регулярно смотреть Large Fetches и sqlTimeMs в логах Directum RX;
  • держать под контролем самые тяжелые запросы через pg_stat_statements;
  • вовремя гонять VACUUM и ANALYZE, следя за свежестью статистики;
  • отслеживать рост основных таблиц и смену планов после того, как данных заметно прибавилось;
  • в прикладной логике не грузить объекты пачками без пагинации и выбирать только нужные поля;
  • периодически проверять папки и представления, которые возвращают слишком много документов.

И держите в голове: Large Fetches — не причина инцидента, а сигнализация. Это встроенный в Directum RX способ поймать кривые выборки задолго до того, как пользователи начнут жаловаться на тормоза.

Инцидент 4. FATAL: sorry, too many clients already

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

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

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

Лечение
Перенастройка по уточненным требованиям — операция на несколько минут. Стоит помнить, что рост max_connections расходует память под каждый бэкенд, поэтому его нужно закладывать вместе с пересмотром work_mem.

Профилактика

  • max_connections, work_mem и shared_buffers настраиваются исходя из реального количества пользователей (купленных лицензий), а не по значениям из статьи;
  • пересмотр расчета при каждом подключении новой интеграции или увеличении числа пользователей.

Чек-лист перед выводом кластера в прод

Железо и ОС

  1. THP отключены (transparent_hugepage=never), huge pages выделены и зафиксированы под фактический shared_buffers.
  2. Файловая система под данные — ext4 или XFS с noatime; отдельные тома под данные, pg_wal и логи.
  3. Замерена реальная производительность дисков (fio) до установки СУБД, цифры записаны и зафиксированы в проектной документации.
  4. Настроен NTP.

Конфигурация СУБД

  1. shared_buffers, work_mem, effective_cache_size, maintenance_work_mem рассчитаны от фактического объема RAM, а не скопированы из статьи (в том числе из этой).
  2. max_connections прописан по количеству лицензий или рассчитан из пикового онлайна пользователей + 20% запаса; при необходимости развернут PgBouncer.
  3. Autovacuum настроен агрессивнее дефолта; для горячих таблиц заданы индивидуальные storage parameters.
  4. Заданы idle_in_transaction_session_timeout, lock_timeout; statement_timeout задан на ролях, а не глобально.
  5. jit = off для OLTP-профиля; при необходимости включен точечно для отчетных ролей.
  6. random_page_cost соответствует типу носителей.
  7. Проверено, что max_wal_size покрывает генерацию WAL за 2–3 интервала checkpoint_timeout под пиком.

Резервное копирование и отказоустойчивость

  1. archive_mode = on, archive_command работает, ошибки архивации попадают в алертинг.
  2. Настроен max_slot_wal_keep_size; есть регламент удаления слотов при выводе реплик.
  3. Выполнено тестовое восстановление из бэкапа на отдельном стенде, зафиксировано фактическое RTO.
  4. Проверена PITR-процедура целиком, включая согласование БД с файловым хранилищем документов и переиндексацию поиска.
  5. Реплика (если предусмотрена) настроена, лаг под нагрузкой измерен, процедура переключения описана и опробована.

Мониторинг

  1. Установлены и настроены pg_stat_statements и pg_profile (или pgpro_pwr для Postgres Pro).
  2. Создан отдельный пользователь мониторинга с ролью pg_monitor; postgres_exporter и дашборды подключены, для каждого сервера СУБД добавлен свой источник данных в Grafana (поля Name и Host совпадают — иначе панели дашборда рисуют пустоту).
  3. SQL_COMMENT_ENABLED: true в config.yml, log_min_duration_statement = 3000, log_lock_waits, log_temp_files, log_checkpoints, track_io_timing включены; сбор логов настроен.
  4. Заведены алерты по всем порогам из первой части (раздел «Пороги алертов»); проверено, что они реально доходят до дежурного, а не в мертвый канал.
  5. Зафиксирована базовая линия ключевых операций сразу после ввода в эксплуатацию — иначе через полгода будет нечем доказать факт деградации.
  6. Разграничены зоны ответственности между администраторами заказчика и интегратором; администраторы прошли обучение.

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

И главная мысль, которую хочется оставить: универсальной конфигурации не существует. Есть требования, есть возможности инфраструктуры, есть характер нагрузки — и есть измерения, которые связывают их вместе. Все остальное гадание.

Сергей Дунаев, руководитель группы инженеров Directum, интегратор TANAiS. 3+ года работы с PostgreSQL.


Если у вас есть свои истории про enterprise-нагрузку на PostgreSQL, особенно про отказы, которые долго не удавалось локализовать, — расскажите в комментариях. Такие разборы полезнее любой документации.

TANAiS
Автор: TANAiS
TANAiS — ответственный ИТ-интегратор. Наша команда разрабатывает и реализовывает сложные ИТ-проекты, от идеи и оценки до их внедрения и сопровождения. Мы поддерживаем долгосрочные партнерские отношения с лидерами отечественного ИТ-бизнеса: Directum, Р7-Офис, Directum RX, 1C, 1С-Битрикс, HyperUp, Ario One и др.
Комментарии: