Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Базы данных · Уровень: Senior ## TL;DR - PostgreSQL — это **процессная** (process-per-connection) СУБД, а не потоковая. Родительский процесс `postmaster` форкает по одному backend-процессу на каждое соединение, плюс набор фоновых служебных процессов. - Все backend'ы общаются через **общую память** (`shared memory`), главная часть которой — **shared buffers** (кэш страниц 8 КБ). Вытеснение страниц — алгоритм **clock-sweep** (приближение к LRU). - Перед записью грязной страницы на диск изменения сначала пишутся в **WAL** (write-ahead log) — это гарантирует durability и crash recovery. Принцип **WAL-before-data**, позиция в логе — **LSN**. - **Checkpoint** — точка, до которой все грязные страницы сброшены на диск; ограничивает объём WAL, который нужно проиграть при восстановлении. Регулируется `checkpoint_timeout`, `max_wal_size`, `checkpoint_completion_target`. - **Autovacuum** убирает dead tuples (следствие MVCC), предотвращает раздувание (bloat) и transaction ID wraparound, обновляет статистику и visibility map. - **TOAST** — механизм хранения значений, не помещающихся в страницу 8 КБ: сжатие + вынос в отдельную таблицу (out-of-line). - На senior-уровне ждут понимания связи MVCC ↔ vacuum ↔ WAL ↔ checkpoint и умения диагностировать проблемы (I/O-всплески, bloat, wraparound, write amplification). ## Теория ### Процессная модель PostgreSQL запускает дерево процессов. В отличие от MySQL/Oracle (потоки), здесь на каждое клиентское соединение создаётся отдельный процесс ОС. Это упрощает изоляцию сбоев (краш одного backend не валит сервер), но делает соединения дорогими — отсюда необходимость пулеров (PgBouncer, pgcat) на высоконагруженных системах. ``` postmaster (главный процесс, слушает порт) ├── backend (клиент 1) ├── backend (клиент 2) ├── background writer ├── walwriter ├── checkpointer ├── autovacuum launcher │ └── autovacuum worker (форкаются по необходимости) ├── logical replication launcher ├── archiver (если archive_mode=on) ├── wal receiver / wal sender (репликация) ├── startup process (только во время recovery) └── custom background workers (расширения, parallel workers) ``` | Процесс | Назначение | |---|---| | **postmaster** | Родитель. Слушает сокет, форкает backend'ы и фоновые процессы, следит за их жизнью, инициирует recovery. Сам не обслуживает запросы и не трогает shared buffers напрямую. | | **backend** | Обслуживает одно соединение: парсинг, планирование, исполнение запросов. Читает/пишет страницы в shared buffers, генерирует WAL-записи. | | **background writer (bgwriter)** | Фоном сбрасывает грязные страницы из shared buffers на диск, чтобы backend'ам реже приходилось делать это самим (синхронно в горячем пути). Сглаживает I/O между чекпойнтами. | | **walwriter** | Периодически флашит WAL-буферы на диск, разгружая backend'ы от записи WAL. | | **checkpointer** | Выполняет чекпойнты: сбрасывает ВСЕ грязные страницы и пишет checkpoint-запись в WAL. | | **autovacuum launcher** | По расписанию решает, какие таблицы нуждаются в vacuum/analyze, и запускает worker'ов. | | **autovacuum worker** | Делает фактический VACUUM/ANALYZE конкретной таблицы. | | **archiver** | Копирует завершённые WAL-сегменты в архив (для PITR / резервных копий). | | **wal sender / receiver** | Потоковая репликация: отправка/приём WAL на реплики. | | **startup process** | При старте после краха проигрывает WAL (redo) от последнего чекпойнта. | | **bgworker** | Универсальный механизм фоновых процессов: parallel query workers, расширения (pg_cron, Citus и т. п.). | Важно: `postmaster` использует `fork()` без `exec()` — backend наследует уже инициализированную общую память и атрибуты. Соединение «прибивается» к одному процессу на всё время жизни. ### Shared buffers (буферный кэш) `shared_buffers` — область общей памяти, кэширующая страницы данных и индексов размером **8 КБ** (`BLCKSZ`, дефолт компиляции). Это центральная разделяемая структура: любой backend читает/пишет данные только через буферный кэш. Ключевые понятия: - **Buffer descriptor** — метаданные буфера: тег страницы (relfilenode, fork, block number), флаги (dirty, valid), `usage_count`, `refcount`, lock. - **Pin / unpin** — backend «пиннит» буфер на время работы с ним, чтобы его не вытеснили. - **Dirty page** — страница, изменённая в памяти, но ещё не записанная на диск. До записи на диск соответствующие WAL-записи уже должны быть на диске (WAL-before-data). **Clock-sweep eviction.** Когда нужен свободный буфер, а все заняты, PostgreSQL ищет жертву по кольцу буферов «стрелкой часов» (clock hand): 1. Стрелка проходит по буферам по кругу. 2. Если у буфера `usage_count > 0` — уменьшает счётчик на 1 и идёт дальше (даёт «второй шанс»). 3. Если `usage_count == 0` и буфер не запинен — это жертва. Если он dirty — сначала записывается на диск (или это уже сделал bgwriter), затем переиспользуется. При каждом обращении `usage_count` увеличивается (до потолка `BM_MAX_USAGE_COUNT = 5`). Это дешёвое приближение LRU без поддержки полноценного списка. **Ring buffer / BAS (Buffer Access Strategy).** Для операций, которые читают много данных «одноразово» (seq scan большой таблицы, COPY, VACUUM), PostgreSQL использует маленькое кольцо буферов вместо вытеснения всего горячего кэша — защита от cache trashing. Практика выбора размера: классическая рекомендация — `shared_buffers ≈ 25% RAM` (но не «чем больше, тем лучше» — ОС-кэш тоже кэширует файлы, и слишком большой `shared_buffers` усиливает работу checkpointer и удваивает кэширование). ```sql SHOW shared_buffers; -- Что сейчас в кэше (расширение pg_buffercache) CREATE EXTENSION IF NOT EXISTS pg_buffercache; SELECT c.relname, count(*) AS buffers, pg_size_pretty(count(*) * 8192) AS size, round(100.0 * count(*) FILTER (WHERE b.isdirty) / count(*), 1) AS dirty_pct FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid) GROUP BY c.relname ORDER BY buffers DESC LIMIT 10; -- Оценка hit ratio (должен быть высоким, обычно >99% на OLTP) SELECT datname, round(100.0 * blks_hit / nullif(blks_hit + blks_read, 0), 2) AS cache_hit_ratio FROM pg_stat_database WHERE datname = current_database(); ``` ### WAL (Write-Ahead Log) WAL — журнал предзаписи. Фундаментальное правило: **запись об изменении попадает в WAL и флашится на диск раньше, чем изменённая страница данных**. Это даёт durability (D в ACID) и возможность crash recovery: после сбоя достаточно проиграть WAL от последнего чекпойнта. **LSN (Log Sequence Number)** — монотонно растущий 64-битный адрес байта в потоке WAL, форматируется как `XXXXXXXX/XXXXXXXX`. Каждая страница хранит `pd_lsn` — LSN последней WAL-записи, которая её изменила. Перед сбросом страницы на диск PostgreSQL гарантирует, что WAL до `pd_lsn` уже на диске. **Жизненный цикл:** 1. Backend меняет страницу в shared buffers → формирует WAL-запись в `wal_buffers`. 2. При COMMIT WAL флашится на диск через `fsync` (если `synchronous_commit=on`) — только после этого клиенту возвращается успех. 3. Грязная страница данных может остаться в памяти и попасть на диск позже (bgwriter/checkpointer). **`fsync`** — обязателен для durability. `fsync=off` ускоряет, но при крахе/потере питания возможна **порча кластера** (не просто потеря последних транзакций!). Никогда не выключайте на проде. **`synchronous_commit`** — тонкая настройка durability vs latency: | Значение | Поведение | |---|---| | `on` (дефолт) | COMMIT ждёт fsync WAL локально (и подтверждения синхронных реплик). | | `off` | COMMIT возвращается, не дожидаясь fsync. Возможна потеря последних транзакций при краше, но **без порчи данных** (целостность кластера сохраняется). | | `local` | Ждать только локальный fsync, игнорировать синхронные реплики. | | `remote_apply` / `remote_write` | Уровни ожидания подтверждения от реплик. | **`full_page_writes`.** Защита от **partial page writes** (torn pages): если ОС/диск пишет страницу 8 КБ не атомарно и происходит сбой посреди записи, на диске останется «полустраница». Поэтому при первом изменении страницы после чекпойнта PostgreSQL пишет в WAL её **полный образ** (FPI — full page image). При recovery такая страница восстанавливается целиком, а не накатывается дельтой на возможно битую страницу. Цена — раздувание WAL: сразу после чекпойнта объём WAL резко растёт за счёт FPI. Это одна из причин, почему слишком частые чекпойнты вредны (см. ниже). Выключать `full_page_writes` можно только если ФС/железо гарантирует атомарную запись блока (например, ZFS с подходящим recordsize, или СХД с защитой) — иначе риск молчаливой порчи. ```sql -- Текущая позиция вставки в WAL SELECT pg_current_wal_lsn(); -- Сколько WAL сгенерировано между двумя точками (в байтах) SELECT pg_wal_lsn_diff('0/16D6358', '0/16C0000'); -- Объём WAL по записям/типам (PG 14+) SELECT * FROM pg_stat_wal; ``` ### Checkpoint (контрольная точка) Checkpoint гарантирует, что все грязные страницы до определённого LSN записаны на диск. После чекпойнта WAL до этой точки больше не нужен для recovery (но может удерживаться для архива/репликации/replication slots). Зачем нужен: ограничивает **время восстановления** (нужно проиграть WAL только от последнего чекпойнта) и позволяет переиспользовать/удалять старые WAL-сегменты. Триггеры чекпойнта: - **по времени** — `checkpoint_timeout` (дефолт 5 мин); - **по объёму WAL** — когда объём WAL приближается к `max_wal_size` (дефолт 1 ГБ); - **вручную** — `CHECKPOINT;` - при `pg_start_backup`, выключении сервера и т. п. **Spread checkpoints.** Если бы checkpointer писал все грязные страницы разом, получился бы I/O-шторм и провал latency. Поэтому запись «размазывается» во времени: `checkpoint_completion_target` (дефолт **0.9**) означает, что чекпойнт должен завершить запись грязных страниц за 90% интервала до следующего ожидаемого чекпойнта. | Параметр | Смысл | Trade-off | |---|---|---| | `checkpoint_timeout` | Макс. интервал между чекпойнтами | Больше → реже чекпойнты, меньше FPI/write amplification, но дольше recovery и больше WAL | | `max_wal_size` | Мягкий потолок объёма WAL до форс-чекпойнта | Больше → реже чекпойнты по объёму | | `min_wal_size` | Сколько WAL-сегментов переиспользовать, а не удалять | Снижает аллокации файлов | | `checkpoint_completion_target` | Доля интервала, за которую размазывается запись | Ближе к 1 → плавнее I/O | | `checkpoint_flush_after` | Подсказка ОС флашить после N байт | Сглаживает всплески page cache | Диагностика частых чекпойнтов — частый кейс на собеседовании: если `checkpoints_req` (по объёму) >> `checkpoints_timed`, значит `max_wal_size` мал и чекпойнты вызываются объёмом WAL → растёт write amplification. ```sql -- PG <17 SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint, buffers_clean, buffers_backend FROM pg_stat_bgwriter; -- PG 17+: статистика чекпойнтов вынесена в отдельное представление SELECT num_timed, num_requested, write_time, sync_time, buffers_written FROM pg_stat_checkpointer; ``` ### MVCC и autovacuum PostgreSQL реализует **MVCC**: UPDATE не перезаписывает строку на месте, а создаёт **новую версию** (tuple) и помечает старую как удалённую (`xmax`). DELETE помечает строку мёртвой. Старые версии (**dead tuples**) остаются физически в странице, пока на них может смотреть какая-то транзакция (по её snapshot). Каждый tuple хранит `xmin` (транзакция-создатель) и `xmax` (транзакция-удалитель). Видимость определяется сравнением с снапшотом транзакции и **transaction ID (XID)**. **Зачем нужен VACUUM:** 1. **Освобождает место от dead tuples** — иначе таблица/индекс раздуваются (**bloat**): размер на диске растёт, кэш-эффективность падает, scans медленнее. 2. **Защита от XID wraparound.** XID 32-битный (~4 млрд). При переполнении старые транзакции «оказались бы в будущем» и данные стали бы невидимыми. VACUUM «замораживает» (freeze) старые tuple, помечая их как заведомо видимые (`FrozenTransactionId`), и продвигает `relfrozenxid`. Если этого не делать, при приближении к лимиту PostgreSQL переходит в защитный режим, а в крайнем случае останавливает приём запросов. 3. **Обновляет visibility map** — что критично для **index-only scans** и для пропуска «всех видимых» страниц при последующих vacuum. 4. **ANALYZE** обновляет статистику планировщика. Различия: - `VACUUM` — убирает dead tuples, **не** возвращает место ОС (только делает доступным для повторного использования внутри таблицы). - `VACUUM FULL` — переписывает таблицу целиком, возвращает место ОС, но берёт **ACCESS EXCLUSIVE lock** (блокирует таблицу) — на проде опасно; альтернатива `pg_repack`. - `FREEZE` — агрессивная заморозка. **Autovacuum** — фоновая автоматизация. Launcher по статистике (`pg_stat_user_tables.n_dead_tup`) решает, что пора: ``` порог = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * reltuples ``` То есть для таблицы из 100 млн строк при дефолтном `scale_factor = 0.2` autovacuum сработает только после ~20 млн мёртвых строк — это слишком поздно для больших таблиц. Типичная senior-практика — снижать `scale_factor` или ставить **per-table** настройки: ```sql ALTER TABLE orders SET ( autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_threshold = 1000, autovacuum_vacuum_cost_limit = 2000, -- агрессивнее на горячих таблицах autovacuum_vacuum_cost_delay = 2 ); ``` Ключевые глобальные параметры: `autovacuum_max_workers`, `autovacuum_naptime`, `autovacuum_vacuum_cost_delay` / `cost_limit` (throttling I/O), `autovacuum_freeze_max_age` (форсирует anti-wraparound vacuum). ```sql -- Кому давно не делали vacuum и кто раздут SELECT relname, n_live_tup, n_dead_tup, round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS dead_pct, last_autovacuum, autovacuum_count FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 20; -- Контроль wraparound: сколько XID осталось до forced vacuum SELECT c.relname, age(c.relfrozenxid) AS xid_age, 2147483648 - age(c.relfrozenxid) AS xids_left FROM pg_class c WHERE c.relkind = 'r' ORDER BY xid_age DESC LIMIT 20; ``` ### TOAST (The Oversized-Attribute Storage Technique) Страница PostgreSQL — 8 КБ, и tuple **не может пересекать границу страницы**. Значит большие значения (длинные `text`, `bytea`, `jsonb`) нужно как-то хранить. Это делает TOAST. Порог — `TOAST_TUPLE_THRESHOLD` (~2 КБ): если строка не влезает, PostgreSQL применяет к toast-able колонкам: 1. **Компрессию** значения «на месте» (in-line). 2. Если всё ещё велико — **выносит** значение в отдельную **TOAST-таблицу** (`pg_toast.pg_toast_`), разбивая на чанки (~2 КБ), а в основной строке оставляет указатель (**TOAST pointer**) — это и есть **out-of-line** хранение. **Стратегии хранения колонки** (`ALTER TABLE ... ALTER COLUMN ... SET STORAGE ...`): | Стратегия | Компрессия | Out-of-line | Когда | |---|---|---|---| | `PLAIN` | нет | нет | Типы фиксированной длины (int, etc.), не toast-able | | `EXTENDED` (дефолт для var-length) | да | да | jsonb, text — обычный случай | | `EXTERNAL` | нет | да | Когда нужны быстрые подстроки (substring без распаковки) | | `MAIN` | да | по возможности нет | Стараться держать сжатым in-line | **Компрессия.** Исторически — **PGLZ**. С PG 14 доступен **LZ4** (`default_toast_compression = lz4`) — быстрее и часто лучше по ratio; задаётся глобально или per-column. Важные следствия для производительности: - TOAST-значения читаются **лениво**: если в запросе колонка не выбрана, чанки не читаются. Поэтому `SELECT *` по таблице с большими jsonb может быть кратно дороже, чем выбор нужных колонок. - Обновление **не-toast** колонок в строке с большим toast-значением обычно **не переписывает** TOAST-чанки (они шарятся по указателю) — это экономит I/O. - TOAST-таблица имеет собственный индекс и тоже подлежит VACUUM и bloat. ```sql -- Storage стратегия колонок SELECT attname, atttypid::regtype, CASE attstorage WHEN 'p' THEN 'plain' WHEN 'e' THEN 'external' WHEN 'm' THEN 'main' WHEN 'x' THEN 'extended' END AS storage FROM pg_attribute WHERE attrelid = 'my_table'::regclass AND attnum > 0 AND NOT attisdropped; -- Найти TOAST-таблицу и её размер SELECT c.relname, pg_size_pretty(pg_relation_size(c.reltoastrelid)) AS toast_size, pg_size_pretty(pg_relation_size(c.oid)) AS main_size FROM pg_class c WHERE c.relname = 'my_table'; ALTER TABLE my_table ALTER COLUMN payload SET STORAGE EXTERNAL; ALTER TABLE my_table ALTER COLUMN payload SET COMPRESSION lz4; -- PG 14+ ``` ### Связь с Go-приложением Процессная модель и дороговизна соединений напрямую влияют на конфигурацию пула в Go. ```go // database/sql: ограничиваем число backend-процессов на сервере db, err := sql.Open("pgx", dsn) // драйвер jackc/pgx через stdlib if err != nil { return err } // На каждое открытое соединение PostgreSQL держит отдельный backend-процесс. // MaxOpenConns не должен превышать разумную долю max_connections сервера // (а лучше — ходить через PgBouncer и держать пул небольшим). db.SetMaxOpenConns(20) db.SetMaxIdleConns(20) // равно MaxOpen, чтобы избежать постоянного reopen db.SetConnMaxIdleTime(5 * time.Minute) db.SetConnMaxLifetime(30 * time.Minute) // переоткрытие, чтобы не копить долгоживущие backend'ы ``` ```go // pgx/v5 нативный пул — даёт доступ к фишкам PostgreSQL (LISTEN/NOTIFY, COPY, типы) import "github.com/jackc/pgx/v5/pgxpool" cfg, _ := pgxpool.ParseConfig(dsn) cfg.MaxConns = 20 cfg.MinConns = 2 cfg.MaxConnLifetime = 30 * time.Minute pool, _ := pgxpool.NewWithConfig(ctx, cfg) defer pool.Close() // Долго не закрытая транзакция в Go (забыли Rollback/Commit) удерживает snapshot // и блокирует autovacuum от очистки dead tuples во ВСЕЙ базе → bloat. // Всегда defer tx.Rollback() и короткие транзакции. ``` Практический мостик: «idle in transaction» соединение из Go-приложения — частая первопричина bloat и роста размера базы, потому что оно держит `xmin` и не даёт vacuum собрать мёртвые версии. ## Подводные камни / gotchas - **Process-per-connection дорог.** Тысячи коротких соединений из Go без пулера → тысячи fork'ов и память на backend. Решение: PgBouncer (transaction pooling) + небольшой пул в приложении. - **`fsync=off` ≠ просто «быстрее».** При краше это порча кластера, а не потеря последних транзакций. Для durability vs latency используйте `synchronous_commit`, а не `fsync`. - **Слишком частые чекпойнты** (мелкий `max_wal_size`) → лавина FPI после каждого чекпойнта (`full_page_writes`) → write amplification и всплески I/O. Смотрите соотношение `checkpoints_req`/`checkpoints_timed`. - **`shared_buffers` «побольше» не панацея.** Double caching с ОС-кэшем, дольше чекпойнты. Тюнинг — по hit ratio и нагрузке, не «по памяти». - **Autovacuum «не успевает» на больших таблицах** из-за дефолтного `scale_factor=0.2`. Ставьте per-table агрессивные настройки. - **Long-running / idle-in-transaction транзакции** держат `xmin` → autovacuum не может удалить dead tuples → bloat по всей базе. Мониторьте `pg_stat_activity` (`state='idle in transaction'`, `xact_start`). - **XID wraparound** — катастрофа: при достижении лимита сервер уходит в read-only / останавливается. Мониторьте `age(relfrozenxid)`. - **`VACUUM` не отдаёт место ОС.** Для реального возврата — `pg_repack` (online) или `VACUUM FULL` (с эксклюзивной блокировкой). - **`SELECT *` по таблице с большими TOAST-значениями** читает все чанки, даже если значение не нужно. Выбирайте только необходимые колонки. - **Index bloat** — индексы тоже раздуваются; `REINDEX CONCURRENTLY` (PG 12+) перестраивает без долгой блокировки. - **HOT updates** (Heap-Only Tuple) работают только если меняемые колонки не входят в индекс и на странице есть место (`fillfactor`); иначе каждый UPDATE трогает все индексы. ## Вопросы на собеседовании **В:** Чем процессная модель PostgreSQL отличается от потоковой и какие у неё последствия для проектирования сервиса? **О:** Один процесс ОС на соединение (`fork` от postmaster). Плюсы: изоляция сбоев, простота. Минусы: соединения дороги по памяти и CPU на fork → нужны пулеры (PgBouncer/pgxpool), небольшой `max_connections`, короткие транзакции. В Go это ограничивает `MaxOpenConns` и делает обязательным контроль времени жизни соединений. **В:** Что такое clock-sweep и почему не используют точный LRU? **О:** Это приближение LRU без поддержки дорогого списка: «стрелка» обходит буферы по кольцу, у каждого есть `usage_count` (второй шанс). При нехватке буфера счётчик уменьшается; буфер с `usage_count=0` и без pin становится жертвой. Точный LRU потребовал бы блокировок и перестроения списка на каждом обращении — слишком дорого при конкурентном доступе многих backend'ов. **В:** Объясните принцип WAL-before-data и роль LSN. **О:** Изменение сначала фиксируется в WAL и флашится на диск, и только потом (возможно, гораздо позже) грязная страница попадает на диск. LSN — адрес в потоке WAL; страница хранит `pd_lsn`, и перед её сбросом гарантируется, что WAL до этого LSN на диске. Это даёт durability и crash recovery: после краха проигрываем WAL от последнего чекпойнта. **В:** Зачем нужен `full_page_writes` и какова его цена? **О:** Защита от torn pages (частичная запись 8 КБ при сбое). При первом изменении страницы после чекпойнта в WAL пишется её полный образ (FPI), чтобы при recovery восстановить страницу целиком. Цена — раздувание WAL сразу после чекпойнтов; усиливается при частых чекпойнтах. **В:** Чем `synchronous_commit=off` отличается от `fsync=off`? **О:** `synchronous_commit=off` не дожидается fsync на COMMIT — при краше теряются последние транзакции, но кластер целостен. `fsync=off` вообще не гарантирует попадание данных/WAL на диск — при краше возможна порча всего кластера. Первое — допустимый компромис latency/durability, второе — почти всегда недопустимо на проде. **В:** Как диагностировать и лечить слишком частые чекпойнты? **О:** Сравнить `checkpoints_req` и `checkpoints_timed` (`pg_stat_bgwriter` / `pg_stat_checkpointer` в PG17+). Если преобладают `req` — чекпойнты вызываются объёмом WAL → увеличить `max_wal_size`, поднять `checkpoint_timeout`, держать `checkpoint_completion_target` ~0.9 для размазывания I/O. Это снизит количество FPI и write amplification. **В:** Почему растёт размер таблицы, хотя строк столько же? Что делать? **О:** Bloat из-за MVCC: UPDATE/DELETE плодят dead tuples, которые autovacuum не успевает/не может собрать (например, из-за long-running транзакции, удерживающей `xmin`, или слабых autovacuum-настроек). Диагностика: `n_dead_tup`, `pg_stat_activity` на idle-in-transaction. Лечение: устранить долгие транзакции, поднять агрессивность autovacuum per-table, при необходимости `pg_repack`. **В:** Что такое XID wraparound и как его не допустить? **О:** XID 32-битный; при переполнении старые транзакции «попадают в будущее», данные становятся невидимыми. VACUUM замораживает старые tuple (`relfrozenxid` продвигается). Мониторим `age(relfrozenxid)`; `autovacuum_freeze_max_age` форсирует anti-wraparound vacuum. При приближении к лимиту сервер уходит в защитный режим вплоть до остановки записи. **В:** Как PostgreSQL хранит большой `jsonb` и как это влияет на запросы? **О:** Через TOAST: сначала компрессия (PGLZ/LZ4), затем при необходимости вынос в отдельную TOAST-таблицу чанками по ~2 КБ с указателем в основной строке (out-of-line). TOAST читается лениво — если колонка не в SELECT, чанки не читаются; поэтому `SELECT *` дорог. Стратегию хранения (`EXTENDED/EXTERNAL/MAIN`) и компрессию можно настроить per-column. **В:** Что такое HOT update и почему он важен? **О:** Heap-Only Tuple — оптимизация UPDATE, когда новая версия строки помещается на ту же страницу и изменённые колонки не входят в индексы. Тогда индексы не обновляются (новая версия достижима через указатель в старой), что резко снижает write amplification и нагрузку на vacuum. Управляется наличием места (`fillfactor`) и составом индексов. ## На что копают на senior+ - **Системная связность.** Умение объяснить цепочку: MVCC порождает dead tuples → нужен vacuum → vacuum продвигает relfrozenxid и обновляет visibility map → visibility map ускоряет index-only scans и сам vacuum. Кандидат должен видеть систему, а не отдельные параметры. - **Write amplification.** Понимание, как `full_page_writes` + частые чекпойнты + не-HOT updates умножают объём записи; как `fillfactor`, `max_wal_size`, LZ4-компрессия WAL (`wal_compression`) это снижают. - **Диагностика на проде.** Не «прочитать доку», а назвать конкретные представления: `pg_stat_bgwriter`/`pg_stat_checkpointer`, `pg_stat_activity` (idle in transaction, longest xact), `pg_stat_user_tables` (dead tuples), `pg_buffercache`, `age(relfrozenxid)`, `pg_stat_wal`. - **Репликация и WAL.** Как replication slots удерживают WAL (риск заполнения диска при отставшей реплике), `synchronous_standby_names`, физическая vs логическая репликация, влияние `hot_standby_feedback` на vacuum на primary. - **Пулинг под процессную модель.** Различие session/transaction/statement pooling в PgBouncer, почему prepared statements ломаются при transaction pooling, как pgx это обходит, расчёт пула под `max_connections`. - **Тонкости TOAST.** Деградация при больших jsonb, `SET STORAGE`, выбор LZ4 vs PGLZ, vacuum/bloat самой TOAST-таблицы, влияние на logical decoding. - **Аварийные сценарии.** Поведение при приближении к wraparound, восстановление после краха (startup process, redo), PITR через archive + base backup, что произойдёт при `checkpoint` во время backup. - **Trade-off durability/latency** под конкретный бизнес-кейс: где допустим `synchronous_commit=off`, где нужны синхронные реплики, как это влияет на RPO/RTO.