Иван Чувашов, DBA Okko и Southbridge, поделился жизненными кейсами с PostgreSQL, которые помогут решить ваши проблемы.

Разберем случаи из PostgreSQL: запросы в статусе idle in transaction, выключенные контрольные суммы данных, переполнение int4, убивающие базу временные файлы и загрузку CPU.

Кейс первый

Ситуация с idle in transaction – приложение открыло транзакцию, отправило изменения в базу данных, а закрыть транзакцию забыло, транзакция висит.

Как решать инженеру такие ситуации? В Интернете можно найти много статей на тему: что такое idle in transaction, с чем оно связано и даже на практике посмотреть примеры реализации этой ситуации, но не получите самую главную информацию – как решать такие проблемы?

У нас была ситуация, когда появились idle in transaction, и приложение начало тормозить – пул подключений в базе данных был забит. Они блокировали ресурсы, и нам нужно было срочно принимать какое-то решение, мы пошли самым простым путём, который напрашивается и предлагается вашему вниманию.

Первый вариант, который может быть – это выполнение команд:

select pg_cancel_backend(pid) from pg_stat_activity where state = 'idle in transactions' and datname = 'название_БД'; или

select pg_terminate_backend(pid) from pg_stat_activity where state = 'idle in transactions' and datname = 'название_БД';

Отличие данных функций друг от друга можно почитать тут: https://postgrespro.ru/docs/postgresql/13/functions-admin#FUNCTIONS-ADMIN-SIGNAL-TABLE.

А может ли упасть PostgreSQL?

Когда выполняете pg_terminate_backend – это не нулевая вероятность. Допустим, у вас есть процесс, который считывает данные и вы выполняете команду pg_terminate_backend, запрос подтягивает эти же данные в оперативную память PostgreSQL. Если в результате данные стали грязными, то их нельзя считывать другим процессам. Поэтому postmaster-у нужно очистить оперативную память, что он и делает, перезагружая себя.

Точечно отстреливать запросы, можно вызвать групповую команду по маске – очевидные плюсы. Но эти команды на самом деле не решают проблему с idle in transaction. Когда ситуация возникает на стороне бэкенда и мы выполнили команду, которая группой срубила запросы, то через некоторое время ситуация может повториться и тут же появятся от бэкенда запросы в статусе idle in transaction.

Другой вариант – перезагрузка сервера PostgreSQL

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

При остановке сервера PostgreSQL мы убиваем подключения к другим базам или подключения других сервисов. Все это усугубляется ситуацией, когда приложение автоматически не может переподключится к базе. Да бывает и такое! Что может привести к каскадному эффекту в перезагрузке сервисов.

Не знаю, если ли тут плюсы?

Небольшой лайхак. Чтобы быстрее перезапустить кластер, перед перезагрузкой выполните команду checkpoint.

Ещё вариант – перезагрузка бэкендов

Через pg_stat_activity определяем IP проблемного сервиса, с которого произошло подключение. Начинаем их перезагружать. В мире IT микроархитектуры этот процесс не будет являться существенной проблемой.

В нашей ситуации получилось таким образом: увидели idle in transaction – начали пачками отключать запросы. Но количество соединений с базой данных не изменялось. Тут же появлялись новые в этом же статусе. Потом мы подумали: нужно перезагружать PostgreSQL.

От этой идеи быстро отказались, потому что у нас были другие сервисы и была вероятность, того, что после перезагрузки PostgreSQL они не поднимут заново соединение с базой. Поэтому мы через pg_stat_activity нашли бэкенды, которые забивают весь пул подключений, и их перезагрузили.

Еще есть другой вариант, не рассмотренный нами выше. Если вы используете инструмент управления пулом соединений, например pgbouncer, то ситуация решилась бы довольно просто:

– установка pgbouncer на паузу – команда pause;

– перезагрузка сервера PostgreSQL;

– снятие pgbouncer с паузы – команда resume.

В другом известном инструменте управления пулом соединений Odyssey функционал, связанный с pause пока не реализован, что может ограничивать его использование в нагруженных проектах.

ЕЩЁ

В августе 2021 провели митап с Иваном по нюансам работы с PostgreSQL.

На канале Слёрм: https://youtu.be/Qx2NoGCHco8

Кейс второй

СУБД PostgreSQL работает с диском, оперативной памятью, процессором. Если выходит аппаратная часть оборудования, то идём и чиним. Но иногда бывают и скрытые проблемы, например сбой в дисковом массиве, который мы можем заметить не сразу, можем вообще не знать об этом.

Давайте воспроизведем проблему сбоя дисковой подсистемы, затем покажем её решение. У нас есть PostgreSQL – тринадцатая версия. Создадим базу и инициализируем pgbench. Она существует, чтобы нагружать сервер, снимать метрики производительности. Но нам она нужна для других целей.

Рассмотрим таблицу pgbench_branches, в ней есть три поля и одна запись.

Найдем, где находится физически эта таблица на диске:

psql -p5432 -d test -c "select pg_relation_filepath('pgbench_branches')"

Остановим кластер PostgreSQL. Откроем файл base/16839/16853 и допишем любой текст в середине файла. Сохраним его.

Можно воспользоваться просто командой:
sed -i 's/@/@123@/' ~/13/main/base/16839/16853

Запустим PostgreSQL. Попробуем прочитать данные из таблицы.

В таблице также одна запись, но теперь в ней просто пустые строки.

Мы знаем, что файл у нас поврежден, но при этом PostgreSQL об этом не знает, и это достаточно серьезная проблема, с которой можно встретится в PostgreSQL.

Как проблему можно решить? Если мы посмотрим: включена ли у нас контрольная сумма страниц данных в кластере, то мы увидим, что off. Когда PostgreSQL обращается к данным, он рассчитывает контрольную сумму данных страницы, сравнивая её с сохраненной в заголовке страницы, и если она не соответствует, то он выдаёт ошибку.

Насколько это просаживает перформанс? Есть статистика, что – от одного до трёх процентов, но при этом вы точно знаете, что данные у вас повреждены или не повреждены. Это стоит того, чтобы включать контрольную сумму данных у себя. В девелоперской базе неважно. В препроде на ваше усмотрение. А в продовской обязательно должно быть включено.

В двенадцатой версии появилась хорошая утилита pg_checksums. Если раньше до двенадцатой версии вам приходилось создавать новый кластер уже с включенной контрольной суммой данных и в него переносить данные, то с двенадцатой версии можно выключить текущий сервер PostgreSQL и запустить эту утилиту, и она просмотрит все страницы и запишет в заголовках контрольные суммы данных.

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

Проделаем ту же процедуру с повреждением данных, что и ранее.

И теперь если прочитаем данные из таблицы pgbench_branches увидим, что у нас появилась ошибка о несовпадении контрольных сумм.

Но если мы всё-таки хотим извлечь эти данные, то есть флаг ignore_checksum_failure. Когда мы его включаем, у нас выдаётся предупреждение, что контрольная сумма данных не совпадает, но запрос исполняется.

Часто приложения используют только оперативные данные. PostgreSQL не обращается к старым страницам данных. И если в них есть повреждения, то мы можем узнать об этом слишком поздно, когда в резервных копиях тоже будут содержаться они.

Для проверки каталога данных можно воспользоваться командой checkdb в утилите pg_probackup. Хотя данная утилита создана для создания/восстановления резервных копий, в ней есть дополнительный инструмент проверки рабочего каталога базы данных и целостности индексов.

Пример из жизни. Запросы шли в базу и некоторые их них повисали. На сутки, двое, трое. Потом пул запросов стало большим и они начал забирать всю оперативную память. Приходил omm killer и убивал PostgreSQL.

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

Предполагали три варианта:

  • что-то с картой видимостью,

  • что-то с индексами на этой таблице,

  • что-то с данными в самой таблице.

Решили удалить индексы и посмотреть, что будет – как только мы это сделали у нас приложение перестало работать. Это был фейл – приложение не работало три часа. Но нам стало сразу ясно, где проблема. Индексы ссылали на данные, которых нет в БД (страница данных нулевого размера).

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

ЕЩЁ. Под спойлером о курсе по PostgreSQL от Ивана.

23–25 сентября 2021 года Иван проводит второй поток обучения продвинутого курса по PostgreSQL.

Посмотреть программу: https://slurm.club/3ko7ts1.

Кейс третий

Кейс разбит на три ситуации и они о предотвращении проблем, а не исправлении.

Ситуация номер один

У нас есть три таблички: заказы, продукты и таблица, которая связывает многие ко многим. В какой-то момент времени бизнес решил, что нужно сравнивать значения в одно регистре, обратите внимание на тип у колонки id в таблице orders. Можно со стороны приложения переводить все данные к нижнему или верхнему регистру и делать сравнение в запросе. Но можно воспользоваться встроенным типом данных citext. Рассмотрим, как разработчики решили переходить на новый тип данных.

Первая команда у нас создаст эксклюзивную блокировку, которая дропнет constraint. Достаточно быстрая операция. Вторая – по изменению типа, он относится к одному виду типов, поэтому быстрее заменится, проблем с этим не будет. Далее меняем тип на связные таблицах и пытаемся создать constraint.

Что у нас получается – эксклюзивная блокировка на две таблицы product orders и orders, чтобы данные не изменялись. И это будет выполняться в одном потоке. Когда у нас 100-200 записей, то проблем нет – это доли секунды. Если записей стало больше, миллионы, тогда эти внешний ключ будет накатываться очень долго.

Разработчики выкатывают релиз, и у нас останавливается сервис. Моя была ошибка, что пропустил этот pull request. Разобрались, срубили запрос. Ночью мы остановили сервис бэкенда, накатили sql-скрипты.

Какие еще есть варианты решения? Можно воспользоваться конструкцией:
ADD CONSTRAINT ... NOT VALID
VALIDATE CONSTRAINT

В этом случае будут наложены более легкие блокировки.

Вариант 2, более специфичный, но рабочий. С десятой версии Postgres появилась логическая репликация product_orders в product_orders_replic, в которой уже есть внешний ключ product_orders_product_id_fkey. Когда мы скопировали все данные, мы взяли и поменяли таблички: product_orders -> product_orders_tmp, product_orders_replic -> product_orders. Это можно делать всё в одной транзакции, и будет достаточно быстро.

Ситуация номер два, из жизни

Представим, что есть таблички folders и folder_files. Мы хотим пробежаться по всем подпапкам и вытаскивать файлы, которые там есть. Когда у нас десятки-сотни тысяч записей, то проблем нет. Но когда появляются десятки миллионов записей, то тут нужно искать другие способы раскрутки дерева.

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

Было выбрано решение – разделить запрос на два:

  • отдельно рекурсивное cte, ограничив его по уровню вложенности;

  • отдельно маппинг результата cte с данными.

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

Ситуация три – немного о другом

Производительность базы данных сильно снизилась, что привело к деградации приложения. Анализ мониторинга железа не показывал явных проблем с производительность.

Загрузка CPU доходит до полтинника. Проблемы наблюдаются достаточно давно. Нагрузка 50% – это не критично. Нагрузка по диску каких-то 400 ops.

Пик в районе трёх часов не относится к данной ситуации
Пик в районе трёх часов не относится к данной ситуации

Смотрим логи, а там постоянно такие записи.

Вставка 62 секунды. Копаем дальше, смотрим различные метрики. И видим, что в таблице items отсутствуют первичный ключ. При вставке данных в таблицу history из-за внешнего ключа проверялось наличие записи в таблице items. Починили.

Проблему не решило. Копаем дальше, смотрим представление pg_statio_all_tables и раскрываем всю суть.

Запрос select * from pg_statio_all_tables показывает кто генерит большую нагрузку на диск. Первая строчка history_text_default. В дефолтовую секцию ничего не должно писаться, но именно она создает нагрузку на диск. А количество чтений с диска на три порядка больше, чем в позиции на втором месте. Дефолтовая секция, большое количество чтений с диска – и мы понимаем, что перестало работать секционирование таблицы history_text.

Мы использовали расширение pg_partman, пересобрали табличку history_text. По ссылке можно найти, как мы это сделали: https://github.com/Doctorbal/zabbix-postgres-partitioning#zabbiz-history-and-trends-tables.

Как только мы устранили проблему, деградация системы прекратилась и приложение заработало.

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

А вот, что ждёт инженеров, которые пройдут продвинутый курс Ивана по PostgreSQL:

— Научитесь оценивать состояние кластера в критический момент, принимать быстрые и эффективные решения по обеспечению работоспособности кластера.

— Узнаете, как внедрить новое оптимальное архитектурное решение в своей команде, а также сможете лидировать этот процесс.— сэкономите компании время и деньги путем оптимизации процессов администрирования.

— Поймете, как действовать в случае критических ситуаций с базой данных PostgreSQL и будете знать, где и с помощью какого алгоритма искать проблему.


Это всё тоже написано на странице курса, а ещё там есть программа и немного о формате обучения: https://slurm.club/3ko7ts1.

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


  1. Jsty
    26.08.2021 16:06

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

    Можете подробнее про процесс рассказать или ткнуть в документацию, где это описано? Какими инструментами это выполняется?

    Или это просто берем по N записей и копируем в другую таблицу, пока записи не кончатся (insert into ... select from ...)?

    При вставке данных в таблицу history из-за внешнего ключа проверялось наличие записи в таблице items. Починили.

    Если нет ключа в items, то на что history ссылалась? Какая схема данных была?

    мы понимаем, что перестало работать секционирование таблицы history_text

    Нативное секционирование имеет какие-то минусы? Не приходилось работать с pg_partman, но с нативным пока не замечал проблем.


    1. geniyoctober Автор
      27.08.2021 10:31

      Спасибо за хороший вопрос, делюсь ответом Ивана:

      Можете подробнее про процесс рассказать или ткнуть в документацию, где это описано? Какими инструментами это выполняется?

      Или это просто берем по N записей и копируем в другую таблицу, пока записи не кончатся (insert into ... select from ...)? «

      Если у вас есть таблица с инкрементальным номером, то проблем вытащить данные не будет. Да выполняем команду insert into … select from where id between A and B Но у нас id был uuid. Поэтому мы потратили больше времени на вытаскивания данных – из-за этого было и время остановки сервера больше. Сначала я пробовал сделать так insert into … select from where ctid in ();

      Об ctid тут https://postgrespro.ru/docs/postgresql/13/ddl-system-columns

      Вытащил так примерно 70 процентов записей. Но потом пришлось пойти по-другому. Я нашел связанную таблицу по внешнему ключу (тут главное условие, что все данные из целевой таблицы точно должны быть в связанной таблице). И начал перебирать uuid из второй таблицы и подставлять в условие where. Так я вытащит все остальные записи.

      По блокам.

      Да вначале формировал блоки из 10 тыс. значений в запросе where id in (). Находит запросы, которые повисли. Вызывал повторно эти запросы, но блок уменьшал до 100 записей. Потом выискивал опять эти блоки и перебирал по 1 записи. Так и вышел на 17 битых записей.

      Если нет ключа в items, то на что history ссылалась? Какая схема данных была?

      Внешнего ключа не было. Так как и items и history были секционированы. Так как секционирование было сделано через наследование, то внешние ключи нельзя создать на родительской таблице. А когда делали секционирование таблицы items, то забыли сделать первичный ключ. Поэтому через какое-то время появилась описанная ситуация.


    1. geniyoctober Автор
      27.08.2021 10:35

      Нативное секционирование имеет какие-то минусы? Не приходилось работать с pg_partman, но с нативным пока не замечал проблем.

      Ой, тут очень большая тема. Я могу дать ссылку на документацию https://postgrespro.ru/docs/postgresql/13/ddl-partitioning. Ограничений много. Но с каждой новой версии PostgreSQL что-то да улучшается в секционировании. В данном кейсе использовалось расширение partman об этом тут: https://pgxn.org/dist/pg_partman/doc/pg_partman.html


  1. CrushBy
    26.08.2021 21:47
    +2

    По первому кейсу : а Вы не настраиваете idle_in_transaction_session_timeout ? Все таки ситуация, когда кто-то открыл подключение, открыл в ней транзакцию, и потом что-то очень долго делает у себя на клиенте - не стандартная. Скорее всего, уже что-то пошло не так и сессию надо закрывать. То есть таймаут можно поставить достаточно небольшой.


    1. geniyoctober Автор
      27.08.2021 10:06

      Спасибо за хороший вопрос, делюсь ответом Ивана:

      Да вы правы, можно было выставить данный параметр. В данном кластере этот параметр имел значение по умолчанию, те отсутствия таймаута. После данного инцидента он был выставлен в 1 час.

      Но я в своей практики видел, когда данным параметром стреляли себе в ногу. Из-за того, что приложение написано не оптимально (что и выше кстати тоже), при выполнении длительного бизнес процесса (длительность доходила до суток) оно работало в одной транзакции. Спасало и БД и приложение, то что пользователей было немного и бизнес процессы работали с разными данными. Но как только мы выставили данный параметр в 1 час, у нас перестало работать приложение. Но так как приложение было большой монолит и переписать его не имело возможности. Поэтому после несколько часов не_работы приложения, данный параметр вернули в значение по умолчанию.


      1. CrushBy
        27.08.2021 10:45

        По-моему, транзакции, висящие длиннее часа в idle in transaction - это не "не оптимально", а "ужасно", и это надо в любом случае переделывать.

        Просто стоит добавить, что длинные транзакции - это не просто ресурсы. Это, как минимум, еще блокировки записей, и update conflict'ы. И одна из самых критичных у нас проблем, с которой мы сталкивались - невозможность сделать vacuum по всей базе. В итоге самые часто изменяемые таблицы (в том числе и системные pg_) разрастались до огромных размеров, что приводил к сильному замедлению даже простых SELECT.

        В итоге мы даже добавили уведомления прямо в систему (хотя можно и zabbix настроить), чтобы видеть длинные транзакции, и оптимизировать их.

        И касательно всей статьи, я не знаю, что вы умудряетесь сделать с PostgreSQL, чтобы порушить там таблицы. У нас несколько десятков баз, многие из которых в несколько террабайт и тысячами одновременно работающих пользователей, делающих сложные изменения данных в одной транзакции (ERP-система). Чего у нас только не делали с СУБД. По oom killer'у они падали десятки раз (и это в момент большой нагрузки с десятками выполняемых транзакций). Снаружи очень много раз внезапно перегружали виртуальные машины (без уведомления гостевой). Падали по segmentation fault (есть там такой баг в PostgreSQL 13). Иногда даже отключили диск с одним tablespace'ом. И за 5 лет только один (!) раз "портилась" одна таблица - ее просто из бэкапа восстановили.


        1. Jsty
          27.08.2021 11:46

          Падали по segmentation fault (есть там такой баг в PostgreSQL 13)

          Неисправленный до сих пор? Тоже используем 13-ю версию и падений не замечали.


          1. CrushBy
            27.08.2021 11:57

            Ошибка возникает в очень редких случаях. У нас это проявлялось в том, что у клиента раз в месяц просто (по логам) завершался процесс по segmentation fault, и дальше postmaster перезапускал весь сервер из-за этого.

            В лог при этом записывался запрос и место, где было падение. После первых 3х случаев выяснилось, что все они падали в одном случае :

            • Была открыта транзакция

            • Происходило выполнение PL/SQL функции внутри блока TRY (возможно внутри CATCH - не помню сейчас).

            • Segmentation fault происходил в момент отмены выполнения через pg_cancel_backend.

            Мы просто переписали этот PL/SQL без TRY и проблема ушла. Как видите - это очень редкий случай, что мы решили не воспроизводить, и не писать bug report (хотя раньше мы уже писали bug report'ы и их фиксили, когда не могли обойти).


            1. RekGRpth
              27.08.2021 13:36

              я писал багрепорт по похожей проблеме, но не смог убедить Тома Лейна, что там есть ошибка :(

              а проблема заключается в стеке SPI, который не сбрасывается при исключениях, из-за чего при перехватывании исключения, вызванного plpgsql, портится стек SPI