PostgreSQL часто описывают через свойства: надёжность, расширяемость, соответствие стандартам. Но за этими свойствами стоит конкретная механика — набор процессов, которые договариваются друг с другом через разделяемую память, сигналы и каналы. Разберём, как именно устроено это взаимодействие: как запускается кластер, как он узнаёт о смерти процессов, как выполняются контрольные точки и фоновое обслуживание. Эти детали нетривиальны: большая часть поведения определяется порядком переходов между состояниями и тем, кто именно владеет ресурсом в каждый момент.
Процессная модель: postmaster и его дети
Во главе кластера стоит postmasterглавный процесс, который запускает и контролирует все остальные процессы кластера. Его поведение описывается машиной состоянийнабором состояний и правил перехода между ними, определяющим, что кластер делает в каждый момент. Начальное состояние — PM_INITфаза запуска postmaster, во время которой выполняется его инициализация. После её завершения postmaster входит в PM_STARTUPфазу ожидания startup-процесса и запускает startup-процесспроцесс, который читает управляющий файл и выполняет восстановление или предварительную инициализацию.
Дальше путь разветвляется. При обычном запуске или после crash recoveryвосстановлении после аварийного завершения по журналу WAL startup-процесс завершается с кодом 0, и postmaster переходит в PM_RUNнормальное состояние работающей базы данных. Если же startup готов начать archive recoveryвосстановление из архивных копий WAL, он сигнализирует postmaster, и тот переходит в PM_RECOVERYрежим архивного восстановления, где startup продолжает применять WAL. При включённом Hot Standbyрежиме горячего резерва, когда реплика принимает только чтение после достижения согласованной точки в WAL redo startup снова сигнализирует, и состояние меняется на PM_HOT_STANDBYрежим горячего резерва: кластер начинает принимать соединения для read-only запросов. По завершении archive recovery startup выходит с кодом 0, и происходит переход в PM_RUN.
Когда кластер останавливается, postmaster из PM_RUN или PM_HOT_STANDBY переходит в PM_STOP_BACKENDS только при двух условиях: новые соединения запрещены, и не осталось ни одного обычного клиентского бэкенда. В PM_STOP_BACKENDS он сначала забывает не запущенные запросы фоновых рабочих, затем посылает SIGTERM детям и переходит в PM_WAIT_BACKENDS.
Отдельного внимания заслуживает состояние PM_WAIT_DEAD_END: в нём postmaster ждёт, пока все дети, кроме логгера, завершатся, и перестаёт принимать запросы на соединение до ухода последнего ребёнка. Это защищает от ситуации, когда оставшийся процесс мешал бы уничтожить сегмент разделяемой памяти. Что касается SIGQUIT вспомогательным процессам (кроме логгера) — он посылается при начале немедленной остановки или при входе в состояние FatalError.
Кто из фоновых процессов когда запускается
Недостающие фоновые процессы postmaster запускает, проверяя для каждого, что соответствующий указатель пуст и что текущее состояние входит в разрешённый набор. Наборы у разных процессов не совпадают:
- WAL writer — только в PM_RUN.
- Autovacuum launcherпроцесс, который запускает рабочие процессы автоматической очистки таблиц от мёртвых строк — в PM_RUN, если не идёт binary upgrade и autovacuum активен.
- Archiver — в PM_RUN при активном архивировании либо в PM_RECOVERY и PM_HOT_STANDBY, если архивирование включено всегда.
- Slot sync workerпроцесс, который переносит слоты репликации между узлами, чтобы после переключения на резервный узел подписки не потеряли своё место в потоке WAL — только в PM_HOT_STANDBY, когда включена синхронизация слотов, параметры синхронизации проходят проверку, разрешён уровень остановки не глубже SmartShutdown и процесс можно перезапустить.
- Walreceiver — в PM_STARTUP, PM_RECOVERY или PM_HOT_STANDBY, когда он запрошен и остановка не глубже SmartShutdown.
- WAL summarizerпроцесс, который заранее строит сводки по журналу WAL, чтобы инкрементальное резервное копирование не перечитывало журнал целиком — при включённом параметре summarize_wal в PM_RUN или PM_HOT_STANDBY, если остановка не глубже SmartShutdown.
- Фоновые рабочие — через отдельный механизм, когда есть запрос на запуск или есть упавший рабочий.
Общее правило: чем «позже» фаза жизненного цикла, тем больше процессов разрешено. Ранние состояния допускают только те процессы, без которых не обойтись на этапе восстановления.
Как postmaster узнаёт о смерти ребёнка
Postmaster в цикле вызывает waitpid и получает pid завершившегося процесса, после чего последовательно сравнивает его с pid известных ему структур-детей. Для каждого совпадения он освобождает слот и обнуляет указатель. Дальше поведение зависит от того, кто именно умер.
Для startup-процесса логика самая богатая. При нормальном завершении во время остановки он помечается как неработающий, и состояние переходит в PM_WAIT_BACKENDS. При неожиданном выходе (кроме случая, когда процессу ранее послали сигнал завершения) он помечается как аварийно завершившийся, и вызывается обработка краха. При успешном завершении состояние переходит в PM_RUN, соединения разрешаются, и выставляется флаг, что на следующей итерации нужно поднять фоновые задачи.
Для bgwriterпроцесса фоновой записи грязных страниц на диск, walwriterпроцесса фонового сброса журнала WAL на диск, autovacuum launcher и WAL summarizer правило одинаковое: нормальный выход игнорируется, любой другой считается крахом. Для checkpointerпроцесса, выполняющего контрольные точки особая логика: если он завершился нормально и кластер ждал именно его остановки, состояние переходит в PM_WAIT_DEAD_END, и всем детям кроме логгера посылается SIGTERM; иначе это крах.
Для обычных бэкендов и фоновых рабочих postmaster находит соответствующего ребёнка по pid и вызывает очистку. Аварийным считается завершение с кодом не 0 и не 1, а также ситуация, когда не удалось освободить слот потомка. На Windows код ERROR_WAIT_NO_CHILDREN (128) трактуется как нефатальный: он логируется, но аварией не считается. При завершении autovacuum worker launcher получает сигнал SIGUSR2. Для фонового рабочего обновляется время краха, pid обнуляется, и выставляется флаг, что есть упавший рабочий.
Перезапуск всего кластера
Когда startup-процесс завершается неожиданно, включая FATAL-выход, postmaster считает это катастрофой: он убивает остальных детей и помечает startup как аварийно завершившийся, чтобы после их ухода не пытаться переинициализироваться. Если при этом уже установлен флаг фатальной ошибки и остановка не немедленная, вызывается обработка фатальной ошибки — иначе машина состояний застряла бы в PM_STARTUP.
В состоянии PM_NO_CHILDREN возможны три исхода. Если startup помечен как аварийно завершившийся, postmaster пишет «shutting down due to startup process failure» и завершается с кодом 1. Если выключен параметр restart_after_crash, он пишет соответствующее сообщение и тоже завершается с кодом 1. Иначе, при установленном флаге фатальной ошибки, кластер переинициализируется: удаляются временные файлы (если включён соответствующий параметр), сбрасывается разделяемая память, заново создаются разделяемая память и семафоры, происходит переход в PM_STARTUP и запускается новый startup-процесс.
Важно различать аварийное завершение и ожидаемое. Если startup ранее получил SIGQUIT, его смерть ожидаема: статус сбрасывается в «неработающий», и при состоянии PM_STARTUP происходит переход в PM_WAIT_BACKENDS — это позволяет перезапустить startup-процесс.
Обнаружение смерти postmaster и проверка postmaster.pid
Дети должны узнавать о смерти postmaster. Для этого устроен канал: postmaster держит открытым конец записи pipeканала межпроцессного взаимодействия, а дети — конец чтения. Ребёнок может передать читаемый дескриптор в select и проснуться при смерти postmaster либо проверить её через чтение, вернувшее 0. Дети обязаны закрыть конец записи как можно раньше после fork: EOFпризнак конца файла не будет сигнализирован на конце чтения, пока все процессы не закроют дескриптор записи. Это делается в процедуре закрытия портов postmaster. На Windows вместо pipe используется дескриптор процесса. Конец чтения переводится в неблокирующий режим, чтобы проверять наличие данных через чтение.
Отдельный механизм — проверка файла postmaster.pid. Раз в минуту postmaster проверяет, что файл не был удалён или перезаписан. Если проверка не прошла, в журнал пишется сообщение о немедленном завершении из-за недействительного блокировочного файла каталога данных, и postmaster посылает самому себе SIGQUIT. Смысл — не оставлять работающие процессы после исчезновения базы и защититься от администратора, вручную удалившего postmaster.pid и запустившего новый postmaster.
С этим соседствует, но не связано напрямую «трогание» Unix-сокета и lock-файлов каждые 58 минут: оно обновляет время модификации, чтобы сторонние очистчики временных каталогов их не удалили. Это два независимых периодических действия с разными счётчиками времени.
Фоновые рабочие процессы
Слоты фоновых рабочих лежат в разделяемой памяти. Флаг занятости слота передаёт ответственность за него между postmaster и остальной системой: пока он ложен, слот могут менять обычные бэкенды, а после установки — слот переходит postmaster. Поэтому бэкенд, инициализирующий слот, обязан полностью его заполнить и вставить барьер памятиинструкцию, гарантирующую порядок видимости записей в памяти для других процессов перед пометкой занятости. Postmaster не берёт блокировки, поэтому использует протокол без блокировок и видит занятость просто по чтению флага. При завершении рабочего postmaster записывает его pid в слот, а затем при необходимости обнуляет pid и снимает флаг занятости с барьером памяти, чтобы слот стал доступен для переиспользования. Обычные бэкенды при чтении pid берут блокировку в разделяемом режиме и сверяют поколение слота.
Статические фоновые рабочие регистрируются только в процессе postmaster: если вызов происходит не в нём, регистрация отклоняется, а при загрузке разделяемых библиотек в бэкенде просто возвращается без ошибки. Динамические рабочие, наоборот, регистрируются из обычного бэкенда, и из postmaster это невозможно.
Решение о запуске принимается сравнением требуемой стадии готовности с текущим состоянием: в PM_RUN запускаются рабочие, которым нужно завершённое восстановление, в PM_HOT_STANDBY — которым нужно согласованное состояние, а в PM_RECOVERY, PM_STARTUP и PM_INIT — которым достаточно старта postmaster. Уже запущенный рабочий пропускается, помеченный на завершение удаляется, а при недавнем падении запуск откладывается до истечения заданного времени перезапуска.
Контрольные точки
checkpointer обнаруживает запрос по ненулевому слову флагов в разделяемой памяти. Зафиксировав запрос под спинлоком — прочитав флаги, обнулив их и увеличив счётчик начатых контрольных точек, — он широковещательно оповещает ожидающих. Запросчик под тем же спинлоком снимает снимок счётчиков и добавляет свои флаги, чтобы не перезаписать более сильный запрос другого бэкенда.
Ожидание завершения идёт в два этапа: сначала цикл ждёт, пока счётчик начатых не изменится, затем — пока разность завершённых и начатых не станет неотрицательной в модульной арифметикеарифметике с переполнением, где сравнение идёт по разности, а не по абсолютным значениям. Если счётчик неудач изменился, выбрасывается ошибка о неудачном запросе.
Когда очередь запросов заполняется более чем наполовину, отправитель будит checkpointer. При переполнении очереди выполняется уплотнение, удаляющее дубликаты: запрос может быть пропущен, если за ним следует более поздний идентичный запрос. Такой подход сохраняет семантику при наличии специальных запросов на забывание или фильтрацию.
При неудаче контрольной точки или restartpoint checkpointer под спинлоком увеличивает счётчик неудач и присваивает счётчику завершённых значение счётчика начатых, после чего будит все ожидающие бэкенды. Затем он спит не менее 1 секунды, чтобы не забивать журнал ошибок при повторяющихся ошибках записи. Для restartpoint, который не удалось выполнить, счётчик неудач не увеличивается — вместо этого время следующей попытки сдвигается так, чтобы повторить через 15 секунд. На уровне кластера неудачная контрольная точка не приводит к краху: postmaster не получает сигнала о катастрофе, и кластер продолжает работу.
Фоновая запись
bgwriter считает, что ничего не происходит, если предыдущее ожидание завершилось по таймауту и два цикла подряд не было активности. Тогда он переходит в режим гибернации: запрашивает уведомление при следующем выделении буфера и спит увеличенный интервал. После каждой контрольной точки освобождаются все объекты менеджера хранения, потому что иначе они никогда не были бы освобождены для удалённых отношений: bgwriter не обрабатывает сообщения об общих инвалидациях.
walwriter выполняет минимальный набор операций очистки после ошибки, аналогичный отмене транзакции: освобождает блокировки, отменяет ожидания, очищает ошибки асинхронного ввода-вывода, разблокирует буферы и освобождает прочие ресурсы. После ошибки он спит не менее 1 секунды и сбрасывает состояние гибернации. В основном цикле он пытается сбросить WAL: если нашлась полезная работа, счётчик до гибернации сбрасывается, иначе уменьшается на единицу. Таймаут ожидания равен базовой задержке, пока счётчик положителен, и увеличенной — когда счётчик исчерпан. Так при длительном отсутствии работы интервал сна растёт для снижения энергопотребления.
Autovacuum
autovacuumподсистема автоматического обслуживания таблиц: удаления мёртвых строк и обновления статистики состоит из launcher и рабочих процессов. Launcher определяет время сна в зависимости от трёх случаев: если запускать рабочего нельзя, сон равен номинальному интервалу; если список баз не пуст, берётся момент следующего пробуждения из хвостового элемента; если список пуст, сон снова равен номинальному интервалу. Если рассчитанный сон оказался ровно нулевым и это не рекурсивный вызов, список перестраивается и расчёт повторяется. Минимальное время сна ограничено снизу, максимальное — сверху. При пустом списке баз рабочий запускается немедленно — это покрывает начальный случай, когда статистики ещё нет.
Рабочий выбирает таблицы, сканируя системный каталог в два прохода: сначала обычные таблицы и материализованные представления, затем TOAST-таблицы, чтобы подставить в них неустановленные параметры хранения из основной таблицы. Для временных таблиц других бэкендов проверяется статус временного пространства имён: если оно свободно, таблица помечается как осиротевшая, иначе пропускается. Список сортируется по убыванию веса таблицычисловая оценка того, насколько таблица нуждается в обслуживании: она складывается из взвешенных долей мёртвых и вставленных кортежей, а также возраста заморозки по транзакциям и мультитранзакциям, но сортировка пропускается, если все веса равны нулю. Осиротевшие временные таблицы удаляются в отдельном цикле: берётся исключительная блокировка, повторно проверяется каталог и статус пространства имён, затем выполняется удаление с каскадом в отдельной транзакции.
Риск wraparoundпереполнения счётчика транзакций, после которого старые версии строк могут стать неотличимы от новых определяется сравнением возраста заморозки с порогом. Если идентификатор заморозки предшествует порогу, принудительный vacuum включается, минуя обычные пороги. Если по идентификаторам транзакций риска нет, аналогично проверяется риск по мультитранзакциям. Обычные пороги применяются отдельно и только при включённом autovacuum, тогда как принудительный anti-wraparound путь работает даже когда autovacuum выключен. Launcher выбирает базу с риском переполнения по её идентификатору заморозки.
Рабочий принудительно понижает уровень синхронной репликации до локального, чтобы регулярное обслуживание не блокировалось ожиданием подключения standby-серверов — это важно для anti-wraparound задач. Дополнительно он всегда запрашивает свежую статистику. Параметры стоимости хранятся в статических переменных, инициализированных недействительными значениями, чтобы не перезаписывать глобальные настройки после перезагрузки конфигурации. При выборе задержки стоимости приоритет отдаётся настройке хранилища, затем настройке autovacuum, затем глобальной. Если хотя бы один из параметров стоимости задан индивидуально для таблицы, алгоритм балансировки отключается.
Что из этого следует на практике
Механика процессов объясняет несколько вещей, которые иначе выглядят произвольными.
Остановка кластера — это последовательность, а не одно действие. Сначала запрещаются новые соединения, потом ждут ухода клиентских бэкендов, потом посылают сигналы остальным. Поэтому «мгновенная» остановка на самом деле проходит несколько фаз, и уровень остановки влияет на то, какие сигналы получают дети.
Аварийное завершение startup-процесса — единственная ситуация, которая ведёт к полной переинициализации кластера. Всё остальное либо игнорируется, либо обрабатывается как крах отдельного процесса. Именно поэтому параметр restart_after_crash имеет смысл: он определяет, переинициализироваться или завершиться.
Проверка postmaster.pid раз в минуту означает, что ручное удаление этого файла при работающем кластере будет замечено в течение минуты и приведёт к немедленной остановке. Это защита от создания второго кластера в том же каталоге.
Неудачная контрольная точка не роняет кластер. Checkpointer уведомляет ожидающих, спит секунду и продолжает цикл. Это осознанный выбор: повторяющиеся ошибки записи не должны забивать журнал и не должны приводить к остановке.
Autovacuum работает даже при выключенном autovacuum, когда речь идёт о защите от переполнения счётчика транзакций. Это не настраиваемая эвристика, а жёсткая гарантия: принудительный vacuum включается независимо от обычных порогов.
Фоновые рабочие и слоты используют протокол без блокировок, потому что postmaster не может брать блокировки: если разделяемая память повреждена, блокировка могла бы его подвесить. Отсюда и барьеры памяти, и передача ответственности за слот через флаг занятости.