1. Архитектура процессов и управление разделяемой памятью
Архитектура процессов и управление разделяемой памятью
Когда вы отправляете запрос SELECT * FROM users, для базы данных это не просто поиск строки в файле. В этот момент запускается сложнейший механизм взаимодействия десятков процессов, которые конкурируют за ресурсы, синхронизируются через системные примитивы и управляют гигабайтами оперативной памяти. PostgreSQL — это не монолитное приложение, а целая экосистема процессов, работающих в рамках модели «процесс на каждое соединение» (process-per-connection). Если в многопоточных системах (как MySQL или Oracle) ошибка в одном потоке может потенциально обрушить весь адресный столп сервера, то PostgreSQL выбирает путь изоляции, где каждый клиентский запрос обслуживается отдельным процессом ОС со своим адресным пространством.
Модель Process-per-Connection и жизненный цикл Postmaster
Основа архитектуры PostgreSQL — многопроцессорность. Центральным элементом является процесс postmaster (в современных версиях исполняемый файл называется postgres). Это «родитель» всех остальных процессов в кластере. Его основная задача — слушать сетевой порт (по умолчанию 5432), принимать входящие TCP-соединения и проводить аутентификацию.
Как только клиент успешно проходит проверку подлинности, postmaster выполняет системный вызов fork(). В результате создается новый процесс — backend process (или postgres backend), который берет на себя обслуживание конкретного сеанса. С этого момента клиент общается напрямую с выделенным ему бэкендом, а postmaster возвращается в режим ожидания новых подключений.
Использование fork() дает PostgreSQL фундаментальное преимущество в надежности. Каждый бэкенд имеет собственную виртуальную память. Если из-за программной ошибки или повреждения памяти один бэкенд аварийно завершится (segmentation fault), операционная система изолирует этот сбой. Postmaster заметит гибель дочернего процесса и, если возникнет подозрение на повреждение критических структур в разделяемой памяти, инициирует процедуру восстановления: принудительно завершит остальные бэкенды, сбросит разделяемую память и перезапустит систему.
Однако у этой модели есть цена — накладные расходы на создание процесса. Операция fork() в современных Linux-системах оптимизирована через механизм Copy-on-Write (CoW), но создание процесса все равно обходится дороже, чем создание легковесного потока. Именно поэтому при достижении порога в несколько сотен или тысяч одновременных соединений в PostgreSQL становится критически важным использование внешних пулеров соединений (например, PgBouncer или Odyssey), которые позволяют держать тысячи клиентских сессий на небольшом количестве реальных процессов бэкенда.
Иерархия вспомогательных процессов
Помимо бэкендов, обслуживающих пользователей, в системе работают «фоновые рабочие процессы» (Background Worker Processes). Они выполняют служебные задачи, без которых СУБД не смогла бы поддерживать целостность и производительность.
checkpointer и минимизировать всплески ввода-вывода.Эта децентрализация позволяет PostgreSQL эффективно масштабироваться на многоядерных системах, распределяя задачи по управлению ресурсами между разными единицами планирования ОС.
Анатомия оперативной памяти: Shared Memory
Поскольку каждый процесс в PostgreSQL имеет свое изолированное адресное пространство, им нужен механизм для обмена данными. Для этого используется разделяемая память (Shared Memory). При запуске сервера postmaster выделяет крупный сегмент памяти, к которому впоследствии подключаются все дочерние процессы.
Разделяемая память PostgreSQL (часто называемая shared_buffers) — это сердце системы. Здесь хранятся данные, к которым обращаются все процессы. Если данных нет в shared_buffers, процесс должен пойти за ними на диск (или в кэш ОС).
Структура Shared Buffers
Размер этой области определяется параметром shared_buffers. Внутри она разбита на страницы фиксированного размера (по умолчанию 8 КБ). Каждая страница в памяти сопровождается дескриптором (Buffer Descriptor), который содержит метаданные:
Для быстрого поиска нужной страницы в огромном массиве shared_buffers используется хеш-таблица. Когда бэкенду нужен блок №42 таблицы users, он вычисляет хеш от идентификатора таблицы и номера блока, находит запись в хеш-таблице и получает указатель на слот в разделяемой памяти.
Алгоритм вытеснения: Clock Sweep
PostgreSQL использует алгоритм, приближенный к LRU (Least Recently Used), который называется Clock Sweep. Представьте себе циферблат часов, где каждое деление — это буфер. Стрелка «часов» постоянно движется по кругу.
Когда стрелка проходит мимо буфера, она проверяет его usage_count:
Если страница была изменена (стала «грязной»), перед вытеснением её необходимо записать на диск. Это одна из причин, почему bgwriter работает в фоне: он старается заранее записывать грязные страницы с низким usage_count, чтобы, когда бэкенду срочно понадобится свободный слот, ему не пришлось ждать завершения дисковой операции записи.
Локальная память процесса (Local Memory)
Помимо общей памяти, у каждого бэкенда есть индивидуальные области, которые не видны другим процессам. Управление этой памятью критически важно для производительности тяжелых запросов.
Work Mem
Параметр work_mem определяет объем памяти, доступный для операций сортировки (ORDER BY, DISTINCT) и хеширования (Hash Join, хеш-агрегаты). Важный нюанс: этот лимит устанавливается не на весь запрос, а на каждый узел плана запроса.
Если ваш запрос выполняет сложный JOIN с последующей сортировкой, он может выделить несколько порций work_mem.
Если данных для сортировки больше, чем помещается в work_mem, PostgreSQL переходит к «внешней сортировке» (external sort), используя временные файлы на диске. Это замедляет выполнение запроса в десятки и сотни раз. Однако бездумное увеличение work_mem может привести к тому, что при всплеске нагрузки система исчерпает всю физическую память и будет убита механизмом OOM Killer в Linux.
Maintenance Work Mem
Этот параметр используется для административных задач: VACUUM, CREATE INDEX, ALTER TABLE. Поскольку такие операции выполняются редко и обычно по одной, значение maintenance_work_mem можно и нужно ставить значительно выше, чем work_mem. Большой объем памяти позволяет индексу строиться быстрее за счет уменьшения количества проходов по данным.
Механизмы синхронизации: LWLocks и Spinlocks
В среде, где сотни процессов одновременно читают и пишут в одну и ту же область памяти, неизбежно возникают конфликты. PostgreSQL не может использовать тяжелые блокировки базы данных (Heavyweight Locks) для защиты структур памяти, так как это уничтожит производительность. Вместо этого используются низкоуровневые примитивы.
Spinlocks (Спин-блокировки)
Это самый быстрый и примитивный вид блокировки. Процесс, который не может получить доступ к ресурсу, входит в цикл «ожидания» (зацикливается), постоянно проверяя состояние переменной. Спин-блокировки удерживаются на очень короткое время (несколько инструкций процессора). Если процесс не может получить спин-локи в течение долгого времени, это обычно признак серьезной проблемы в архитектуре или «зависания» ядра ОС.
LWLocks (Lightweight Locks)
Легковесные блокировки используются для защиты данных в разделяемой памяти, например, при чтении или записи страницы в буферном кэше. У них есть два режима:
Если процесс хочет прочитать страницу, он берет LWLock в режиме Shared. Если в это время bgwriter хочет сбросить эту страницу на диск, он запросит Exclusive Lock и будет ждать, пока все читатели отпустят свои блокировки.
Одной из самых известных точек раздора (contention) в PostgreSQL является WALInsertLock. Все процессы, фиксирующие транзакции, должны записать данные в WAL-буфер. Чтобы избежать перемешивания данных, они должны по очереди захватывать эту блокировку. В современных версиях PostgreSQL эта проблема частично решена через разделение WAL-буферов на несколько сегментов, но она остается классическим примером борьбы за разделяемый ресурс.
Взаимодействие с кэшем операционной системы
Уникальная особенность PostgreSQL — это стратегия «двойного кэширования». PostgreSQL не использует O_DIRECT (прямой ввод-вывод в обход кэша ОС) по умолчанию. Когда данные читаются с диска, они сначала попадают в кэш страниц операционной системы (Page Cache), а затем копируются в shared_buffers.
Это кажется избыточным, но дает ряд преимуществ:
fsync(), который гарантирует, что данные покинули кэш ОС и физически записаны на пластины диска.shared_buffers, велика вероятность, что она все еще находится в кэше ОС. Чтение из памяти ядра происходит быстрее, чем физическое обращение к SSD/HDD.Однако это накладывает ограничения на настройку. Обычно рекомендуется выделять под shared_buffers не более 25% от общего объема оперативной памяти. Оставшуюся часть должна использовать ОС для кэширования файлов, иначе возникнет ситуация «двойного буферирования», где одни и те же данные занимают место дважды, не принося пользы.
Динамические области памяти (DSM) и параллелизм
Начиная с версии 9.6, PostgreSQL начал активно развивать параллельное выполнение запросов (Parallel Query). Это потребовало создания механизмов для динамического выделения разделяемой памяти (Dynamic Shared Memory, DSM).
Когда планировщик решает, что запрос можно распараллелить, он назначает основной процесс (Leader) и несколько рабочих процессов (Workers). Чтобы рабочие процессы могли передавать лидеру промежуточные результаты (например, отсканированные строки из разных частей таблицы), PostgreSQL на лету создает временные сегменты разделяемой памяти. После завершения запроса эти сегменты уничтожаются.
Управление DSM реализовано через различные системные механизмы в зависимости от ОС: mmap на Linux, shm_open на POSIX-системах или именованные разделяемые объекты в Windows. Это позволяет PostgreSQL гибко адаптироваться к нагрузке, не резервируя всю память заранее.
Проблема Huge Pages
В системах с большим объемом оперативной памяти (сотни гигабайт) стандартный размер страницы памяти в 4 КБ становится неэффективным. Таблица страниц (Page Table), которую ведет ядро ОС для отображения виртуальных адресов процессов на физические адреса, разрастается до гигантских размеров. Это приводит к промахам в TLB (Translation Lookaside Buffer) — кэше процессора для адресов памяти.
PostgreSQL поддерживает использование Huge Pages (обычно 2 МБ в Linux). Использование Huge Pages позволяет:
shared_buffers в памяти, предотвращая их сброс в swap.Настройка huge_pages = try или on является обязательной для высоконагруженных серверов баз данных, так как это дает прямой прирост производительности в 5-10% просто за счет более эффективной работы с «железом».
Резюмируя архитектурный подход
Архитектура PostgreSQL — это торжество принципа разделения ответственности. Модель процессов обеспечивает изоляцию и стабильность: если один клиент пришлет «запрос-убийцу», который вызовет падение бэкенда, остальные пользователи этого даже не заметят. Разделяемая память служит общим пространством для данных, а сложная иерархия фоновых процессов гарантирует, что эти данные будут вовремя записаны на диск и очищены от мусора.
Понимание того, как shared_buffers взаимодействует с кэшем ОС и как work_mem ограничивает аппетиты отдельных запросов, позволяет администраторам и разработчикам не просто «крутить ручки» конфига, а осознанно балансировать между скоростью обработки данных и риском падения системы по нехватке памяти. В следующей главе мы спустимся еще глубже и разберем, как именно эти 8-килобайтные страницы данных, живущие в shared_buffers, структурированы внутри и как они преобразуются в файлы на дисковой подсистеме.