1. Архитектура PostgreSQL
В отличие от вашего приложения на Go, где сотни тысяч горутин легковесно мультиплексируются на несколько системных потоков ОС силами планировщика рантайма (модель G-M-P), или от MySQL, где традиционно применяется многопоточная архитектура (thread-per-connection), PostgreSQL исторически пошел по бескомпромиссно надежному пути Process-Per-Connection (выделенный процесс на каждое клиентское соединение).
Для бэкенд-инженера это фундаментальное архитектурное решение диктует жесткие правила игры: то, как мы настраиваем пулинг соединений в Go, как СУБД утилизирует оперативную память и почему тысячи наивных коннектов способны поставить сервер базы данных на колени быстрее, чем самые неоптимальные аналитические запросы.
Process-Per-Connection: Анатомия запуска и изоляция
PostgreSQL принципиально не использует потоки выполнения (threads) внутри своих рабочих обработчиков. Вся его архитектура опирается на классические Unix-процессы и механизмы разделяемой памяти (Shared Memory IPC).
Когда сервер базы данных инициализируется, операционная система запускает главный процесс — Postmaster (в современных версиях исполняемый файл называется просто postgres). Он открывает сетевой сокет, слушает заданный TCP-порт (по умолчанию 5432) и ожидает входящих клиентских подключений.
Когда ваше Go-приложение вызывает sql.Open(), инициализирует пул через pgx или выполняет db.Query(), происходит следующий каскад событий:
- Postmaster принимает входящее TCP-соединение через системный вызов
accept(). - Postmaster совершает системный вызов
fork(), порождая точную копию своего адресного пространства. - Дочерний процесс превращается в Backend Process (индивидуальный серверный рабочий процесс с собственным PID), перехватывает файловый дескриптор клиентского сокета и берет на себя всю дальнейшую обработку SQL-команд данной сессии.
- Этот процесс живет ровно столько, сколько активно данное сетевое соединение, после чего завершается (
exit()), отдавая ресурсы операционной системе.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
C["Go App <br> (database/sql / pgx)"]:::entry -->|"TCP Socket (Port 5432)"| PM["Postmaster <br> (Main Daemon Process)"]
PM -->|"syscall fork()"| BP1["Backend Process 1 <br> (PID 101, Client Session A)"]
PM -->|"syscall fork()"| BP2["Backend Process 2 <br> (PID 102, Client Session B)"]
subgraph SharedMem ["PostgreSQL Shared Memory (IPC)"]
SB["Shared Buffers <br> (Кэш страниц данных 8 KB)"]
WB["WAL Buffers <br> (Буфер журнала предзаписи)"]
LW["Lock & Proc Tables <br> (Блокировки и служебные структуры)"]
end
BP1 -->|"read / write pages"| SB
BP1 -->|"append log records"| WB
BP2 -->|"read / write pages"| SB
BP2 -->|"append log records"| WB
subgraph BGWorkers ["Background Helper Processes"]
BGW["Background Writer <br> (Сброс dirty pages)"]
WALW["WAL Writer <br> (Периодический fsync)"]
CHK["Checkpointer <br> (Точки согласованности)"]
AV["Autovacuum Launcher <br> & Workers"]:::process
end
SB -->|"flush dirty pages"| BGW
WB -->|"fsync WAL buffers"| WALW
SB -->|"checkpoint sync"| CHK
Однако за надежность и простоту отладки приходится платить существенную системную цену:
- Накладные расходы
fork(): Системный вызовfork()требует от ядра ОС дублирования таблиц страниц памяти (Page Tables) процесса. Даже с оптимизацией Copy-On-Write (CoW) создание сотен процессов в секунду генерирует огромную нагрузку на подсистему управления памятью Linux. - Переключение контекста (Context Switching): Переключение контекста между независимыми процессами (смена таблиц страниц в регистре
CR3, сброс TLB-кэша процессора) стоит в разы дороже переключения потоков, и несоизмеримо дороже кооперативного планирования горутин Go в пространстве пользователя. - Базовый overhead памяти: Каждый Backend Process резервирует индивидуальный стек, служебные структуры дескрипторов и локальные буферы (от 5–10 МБ оперативной памяти просто по факту своего существования).
Архитектура памяти PostgreSQL
Вся оперативная память экземпляра PostgreSQL строго разделена на две изолированные категории: Локальная память процессов и Разделяемая память инсталляции.
graph TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph HostRAM ["Оперативная память сервера (RAM)"]
subgraph SharedMem ["Shared Memory (Разделяемая память)"]
SB["shared_buffers (~25% RAM)"]
WB["wal_buffers (обычно 16MB)"]
LGT["Lock Space & Global State"]:::process
end
subgraph LinuxCache ["Linux OS Cache (~50-70% RAM)"]
PC["Page Cache (Файловый кэш ядра Linux)"]
end
subgraph LocalMem ["Backend Process Local Memory (на каждый процесс)"]
WM["work_mem (сортировки, Hash JOIN, GROUP BY)"]
MWM["maintenance_work_mem (VACUUM, CREATE INDEX)"]
TM["temp_buffers (временные таблицы)"]
end
end
1. Локальная память процесса (Local Memory)
Локальная память динамически выделяется внутри адресного пространства конкретного Backend-процесса при выполнении текущего запроса и мгновенно возвращается системе после завершения операции.
work_mem: Критически важный параметр настройки. Это объем памяти, выделяемый для внутренних операций сортировки (ORDER BY), построения хэш-таблиц для соединений (Hash JOIN) и группировок (GROUP BY).- Mechanical Sympathy: Обратите внимание:
work_memвыделяется не на запрос, а на каждый отдельный узел плана выполнения! Запрос с тремя сложными JOIN и двумяORDER BYможет затребовать5 * work_mem. - Если размер промежуточных данных превышает лимит
work_mem, PostgreSQL не падает с ошибкой памяти. Он незаметно переключается на сброс данных во временные файлы на диске (spill to disk). В результате производительность запроса драматически обваливается на порядки из-за дискового ввода-вывода. При анализе медленных запросов черезEXPLAIN ANALYZEищите ключевую фразу:external merge Disk: ...kB.
- Mechanical Sympathy: Обратите внимание:
maintenance_work_mem: Память для сервисных административных процедур: построения индексов (CREATE INDEX), изменения схем (ALTER TABLE) и фоновой сборки мусора (механизма очистки мертвых версий строк, подробно разобранного в 10. VACUUM, ANALYZE).temp_buffers: Локальный кэш страниц для временных пользовательских таблиц (CREATE TEMP TABLE), не требующих координации с другими процессами.
2. Разделяемая память (Shared Memory)
Единый сегмент физической памяти, выделяемый Postmaster'ом на этапе инициализации СУБД (через POSIX mmap или System V shmget) и разделяемый между всеми дочерними бэкендами и фоновыми демонами.
shared_buffers: Ключевой кэш страниц с данными (стандартный размер страницы — 8 КБ). PostgreSQL принципиально не читает данные напрямую с блочного устройства при обработке запросов. Сервер запрашивает блок с диска, помещает его вshared_buffers, и лишь оттуда отдает строки клиентскому запросу. Рекомендуемое значение для большинства серверов — около 25% от общего объема RAM.wal_buffers: Буфер страниц журнала предзаписи (8. WAL. Write Ahead Log). Именно сюда помещаются дельты изменений перед тем, как они будут сброшены на постоянный носитель для обеспечения свойства Durability (буква D в гарантиях ACID).
Фоновые процессы (Background Workers)
Чтобы пользовательские Backend-процессы не простаивали в ожидании завершения медленных дисковых операций записи, в ядре PostgreSQL функционирует пул специализированных фоновых демонов:
- Background Writer (BgWriter):
Непрерывно и плавно сканируетshared_buffersпо кольцевому алгоритму, выявляя "грязные" (измененные) страницы и асинхронно записывая их на накопитель. Главная цель BgWriter — сделать так, чтобы новый серверный процесс, которому потребовалось загрузить страницу с диска, всегда находил готовый чистый слот вshared_buffersи не тратил время на вытеснение чужих грязных страниц. - WAL Writer:
Периодически сбрасывает накопившиеся дельты изwal_buffersна диск, инициируя системный вызовfsync(). Гарантирует, что даже при внезапном обесточивании стойки с серверами зафиксированные транзакции не будут утеряны. Для оптимизации пропускной способности WAL Writer объединяет коммиты разных транзакций в группы (Group Commit). - Checkpointer:
Выполняет процедуру контрольной точки (Checkpoint): раз в заданный период времени (или по накоплению объема WAL) сбрасывает на накопитель абсолютно все грязные страницы изshared_buffersи ставит в журнале специальную метку согласованности. Это позволяет ограничить время восстановления БД после аварии (Recovery Time Objective): при старте после сбоя рантайму достаточно проиграть WAL только от последней контрольной точки (подробнее в 9. Checkpointing). - Autovacuum Launcher & Workers:
Координирующий процесс и пул воркеров, которые в фоновом режиме инспектируют таблицы на скопление "мертвых" кортежей (dead tuples), оставшихся после операцийUPDATEиDELETE, собирают мусор и актуализируют статистику для оптимизатора. Это жизненно необходимая часть многоверсионного контроля конкурентности (3. MVCC в PostgreSQL).
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
BP["Backend Process <br> (UPDATE accounts SET...)"]:::process -->|"1. Запись дельты в буфер"| WB["wal_buffers"]
BP -->|"2. Модификация страницы (8KB)"| SB["shared_buffers <br> (Dirty Page)"]
WB -->|"3. fsync() при COMMIT"| WALW["WAL Writer"]
WALW -->|"Синхронный сброс"| WAL["Файлы WAL (pg_wal) <br> NVMe / SSD"]
SB -.->|"4. Асинхронный сброс"| CHK["Checkpointer / BgWriter"]
CHK -->|"Периодический pwrite()"| DBF["Файлы таблиц (base/...) <br> NVMe / SSD"]
Жизненный цикл SQL-запроса
Когда ваше Go-приложение отправляет в соединение байты строки "SELECT * FROM users WHERE id = 1", внутри обслуживающего Backend-процесса запрос проходит классический многостадийный конвейер:
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
SQL["SQL Текст запроса"]:::process --> Parser["1. Parser (Синтаксический анализ)"]
Parser -->|"Parse Tree (Дерево разбора)"| Analyzer["2. Analyzer & Rewriter (Семантический анализ)"]
Analyzer -->|"Query Tree (Проверка каталога, правил, View)"| Planner["3. Planner / Optimizer (Стоимостной оптимизатор)"]
Planner -->|"Execution Plan (План выполнения)"| Executor["4. Executor (Исполнитель)"]
Executor <-->|"Чтение/запись страниц 8KB"| SB["shared_buffers"]
Executor -->|"Строки результата (Rows)"| Client["Go App (Driver)"]
- Parser (Парсер): Проверяет лексику и грамматику SQL-выражения, генерируя абстрактное синтаксическое дерево разбора (Parse Tree).
- Analyzer & Rewriter (Анализатор и модуль перезаписи): Проверяет схему данных по системному каталогу (существуют ли таблица
usersи атрибутid, есть ли у пользователя права доступа). Применяет правила перезаписи (подставляет реальные таблицы вместо представленийVIEW). - Planner / Optimizer (Планировщик / Оптимизатор): Интеллектуальное ядро базы данных. Опираясь на математическую статистику распределения значений (гистограммы, селективность колонок), оптимизатор вычисляет стоимость сотен альтернативных путей извлечения данных (Sequential Scan, Index Scan, Bitmap Scan) и формирует оптимальный план выполнения с наименьшей оценочной стоимостью (Cost Based Optimizer, подробно в 11. Cost based optimizer).
- Executor (Исполнитель): Рекурсивно обходит узлы плана выполнения, запрашивает страницы у подсистемы
shared_buffers, применяет предикаты фильтрации и возвращает готовые кортежи в сокет Go-клиенту.
Памятка для Go-разработчика
Понимание процессной природы и архитектуры памяти PostgreSQL формирует зрелый инженерный подход к интеграции:
- Управление пулом соединений: Всегда явно конфигурируйте
db.SetMaxOpenConns(),db.SetMaxIdleConns()иdb.SetConnMaxLifetime(). Допущение бесконечного роста пула подключений превратит сервер БД в полигон борьбы за процессорные кванты и память. - Внешний пул при микросервисах: Если ваш кластер масштабируется на 50 подов Kubernetes, и каждый под по умолчанию открывает пул из 20 соединений (в сумме 1 000 коннектов к БД), прямое подключение противопоказано. Обязательно внедряйте транзакционный пулер (PgBouncer или Odyssey), который будет мультиплексировать тысячи клиентских сессий в десятки реальных Backend-процессов PostgreSQL.
- Диагностика памяти запросов: Если сложный аналитический запрос с сортировкой или хэш-соединением неожиданно проседает по времени отклика, проверьте
EXPLAIN (ANALYZE, BUFFERS). ОбнаружениеSort Method: external merge Diskсвидетельствует о том, что процессу не хватилоwork_memи запрос начал бомбардировать диск временными файлами.
Поняв, как процессы PostgreSQL взаимодействуют с операционной системой и распределяют оперативную память, мы готовы спуститься на уровень хранения и разобрать, как устроены страницы данных на диске. Об этом — в следующей лекции: 2. Storage engine PostgreSQL.