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 - Установка TCP+TLS соединения и форк backend-процесса в PostgreSQL — дорогая операция (десятки мс, мегабайты RAM на backend). Пул переиспользует физические соединения, амортизируя стоимость. - PostgreSQL — process-per-connection: каждое соединение это отдельный ОС-процесс. `max_connections` ограничен сверху, и реальная пропускная способность падает задолго до лимита из-за конкуренции за CPU, lock manager и shared buffers. - `database/sql` даёт встроенный пул: `SetMaxOpenConns` (жёсткий потолок), `SetMaxIdleConns` (сколько держать свободными), `SetConnMaxLifetime` (ротация), `SetConnMaxIdleTime` (закрытие простаивающих). Метрики — `db.Stats()`. - `pgx` имеет два режима: native API (`pgx.Conn`, `pgxpool.Pool`) с extended-протоколом и кэшем prepared statements, и совместимый `database/sql` драйвер (`stdlib`). - `pgxpool` — production-grade пул pgx: `MaxConns`, `MinConns`, `MaxConnLifetime`, `MaxConnIdleTime`, `HealthCheckPeriod`, lifecycle-хуки. - PgBouncer — внешний пулер. В `transaction` mode ломаются: серверные prepared statements, session-level advisory locks, `SET`, `LISTEN/NOTIFY`, temp tables, `WITH HOLD` cursors. Лечится `DefaultQueryExecMode` = simple protocol или `statement_cache_capacity=0` (на новых PgBouncer >= 1.21 есть серверный prepared statement support). - Размер пула считается, а не «ставится побольше». Базовая формула: `connections ≈ ((core_count * 2) + effective_spindle_count)`. Больше соединений почти всегда хуже из-за context switching. - Утечки соединений — главный production-инцидент: незакрытые `Rows`, забытый `Close()` у транзакции, отсутствие `context` с таймаутом. Пул исчерпывается, запросы виснут на ожидании свободного соединения. ## Теория ### Зачем вообще пул: цена соединения в PostgreSQL PostgreSQL использует модель **process-per-connection**. Каждое новое клиентское соединение приводит к тому, что главный процесс postmaster делает `fork()`, создавая отдельный backend-процесс. Это влечёт: 1. **Стоимость установки (latency).** TCP-handshake, опционально TLS-handshake, аутентификация (SCRAM-SHA-256 — несколько round-trip'ов), затем форк процесса и инициализация его памяти. На реальной сети это легко 5–50 мс. Для запроса, который сам выполняется 1 мс, это катастрофа. 2. **Стоимость памяти.** Каждый backend имеет собственный `work_mem`, кэши, каталоговый кэш (relcache/catcache), prepared statements. Идле-backend «весит» порядка нескольких МБ резидентной памяти; под нагрузкой с большими `work_mem` — кратно больше. 3. **Стоимость на стороне ядра БД.** Lock manager, snapshot management (`GetSnapshotData` исторически O(N) по числу активных backend'ов — заметно улучшено в PG 14), процессы вроде autovacuum конкурируют за CPU. Чем больше backend'ов, тем дороже служебная работа. 4. **`max_connections`.** Жёсткий потолок (по умолчанию 100). Превышение — клиент получает `FATAL: sorry, too many clients already`. Поднятие `max_connections` не бесплатно: резервируется память под структуры синхронизации (semaphores, lock table) пропорционально лимиту. Пул решает это, **переиспользуя** уже установленные физические соединения между логическими операциями приложения. Соединение открывается один раз и обслуживает тысячи запросов. > Ключевая мысль для senior: пул нужен не «чтобы было быстрее», а чтобы (а) убрать latency установки из горячего пути и (б) ограничить число одновременных backend'ов величиной, которую БД реально переваривает без деградации. ### Пул в `database/sql` `*sql.DB` — это **не одно соединение, а пул**. Частая ошибка джунов: открывать `sql.Open` на каждый запрос. `sql.Open` создаёт пул и не открывает физических соединений сразу (ленивая инициализация при первом запросе или `Ping`). ```go db, err := sql.Open("pgx", dsn) // pgx через stdlib-драйвер if err != nil { return err } db.SetMaxOpenConns(25) db.SetMaxIdleConns(25) db.SetConnMaxLifetime(30 * time.Minute) db.SetConnMaxIdleTime(5 * time.Minute) if err := db.PingContext(ctx); err != nil { // здесь реально откроется соединение return err } ``` Параметры: | Метод | Что делает | Дефолт | Замечания | |---|---|---|---| | `SetMaxOpenConns(n)` | Максимум одновременно открытых соединений (in-use + idle) | 0 (без лимита) | **Обязательно ставить.** 0 = неограниченно — путь к исчерпанию `max_connections`. | | `SetMaxIdleConns(n)` | Сколько свободных соединений держать в пуле | 2 | Если меньше `MaxOpenConns`, лишние соединения закрываются после возврата. | | `SetConnMaxLifetime(d)` | Максимальный «возраст» соединения | 0 (бесконечно) | Защищает от «протухших» соединений за load balancer'ом, помогает ротации после failover. | | `SetConnMaxIdleTime(d)` | Сколько соединение может простаивать до закрытия | 0 (бесконечно) | Освобождает ресурсы БД в тихие периоды. | **Тонкости:** - `MaxIdleConns > MaxOpenConns` бессмысленно — drvier сам подрежет idle до open. - Если `MaxIdleConns < MaxOpenConns`, под пиком соединения постоянно открываются/закрываются («churn») — anti-pattern. Частая рекомендация: `MaxIdleConns == MaxOpenConns`. - `ConnMaxLifetime` не закрывает соединение мгновенно посреди запроса — проверка делается при возврате в пул. - Когда все соединения заняты, новый запрос **блокируется** до освобождения или отмены через `context`. Это не ошибка — это backpressure. **Метрики `db.Stats()` (тип `sql.DBStats`):** ```go s := db.Stats() log.Printf("open=%d inUse=%d idle=%d wait=%d waitDur=%s maxIdleClosed=%d maxLifeClosed=%d", s.OpenConnections, s.InUse, s.Idle, s.WaitCount, s.WaitDuration, s.MaxIdleClosed, s.MaxLifetimeClosed) ``` | Поле | Смысл | Сигнал | |---|---|---| | `MaxOpenConnections` | сконфигурированный лимит | — | | `OpenConnections` | сейчас открыто (InUse+Idle) | если упирается в Max постоянно — пул мал | | `InUse` | соединения в работе | — | | `Idle` | свободные | — | | `WaitCount` | сколько раз запросы ждали соединение | растёт → пул узкое место | | `WaitDuration` | суммарное время ожидания | растёт → латентность из-за пула | | `MaxIdleClosed` / `MaxIdleTimeClosed` | закрыто из-за лимита idle / idle-time | высокий MaxIdleClosed → churn, поднимите MaxIdleConns | | `MaxLifetimeClosed` | закрыто по lifetime | нормально при ротации | Эти метрики — первое, что смотрит senior при разборе «база тормозит»: высокий `WaitCount`/`WaitDuration` означает, что узкое место — размер пула или медленные запросы, держащие соединения. ### `pgx` и `pgxpool` `pgx` (jackc/pgx) — самый используемый Go-драйвер PostgreSQL. Два способа использования: 1. **Через `database/sql`** (`github.com/jackc/pgx/v5/stdlib`): pgx как драйвер за стандартным интерфейсом. Получаете встроенный пул `database/sql`, но теряете часть нативных возможностей (батчи, `CopyFrom`, богатые типы pgx, тонкий контроль протокола). 2. **Native API**: `pgx.Conn` (одно соединение) или `pgxpool.Pool` (пул). Полный доступ к фичам pgx. **Чем native pgx отличается от `database/sql`:** - Использует **extended query protocol** по умолчанию (раздельные Parse/Bind/Execute), а не текстовую подстановку. Параметры передаются типизированно, защита от SQL-инъекций на уровне протокола. - Богатая система типов: `pgtype` (массивы, `jsonb`, `numeric`, `inet`, ranges, composite types, `hstore`) — то, что `database/sql` отдаёт как `[]byte`. - Нативные `Batch` (pipelining), `CopyFrom` (быстрая массовая загрузка через COPY protocol), `LISTEN/NOTIFY`. - Автоматический кэш prepared statements на каждом соединении. **Режимы выполнения запросов (`QueryExecMode`):** | Режим | Протокол | Prepared statements | |---|---|---| | `QueryExecModeCacheStatement` (дефолт) | extended | да, кэшируются по тексту запроса, переиспользуются | | `QueryExecModeCacheDescribe` | extended | описание кэшируется, без серверного PS | | `QueryExecModeDescribeExec` | extended | describe на каждый запрос, без кэша | | `QueryExecModeExec` | extended (unnamed) | без кэша имён | | `QueryExecModeSimpleProtocol` | simple | нет PS вообще; параметры подставляются на клиенте (с экранированием) | Simple protocol важен для совместимости с PgBouncer в transaction mode (см. ниже). **Конфигурация `pgxpool`:** ```go import ( "context" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) func newPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) { cfg, err := pgxpool.ParseConfig(dsn) if err != nil { return nil, err } cfg.MaxConns = 25 // верхний предел соединений cfg.MinConns = 5 // минимум держим тёплыми cfg.MaxConnLifetime = 30 * time.Minute // полная ротация соединения cfg.MaxConnLifetimeJitter = 5 * time.Minute // разброс, чтобы не закрывать все разом cfg.MaxConnIdleTime = 5 * time.Minute // закрытие простаивающих сверх MinConns cfg.HealthCheckPeriod = 1 * time.Minute // период фоновой проверки/поддержки MinConns // Тонкая настройка соединения cfg.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeCacheStatement // Lifecycle-хуки cfg.BeforeAcquire = func(ctx context.Context, c *pgx.Conn) bool { return true // false — выбросить соединение из пула перед выдачей } cfg.AfterRelease = func(c *pgx.Conn) bool { return true // false — не возвращать в пул, закрыть } cfg.BeforeClose = func(c *pgx.Conn) { // метрики / логирование } return pgxpool.NewWithConfig(ctx, cfg) } ``` Использование: ```go // Пул сам берёт и возвращает соединение для одиночных операций var n int err := pool.QueryRow(ctx, "SELECT count(*) FROM users WHERE active = $1", true).Scan(&n) // Если нужно несколько операций на одном соединении (например, SET, advisory lock): conn, err := pool.Acquire(ctx) if err != nil { return err } defer conn.Release() // КРИТИЧНО: вернуть соединение в пул _, err = conn.Exec(ctx, "SET LOCAL statement_timeout = '5s'") ``` | Параметр pgxpool | Аналог в database/sql | Смысл | |---|---|---| | `MaxConns` | `SetMaxOpenConns` | потолок соединений | | `MinConns` | (нет точного) | минимум тёплых соединений, поддерживается health-check'ом | | `MaxConnLifetime` | `SetConnMaxLifetime` | возраст соединения | | `MaxConnIdleTime` | `SetConnMaxIdleTime` | время простоя | | `HealthCheckPeriod` | (нет) | фоновой пинг + досоздание до MinConns + закрытие протухших | `MaxConnLifetimeJitter` — фича, которой нет в `database/sql`: предотвращает «thundering herd» одновременного переподключения всех соединений (важно за балансировщиком/при failover). **Prepared statement cache.** В режиме `CacheStatement` pgx при первом выполнении запроса делает `PREPARE` на соединении и переиспользует его. Кэш — per-connection (LRU). Это даёт: - экономию на повторном парсинге/планировании на стороне сервера; - но: prepared statement привязан к конкретному backend, поэтому **не переживает** transaction-pooling в PgBouncer. ### PgBouncer: внешний пулер Даже с пулом в приложении возникает проблема: при множестве инстансов приложения (10 подов × 25 соединений = 250) суммарное число соединений к БД превышает разумное. **PgBouncer** — лёгкий внешний пулер, который мультиплексирует множество клиентских соединений на небольшой набор серверных. **Режимы пулинга:** | Режим | Когда серверное соединение возвращается в пул | Уровень мультиплексирования | |---|---|---| | `session` | когда клиент отключается | низкий (1:1 на время сессии) | | `transaction` | в конце каждой транзакции | высокий — стандарт для serverless/микросервисов | | `statement` | после каждого оператора | максимальный; запрещает multi-statement транзакции | `transaction` mode — самый популярный: позволяет сотням клиентов делить десяток серверных соединений, потому что между транзакциями соединение свободно. **Что ломается в `transaction` mode** (потому что клиент не «владеет» одним серверным backend'ом постоянно): 1. **Серверные prepared statements.** `PREPARE` на одном backend, `EXECUTE` может попасть на другой → ошибка `prepared statement "..." does not exist`. Это самая частая боль pgx + PgBouncer. 2. **Session-level advisory locks** (`pg_advisory_lock`) — лок привязан к сессии backend'а, который вы потеряете после COMMIT. Используйте **транзакционные** advisory locks (`pg_advisory_xact_lock`). 3. **`SET` / `SET SESSION`** — настройки сессии (`search_path`, `statement_timeout`, `timezone`) теряются. Используйте `SET LOCAL` внутри транзакции или `options` в DSN. 4. **`LISTEN/NOTIFY`** — `LISTEN` это session-state; не работает через transaction pooling. Для notify-канала держите выделенное session-mode соединение (отдельный порт/пул). 5. **Temp tables** (`CREATE TEMP TABLE`) — живут в сессии backend'а, исчезают/недоступны на другом. 6. **`WITH HOLD` курсоры**, незакрытые курсоры между транзакциями. 7. **`WITH HOLD`/unnamed portals**, `DEALLOCATE`, любая логика, полагающаяся на постоянство backend'а. **Как с этим жить (pgx + PgBouncer transaction mode):** Вариант А — отключить серверные prepared statements в pgx: ```go cfg, _ := pgxpool.ParseConfig(dsn) // Самый надёжный способ для PgBouncer transaction mode (старые версии): cfg.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol // Альтернатива — extended-протокол без серверного кэша имён: // cfg.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeExec ``` В DSN это же выражается параметром: ``` postgres://user:pass@pgbouncer:6432/db?default_query_exec_mode=simple_protocol ``` (в старом pgx v4 был флаг `prefer_simple_protocol=true`; в v5 — `default_query_exec_mode`). Вариант Б — для PgBouncer **>= 1.21** появилась поддержка prepared statements в transaction mode (`max_prepared_statements > 0` в конфиге PgBouncer). Тогда можно оставить `QueryExecModeCacheStatement`. Это предпочтительный современный путь, т.к. сохраняет выигрыш от prepared statements. > Замечание про `statement_cache`: в старом pgx (`lib/pq` стиль) фигурировал `statement_cache_mode`/`statement_cache_capacity`. В pgx v5 эквивалент — выбор `QueryExecMode`. «Выключить кэш» = `QueryExecModeExec`/`QueryExecModeSimpleProtocol`. PgBouncer (.ini) для transaction mode: ```ini [databases] mydb = host=postgres port=5432 dbname=mydb [pgbouncer] pool_mode = transaction max_client_conn = 1000 ; сколько клиентов принимаем default_pool_size = 20 ; серверных соединений на пару (user,db) reserve_pool_size = 5 server_idle_timeout = 600 max_prepared_statements = 200 ; >=1.21, чтобы PS работали в transaction mode ``` ### Расчёт размера пула Главный контринтуитивный факт: **больше соединений ≠ больше throughput.** За пределами некоторой точки добавление соединений снижает производительность из-за context switching, конкуренции за блокировки и cache thrashing. CPU может одновременно реально выполнять работу только для числа потоков ≈ числу ядер. Формула из документации PgBouncer / wiki HikariCP (эмпирическая база): ``` connections = ((core_count * 2) + effective_spindle_count) ``` - `core_count` — физические ядра (не hyperthread'ы). - `effective_spindle_count` — грубо число дисков, способных обслуживать одновременный I/O (для SSD/облака — оценивается отдельно, часто берут небольшое число или возможность параллельного I/O). Для сервера с 8 ядрами и SSD это даёт ориентир ~20–30 соединений — и это часто оптимум даже для высоконагруженного сервиса. Маленький пул, обрабатывающий запросы быстро, обгоняет большой пул, тонущий в контеншене. **Распределение по инстансам.** Если 10 подов приложения, а суммарно к БД должно идти ≤ 25 активных соединений, то либо `MaxConns ≈ 2-3` на под (мало, churn), либо ставите PgBouncer и каждый под держит свои соединения к PgBouncer, а тот мультиплексирует на 25 серверных. Поэтому в Kubernetes-окружениях PgBouncer почти обязателен. **Под нагрузкой:** - `MinConns` (pgx) / достаточный `MaxIdleConns` — чтобы не платить latency установки на каждом всплеске. - `MaxConnLifetime` с jitter — чтобы соединения ротировались и не «застревали» на упавшей реплике после failover. - `statement_timeout` / `lock_timeout` на стороне БД — чтобы один зависший запрос не держал соединение пула вечно. ### Утечки соединений и context cancellation Утечка соединения = соединение взято из пула и не возвращено. Пул исчерпывается, новые запросы виснут на ожидании. Источники: ```go // ❌ Утечка: Rows не закрыты — соединение держится до GC (а его может и не быть) rows, _ := db.QueryContext(ctx, "SELECT ...") for rows.Next() { /* ... return из середины без rows.Close() */ } // ✅ Всегда defer rows.Close() и проверка rows.Err() rows, err := db.QueryContext(ctx, "SELECT ...") if err != nil { return err } defer rows.Close() for rows.Next() { /* scan */ } return rows.Err() ``` ```go // ❌ Утечка транзакции: нет Rollback при ошибке/панике tx, _ := db.BeginTx(ctx, nil) // ... если return до Commit без Rollback — соединение занято // ✅ defer rollback (после Commit он no-op / даёт ErrTxDone — игнорируем) tx, err := db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() // ... do work ... return tx.Commit() ``` ```go // ❌ pgxpool: забыли Release conn, _ := pool.Acquire(ctx) // ... return без conn.Release() // ✅ conn, err := pool.Acquire(ctx) if err != nil { return err } defer conn.Release() ``` **Context cancellation** — критичная связка с пулом: - Всегда передавайте `context` с дедлайном/таймаутом в запросы. Отмена контекста прерывает ожидание свободного соединения **и** отменяет выполняющийся запрос (драйвер шлёт cancel-request на сервер). - Без таймаута: медленный/зависший запрос держит соединение, под нагрузкой это каскадом исчерпывает пул → весь сервис стоит. - Нюанс: при отмене контекста во время активного запроса драйвер должен «починить» соединение (или закрыть его), что само по себе стоит ресурсов — слишком агрессивные таймауты при медленной БД приводят к churn соединений. ```go ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() row := pool.QueryRow(ctx, "SELECT ...") ``` ## Подводные камни / gotchas - **`sql.Open` на каждый запрос.** Это создаёт новый пул каждый раз. `*sql.DB` — долгоживущий синглтон на приложение. - **`MaxOpenConns = 0` (дефолт).** Без лимита приложение под нагрузкой выест `max_connections` и положит БД для всех. Всегда ставьте лимит. - **`MaxIdleConns` по умолчанию = 2.** При `MaxOpenConns=50` это вызывает постоянный churn под нагрузкой. Выравнивайте idle с open. - **prepared statements + PgBouncer transaction mode.** Самая частая прод-ошибка с pgx: `prepared statement "lrupsc_..." does not exist`. Лечение — simple protocol или PgBouncer ≥ 1.21 с `max_prepared_statements`. - **`SET search_path` через PgBouncer transaction mode** молча «теряется» между транзакциями. Используйте `SET LOCAL` или `options=-c search_path=...` в DSN. - **`pg_advisory_lock` (session-level) в transaction pooling** — лок не освобождается ожидаемо / не виден. Берите `pg_advisory_xact_lock`. - **Незакрытые `Rows` / отсутствие `defer Close()`** — медленная утечка, проявляется только под нагрузкой. То же для `pgxpool.Acquire` без `Release()`. - **Отсутствие `context` с таймаутом** — зависший запрос держит соединение неограниченно. Обязательно `statement_timeout` на стороне БД как страховка. - **`ConnMaxLifetime = 0` за балансировщиком/прокси.** Соединения «протухают», вы получаете `connection reset` после простоя. Ставьте разумный lifetime. - **Большой пул «для запаса».** 200 соединений к 8-ядерной БД медленнее, чем 25. Контеншен растёт нелинейно. - **Слишком много инстансов × локальный пул** без PgBouncer → суммарно тысячи соединений к БД. - **`MinConns` без `HealthCheckPeriod`** — pgxpool не будет активно поддерживать минимум; health-check досоздаёт и проверяет соединения. - **`db.Stats()` игнорируют.** Без экспорта `WaitCount`/`WaitDuration` в метрики невозможно понять, что пул — узкое место. ## Вопросы на собеседовании **В:** Почему пул соединений в PostgreSQL критичнее, чем, скажем, в MySQL/потоковых СУБД? **О:** Потому что PostgreSQL использует process-per-connection: каждое соединение — отдельный ОС-процесс, созданный через `fork()` postmaster'ом. Это дорого по памяти (несколько МБ на backend + `work_mem`), по latency установки (handshake + аутентификация + форк), и нагружает ядро (lock manager, снапшоты). Пул амортизирует это и ограничивает число одновременных backend'ов. **В:** Что делают `SetMaxIdleConns` и `SetConnMaxLifetime` и какие значения вы ставите? **О:** `SetMaxIdleConns` — сколько свободных соединений держать готовыми; обычно равняю с `MaxOpenConns`, чтобы избежать churn под нагрузкой. `SetConnMaxLifetime` — максимальный возраст соединения, после чего оно закрывается при возврате в пул; ставлю 15–30 мин для ротации и устойчивости к failover за балансировщиком. Дополнительно `SetConnMaxIdleTime`, чтобы освобождать ресурсы в тихие периоды. **В:** По каким метрикам вы поймёте, что пул — узкое место? **О:** Из `db.Stats()`: растущие `WaitCount` и `WaitDuration` означают, что запросы ждут свободное соединение. Если `OpenConnections` постоянно упирается в `MaxOpenConnections` — пул мал или запросы держат соединения слишком долго (медленные запросы/утечки). Высокий `MaxIdleClosed` — churn, надо поднять idle. **В:** В чём разница между native pgx и использованием pgx через `database/sql`? **О:** Native pgx (`pgxpool.Pool`) даёт extended-протокол по умолчанию, богатую систему типов `pgtype` (jsonb, массивы, numeric, ranges), батчи/pipelining, `CopyFrom`, `LISTEN/NOTIFY`, кэш prepared statements и lifecycle-хуки. Через `database/sql` (драйвер `stdlib`) вы получаете стандартный интерфейс и его пул, но теряете часть нативных возможностей и тонкий контроль над протоколом. **В:** Что ломается при использовании prepared statements за PgBouncer в transaction mode и почему? **О:** В transaction mode серверное соединение возвращается в пул после каждой транзакции, поэтому клиент не привязан к одному backend'у. Серверный prepared statement живёт на конкретном backend; `EXECUTE` может попасть на другой → `prepared statement does not exist`. Решения: переключить pgx на simple protocol (`DefaultQueryExecMode = QueryExecModeSimpleProtocol`) или `QueryExecModeExec`; либо использовать PgBouncer ≥ 1.21 с `max_prepared_statements > 0`, где пулер сам управляет PS. **В:** Что ещё, кроме prepared statements, ломается в transaction pooling и как обходите? **О:** Session-level advisory locks (`pg_advisory_lock`) → беру `pg_advisory_xact_lock`. `SET`/`search_path` теряется между транзакциями → `SET LOCAL` или `options` в DSN. `LISTEN/NOTIFY` не работает → выделенное session-mode соединение. Temp tables и `WITH HOLD` курсоры — тоже session-state, недоступны на другом backend. **В:** Как вы рассчитываете размер пула? Почему не «поставить побольше»? **О:** Ориентир — эмпирическая формула `((core_count*2) + effective_spindle_count)`, для 8-ядерной БД с SSD это ~20–30. Больше соединений увеличивает context switching, конкуренцию за блокировки и cache thrashing — throughput падает. CPU реально параллелит работу примерно на число ядер; «лишние» соединения только создают очередь внутри БД вместо очереди в пуле, где ей и место. **В:** Как утечка соединений выглядит в проде и как её предотвратить? **О:** Симптом: запросы начинают виснуть на ожидании соединения, `WaitCount`/`WaitDuration` взлетают, при этом БД не загружена. Причины: незакрытые `Rows`, забытый `tx.Rollback()`/`conn.Release()`, отсутствие таймаута у контекста. Предотвращение: всегда `defer rows.Close()`, `defer tx.Rollback()`, `defer conn.Release()`; контекст с дедлайном на каждый запрос; `statement_timeout`/`lock_timeout` на стороне БД как страховка. **В:** Зачем `MaxConnLifetimeJitter` в pgxpool? **О:** Чтобы избежать «thundering herd»: без jitter все соединения, открытые примерно одновременно, истекут одновременно и одновременно переподключатся, создав всплеск нагрузки на БД и downtime-окно (особенно болезненно после failover за балансировщиком). Jitter размазывает ротацию во времени. ## На что копают на senior+ - **Process-per-connection и его следствия:** почему высокий `max_connections` сам по себе вреден (память под lock table/semaphores, стоимость снапшотов до PG 14, autovacuum контеншен). Знание улучшений `GetSnapshotData` в PG 14. - **Архитектура многоуровневого пулинга:** app pool (pgxpool) → PgBouncer (transaction) → PostgreSQL; как считать суммарное число серверных соединений при N инстансах в Kubernetes; почему локальные пулы плохо масштабируются без внешнего пулера. - **Глубокое понимание PgBouncer modes** и полного списка session-state, который ломается в transaction mode, с конкретными обходами (xact advisory locks, SET LOCAL, выделенный LISTEN-канал, серверный PS-support в 1.21+). - **Extended vs simple protocol** на уровне wire-протокола: Parse/Bind/Describe/Execute, что значит unnamed statement, как pgx именует prepared statements (`lrupsc_*`), почему simple protocol совместим с PgBouncer но теряет типизацию/PS-кэш. - **Поведение пула при отмене контекста:** что происходит с соединением, когда context отменяется во время активного запроса (cancel request на сервер, возможное закрытие/«починка» соединения), баланс между агрессивными таймаутами и churn. - **Tuning под конкретную нагрузку:** связь размера пула, `statement_timeout`, `work_mem` (помните: `work_mem` × число одновременных сортировок/хэшей × соединений = потенциальный OOM), и почему ограничение пула косвенно ограничивает пиковое потребление памяти БД. - **Наблюдаемость:** экспорт `db.Stats()` / `pool.Stat()` в Prometheus, алерты на `WaitDuration`, `AcquireWaitTime` (pgxpool), корреляция с p99 latency запросов. - **Failover и health-checks:** как `MaxConnLifetime` + jitter + `HealthCheckPeriod` помогают пулу «отпустить» соединения к упавшей primary/реплике, взаимодействие с `target_session_attrs` и read/write splitting.