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 - **ACID** = Atomicity (всё или ничего, реализуется через WAL и откат), Consistency (инварианты БД: constraints, triggers, FK), Isolation (параллельные транзакции не мешают друг другу — в PostgreSQL через **MVCC** + уровни изоляции), Durability (после `COMMIT` данные переживут крах — гарантируется **WAL** + `fsync`). - PostgreSQL не имеет настоящих вложенных транзакций — `SAVEPOINT` создаёт **субтранзакции** (получают свой `subxid`), которые дёшевы в небольшом количестве, но при >64 на бэкенд вызывают **SLRU overflow** и резкую деградацию (`subtrans` SLRU misses). - **Длинные транзакции — главное зло в PostgreSQL**: удерживают `xmin` горизонт → `VACUUM` не может удалить dead tuples → bloat, распухание индексов, деградация. `idle in transaction` особенно опасен. - В Go: всегда `BeginTx(ctx, opts)`, паттерн `defer tx.Rollback()` (после `Commit` это no-op), не забывать `ctx` для отмены. В `pgx` — `pgxpool` + `tx.Begin`/`pgx.BeginFunc`. - На уровне `SERIALIZABLE` (и `REPEATABLE READ` при write skew конфликтах) транзакция может упасть с **40001 (serialization_failure)** или **40P01 (deadlock_detected)** — это **нормально и ожидаемо**, надо реализовать **retry с экспоненциальным backoff**. --- ## Теория ### ACID подробно **ACID** — набор гарантий, которые СУБД даёт относительно транзакций. Важно понимать не определения из учебника, а *как именно* они реализованы в PostgreSQL. | Свойство | Что гарантирует | Как реализовано в PostgreSQL | |----------|-----------------|------------------------------| | Atomicity | Транзакция применяется целиком или не применяется вовсе | MVCC: изменения видны только при валидном `xmax`/`xmin`; `ROLLBACK` помечает транзакцию как aborted в `pg_clog`/`pg_xact`, версии строк просто не становятся видны | | Consistency | Не нарушаются инварианты БД (PK, FK, CHECK, NOT NULL, триггеры) | Проверка constraints на момент commit (или сразу, или DEFERRED) | | Isolation | Конкурентные транзакции не «протекают» друг в друга | MVCC + уровни изоляции (snapshot per statement/transaction) | | Durability | Зафиксированные данные не теряются при крахе | WAL (Write-Ahead Log) + `fsync` журнала при commit | #### Atomicity Атомарность означает: если внутри транзакции 10 `UPDATE`, и на 7-м произошла ошибка — ни один из них не должен остаться. В PostgreSQL это естественное следствие MVCC: каждая версия строки помечена `xmin` (id транзакции-создателя) и `xmax` (id транзакции-удалителя). Видимость строки определяется статусом этих транзакций в `pg_xact` (commit log). Если транзакция откатилась — её `xmin` имеет статус `ABORTED`, и созданные ею версии строк просто никогда не станут видимы. Никакого «физического отката» данных не происходит — это делает `ROLLBACK` очень дешёвым (откатить почти так же быстро, как и не делать). ```sql BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- если здесь произойдёт сбой / ROLLBACK, оба UPDATE «исчезнут» COMMIT; ``` #### Consistency Это самое «прикладное» свойство: БД переходит из одного валидного состояния в другое. PostgreSQL обеспечивает её через декларативные ограничения. Важная деталь senior-уровня — **DEFERRED constraints**: FK можно отложить до конца транзакции. ```sql -- Отложенная проверка FK позволяет временно нарушить инвариант внутри транзакции ALTER TABLE orders ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers(id) DEFERRABLE INITIALLY DEFERRED; BEGIN; -- можно вставить order до customer, проверка будет на COMMIT INSERT INTO orders(id, customer_id) VALUES (1, 42); INSERT INTO customers(id) VALUES (42); COMMIT; -- проверка FK здесь ``` #### Isolation — MVCC PostgreSQL использует **MVCC (Multi-Version Concurrency Control)**: читатели не блокируют писателей и наоборот. Вместо блокировок на чтение каждая транзакция видит **снимок (snapshot)** данных. - **Снимок** — это набор: `xmin` (нижняя граница активных транзакций), `xmax` (верхняя), и список `xip` (in-progress xids). Строка видна, если её создавшая транзакция закоммитилась *до* снимка и не видна удаляющая. - На уровне `READ COMMITTED` (дефолт) **новый снимок берётся на каждый statement**. Поэтому два `SELECT` в одной транзакции могут вернуть разные данные (non-repeatable read). - На уровне `REPEATABLE READ` и `SERIALIZABLE` снимок берётся **один раз на всю транзакцию** (на первый statement, читающий/пишущий данные). | Уровень изоляции | Dirty read | Non-repeatable read | Phantom read | Write skew | Реализация в PG | |------------------|-----------|---------------------|--------------|------------|-----------------| | READ UNCOMMITTED | нет* | возможно | возможно | возможно | = READ COMMITTED (PG не имеет dirty read) | | READ COMMITTED | нет | возможно | возможно | возможно | snapshot per statement | | REPEATABLE READ | нет | нет | нет** | возможно | snapshot per transaction (Snapshot Isolation) | | SERIALIZABLE | нет | нет | нет | нет | SSI (Serializable Snapshot Isolation) | \* В PostgreSQL грязное чтение невозможно ни на одном уровне. \*\* В стандарте SQL `REPEATABLE READ` допускает фантомы, но в PostgreSQL за счёт снимка фантомов нет (это строже стандарта). > Senior-деталь: PostgreSQL `REPEATABLE READ` — это **Snapshot Isolation**, которая защищает от фантомов, но **не от write skew**. От write skew защищает только `SERIALIZABLE` (SSI), который отслеживает зависимости чтения/записи (predicate locks, `SIReadLock`) и при обнаружении опасного цикла откатывает одну из транзакций с ошибкой `40001`. ```sql -- Write skew: классический пример с дежурствами врачей. -- Инвариант: всегда хотя бы один врач на дежурстве. -- На REPEATABLE READ обе транзакции видят "на дежурстве 2 врача" и обе снимают себя → инвариант нарушен. -- На SERIALIZABLE одна из них упадёт с 40001. BEGIN ISOLATION LEVEL SERIALIZABLE; SELECT count(*) FROM doctors WHERE on_call = true; -- видит 2 UPDATE doctors SET on_call = false WHERE id = 1; COMMIT; ``` #### Durability — WAL **WAL (Write-Ahead Log)** — принцип «сначала пиши в журнал, потом в данные». Перед тем как изменить страницу данных в shared buffers, PostgreSQL пишет запись об изменении в WAL. При `COMMIT` происходит `fsync` WAL-сегмента на диск (если `synchronous_commit = on`). Даже если сервер упадёт до того, как «грязные» страницы данных будут сброшены на диск (это делает checkpoint), при старте PostgreSQL проиграет WAL (**redo / crash recovery**) и восстановит изменения. Ключевые параметры долговечности: | Параметр | Значение | Эффект | |----------|----------|--------| | `synchronous_commit = on` | дефолт | `COMMIT` ждёт `fsync` WAL → полная durability | | `synchronous_commit = off` | | `COMMIT` не ждёт `fsync`; при крахе можно потерять последние ~`wal_writer_delay` мс закоммиченных транзакций, **но БД не повредится** (consistency сохранится) | | `fsync = off` | | отключает `fsync` совсем — НИКОГДА в проде, риск повреждения БД | | `synchronous_commit = remote_apply` | | для синхронной репликации: ждать применения на реплике | > Senior-деталь: `synchronous_commit = off` — частый способ ускорить bulk-нагрузку. Он жертвует durability *последних* транзакций, но НЕ atomicity/consistency. Это компромисс, не баг. `fsync = off` — другое дело: жертвует и целостностью. ### BEGIN / COMMIT / ROLLBACK ```sql BEGIN; -- или START TRANSACTION -- ... запросы ... COMMIT; -- зафиксировать -- или ROLLBACK; -- откатить ``` - Вне явного `BEGIN` каждый statement — это **автокоммит** (своя неявная транзакция). - `BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;` — можно задать уровень, режим и `DEFERRABLE`. - `READ ONLY DEFERRABLE` — особый режим: транзакция дождётся «безопасного» снимка, при котором она гарантированно не упадёт с serialization failure (полезно для долгих отчётов на SERIALIZABLE без риска retry). - Если в транзакции произошла ошибка, она переходит в состояние **aborted**, и все последующие команды отвергаются с `25P02 (in_failed_sql_transaction)` до `ROLLBACK`. ### Savepoints и субтранзакции `SAVEPOINT` создаёт точку, к которой можно откатиться, не отменяя всю транзакцию. В PostgreSQL каждый savepoint реализуется как **субтранзакция (subtransaction)** со своим виртуальным/реальным `subxid`. ```sql BEGIN; INSERT INTO logs(msg) VALUES ('start'); SAVEPOINT sp1; INSERT INTO accounts(id) VALUES (1); -- допустим, упадёт на duplicate key ROLLBACK TO SAVEPOINT sp1; -- откатили только эту часть -- транзакция снова в рабочем состоянии, не aborted! INSERT INTO logs(msg) VALUES ('recovered'); RELEASE SAVEPOINT sp1; -- "забыть" savepoint (опционально) COMMIT; ``` - `SAVEPOINT name` — создать точку. - `ROLLBACK TO SAVEPOINT name` — откатить изменения после точки, но транзакция продолжается. - `RELEASE SAVEPOINT name` — убрать savepoint (его субтранзакция «вливается» в родителя), сделанные изменения сохраняются. - Вложенность: savepoint внутри savepoint → дерево субтранзакций. **Влияние на производительность (важно для senior):** 1. **Назначение реального `subxid`.** Субтранзакция получает настоящий xid только когда она *пишет*. До этого она «virtual». 2. **SLRU `pg_subtrans` и порог 64.** Каждый бэкенд может закэшировать до **64** субтранзакций (`PGPROC_MAX_CACHED_SUBXIDS`). При превышении происходит **subxid overflow**: снимок транзакции помечается как `suboverflowed`, и проверки видимости вынуждены лезть в `pg_subtrans` SLRU. Это вызывает **lock contention на `SubtransSLRULock`** и резкую деградацию производительности на всём кластере (не только у виновного бэкенда). 3. **Цена abort-а субтранзакции.** Откат savepoint требует обработки — отмены lock-ов, очистки и т.п. Частые savepoints в горячем цикле дороги. > Антипаттерн: ORM/драйверы, которые оборачивают **каждый** запрос в savepoint для эмуляции «вложенных транзакций» или для отказоустойчивости. Под нагрузкой это пробивает порог 64 и кладёт производительность кластера. Это одна из любимых тем на senior+ собеседованиях. ```sql -- Диагностика overflow: SELECT subxact_overflowed FROM pg_stat_get_activity(pid) ... -- через мониторинг -- метрики: pg_stat_slru (name = 'Subtrans'), рост blks_read / blks_hit ``` ### Длинные транзакции и их вред Это **критическая** тема для PostgreSQL. Длинная транзакция (особенно `idle in transaction`) — частая причина деградации продакшена. **Механизм вреда — `xmin` горизонт:** `VACUUM` может удалить (vacuum) только те dead tuples (мёртвые версии строк), которые не видны *ни одной* активной транзакции. Граница видимости — **`xmin` горизонт** = самый старый `xid` среди всех активных транзакций / снимков / replication slots / prepared transactions. Длинная транзакция держит свой снимок, тем самым «замораживает» горизонт на своём `xid`. Пока она жива: - `VACUUM` (и autovacuum) **не может убрать dead tuples**, накопившиеся за это время по всему кластеру. - Растёт **bloat** таблиц и индексов — физический размер пухнет, кэш «забивается мусором», производительность падает. - Висящие dead tuples заставляют сканировать больше страниц. - В крайнем случае — риск приближения к **transaction ID wraparound** (если горизонт стоит очень долго). **Дополнительные проблемы:** | Проблема | Описание | |----------|----------| | `idle in transaction` | Транзакция открыта (`BEGIN` сделан), но приложение ничего не шлёт (часто из-за бизнес-логики/внешнего вызова внутри транзакции). Держит горизонт и блокировки впустую. | | Удержание блокировок | Долгая транзакция держит row/table locks → конкуренты ждут, копятся в очереди. | | Bloat индексов | Особенно больно для часто обновляемых таблиц. | | Реплики | `max_standby_streaming_delay` / конфликты восстановления; долгие запросы на реплике тоже двигают горизонт при `hot_standby_feedback = on`. | **Защитные настройки:** ```sql -- Убивать транзакции, висящие в idle in transaction дольше N SET idle_in_transaction_session_timeout = '30s'; -- Ограничить время выполнения statement SET statement_timeout = '5s'; -- (PG 17+) ограничить время в idle-сессии вне транзакции SET idle_session_timeout = '5min'; -- (PG 14+) ограничить время удержания транзакцией снимка / горизонта SET transaction_timeout = '60s'; ``` ```sql -- Поиск долгих/idle транзакций SELECT pid, state, xact_start, now() - xact_start AS xact_age, query FROM pg_stat_activity WHERE state IN ('idle in transaction', 'active') AND xact_start IS NOT NULL ORDER BY xact_start; -- Кто держит xmin горизонт SELECT max(age(backend_xmin)) FROM pg_stat_activity; ``` > Senior-правило: **не делать сетевые/внешние вызовы (HTTP, очереди, RPC) внутри открытой транзакции**. Выполни всё, что можно, *до* `BEGIN`, держи транзакцию максимально короткой. Транзакция должна охватывать только атомарную работу с БД. ### Транзакции в Go: database/sql #### Базовый паттерн с `BeginTx` ```go func transfer(ctx context.Context, db *sql.DB, from, to int64, amount int64) (err error) { tx, err := db.BeginTx(ctx, &sql.TxOptions{ Isolation: sql.LevelSerializable, // уровень изоляции ReadOnly: false, }) if err != nil { return fmt.Errorf("begin: %w", err) } // КЛЮЧЕВОЙ паттерн: defer Rollback. // После успешного Commit Rollback вернёт sql.ErrTxDone — это no-op, безопасно. defer func() { if rbErr := tx.Rollback(); rbErr != nil && !errors.Is(rbErr, sql.ErrTxDone) { // логируем, но не перетираем основную ошибку log.Printf("rollback failed: %v", rbErr) } }() if _, err = tx.ExecContext(ctx, `UPDATE accounts SET balance = balance - $1 WHERE id = $2`, amount, from); err != nil { return fmt.Errorf("debit: %w", err) } if _, err = tx.ExecContext(ctx, `UPDATE accounts SET balance = balance + $1 WHERE id = $2`, amount, to); err != nil { return fmt.Errorf("credit: %w", err) } if err = tx.Commit(); err != nil { return fmt.Errorf("commit: %w", err) } return nil } ``` **Почему `defer tx.Rollback()` — правильный паттерн:** - Гарантирует освобождение соединения при любом раннем `return` (включая panic, если обёрнуто). - После `Commit()` транзакция завершена; `Rollback()` вернёт `sql.ErrTxDone`, который мы игнорируем. Это идиоматично. - Без `defer Rollback` при ошибке посередине транзакция «протечёт»: соединение из пула останется занятым с открытой транзакцией → `idle in transaction` на стороне PG. **`sql.TxOptions`:** | Поле | Тип | Назначение | |------|-----|-----------| | `Isolation` | `sql.IsolationLevel` | `LevelReadCommitted` (дефолт PG), `LevelRepeatableRead`, `LevelSerializable` и др. Не все драйверы поддерживают все уровни. | | `ReadOnly` | `bool` | транслируется в `READ ONLY` — PG может оптимизировать и запрещает запись | **Контекст и отмена:** - Все методы должны быть `...Context` (`ExecContext`, `QueryContext`, `QueryRowContext`). - Если `ctx` отменяется/истекает таймаут — `database/sql` пытается отменить запрос (через `pgx`/драйвер отправляется cancel-запрос на сервер) и откатывает транзакцию. - **Подводный камень:** все запросы транзакции должны использовать **один и тот же** `ctx`, привязанный к жизни транзакции. Нельзя выполнять запрос на `tx` после отмены `ctx`. - Внутри транзакции работает **одно физическое соединение** — `sql.Tx` «прибивает» соединение из пула на всё время транзакции (важно для расчёта `SetMaxOpenConns`). #### `database/sql` пул и транзакции - `db.SetMaxOpenConns(n)` — максимум открытых соединений. **Каждая активная `sql.Tx` занимает одно** — длинные транзакции исчерпывают пул. - `db.SetConnMaxLifetime` / `SetConnMaxIdleTime` — ротация соединений. - Запросы на одном `tx` сериализуются — нельзя гонять их параллельно из разных горутин на одном `*sql.Tx`. ### Транзакции в Go: pgx `pgx` — нативный драйвер PostgreSQL (быстрее, чем через `database/sql`, поддерживает больше типов и protocol-level фичи). Используется либо напрямую (`pgx.Conn`/`pgxpool.Pool`), либо как драйвер для `database/sql` (`stdlib`). #### `pgxpool` + транзакция ```go import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) pool, err := pgxpool.New(ctx, "postgres://user:pass@host:5432/db?pool_max_conns=20") // ... defer pool.Close() func transfer(ctx context.Context, pool *pgxpool.Pool, from, to, amount int64) error { tx, err := pool.BeginTx(ctx, pgx.TxOptions{ IsoLevel: pgx.Serializable, AccessMode: pgx.ReadWrite, }) if err != nil { return err } defer tx.Rollback(ctx) // после Commit — no-op (pgx.ErrTxClosed) if _, err = tx.Exec(ctx, `UPDATE accounts SET balance = balance - $1 WHERE id = $2`, amount, from); err != nil { return err } if _, err = tx.Exec(ctx, `UPDATE accounts SET balance = balance + $1 WHERE id = $2`, amount, to); err != nil { return err } return tx.Commit(ctx) } ``` #### `pgx.BeginFunc` — безопасная обёртка `pgx` предоставляет helper, который сам делает `Commit`/`Rollback`: если функция вернула ошибку → `Rollback`, иначе → `Commit`. Меньше шансов забыть откат. ```go err := pgx.BeginTxFunc(ctx, pool, pgx.TxOptions{IsoLevel: pgx.Serializable}, func(tx pgx.Tx) error { if _, err := tx.Exec(ctx, `UPDATE ...`); err != nil { return err // -> Rollback } return nil // -> Commit }) ``` `pgx.Tx` поддерживает вложенные транзакции через savepoints прозрачно: `tx.Begin(ctx)` на уже открытой `tx` создаёт `SAVEPOINT` (помни про порог 64!). | Особенность | database/sql | pgx (native) | |-------------|--------------|--------------| | Уровни изоляции | `sql.TxOptions{Isolation, ReadOnly}` | `pgx.TxOptions{IsoLevel, AccessMode, DeferrableMode}` | | Helper-обёртка | нет (пишешь сам defer Rollback) | `pgx.BeginTxFunc` | | Вложенные tx | через ручной `SAVEPOINT` | `tx.Begin()` → savepoint автоматически | | Batch / COPY | ограниченно | `tx.SendBatch`, `tx.CopyFrom` | | Производительность | + слой database/sql | нативный wire protocol, prepared statement cache | ### Retry на serialization failure (40001) и deadlock (40P01) При `SERIALIZABLE` (а deadlock — на любом уровне) транзакция **легально** может быть отклонена сервером. Приложение **обязано** уметь её повторить. Это не ошибка кода, а контракт SERIALIZABLE. - **40001 `serialization_failure`** — SSI обнаружил опасный цикл зависимостей или write skew → откатил транзакцию. - **40P01 `deadlock_detected`** — взаимная блокировка, detector выбрал жертву. Оба класса начинаются с `40` (`SQLSTATE class 40 — Transaction Rollback`) и являются **transient** — повтор с большой вероятностью пройдёт. **Важно:** повторять надо **всю транзакцию целиком** (с нуля, `BEGIN ... COMMIT`), а не отдельный statement — снимок надо взять заново. Поэтому код транзакции оформляют как **идемпотентную замыкание-функцию**. ```go import ( "context" "errors" "math/rand" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" ) // isRetryable проверяет SQLSTATE: 40001 (serialization) и 40P01 (deadlock). func isRetryable(err error) bool { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { switch pgErr.Code { case "40001", "40P01": return true } } return false } // runSerializable повторяет транзакцию с экспоненциальным backoff + jitter. func runSerializable( ctx context.Context, pool *pgxpool.Pool, maxAttempts int, fn func(tx pgx.Tx) error, ) error { const baseDelay = 5 * time.Millisecond const maxDelay = 1 * time.Second var lastErr error for attempt := 0; attempt < maxAttempts; attempt++ { // Проверяем контекст перед каждой попыткой. if err := ctx.Err(); err != nil { return err } err := pgx.BeginTxFunc(ctx, pool, pgx.TxOptions{IsoLevel: pgx.Serializable}, fn) if err == nil { return nil // успех } lastErr = err if !isRetryable(err) { return err // не transient — нет смысла повторять } // Экспоненциальный backoff: base * 2^attempt, с потолком. backoff := baseDelay * (1 << attempt) if backoff > maxDelay { backoff = maxDelay } // Full jitter: случайная задержка в [0, backoff) — снижает thundering herd. jittered := time.Duration(rand.Int63n(int64(backoff) + 1)) select { case <-ctx.Done(): return ctx.Err() case <-time.After(jittered): // повторяем } } return fmt.Errorf("transaction failed after %d attempts: %w", maxAttempts, lastErr) } ``` Использование: ```go err := runSerializable(ctx, pool, 5, func(tx pgx.Tx) error { var balance int64 if err := tx.QueryRow(ctx, `SELECT balance FROM accounts WHERE id = $1`, fromID).Scan(&balance); err != nil { return err } if balance < amount { return errors.New("insufficient funds") // НЕ retryable — выйдет сразу } if _, err := tx.Exec(ctx, `UPDATE accounts SET balance = balance - $1 WHERE id = $2`, amount, fromID); err != nil { return err } _, err := tx.Exec(ctx, `UPDATE accounts SET balance = balance + $1 WHERE id = $2`, amount, toID) return err }) ``` **Нюансы retry, которые ждут на senior:** - Транзакционная функция должна быть **идемпотентной/чистой**: её повторят. Никаких побочных эффектов вне БД (отправка email, инкремент внешнего счётчика) до коммита. - Ограничивать `maxAttempts` — иначе при постоянной contention бесконечный retry усилит нагрузку. - **Jitter обязателен** — без него все конкурирующие транзакции повторятся одновременно и снова столкнутся (thundering herd). - Различать retryable (40001/40P01) и не-retryable ошибки (`23505 unique_violation`, бизнес-ошибки) — повторять только первые. - На `READ COMMITTED` тоже бывают deadlock (40P01) — retry полезен и там, но 40001 практически только на REPEATABLE READ/SERIALIZABLE. - Метрики: считать число retry — рост сигнализирует о hot-spot contention, который, возможно, лучше решить иначе (изменить порядок доступа, `SELECT ... FOR UPDATE`, advisory locks). --- ## Подводные камни / gotchas - **Забыли `defer tx.Rollback()`** → при ошибке посередине соединение «утекает» с открытой транзакцией → `idle in transaction` в PG + исчерпание пула. Самый частый баг. - **Внешний вызов (HTTP/RPC) внутри транзакции** → транзакция держится секундами/минутами → замораживает `xmin` горизонт → bloat по всему кластеру. - **`SAVEPOINT` на каждый запрос** (ORM-эмуляция nested tx) → пробивает порог 64 субтранзакций → `SubtransSLRULock` contention → деградация ВСЕГО кластера. - **Чтение после ошибки без savepoint:** после любой ошибки транзакция в PG переходит в aborted; все запросы дают `25P02`, пока не `ROLLBACK` (или `ROLLBACK TO SAVEPOINT`). - **Параллельные запросы на одном `*sql.Tx`/`pgx.Tx`** из разных горутин — гонка, одно физическое соединение. Нельзя. - **`SERIALIZABLE` без retry** → приложение падает на 40001 «на ровном месте» под нагрузкой. SERIALIZABLE требует retry-логики by design. - **`READ COMMITTED` и «потерянное обновление»:** `UPDATE accounts SET balance = balance - 100` безопасен (PG перечитает строку), но read-modify-write в коде (`SELECT` → вычислить в Go → `UPDATE`) на READ COMMITTED теряет апдейты. Нужен `SELECT ... FOR UPDATE` или SERIALIZABLE. - **`ReadOnly: true` не делает реплику** — это просто `READ ONLY` режим транзакции на том же узле; роутинг на реплику — отдельная задача (на уровне connection string / proxy). - **`synchronous_commit = off` ≠ потеря целостности**, но потеря последних коммитов при крахе. Частая путаница на собеседовании. - **Context cancel в середине транзакции:** запрос отменится, транзакция должна откатиться; но если `ctx` уже мёртв, сам `tx.Rollback(ctx)` может не пройти — используйте `context.Background()` или короткий отдельный ctx для финального rollback в edge-кейсах. --- ## Вопросы на собеседовании **В:** Как PostgreSQL обеспечивает Durability и что произойдёт при крахе сервера сразу после `COMMIT`? **О:** Через WAL: перед изменением страниц данных пишется журнальная запись, при `COMMIT` (с `synchronous_commit = on`) делается `fsync` WAL на диск. Если сервер упадёт до сброса грязных страниц данных, при старте выполнится crash recovery (redo) — WAL проигрывается и зафиксированные изменения восстанавливаются. Закоммиченная транзакция не потеряется. **В:** Чем `REPEATABLE READ` в PostgreSQL отличается от стандарта SQL и от `SERIALIZABLE`? **О:** В стандарте RR допускает фантомы — в PG за счёт снимка на всю транзакцию (Snapshot Isolation) фантомов нет, это строже стандарта. Но RR не защищает от **write skew**. От него защищает только `SERIALIZABLE` через SSI: он отслеживает зависимости чтение/запись (predicate locks) и при опасном цикле откатывает транзакцию с `40001`. **В:** Почему длинная транзакция — это проблема, даже если она просто `idle in transaction` и ничего не делает? **О:** Она держит свой снимок и тем самым **`xmin` горизонт** на своём `xid`. `VACUUM` не может удалить dead tuples, которые «новее» горизонта, по всему кластеру → накапливается bloat таблиц и индексов, растёт размер, падает производительность, в пределе — риск приближения к wraparound. Плюс может держать блокировки. Лечится `idle_in_transaction_session_timeout`, `transaction_timeout` и архитектурно — короткими транзакциями без внешних вызовов. **В:** Что такое субтранзакции и почему «savepoint на каждый запрос» опасен? **О:** Каждый `SAVEPOINT` — это субтранзакция со своим `subxid`. Бэкенд кэширует до 64 субтранзакций; при превышении снимок помечается `suboverflowed`, и проверки видимости начинают лезть в `pg_subtrans` SLRU, вызывая contention на `SubtransSLRULock`. Это деградирует производительность всего кластера, а не только виновного соединения. **В:** Объясните паттерн `defer tx.Rollback()` — не сломает ли он успешный коммит? **О:** Нет. После `Commit()` транзакция завершена, и `Rollback()` вернёт `sql.ErrTxDone` (в pgx — `pgx.ErrTxClosed`), который мы игнорируем — это no-op. Паттерн гарантирует, что при любом раннем `return`/ошибке/панике транзакция откатится и соединение вернётся в пул, предотвращая утечку `idle in transaction`. **В:** Когда транзакция может упасть с 40001/40P01 и что с этим делать? **О:** `40001` (serialization_failure) — на REPEATABLE READ/SERIALIZABLE при конфликте сериализации/write skew; `40P01` (deadlock_detected) — на любом уровне при взаимной блокировке. Это transient-ошибки класса 40. Нужно **повторять всю транзакцию целиком** с экспоненциальным backoff и jitter, ограничив число попыток. Транзакционная функция должна быть идемпотентной (её повторят). **В:** Чем `pgx` (native) отличается от использования его через `database/sql`? **О:** Native `pgx` использует бинарный wire-протокол напрямую: быстрее, богаче типы (массивы, json, hstore, range), есть `CopyFrom`, `SendBatch`, кэш prepared statements, helper `BeginTxFunc`, вложенные транзакции через `tx.Begin()` (savepoints). Через `database/sql` (`stdlib`) теряется часть фич ради совместимости со стандартным интерфейсом, но получаем единый API и совместимость с инструментами. **В:** На READ COMMITTED у вас «теряются» обновления баланса при конкуренции. Почему и как чинить? **О:** Если делается read-modify-write (`SELECT balance` → вычисление в коде → `UPDATE balance = $new`), две транзакции читают одно значение и затирают друг друга — lost update. Чинится либо атомарным `UPDATE ... SET balance = balance - $1` (PG перечитает актуальную строку), либо `SELECT ... FOR UPDATE` (блокировка строки), либо переходом на SERIALIZABLE с retry. **В:** Что именно делает `synchronous_commit = off` и безопасно ли это? **О:** `COMMIT` перестаёт ждать `fsync` WAL — отвечает клиенту сразу. При крахе можно потерять несколько последних *закоммиченных* транзакций (до `wal_writer_delay`), НО база останется консистентной и не повредится — теряется только durability хвоста. Это легитимный компромисс для throughput. В отличие от `fsync = off`, который рискует повреждением БД и недопустим в проде. --- ## На что копают на senior+ - **MVCC внутри:** структура снимка (`xmin`/`xmax`/`xip`), `t_xmin`/`t_xmax`/`t_cid`/`ctid` в кортежах, как hint bits ускоряют проверку видимости, роль `pg_xact` (clog). Что такое HOT-updates и как они снижают bloat. - **SSI устройство:** predicate locks (`SIReadLock`), отслеживание rw-зависимостей, dangerous structures (rw-conflict in + out), почему false positive serialization failures возможны. - **Subtransaction overflow в деталях:** `PGPROC_MAX_CACHED_SUBXIDS = 64`, `suboverflowed` снимок, эффект на всех бэкендах, мониторинг через `pg_stat_slru`, связь с replication (subtrans на репликах). - **`xmin` горизонт со всех источников:** не только активные транзакции, но и **replication slots** (неактивный слот замораживает горизонт навсегда), **prepared transactions** (`2PC`), `hot_standby_feedback`. Диагностика через `pg_stat_activity.backend_xmin`, `pg_replication_slots`. - **Transaction ID wraparound** и **freezing**: `autovacuum_freeze_max_age`, aggressive vacuum, опасность «висящего» горизонта, отказ кластера в read-only при приближении wraparound. - **2PC (PREPARE TRANSACTION):** распределённые транзакции, висящие prepared-транзакции как причина зависшего горизонта, `max_prepared_transactions`. - **Connection pooling и транзакции:** transaction-pooling режим PgBouncer ломает session-level фичи (prepared statements, `SET`, advisory locks по сессии) — как это взаимодействует с `pgx`/`database/sql` пулом. - **Lock vs MVCC:** когда нужны явные блокировки (`SELECT FOR UPDATE/SHARE`, `SKIP LOCKED` для очередей, advisory locks), `NOWAIT`, эскалация и lock queue в PG (PG не эскалирует row→table locks, в отличие от SQL Server). - **Идемпотентность и saga vs 2PC** для распределённых бизнес-транзакций, outbox-паттерн как замена транзакций между БД и брокером сообщений. - **Точная семантика retry:** почему повторять надо транзакцию целиком, почему jitter обязателен, как считать budget попыток, и когда contention лечится архитектурно (изменение порядка lock-ов, шардирование hot-row) вместо retry.