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 использует **MVCC** (Multi-Version Concurrency Control): каждое изменение строки создаёт **новую физическую версию** (tuple), а старая помечается «мёртвой», но физически остаётся на странице до очистки. - Видимость версий определяется **системными полями** `xmin` (транзакция-создатель), `xmax` (транзакция-удалитель), `cmin/cmax` (command id внутри транзакции) и **снапшотом** транзакции (`xmin`, `xmax`, список активных `xip`). - Главное следствие: **читатели не блокируют писателей, а писатели не блокируют читателей** — в отличие от блокировочников (2PL, MySQL MyISAM с table-level locks). - Цена MVCC — **dead tuples** и **bloat** (раздувание таблиц/индексов). Их убирает **VACUUM** (помечает место как переиспользуемое + freezing против wraparound) и **autovacuum** (автоматически). - `VACUUM` обычный не отдаёт место ОС и не блокирует; `VACUUM FULL` переписывает таблицу (`ACCESS EXCLUSIVE` lock), отдаёт место, но кладёт нагрузку. Для онлайн-дефрагментации — **pg_repack**. - 32-битный `xid` (`2^31` «прошлых» транзакций) требует **freezing**, иначе **transaction id wraparound** = катастрофа. **HOT updates** снижают bloat индексов. ## Теория ### Зачем нужен MVCC: блокировочники vs многоверсионники В классических блокировочных СУБД (двухфазная блокировка, 2PL) согласованность чтения обеспечивается **блокировками**: транзакция, читающая строку, ставит shared-lock, что мешает писателям; писатель ставит exclusive-lock, что мешает читателям. Крайний случай — **MySQL MyISAM**, где блокировки уровня **таблицы**: один писатель останавливает всех. MVCC решает это иначе: вместо того чтобы блокировать данные, СУБД **хранит несколько версий** одной логической строки. Каждая транзакция видит «свой» консистентный срез данных на момент старта (или старта запроса). Поэтому: | Сценарий | Блокировочник (2PL) | PostgreSQL (MVCC) | |---|---|---| | Чтение во время записи | reader ждёт writer | reader видит старую версию, не ждёт | | Запись во время чтения | writer ждёт reader | writer создаёт новую версию, не ждёт | | Конфликт write-write одной строки | блокировка | блокировка (`UPDATE`/`DELETE` ждёт `xmax`) | | Цена согласованности | контеншн на локах | старые версии + VACUUM | Важный нюанс: **writer-writer одной и той же строки всё равно блокируются** друг другом в PostgreSQL — это row-level lock. MVCC устраняет конфликты read↔write, но не write↔write по одной строке. ### Физическое устройство: tuple и системные поля Таблица в PostgreSQL — это набор страниц (page, по умолчанию 8 КБ). Каждая строка хранится как **heap tuple** с заголовком, где есть системные (скрытые) поля. Их можно увидеть напрямую: ```sql SELECT ctid, xmin, xmax, cmin, cmax, * FROM accounts; ``` | Поле | Смысл | |---|---| | `xmin` | txid транзакции, которая **создала** эту версию (INSERT или UPDATE) | | `xmax` | txid транзакции, которая **удалила/обновила** эту версию; `0` если версия живая | | `cmin` | command id (порядковый номер команды внутри транзакции), создавший версию | | `cmax` | command id, удаливший версию (видимость внутри своей же транзакции) | | `ctid` | физический адрес версии: `(номер_страницы, номер_слота)` | Дополнительно в заголовке есть `t_infomask` с **hint bits** (`HEAP_XMIN_COMMITTED`, `HEAP_XMAX_COMMITTED` и т.д.) — кэш статуса коммита, чтобы не дёргать `clog`/`commit log` (а ныне `pg_xact`) на каждое чтение. Важно: `ctid` **не стабилен** — он меняется при `UPDATE` и `VACUUM FULL`/`pg_repack`. Использовать его как «первичный ключ» нельзя, но он удобен для точечного доступа в рамках одной транзакции. ### Как UPDATE и DELETE работают на самом деле В PostgreSQL **нет in-place update** для версий, видимых другим транзакциям. - **INSERT**: создаётся новый tuple с `xmin = текущий txid`, `xmax = 0`. - **DELETE**: существующий tuple **не удаляется физически** — ему проставляется `xmax = текущий txid`. Версия становится «мёртвой» после коммита. - **UPDATE = DELETE + INSERT**: старой версии проставляется `xmax = txid`, создаётся **новая версия** с `xmin = txid`. У старой версии в заголовке остаётся ссылка (`t_ctid`) на новую — это нужно для `EvalPlanQual` (повторной проверки при write-write конфликте). ```sql -- Демонстрация смены версии BEGIN; SELECT ctid, xmin, xmax FROM accounts WHERE id = 1; -- (0,1) | 1001 | 0 UPDATE accounts SET balance = balance + 100 WHERE id = 1; SELECT ctid, xmin, xmax FROM accounts WHERE id = 1; -- (0,2) | 1002 | 0 (новая версия) COMMIT; -- старая версия (0,1) теперь dead: её xmax = 1002 ``` Следствие: **частые UPDATE на «горячих» строках = поток мёртвых версий = bloat**. Это типичная причина деградации (счётчики, статусы, очереди в таблице). ### Снапшоты и правила видимости Снапшот (snapshot) — это «фотография» состояния транзакций на определённый момент. Состоит из трёх частей: - `xmin` снапшота — самый младший **ещё активный** txid; всё, что меньше и закоммичено, точно видимо. - `xmax` снапшота — следующий неназначенный txid (граница «будущего»); всё `>= xmax` невидимо. - `xip[]` (in-progress) — список txid, **активных** на момент снапшота (между `xmin` и `xmax`), которые невидимы. ``` видимо (если committed) невидимо ┌──────────────────────────────┐ ┌─────────────┐ │ txid < snapshot.xmin │ │ txid >= xmax │ └──────────────────────────────┘ └─────────────┘ ▲ из диапазона [xmin, xmax) невидимы те, что в xip[] ``` Версия tuple **видима** транзакции, если упрощённо: 1. `xmin` закоммичен и виден по снапшоту (создатель завершился до снапшота и не в `xip`), И 2. `xmax` либо `0`/aborted, либо не виден по снапшоту (удалитель ещё не завершился относительно снапшота). Это реализует функция `HeapTupleSatisfiesMVCC`. Статус коммита транзакции берётся из `pg_xact` (бывший `clog`) и кэшируется hint-битами. ```sql SELECT txid_current(); -- текущий txid (64-битный счётчик-обёртка над 32-битным xid) SELECT * FROM txid_current_snapshot(); -- xmin:xmax:xip_list, например 1005:1010:1006,1008 SELECT pg_current_xact_id(); -- PG 13+, аналог txid_current() ``` ### Уровни изоляции и снапшоты - **READ COMMITTED** (дефолт): **новый снапшот берётся на каждую команду**. Поэтому в одной транзакции два `SELECT` могут увидеть разные данные (non-repeatable read). - **REPEATABLE READ**: снапшот берётся **один раз** на первую команду и держится всю транзакцию — стабильный срез. В PostgreSQL это фактически snapshot isolation (нет phantom reads). - **SERIALIZABLE**: REPEATABLE READ + **SSI** (Serializable Snapshot Isolation) — отслеживание опасных паттернов чтения/записи (`rw-dependencies`), может откатить транзакцию с `serialization_failure` (`SQLSTATE 40001`). ```go // pgx: явный уровень изоляции + обязательный retry на 40001 tx, err := pool.BeginTx(ctx, pgx.TxOptions{ IsoLevel: pgx.Serializable, // или pgx.RepeatableRead }) ``` ```go // Паттерн retry для SERIALIZABLE / serialization_failure func withRetry(ctx context.Context, pool *pgxpool.Pool, fn func(pgx.Tx) error) error { for attempt := 0; attempt < 5; attempt++ { tx, err := pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable}) if err != nil { return err } err = fn(tx) if err == nil { if err = tx.Commit(ctx); err == nil { return nil } } _ = tx.Rollback(ctx) var pgErr *pgconn.PgError if errors.As(err, &pgErr) && (pgErr.Code == "40001" || pgErr.Code == "40P01") { continue // 40001 serialization_failure, 40P01 deadlock_detected } return err } return errors.New("retry limit exceeded") } ``` ```go // database/sql: уровни через sql.TxOptions tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) ``` ### txid, 32-битный xid и freezing Внутри `xid` — **32-битное** беззнаковое число. Оно по кругу «обёртывается» (wraparound). Видимость работает по принципу «половина прошлого / половина будущего»: для любого xid примерно `2^31` значений считаются прошлыми (видимыми), `2^31` — будущими. Проблема: если очень старый tuple не «заморозить», то спустя ~2 млрд транзакций его `xmin` окажется в «будущем» и строка **внезапно исчезнет** из видимости — это **transaction id wraparound**, потенциальная потеря данных. **Freezing** — это пометка старых, видимых всем версий как «вечно прошлых». Раньше `xmin` заменялся на спец-значение `FrozenTransactionId (2)`; в современных версиях ставится **hint-bit `HEAP_XMIN_FROZEN`** (xid остаётся для отладки). Замороженная версия видна всем всегда. ```sql -- Сколько транзакций осталось до принудительного wraparound-vacuum SELECT datname, age(datfrozenxid) AS xid_age, 2^31 - age(datfrozenxid) AS xids_left FROM pg_database ORDER BY xid_age DESC; ``` Ключевые параметры: - `vacuum_freeze_min_age` — возраст, с которого tuple можно замораживать. - `autovacuum_freeze_max_age` (дефолт 200 млн) — при достижении возраста запускается **обязательный anti-wraparound autovacuum**, который нельзя «отменить» (он перетерпит даже `autovacuum = off`). - При приближении к лимиту PostgreSQL начинает кричать в лог, а потом переходит в **режим только-чтение**, чтобы не потерять данные. Это известный «инцидент Sentry/Mailchimp». В PG 14+ механизм частично переработан: `vacuum_failsafe_age` и более агрессивное замораживание, чтобы не доводить до read-only. ### VACUUM: что делает обычный VACUUM Обычный `VACUUM` (и autovacuum): 1. Находит **dead tuples** (чьи `xmax` закоммичен и не виден ни одному снапшоту по `OldestXmin` горизонту). 2. Помечает занятое ими место как **переиспользуемое** в пределах той же страницы (через free space map, FSM). **Место ОС не возвращается** (за исключением полностью пустых страниц в хвосте таблицы). 3. Чистит соответствующие записи в **индексах**. 4. Выполняет **freezing** старых версий. 5. Обновляет **Visibility Map** (страницы, где все версии видимы всем) — это включает **Index-Only Scans** и ускоряет будущие vacuum. `VACUUM` берёт лишь **`SHARE UPDATE EXCLUSIVE`** lock — не блокирует `SELECT`/`INSERT`/`UPDATE`/`DELETE`, конфликтует только с DDL и другим vacuum по той же таблице. ```sql VACUUM (VERBOSE, ANALYZE) accounts; -- очистка + обновление статистики планировщика VACUUM (FREEZE) accounts; -- агрессивная заморозка ``` `ANALYZE` (часть `VACUUM ANALYZE`) — это отдельная вещь: обновляет статистику в `pg_statistic` для планировщика, к очистке отношения не имеет, но часто запускается вместе. ### VACUUM FULL vs обычный VACUUM | | `VACUUM` | `VACUUM FULL` | |---|---|---| | Механизм | помечает место reusable in-place | **переписывает всю таблицу** в новый файл | | Возврат места ОС | практически нет | да, файл сжимается | | Блокировка | `SHARE UPDATE EXCLUSIVE` (онлайн) | **`ACCESS EXCLUSIVE`** (полная блокировка таблицы) | | Доп. место | нет | нужно ~ x2 размера таблицы на время | | Индексы | чистит | пересоздаёт (бонус: устраняет index bloat) | | Когда | регулярно, постоянно | редко, при сильном bloat и в окно простоя | `VACUUM FULL` нельзя использовать на проде «вживую» под нагрузкой — он остановит все запросы к таблице. Для онлайн-сценария — **pg_repack**. ### autovacuum Фоновый процесс, запускающий `VACUUM`/`ANALYZE` по порогам. Триггер для vacuum по умолчанию: ``` threshold = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * n_live_tuples ``` - `autovacuum_vacuum_scale_factor` = 0.2 (20% мёртвых) — для больших таблиц это слишком много, часто снижают до 0.01–0.05 пер-таблично. - `autovacuum_vacuum_cost_limit` / `autovacuum_vacuum_cost_delay` — троттлинг I/O. - `autovacuum_max_workers` — число параллельных воркеров. ```sql -- Пер-табличная настройка для горячей таблицы ALTER TABLE events SET ( autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_cost_delay = 2, autovacuum_vacuum_cost_limit = 1000 ); ``` Типичная senior-проблема: на больших, активно обновляемых таблицах дефолтный autovacuum **не успевает**, копится bloat и растёт `dead_tuples`; лечится агрессивными пер-табличными настройками, а не глобальным выключением. ### Bloat: раздувание таблиц и индексов **Bloat** — это «пустое» место внутри файлов таблицы/индекса от мёртвых версий, которое не отдано ОС и которое всё равно читается с диска и кэшируется в shared_buffers. Причины: - Массовые/частые `UPDATE` и `DELETE`. - **Долгие транзакции** и зависшие `idle in transaction` / открытые курсоры / реплики с `hot_standby_feedback`: они держат **снапшот**, поэтому vacuum не может удалить версии новее самого старого снапшота (горизонт `xmin`). Это причина №1 «vacuum работает, а bloat растёт». - Откаты больших операций. Диагностика: ```sql SELECT relname, n_live_tup, n_dead_tup, round(n_dead_tup::numeric / nullif(n_live_tup,0), 3) AS dead_ratio, last_vacuum, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC; ``` ```sql -- Кто держит старый снапшот и мешает vacuum SELECT pid, state, age(backend_xmin) AS xmin_age, query FROM pg_stat_activity WHERE backend_xmin IS NOT NULL ORDER BY age(backend_xmin) DESC; -- Самая старая активная транзакция SELECT max(age(backend_xmin)) FROM pg_stat_activity; ``` Для оценки реального bloat обычно используют расширение `pgstattuple` или известные query от check_postgres/pgexperts. Борьба с bloat: 1. **Профилактика**: адекватный autovacuum, отсутствие долгих транзакций, `fillfactor` < 100 для HOT. 2. **Index bloat**: `REINDEX CONCURRENTLY idx_name;` (PG 12+, без долгой блокировки). 3. **Table bloat без простоя**: **pg_repack** — пересоздаёт таблицу/индексы в фоне, держит блокировку только на короткое финальное переключение (через триггеры собирает изменения). 4. **С простоем**: `VACUUM FULL` или `CLUSTER`. ```bash # pg_repack: онлайн-устранение bloat таблицы и её индексов pg_repack -h localhost -d mydb -t accounts --jobs 4 ``` ### HOT updates (Heap-Only Tuples) **HOT** — оптимизация, снижающая стоимость UPDATE и index bloat. Условие: **обновляемые колонки не входят ни в один индекс** и на странице есть место (`fillfactor`). При HOT-update: - Новая версия создаётся **на той же странице**. - **Индексы НЕ обновляются** — старая индексная запись указывает на старый tuple, а старый tuple через `t_ctid` образует **HOT-chain** к новой версии (redirect через item pointer). - Очистка HOT-цепочек возможна даже **page-level «HOT pruning»** при обычном `SELECT`, без полноценного vacuum. Польза: меньше записей в индексы (меньше WAL, меньше index bloat), быстрее UPDATE. ```sql -- Оставляем место на странице под HOT-обновления ALTER TABLE accounts SET (fillfactor = 80); VACUUM FULL accounts; -- чтобы применить fillfactor к существующим данным -- Доля HOT-обновлений SELECT relname, n_tup_upd, n_tup_hot_upd, round(100.0 * n_tup_hot_upd / nullif(n_tup_upd,0), 1) AS hot_pct FROM pg_stat_user_tables ORDER BY n_tup_upd DESC; ``` Senior-вывод: чтобы UPDATE были HOT, **не индексируйте часто меняющиеся колонки** (например, `updated_at`, счётчики), и держите `fillfactor` ниже 100 на горячих таблицах. ## Подводные камни / gotchas - **`ctid` нестабилен.** Меняется при UPDATE, VACUUM FULL, pg_repack, CLUSTER. Никогда не использовать как идентификатор строки между транзакциями. - **VACUUM не уменьшает файл.** Если ждали возврата места ОС после `DELETE` — его не будет; нужен `VACUUM FULL`/pg_repack. - **Долгая транзакция замораживает горизонт vacuum.** Одна забытая `idle in transaction` или аналитический запрос на час может раздуть всю БД. Ставьте `idle_in_transaction_session_timeout`, мониторьте `backend_xmin`. - **Реплика с `hot_standby_feedback = on`** удерживает горизонт на мастере — bloat «прилетает» от долгих запросов на standby. - **`SELECT ... FOR UPDATE` создаёт write-write точку** и может вызвать ожидания/deadlock-и, хотя обычный SELECT — нет. - **READ COMMITTED ≠ стабильный снапшот.** В пределах одной транзакции два запроса могут вернуть разное. Для отчётов нужен REPEATABLE READ. - **SERIALIZABLE требует retry-логики** на `40001`. Без неё приложение будет падать под конкуренцией. - **Anti-wraparound vacuum нельзя пропустить.** Он запустится даже на «тихой» таблице и нагрузит I/O; на огромных таблицах это сюрприз в проде. Лучше замораживать заранее. - **HOT ломается от индекса на горячей колонке.** Добавили индекс на `updated_at` — UPDATE перестали быть HOT, индексы начали раздуваться. - **Hint bits = «первое чтение медленное».** После массового INSERT первый SELECT/VACUUM дописывает hint-биты (генерируя dirty pages и WAL при checksum), что выглядит как «непонятная» нагрузка. - **`n_dead_tup` в `pg_stat_user_tables` — оценка**, обновляется по статистике; для точной картины — `pgstattuple`. ## Вопросы на собеседовании **В:** Что физически происходит при `UPDATE` одной строки в PostgreSQL? **О:** Создаётся новая версия tuple с `xmin = txid` (часто на той же странице, если HOT), старой версии проставляется `xmax = txid` и ссылка `t_ctid` на новую. In-place изменения нет, пока версия видима другим. Старая версия становится dead после коммита и удаляется vacuum-ом. Если обновлённая колонка не в индексе — UPDATE может быть HOT и не трогать индексы. **В:** Как PostgreSQL определяет, какую версию строки видит транзакция? **О:** По снапшоту (`xmin`, `xmax`, `xip[]`) и системным полям версии. Версия видна, если её `xmin` закоммичен и виден по снапшоту, а `xmax` равен 0 / aborted / ещё не виден по снапшоту. Статус коммита берётся из `pg_xact` и кэшируется hint-битами. READ COMMITTED берёт новый снапшот на каждую команду, REPEATABLE READ — один на транзакцию. **В:** В чём принципиальное отличие MVCC от блокировочных СУБД? **О:** В MVCC читатели не блокируют писателей и наоборот — каждый видит свою консистентную версию. В 2PL/MyISAM согласованность чтения держится на блокировках, что создаёт контеншн read↔write. Платой за MVCC являются мёртвые версии и необходимость vacuum. При этом write↔write по одной строке блокируются и в PostgreSQL. **В:** Что делает VACUUM и почему после него не уменьшается размер файла? **О:** VACUUM помечает место от dead tuples как переиспользуемое (через FSM), чистит индексы, замораживает старые версии и обновляет visibility map. Но он не дефрагментирует файл и не отдаёт место ОС (кроме пустых страниц в хвосте). Для возврата места нужен `VACUUM FULL` (переписывает таблицу под `ACCESS EXCLUSIVE`) или онлайн pg_repack. **В:** Что такое transaction id wraparound и как он предотвращается? **О:** `xid` 32-битный; видимость работает по принципу «2^31 прошлых / 2^31 будущих». Если очень старую версию не заморозить, через ~2 млрд транзакций её `xmin` окажется «в будущем» и строка исчезнет — потеря данных. Предотвращается freezing-ом (hint-bit `HEAP_XMIN_FROZEN`). При возрасте `autovacuum_freeze_max_age` запускается принудительный anti-wraparound vacuum; у самого края БД уходит в read-only. **В:** Почему vacuum работает, а bloat продолжает расти? **О:** Скорее всего есть долгоживущий снапшот, удерживающий горизонт `OldestXmin`: `idle in transaction`, долгий аналитический запрос, открытый курсор, или standby с `hot_standby_feedback`. Vacuum не может удалить версии новее самого старого снапшота. Диагностика — `pg_stat_activity.backend_xmin` и `age(backend_xmin)`; лечение — `idle_in_transaction_session_timeout`, борьба с долгими транзакциями. **В:** Что такое HOT updates и когда они не срабатывают? **О:** Heap-Only Tuple — UPDATE, при котором новая версия создаётся на той же странице, а индексы не обновляются (старая запись индекса через HOT-chain ведёт к новой версии). Условия: обновляемые колонки не входят в индексы и на странице есть место (`fillfactor < 100`). Не срабатывает, если меняется индексируемая колонка или страница переполнена. HOT снижает WAL и index bloat. **В:** Чем `VACUUM FULL` отличается от `pg_repack`? **О:** Оба возвращают место и устраняют bloat, но `VACUUM FULL` держит `ACCESS EXCLUSIVE` lock на всю таблицу всё время (полная недоступность) и требует ~двойного места. pg_repack пересоздаёт таблицу/индексы в фоне, собирая изменения через триггеры, и берёт короткую исключительную блокировку только на финальное переключение — пригоден для онлайн-прода. **В:** Как настроить autovacuum для большой горячей таблицы? **О:** Дефолтный `scale_factor = 0.2` означает ожидание 20% мёртвых строк — на большой таблице это огромный объём. Снижают пер-таблично `autovacuum_vacuum_scale_factor` до 0.01–0.05, при необходимости ослабляют троттлинг (`autovacuum_vacuum_cost_delay`/`cost_limit`), увеличивают `autovacuum_max_workers`. Глобально выключать autovacuum нельзя — упрётесь в wraparound. ## На что копают на senior+ - **Горизонт видимости (`OldestXmin`) и почему его держат разные сущности**: prepared transactions, replication slots (`pg_replication_slots`), `hot_standby_feedback`, долгие транзакции — умение найти виновника через `pg_stat_activity`, `pg_replication_slots`, `pg_prepared_xacts`. - **Snapshot isolation vs serializable**: чем REPEATABLE READ в PG отличается от настоящей сериализуемости, что такое write skew, как SSI ловит rw-конфликты, цена `SERIALIZABLE` под нагрузкой и обязательность retry на `40001`. - **Переход на 64-битные xid**: понимание, что `txid_current()` отдаёт 64-битное значение (epoch + xid), но на диске `xmin/xmax` всё ещё 32-битные — отсюда необходимость freezing. Знание про `vacuum_failsafe_age` (PG 14+). - **Visibility Map и Index-Only Scan**: как all-visible страницы позволяют не ходить в heap, и почему свежезаписанные/часто обновляемые таблицы теряют этот выигрыш. - **Стоимость MVCC на чтение**: hint bits, обращения к `pg_xact`, «первое чтение после загрузки медленнее», влияние на WAL при `wal_log_hints`/checksums. - **Bloat-инжиниринг**: измерение через `pgstattuple`, выбор между `REINDEX CONCURRENTLY`, pg_repack и `VACUUM FULL`, подбор `fillfactor` под паттерн нагрузки, мониторинг `n_tup_hot_upd`. - **Anti-wraparound в проде**: как мониторить `age(datfrozenxid)`, что делать при приближении к read-only, как форсировать `VACUUM FREEZE` без даунтайма и почему нельзя «просто перезапустить». - **Взаимодействие MVCC и логической репликации**: как старые версии и горизонт влияют на `pg_logical` слоты и почему «застрявший» слот раздувает мастер.