Главная » Хабрахабр » Типичные ошибки при работе с PostgreSQL. Часть 2

Типичные ошибки при работе с PostgreSQL. Часть 2

Мы продолжаем публиковать видео и расшифровки лучших докладов с конференции PGConf.Russia 2019. В первой части доклада Ивана Фролкова речь шла о непоследовательном именовании, о constraints, о том, где лучше сосредоточить логику — в базе или в приложении. В этой части вас ждет разбор обработки ошибок, конкурентного доступа, неотменяемых операций, CTE и JSON.

Наш клиент говорит: «Медленно работает база, а наше приложение занимается обслуживаем населения. Расскажу такую историю. Выяснилось, что у них было очень много процессов в состоянии idle in transaction. Мы боимся, что нас тут поднимут на вилы». Если вы взаимодействуете с какими-то внешними сервисами, то, в принципе, это нормальная ситуация. Приложение начало транзакцию, ничего не делает, но и транзакцию не завершает. Начинают разбухать таблицы, индексы становятся всё менее эффективными. Другое дело, что если у вас состояние idle in transaction длится долго (больше минуты уже подозрительно), то это плохо потому, что PostgreSQL очень не любит долгие транзакции: VACUUM не сможет почистить все те строки, которые он мог бы увидеть, и долго висящая транзакция эффективно блокирует VACUUM.

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

А появлялись они вот в какой ситуации: приложение лезет в базу, начинает транзакцию, лезет на какой-то внешний сервис, получает там ошибку, а дальше всё просто осыпается, печатаем в лог stack trace, и на этом успокаиваемся. Но это не объясняло, почему у них появляется столько процессов в idle in transaction. Соединение остается заброшенным, висит и мешается.

Во-первых, надо обрабатывать ошибки всегда. Что с этим делать? Хорошо еще, если PostgreSQL потерял соединение: он откатит транзакцию, переживём. Если к вам прилетела прилетела ошибка, пожалуйста, не игнорируйте ее. Ну а если есть код, который править совсем нет времени, то у нас еще есть max idle in transaction — можно поставить, и будет просто вышибать неактивные транзакции. На этом я еще остановлюсь.

Как-то мы спорили с коллегой о терминологии. Типичный случай «обработки» ошибок: EXEPTION WHEN OTHERS THAN NULL. Если у нас нечто плохое произошло, то, даже если всё с руганью осыпалось в лог, это всё же лучше, чем полная тишина — как здесь. Я говорил, что это переводится как «гори оно всё синим пламенем», а он — «пропади оно всё пропадом».

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

И каждую ошибку обрабатываем. Если делаем операцию на клиенте, то, обычно, возвращаем значение: всё прошло удачно либо неудачно. Но SQLSTATE не возвращали. Я видел, как люди специально писали код plpgsql, где перехватывали ошибку, писали в лог, что, мол, да, была ошибка и довольно грубая, вставляли свой текст сообщения. Это выполняется всегда, поэтому, если они забывали что-то проверить, то у них начинались проблемы.

А если не придумывать что-то свое, а пользоваться стандартными возможностями языка, все обычно получается хорошо. Все, почему-то боятся исключений — как в plpgsql, так и в других языках. Оно упало, процесс idle in transaction, база заполняется, падает производительность. Особенно эта проблема часто встречается, когда падает соединение. Поэтому добавляйте в код обработки ошибки finally и там вычищайте соединение, отдавайте его обратно серверу. Между прочим, такая транзакция может еще оставить блокировки, но это, почему-то, встречается не так часто.

В spring есть exception translation, в php, соответственно, set_exception_handler. Более того, в случае, если у вас хорошо, правильно поименованы constraint-ы, вы можете уже при обработке ошибки выкинуть исключение не из базы, а из приложения. Обратите внимание на те средства, которые вам предоставляет ваш фреймворк, они там неспроста появились.

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

Лично я классифицирую по таким критериям: операцию можно повторить (например, у нас возник deadlock); операцию повторить нельзя, она уже выполнена; операция не может быть выполнена в принципе.

Как ни парадоксально, с точки зрения приложения, ситуации, когда возникнет deadlock, когда потеряно соединение и когда у нас кончились деньги для выплаты, — это ситуации одинаковые: обработчик ошибки попытается через некоторое время выполнить операцию снова.

Я лишь призываю аккуратно обрабатывать ошибки, иначе: idle in transaction, залоченные строки, пухнущие базы и так далее. С другой стороны, что пишут в приложении, в общем-то, не мое дело: я занимаюсь базой.

И это плюс всем реляционным СУБД потому, что, как ни странно, при этом всё работает, как правило, очень хорошо, даже со стандартным уровнем изоляции READ COMMITTED, а не SERIALIZABLE. Большинство разработчиков считает, что они работают с базой одни, и их приложение выполняет операции строго последовательно. Первый пришел ругаться: «как же так, я столько написали, и всё потеряно». В то же время, случаются ситуации, когда теряются обновления: один грузит форму, другой грузит эту же форму, один написал и сохранил, другой сохранил старую — изменения стерли.

Они должны
были меняться через раз, но, тем не менее, однажды полезли одновременно и сделали две выплаты на одного человека. Из моего опыта: раз в неделю по пятницам два менеджера проводили выплаты. Вопрос когда. Если у вас есть хоть какая-то возможность ошибки конкурентного доступа, она рано или поздно случится.

Я неоднократно видел, как уникальность пытались обеспечить триггерами. Кроме того, обращаю ваше внимание на ограничения. Либо вам тогда нужно будет блокировать всю таблицу, либо делать еще какие-то сложные телодвижения. Сходу триггерами уникальность в таблице вы не обеспечите. Вы рано или поздно на этом споткнетесь.

Там проводились какие-то операции, изменяющие внешние сущности. Пару раз натыкался на совершенно кошмарную вещь: из базы вызывается внешний web-сервис. Это плохо тем, что в базе транзакция может откатиться, но операции на удаленном сервисе откачены не будут.

Давайте представим: мы обрабатываем транзакцию, вызываем внешний web-сервис, что-то поменяли, после этого у нас возникнет deadlock, и мы откатываемся, потом пытаемся выполнить операцию еще раз, вызываем еще раз, при хорошем стечении обстоятельств еще раз возникает deadlock, опять откатываемся — так может
происходить много раз (я натыкался на пару сотен повторов). Еще более тонкий момент — deadlock-и. И вот вы обрабатываете эти deadlock-и более-менее корректно, повторяете операции и вдруг обнаруживаете, что уже в течении двух месяцев выплачиваете кому-то двойную сумму.

Во-первых, возникает проблема в случае повтора, во-вторых, непонятно, что делать, если прервалось соединение. Я встречался с платежными сервисами, у которых был небогатый API: «выплатить такую-то сумму такому-то пользователю»; функция возвращает результат — выплачено / не выплачено. Почему-то на эту тему тоже очень мало кто заморачивается.

На слайде пример: такая операция должна выполняться в два этапа: как бы предупреждение — «будем сейчас что-то делать»; сама операция.

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

Последний — неидемпотентные операции.
На слайде 4 типа неотменяемых операций. Я в начале говорил о товарище, который всё делал на триггерах именно чтобы обеспечить идемпотентность своих операций. Это совсем грустный случай.

К сожалению, CTE в PostgreSQL не бесплатны: они требуют под себя work_mem.
На конференции люди будут рассказать о Common Table Expressions, о том, как это хорошо. А если вдруг у вас она большая, то у вас начинаются проблемы. Если у вас выборка небольшая, то, в общем, ничего страшного. CTE очень востребованные. Люди очень часто используют CTE в качестве этаких мини-вьюшек — для того, чтобы можно было как-то структурировать приложение.

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

В принципе, всё работает неплохо. О JSON обычно рассказывают в превосходных тонах, но есть тенденция в приложении в JSON пихать вообще все что угодно. Еще хуже, если у вас JSON большой, и его вынесло в TOAST. С другой стороны, из JSON-а данные достаются хоть и быстро, но не так быстро, как из колонок. Чтобы JSON оттуда взять, его нужно поднять из TOAST-а.

Еще хуже получается при большом объеме, когда база большая, когда у вас bitmap index scan. Если все колонки в JSON-е, по ним даже построен функциональный индекс, то все равно оттуда доставать надо. Если с небольшими колонками это работает хорошо, то с JSON это большая проблема. Тогда у нас ссылки не на строки, а на целую страницу, и, для того, чтобы понять, что со страницы брать, PostgreSQL сделает Recheck, то есть он поднимает строчку из TOAST и проверяет, есть там это значение или нет, и соответственно уже пропускает или не пропускает. Слишком увлекаться JSON-ами не надо.

Какие варианты есть? — Как проверять, когда со строкой работают несколько пользователей?

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

Но есть ограничение в 60 символов на имя constraint-а. — Вы говорите, что что надо именовать constraint-ы хорошими именами, чтобы пользователь мог понять, что происходит. Как с этим бороться? Этого часто не хватает.

В PostgreSQL это специальный тип длиной 64. — Думаю, бороться самоограничением. В принципе можно перекомпилировать на бОльшую длину, но это не очень хорошо.

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

Какой способ вам наболее удобен, тем и пользуйтесь. — Как я в самом начале уже говорил, при должном усердии работает всё.

Timing: 2-я часть доклада начинается с 25:16

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

Или достаточно транзакции? — Именно процедура?

— Именно процедура, которая вызывается в некоторой транзакции.

Были бы сложности, если б у вас было условие, скажем, не больше 3 одновременно. — Вы можете поставить блокировку на объект. Я обычно использую транзакционные блокировки, но можно и внетранзакционные. Но и это реализуемо.

Вы говорили о
возможности хранения архива так, чтобы из приложения данные были также доступны. — Я бы хотела все-таки еще раз вернуться к архивным данным. Какие еще есть варианты? Мне приходила в голову мысль просто сделать отдельную архивную базу.

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

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

Уступаю место следующему докладчику. — Конечно, почему бы нет.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Прыжки китайского «Кузнечика» от LinkSpace

Китайская частная космическая компания LinkSpace построила тестовый ракетный стенд вертикального взлета и посадки, аналогичный «Кузнечику» от SpaceX, и с января этого года проводит все более сложные испытания. После тестов зависания на привязи компания перешла к полноценным подскокам, 27 марта поднявшись ...

Искусственный интеллект улучшает качество графики старых видеоигр и делает это действительно хорошо

Недавно стало известно еще об одной задаче, которую ИИ решает очень хорошо. Технологии искусственного интеллекта постепенно развиваются, помогая решать задачи и проблемы самых разных сфер — от бизнеса до медицины и развлечений. Речь идет об игровых модах — как оказалось, ...