PostgreSQL в проде: пулер, очистка, блокировки
Поднял базу с таблицей заказов на 200 тысяч строк - 14 мегабайт. Открыл 30 соединений и посмотрел, что стало с процессами: их в контейнере 69 против 8 у простаивающей базы, память 120 мегабайт против 49. Ни одного запроса при этом никто не выполнял. Соединение к PostgreSQL - это отдельный процесс операционной системы, и он стоит денег ещё до первого запроса.
Стержень: соединение стоит процесса, долгие транзакции держат старые версии строк, а безобидная миграция встаёт в очередь за читателем и блокирует всех, кто пришёл после.
// Формулировки: «зачем нужен пулер соединений?», «почему таблица растёт, хотя строк столько же?», «alter table встал, что происходит?»
Соединение это процесс
В PostgreSQL на каждое клиентское соединение поднимается отдельный процесс. Мой замер: пустая база держит 8 процессов, при 30 открытых соединениях их 69, а занятая память выросла с 49 до 120 мегабайт. Соединения при этом простаивали - платим просто за факт их существования.
Отсюда предел на число соединений, и он не про капризы: у меня он стоял на 50, и упереться в него легко. Приложение с пулом на 50 соединений и десятью экземплярами хочет 500, а база столько процессов не потянет. Дальше начинается интересное: превышение даёт не медленную работу, а прямой отказ в соединении.
// Лечится пулером - посредником, который держит небольшое число настоящих соединений к базе и раздаёт их приложениям по очереди. Приложение думает, что у него сотни соединений, а база видит десятки. Режим раздачи выбирают по тому, что нужно приложению: соединение выдаётся на одну транзакцию (транзакция - это группа действий, которая применяется целиком или не применяется вовсе) либо на весь сеанс. Первый режим экономнее в разы, но ломает всё, что живёт между транзакциями: временные таблицы, заранее разобранные запросы, настройки сеанса. Второй совместим со всем и экономит меньше.
- предел соединений
- сколько клиентских процессов база согласна поднять; сверху - отказ
- пулер
- посредник, держащий немного настоящих соединений и раздающий их приложениям
Долгая транзакция держит мусор
PostgreSQL не правит строку на месте: обновление создаёт НОВУЮ версию строки, а старая остаётся, пока её кто-то может увидеть. Этот механизм называется MVCC (multiversion concurrency control) - многоверсионность, и благодаря ему читатели не блокируют писателей. Убирает старые версии отдельный процесс очистки.
Проверил, что бывает, когда очистке мешают. Открыл транзакцию, которая просто читает и не завершается, и параллельно трижды обновил всю таблицу. Таблица выросла с 14 мегабайт до 53, мёртвых версий строк накопилось 600 тысяч, и очистка не смогла убрать НИ ОДНОЙ: старая транзакция всё ещё имеет право их видеть. Как только транзакция завершилась, та же очистка убрала все 600 тысяч.
// И важная деталь: после очистки таблица осталась размером 53 мегабайта. Место внутри файла переиспользуется под новые строки, но файл сам по себе не сжимается. Чтобы вернуть место операционной системе, нужна полная перестройка таблицы, а она берёт исключительную блокировку. Отсюда две практики: следить за самой старой открытой транзакцией и не оставлять открытыми сеансы, которые «просто посмотреть».
- MVCC
- многоверсионность: обновление создаёт новую версию строки, старая живёт до очистки
- мёртвая версия строки
- старая версия, которую уже никто не увидит; занимает место до очистки
- очистка
- процесс, который освобождает место от мёртвых версий внутри файла
Миграция и очередь блокировок
Добавление колонки со значением по умолчанию на 200 тысячах строк заняло у меня 85 миллисекунд - в современных версиях это правка только описания таблицы, данные не переписываются. Казалось бы, безопасно.
Теперь тот же запрос при живом трафике. Я открыл транзакцию, которая читает таблицу и не завершается, и запустил добавление колонки. Оно встало в ожидание блокировки. Дальше пришёл обычный запрос на чтение - и он тоже встал, хотя читатели друг другу не мешают. Причина в очереди: запрос на исключительную блокировку встаёт в неё и не пропускает вперёд тех, кто пришёл после. В моём замере в очереди оказалось два запроса, и картина «база встала на ровном месте» готова.
// Отсюда правила миграций (миграция - это изменение структуры базы, разложенное в скрипт и применяемое при выкатке). Ставить короткое ограничение времени ожидания блокировки, чтобы миграция отваливалась сама, а не копила очередь. Разбивать тяжёлые изменения на шаги, каждый из которых берёт блокировку на миллисекунды. Строить индексы в режиме без блокировки записи. И проверять, нет ли долгих транзакций, ПЕРЕД тем как запускать миграцию.
- исключительная блокировка
- право на таблицу, несовместимое ни с чем; берётся изменением её структуры
- очередь блокировок
- ждущий исключительную блокировку не пропускает вперёд пришедших после него
- ограничение ожидания блокировки
- настройка, после которой запрос сам отваливается, а не копит очередь
Как отвечать: «Запустили alter table, и база встала. Что произошло?»
Скорее всего, изменение структуры встало за долгой транзакцией, а очередь блокировок закрыла проход всем остальным. Я это воспроизводил: сама операция добавления колонки на двухстах тысячах строк занимает около ста миллисекунд, потому что переписывать данные не надо. Но если рядом живёт открытая транзакция, которая просто читает таблицу, изменение структуры уходит в ожидание, и следующий за ним обычный запрос на чтение тоже встаёт, хотя читатели друг другу не мешают. Лечение и профилактика: ставить короткое ограничение ожидания блокировки, чтобы миграция отваливалась сама, смотреть на самую старую открытую транзакцию перед запуском и разбивать тяжёлые изменения на шаги, каждый из которых держит блокировку миллисекунды.
Ответ отделяет длительность самой операции от времени ожидания и объясняет, почему страдают невиновные запросы. Это точная механика, а не «alter блокирует таблицу».
На чём валятся
- −Считают соединение бесплатным и открывают их сотнями без пулера.
- −Держат открытую транзакцию «просто посмотреть» и останавливают очистку по всей базе.
- −Думают, что очистка вернёт место операционной системе. Она освобождает его только внутри файла.
- −Запускают миграцию без ограничения ожидания блокировки и получают очередь из невиновных запросов.
- −Судят о безопасности миграции по времени самой операции, а не по взятой блокировке.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Перед PostgreSQL ставят пулер соединений. Зачем, если пул есть и в приложении?A)Пулер кэширует результаты запросов и разгружает базуB)Соединение — это процесс с памятью, а сервисов многоC)Пулер шифрует трафик до базы и снимает эту работу с неёD)Через пулер работают миграции схемы, напрямую они запрещены
показать ответ и разбор
+B)Соединение — это процесс с памятью, а сервисов много// разбор: PostgreSQL на каждое соединение поднимает процесс со своей памятью, поэтому сотни клиентских пулов от десятков подов упираются в предел раньше, чем в процессор. Внешний пулер держит небольшое число реальных соединений и мультиплексирует в них клиентов. Побочно он даёт единую точку ограничения нагрузки и переживания переключений — правда, в режиме транзакций часть возможностей сессии становится недоступной.
- Таблица распухла, место кончается, автоочистка работает, но не помогает. Что смотреть?A)Индексы: их надо перестроить, чтобы вернуть местоB)Настройки контрольных точек: они удерживают старые страницыC)Кэш страниц: он не отдаёт устаревшие версии строкD)Долгие открытые транзакции держат старые версии
показать ответ и разбор
+D)Долгие открытые транзакции держат старые версии// разбор: База хранит старые версии строк, пока их может увидеть хоть одна открытая транзакция. Забытая сессия в состоянии idle in transaction, зависший отчёт или репликация с задержкой держат горизонт видимости, и очистка не имеет права освободить место — таблица растёт, а очистка каждый раз отчитывается «нечего убирать». Смотрят самые старые транзакции, ставят таймауты на простой в транзакции и следят за отставанием реплик.
- Миграция добавляет индекс на большую таблицу в час пик. Что произойдёт и как делать правильно?A)Обычное построение блокирует записьB)Ничего страшного: индекс строится в фоне и запросы не задеваетC)База отложит операцию до снижения нагрузки автоматическиD)Запись продолжится, но новые строки не попадут в индекс до перестроения
показать ответ и разбор
+A)Обычное построение блокирует запись// разбор: Обычное построение индекса берёт блокировку, несовместимую с записью: на большой таблице это минуты простоя, а очередь ожидающих запросов накрывает и чтение. Строят конкурентно (concurrently) — дольше, зато без блокировки записи, — и обязательно с ограничением времени ожидания блокировки, чтобы миграция не встала сама и не собрала за собой очередь. Отдельно проверяют, что после сбоя остаётся невалидный индекс, который надо убрать.
- Разработчик хочет навесить индексы на все колонки «чтобы всё было быстро». Чем это плохо?A)Индекс ускоряет чтение, но замедляет запись и ест местоB)Индексы ускоряют и чтение, и запись одновременноC)Лишние индексы влияют только на размер дампаD)Индекс на колонке запрещает её обновление
показать ответ и разбор
+A)Индекс ускоряет чтение, но замедляет запись и ест место// разбор: Индекс — это отдельная структура, которую БД поддерживает при каждой записи: INSERT/UPDATE/DELETE должны обновить и таблицу, и все её индексы, плюс индексы занимают диск и память под кэш. Поэтому индексируют избирательно — колонки, по которым реально фильтруют/джойнят/сортируют, а не всё подряд. Лишние индексы замедляют запись и раздувают базу, не давая выигрыша на чтении.
- Запрос, который раньше летал, стал медленным по мере роста таблицы. Чем диагностировать причину?A)Просто перезапустить базу, чтобы сбросить кэш планаB)Увеличить память под кэш и не смотреть планC)EXPLAIN (ANALYZE): увидеть план и seq scan вместо индексаD)Переписать запрос на несколько мелких вслепую
показать ответ и разбор
+C)EXPLAIN (ANALYZE): увидеть план и seq scan вместо индекса// разбор: Когда запрос деградирует с ростом данных, первым делом смотрят план: EXPLAIN (ANALYZE) показывает, как БД его выполняет и сколько это стоит. Частая причина — Seq Scan (полный проход таблицы) там, где по условию нужен индекс: на маленькой таблице это было незаметно, на большой — дорого. Дальше добавляют подходящий индекс, переписывают условие под индекс или устраняют то, что мешает его использовать. Гадание без плана — трата времени.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.