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 - **Физическая (streaming) репликация** — побайтовая копия кластера на уровне WAL. Standby проигрывает WAL primary. Всё или ничего: реплицируется весь инстанс, версии PG должны совпадать, архитектура CPU тоже. Основа HA. - **Логическая репликация** — на уровне логических изменений строк (decode WAL → INSERT/UPDATE/DELETE). Выборочно по таблицам (publication/subscription), между разными мажорными версиями, можно писать в таблицы-подписчики. Цена — нет DDL, нужны PK/REPLICA IDENTITY, выше overhead. - **sync vs async**: `synchronous_commit` управляет, ждать ли подтверждения standby перед возвратом COMMIT. Async = низкая latency, риск потери данных при падении primary. Sync = durability, но latency растёт и primary может «зависнуть», если standby недоступен. `quorum`/`ANY` — компромисс. - **Replication lag** = задержка между primary и standby. Три фазы: write → flush → replay. Мониторинг через `pg_stat_replication` (на primary) и `pg_stat_wal_receiver` / `pg_last_wal_replay_lsn()` (на standby). - **Чтение с реплик** даёт масштабирование read-нагрузки, но порождает stale reads и проблему read-your-writes. Нужен sticky-routing или чтение свежих данных с primary. - **Failover**: `pg_promote()` переводит standby в primary. Опасность split-brain — нужен fencing. В проде это Patroni/repmgr + DCS (etcd/Consul), а не ручной promote. - **Go**: два пула `*pgxpool.Pool` / `*sql.DB` (writer → primary, reader → реплики), роутинг по типу запроса, с fallback на primary для read-your-writes. ## Теория ### 1. Физическая (streaming / WAL) репликация PostgreSQL пишет все изменения сначала в WAL (Write-Ahead Log) — последовательность записей, описывающих физические изменения страниц данных (блоков 8 КБ). Физическая репликация — это передача потока WAL с primary на standby и его непрерывное применение. **Ключевые свойства:** - Бинарная, побайтовая идентичность кластера. Standby — точная копия primary на уровне файлов данных. - Реплицируется **весь** инстанс целиком (все базы, все таблицы). Нельзя выбрать отдельную таблицу. - Требования: одинаковая мажорная версия PostgreSQL, одинаковая архитектура (endianness, размер указателя), совместимые сборки. - Standby по умолчанию — read-only (если включён `hot_standby = on`, можно выполнять SELECT). **Как поток устроен:** 1. На primary процесс `walsender` читает WAL и шлёт его по протоколу репликации. 2. На standby `walreceiver` принимает WAL, пишет его на диск. 3. `startup`/recovery процесс проигрывает (replay) WAL, применяя изменения к файлам данных. **Hot standby** — режим, когда standby принимает read-only запросы во время проигрывания WAL. Возникает конфликт: replay может удалить версии строк (vacuum cleanup), которые нужны долгому SELECT на реплике. PostgreSQL разрешает его, либо задерживая replay (`max_standby_streaming_delay`), либо отменяя запрос (`ERROR: canceling statement due to conflict with recovery`). **Настройка primary** (`postgresql.conf`): ```ini wal_level = replica # минимум для физической репликации max_wal_senders = 10 # сколько standby/pg_basebackup одновременно wal_keep_size = '1GB' # сколько WAL держать на случай отставания (без слота) hot_standby = on ``` `pg_hba.conf` на primary — разрешить подключение по replication: ``` host replication repl_user 10.0.0.0/24 scram-sha-256 ``` **Создание standby** через `pg_basebackup`: ```bash pg_basebackup -h primary-host -U repl_user -D /var/lib/postgresql/data \ -Fp -Xs -P -R -C -S standby1_slot # -R создаёт standby.signal + primary_conninfo в postgresql.auto.conf # -C -S создаёт replication slot standby1_slot ``` После старта `postgresql.auto.conf` standby содержит: ```ini primary_conninfo = 'host=primary-host user=repl_user ... application_name=standby1' primary_slot_name = 'standby1_slot' ``` #### Replication slots Слот — это объект на primary, который гарантирует, что primary **не удалит** WAL (и для логической — не «отвакуумит» строки), пока standby их не получил. Без слота при долгом отставании standby primary может удалить нужный WAL → standby «отвалится» и потребует пересоздания (или восстановления из архива). ```sql -- физический слот SELECT pg_create_physical_replication_slot('standby1_slot'); -- посмотреть слоты и сколько WAL «застряло» SELECT slot_name, slot_type, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal FROM pg_replication_slots; ``` **Подвох слотов**: неактивный слот (standby умер и не вернулся) будет бесконечно держать WAL → переполнение `pg_wal` → primary падает по нехватке места на диске. Защита: `max_slot_wal_keep_size` (PG 13+) ограничивает объём удерживаемого WAL; при превышении слот инвалидируется, но primary выживает. ```ini max_slot_wal_keep_size = '50GB' ``` #### Cascading replication Standby может сам отдавать WAL другим standby (`walsender` работает и на реплике), снижая нагрузку на primary. ### 2. Логическая репликация Логическая репликация декодирует WAL в логический поток изменений (через output plugin `pgoutput`) и применяет их как обычные SQL-операции на подписчике. Работает по модели publish/subscribe. **Отличия от физической:** | Свойство | Физическая (streaming) | Логическая | |---|---|---| | Уровень | байты/блоки WAL | строки (INSERT/UPDATE/DELETE) | | Гранулярность | весь кластер | выбранные таблицы/публикации | | Версии PG | должны совпадать | можно между мажорными (10→16) | | Архитектура | должна совпадать | может отличаться | | Standby writable? | нет (read-only) | да (подписчик — обычная база) | | DDL | реплицируется | НЕ реплицируется (до PG 16/17 ограниченно) | | Накладные расходы | минимальные | выше (decode + apply как SQL) | | Сценарии | HA, read-replicas | мажорный upgrade без downtime, выборочная синхронизация, ETL/CDC, мульти-источник | **Publication на источнике:** ```sql CREATE PUBLICATION my_pub FOR TABLE orders, customers; -- или для всех таблиц: CREATE PUBLICATION all_pub FOR ALL TABLES; -- выборочно по операциям и даже по строкам (PG 15+): CREATE PUBLICATION active_orders FOR TABLE orders WHERE (status = 'active') WITH (publish = 'insert, update'); ``` **Subscription на приёмнике:** ```sql CREATE SUBSCRIPTION my_sub CONNECTION 'host=source-host dbname=app user=repl_user password=...' PUBLICATION my_pub WITH (copy_data = true, create_slot = true, slot_name = 'my_sub_slot'); ``` Требования: `wal_level = logical` на источнике. Таблицы-подписчики должны существовать заранее (схема не реплицируется). Каждой таблице нужен **REPLICA IDENTITY** для UPDATE/DELETE: ```sql -- по умолчанию = PRIMARY KEY. Если PK нет: ALTER TABLE orders REPLICA IDENTITY FULL; -- сравнение по всем колонкам (дорого) -- или по уникальному индексу: ALTER TABLE orders REPLICA IDENTITY USING INDEX orders_uniq_idx; ``` **Мониторинг логической:** ```sql -- на подписчике SELECT subname, received_lsn, latest_end_lsn, last_msg_send_time FROM pg_stat_subscription; -- ошибки применения (PG 15+) SELECT * FROM pg_stat_subscription_stats; ``` **Подводные камни логической репликации:** - DDL не реплицируется — добавили колонку на источнике, забыли на подписчике → ошибка применения, репликация встаёт. - Конфликты (дубликат PK на подписчике) останавливают подписку до ручного вмешательства (skip LSN или `pg_replication_origin_advance`). - Sequences не синхронизируются автоматически (до PG 16). После failover на логическую копию нужно вручную выставить sequence. - `TRUNCATE` реплицируется с PG 11. Большие транзакции стримятся с PG 14 (`streaming = on`). - Начальный `copy_data` снимок может быть тяжёлым для больших таблиц. ### 3. Synchronous vs Asynchronous `synchronous_commit` определяет, на каком этапе COMMIT возвращает управление клиенту. | Значение | Семантика | Durability при падении primary | |---|---|---| | `off` | не ждёт даже локального fsync WAL | можно потерять последние транзакции даже без репликации | | `local` | ждёт локальный fsync WAL, не ждёт standby | потеря данных, не дошедших до standby (async-репликация по факту) | | `remote_write` | standby принял и записал в ОС (не fsync) | потеря при одновременном крахе ОС standby | | `on` | standby сделал fsync WAL | нет потери при падении primary (есть на standby) | | `remote_apply` | standby **применил** WAL (виден в SELECT на реплике) | read-your-writes на реплике гарантирован | **`synchronous_standby_names`** на primary определяет, какие standby считаются синхронными: ```ini # первый доступный из списка synchronous_standby_names = 'standby1, standby2' # FIRST N — ждать первых N из списка по приоритету synchronous_standby_names = 'FIRST 1 (standby1, standby2, standby3)' # ANY N — кворум: ждать ЛЮБЫХ N из списка (PG 9.6+) synchronous_standby_names = 'ANY 2 (s1, s2, s3, s4)' ``` `application_name` standby (из `primary_conninfo`) должен совпадать с именами в этом списке. **Компромисс latency vs durability:** - Синхронная репликация добавляет к каждому COMMIT сетевой round-trip до standby. Под нагрузкой это резко увеличивает latency записи и снижает throughput. - Если синхронный standby недоступен и нет других кандидатов, COMMIT на primary **зависает** (primary доступен на запись, но коммиты блокируются). Это сознательный выбор: durability важнее доступности. - Решение для баланса: `ANY 1 (s1, s2)` — нужен ответ любого одного из двух. Один может упасть без остановки primary. Можно выставлять `synchronous_commit` на уровне транзакции/сессии: критичные платежи — `remote_apply`, фоновые логи — `off`. ```sql SET synchronous_commit = 'off'; -- для конкретной сессии/транзакции ``` ### 4. Replication lag **Lag** — насколько standby отстаёт от primary. Измеряется в LSN (объём WAL) и во времени. **Три фазы (для каждого standby):** - **write lag** — WAL дошёл до standby, но ещё не записан на диск standby. - **flush lag** — WAL записан (fsync) на standby. - **replay lag** — WAL применён к файлам данных и виден для запросов. `replay_lsn` всегда ≤ `flush_lsn` ≤ `write_lsn` ≤ `sent_lsn`. **Причины lag:** - Высокий объём записи на primary (поток WAL шире, чем сеть/диск standby). - Медленный диск или CPU на standby (replay — однопоточный startup-процесс, не параллелится так, как параллельные бэкенды на primary). - Долгие запросы/локи на standby с `hot_standby_feedback`, блокирующие replay (конфликт recovery). - Сетевые задержки между ЦОД. - Контрольные точки / тяжёлые batch-операции (VACUUM FULL, массовый UPDATE, индексация). **Мониторинг на primary:** ```sql SELECT application_name, client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, write_lag, flush_lag, replay_lag, sync_state FROM pg_stat_replication; -- лаг в байтах для каждого standby: SELECT application_name, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_lag_bytes FROM pg_stat_replication; ``` **Мониторинг на standby:** ```sql -- лаг по времени (на самом standby) SELECT now() - pg_last_xact_replay_timestamp() AS replication_delay; -- состояние приёмника SELECT status, received_lsn, latest_end_lsn, last_msg_receipt_time FROM pg_stat_wal_receiver; SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn(); ``` `pg_last_xact_replay_timestamp()` врёт, если на primary нет транзакций (lag будет «расти», хотя реально standby в актуальном состоянии — нечего применять). Поэтому для алертов комбинируйте LSN-diff с временной метрикой. ### 5. Чтение с реплик и его подвохи Чтение с реплик масштабирует read-нагрузку, но реплики **eventually consistent** (async) — данные на них могут отставать. **Read-your-writes проблема:** пользователь записал данные (на primary), сразу читает (с реплики), но реплика ещё не получила изменение → видит старое состояние. Классика: создал заказ → редирект на список → заказа нет. **Stale reads:** любой SELECT с реплики может вернуть устаревшие данные. Опасно для бизнес-логики, основанной на свежем состоянии (проверка баланса перед списанием). **Стратегии маршрутизации:** 1. **Write → primary, read → реплики** по умолчанию. Простой выигрыш для read-heavy нагрузки. 2. **Sticky-to-primary после записи**: в течение N секунд / до конца сессии после write читать с primary. Решает read-your-writes. 3. **LSN-tracking (causal consistency)**: после write запоминаем `pg_current_wal_lsn()`, при чтении ждём, пока реплика догонит этот LSN (`pg_wal_replay_wait` в PG 17, или проверка `pg_last_wal_replay_lsn()` ≥ нужного, иначе fallback на primary). 4. **`synchronous_commit = remote_apply`** для критичных записей — гарантирует, что синхронная реплика уже видит изменение. ```sql -- PG 17: дождаться применения конкретного LSN на реплике перед чтением CALL pg_wal_replay_wait('0/170E180', timeout => 1000); ``` **`hot_standby_feedback` и его влияние на vacuum primary:** По умолчанию primary не знает о долгих запросах на standby и может через VACUUM удалить (или заморозить) версии строк, которые ещё нужны запросу на реплике → конфликт recovery → запрос отменяется (`canceling statement due to conflict with recovery`). `hot_standby_feedback = on` на standby заставляет standby сообщать primary самый старый `xmin`, который ему нужен. Primary тогда **не вакуумит** эти версии строк. ```ini # на standby hot_standby_feedback = on ``` Цена: на primary накапливается «мусор» (dead tuples), который нельзя вычистить, пока долгий запрос на реплике жив → **table bloat**, рост размера таблиц и индексов, деградация. Долгий зависший запрос на реплике (или idle-in-transaction) фактически блокирует VACUUM на primary — это частая причина незаметного раздувания базы. Альтернатива без feedback — увеличить `max_standby_streaming_delay`, разрешая standby отставать ради завершения запросов. ### 6. Failover **Promote** — перевод standby в роль primary: ```sql SELECT pg_promote(); -- из SQL (PG 12+) ``` ```bash pg_ctl promote -D /var/lib/postgresql/data ``` После promote standby перестаёт быть read-only, начинает новую timeline (timeline ID +1) — это важно, чтобы старый primary и другие standby не «смешали» WAL разных историй. **Split-brain** — два узла одновременно считают себя primary и принимают записи → расхождение данных, потеря записей при «склейке». Главная опасность автоматического failover. **Fencing (STONITH)** — гарантия, что старый primary точно не принимает записи перед promote нового: отключение сети, остановка сервиса, IPMI-reset, отзыв VIP. Без fencing нельзя безопасно автоматизировать failover. **Recovery старого primary:** после failover бывший primary отстаёт и имеет «расходящуюся» timeline. Чтобы вернуть его как standby к новому primary, используется `pg_rewind` — синхронизирует только разошедшиеся блоки, не делая полный basebackup: ```bash pg_rewind --target-pgdata=/var/lib/postgresql/data \ --source-server='host=new-primary user=repl_user ...' -R ``` Требует `wal_log_hints = on` (или data checksums) на узлах. **Инструменты HA:** | Инструмент | Суть | |---|---| | **Patroni** | Шаблон HA на Python; лидер-выборы через DCS (etcd/Consul/ZooKeeper); автоматический failover, fencing, REST API, интеграция с HAProxy/pgBouncer. Де-факто стандарт. | | **repmgr** | Управление репликацией и failover, witness-узел против split-brain, repmgrd-демон. | | **pg_auto_failover** | Microsoft; отдельный monitor-узел, проще Patroni. | | **Stolon** | HA на Go, поверх etcd/Consul, k8s-friendly. | Patroni использует DCS как источник истины: лидер держит lock с TTL; если не продлил — другой узел берёт lock и промоутится. DCS-консенсус (Raft) предотвращает split-brain — лидером может быть только тот, кто держит lock. Приложение ходит не напрямую, а через HAProxy (опрашивает Patroni REST `/primary`, `/replica`) или service discovery. ### 7. Go: разделение connection на read/write пулы Идея: два пула — один на primary (writer), один на реплику/балансировщик реплик (reader). Роутинг по типу операции. **pgx (pgxpool):** ```go type DB struct { writer *pgxpool.Pool // primary reader *pgxpool.Pool // реплика(и) / HAProxy reader-порт } func New(ctx context.Context, primaryDSN, replicaDSN string) (*DB, error) { w, err := pgxpool.New(ctx, primaryDSN) if err != nil { return nil, err } r, err := pgxpool.New(ctx, replicaDSN) if err != nil { w.Close() return nil, err } return &DB{writer: w, reader: r}, nil } // Чтение по умолчанию с реплики. func (db *DB) QueryRead(ctx context.Context, sql string, args ...any) (pgx.Rows, error) { return db.reader.Query(ctx, sql, args...) } // Запись и read-your-writes — на primary. func (db *DB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { return db.writer.Exec(ctx, sql, args...) } // Транзакция всегда на primary (она может писать). func (db *DB) Begin(ctx context.Context) (pgx.Tx, error) { return db.writer.Begin(ctx) } ``` **Read-your-writes через контекст** (sticky-to-primary): ```go type ctxKey struct{} // помечаем контекст после записи: следующие чтения идут на primary func ForcePrimary(ctx context.Context) context.Context { return context.WithValue(ctx, ctxKey{}, true) } func (db *DB) pool(ctx context.Context) *pgxpool.Pool { if v, _ := ctx.Value(ctxKey{}).(bool); v { return db.writer } return db.reader } func (db *DB) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) { return db.pool(ctx).Query(ctx, sql, args...) } ``` **database/sql** — то же с двумя `*sql.DB`. Балансировку нескольких реплик отдают либо HAProxy/pgBouncer (одна DSN на reader-порт), либо клиентскому списку хостов в connection string (`host=r1,r2 target_session_attrs=...`). **target_session_attrs** (libpq/pgx) для авто-выбора узла из списка хостов: ``` postgres://r1,r2,r3/app?target_session_attrs=read-only # только реплики postgres://p,s1,s2/app?target_session_attrs=read-write # только узел, принимающий запись (= primary) postgres://...?target_session_attrs=prefer-standby ``` Это даёт примитивный failover на стороне клиента: при потере primary `read-write` найдёт новый promote'нутый узел. **Практические замечания для Go-сервиса:** - Раздельные таймауты/размеры пулов: writer обычно меньше и «дороже», reader масштабируется. - Метрики: экспортировать `pool.Stat()` (AcquiredConns, IdleConns, TotalConns) по обоим пулам. - Не открывать транзакцию на reader для записи — получите `cannot execute INSERT in a read-only transaction`. - Health-check реплики: при большом lag временно слать чтения на primary (circuit breaker по метрике lag из `pg_stat_wal_receiver`). ## Подводные камни / gotchas - **Неактивный replication slot заполняет диск primary.** Standby упал и не вернулся — слот вечно держит WAL → `pg_wal` растёт → primary падает. Лечится `max_slot_wal_keep_size` и алертами на неактивные слоты. - **Синхронная репликация без кворума = единая точка отказа на запись.** `synchronous_standby_names = 'standby1'` + standby1 недоступен → все COMMIT висят. Используйте `ANY N` или мониторьте. - **`hot_standby_feedback = on` → bloat на primary.** Долгий/зависший запрос на реплике блокирует VACUUM. Следите за `idle in transaction` и долгими запросами на standby. - **Логическая репликация молча встаёт при конфликте/DDL.** Добавили колонку на источнике — apply падает на подписчике, lag растёт. Нужен мониторинг `pg_stat_subscription` и алерты. - **Sequences не реплицируются логически (до PG 16).** После failover на логическую копию — дубликаты ключей. Выставляйте sequence вручную. - **`pg_last_xact_replay_timestamp()` показывает «фейковый лаг» при простое primary** (нет новых транзакций). Комбинируйте с LSN-diff. - **Split-brain при ручном/наивном авто-failover.** Без fencing старый primary продолжит принимать записи. Никогда не автоматизируйте promote без DCS-консенсуса и fencing. - **Конфликт recovery на standby отменяет запросы** (`canceling statement due to conflict with recovery`). Либо `hot_standby_feedback` (ценой bloat), либо `max_standby_streaming_delay` (ценой lag). - **timeline mismatch после failover.** Старые standby не подключатся к новому primary без `pg_rewind` или восстановления через общий WAL-архив (`restore_command`). - **`REPLICA IDENTITY` по умолчанию = PK.** Таблица без PK → UPDATE/DELETE не реплицируются логически (или требуют дорогой `FULL`). - **Чтение с реплики в той же бизнес-операции, что и запись, без sticky-логики = баги «исчезающих данных».** ## Вопросы на собеседовании **В:** В чём принципиальная разница между физической и логической репликацией и когда что выбирать? **О:** Физическая работает на уровне WAL (байты/блоки), копирует весь кластер побайтово, требует одинаковых версий и архитектуры, standby read-only — это HA и read-replicas. Логическая декодирует WAL в строки (INSERT/UPDATE/DELETE), реплицирует выбранные таблицы между разными версиями, подписчик writable — это zero-downtime мажорный upgrade, выборочная синхронизация, CDC/ETL. Логическая не реплицирует DDL и дороже по overhead. **В:** Что делает replication slot и какой главный риск с ним связан? **О:** Слот гарантирует, что primary не удалит WAL (а для логических — не вакуумит нужные версии строк), пока standby их не получил, защищая от рассинхрона при отставании. Риск: неактивный слот (упавший standby) бесконечно держит WAL → переполнение `pg_wal` → падение primary. Защита — `max_slot_wal_keep_size` и мониторинг `active = false`. **В:** Объясните разницу между `synchronous_commit = on`, `remote_write`, `remote_apply` и `local`. **О:** `local` — ждём только локальный fsync, не ждём standby (фактически async). `remote_write` — standby принял и записал в ОС, но без fsync. `on` — standby сделал fsync WAL (нет потери при падении primary). `remote_apply` — standby применил WAL и изменение видно в его SELECT (даёт read-your-writes на реплике). Чем сильнее гарантия, тем выше latency коммита. **В:** Что произойдёт, если единственный синхронный standby станет недоступен? **О:** Все COMMIT на primary зависнут — primary жив, но коммиты блокируются, ожидая подтверждения недоступного standby. Это by design: durability приоритетнее. Чтобы избежать остановки, используют `ANY N (...)` (кворум) с несколькими кандидатами или несколько синхронных standby в `FIRST N`. **В:** Какие фазы replication lag существуют и как его мониторить? **О:** write (WAL дошёл до standby), flush (записан/fsync на standby), replay (применён и виден запросам). На primary — `pg_stat_replication` (`write_lag`, `flush_lag`, `replay_lag`, LSN-поля). На standby — `pg_stat_wal_receiver`, `pg_last_wal_replay_lsn()`, `now() - pg_last_xact_replay_timestamp()`. Лаг в байтах через `pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)`. **В:** Что такое read-your-writes проблема при чтении с реплик и как её решить? **О:** Пользователь пишет на primary и сразу читает с реплики, которая ещё не получила изменение → видит старые данные. Решения: sticky-to-primary после записи (читать с primary N секунд), LSN-tracking (запомнить LSN записи, дождаться его применения на реплике или fallback на primary, `pg_wal_replay_wait` в PG 17), либо `synchronous_commit = remote_apply` для критичных записей. **В:** Как `hot_standby_feedback` влияет на primary? **О:** Standby сообщает primary свой минимальный нужный `xmin`, чтобы VACUUM на primary не удалял версии строк, которые читает долгий запрос на реплике (избегаем `conflict with recovery`). Цена — VACUUM на primary не может вычистить эти dead tuples, что ведёт к bloat таблиц и индексов. Долгий/зависший запрос на реплике фактически блокирует очистку на primary. **В:** Что такое split-brain и как HA-инструменты его предотвращают? **О:** Split-brain — два узла одновременно считают себя primary и принимают записи, данные расходятся. Предотвращение: DCS-консенсус (etcd/Consul с Raft) — лидером может быть только держатель распределённого lock; fencing/STONITH старого primary (отзыв VIP, остановка сервиса) перед promote нового. Patroni/repmgr реализуют это автоматически; ручной `pg_promote` без fencing опасен. **В:** Как организовать read/write splitting в Go-приложении? **О:** Два пула — writer на primary, reader на реплику(и)/HAProxy. Записи и транзакции → writer, обычные SELECT → reader. Read-your-writes решаем sticky-флагом в контексте (после записи следующие чтения идут на primary) или LSN-tracking. Балансировку реплик отдаём HAProxy/pgBouncer или `target_session_attrs` в connection string; мониторим `pool.Stat()` и lag реплики для circuit breaker. **В:** Зачем нужен `pg_rewind` после failover? **О:** После promote старый primary имеет разошедшуюся timeline и не может просто стать standby нового primary. `pg_rewind` синхронизирует только разошедшиеся блоки данных (а не делает полный basebackup), быстро возвращая старый узел в кластер как standby. Требует `wal_log_hints = on` или data checksums. ## На что копают на senior+ - **Durability-математика sync-репликации.** Понимаете ли вы, что `synchronous_commit = on` защищает от потери на primary, но НЕ гарантирует, что данные применены и видны на standby (это `remote_apply`)? Что async может потерять подтверждённые клиенту транзакции при крахе primary? - **Кворумная репликация и trade-off CAP.** `ANY N` vs `FIRST N`, как выбрать N относительно числа реплик и требований к доступности записи vs durability. Геораспределённые синхронные реплики и влияние RTT на latency коммита. - **Конфликт recovery vs vacuum.** Глубокое понимание треугольника `hot_standby_feedback` ↔ bloat на primary ↔ `max_standby_streaming_delay` ↔ отмена запросов. Как диагностировать bloat, вызванный зависшим запросом на реплике. - **Логическая репликация в проде.** Обработка конфликтов (skip LSN, `pg_replication_origin_advance`), стриминг больших транзакций (PG 14+), DDL-проблема и как её обходят (вручную или через инструменты вроде pglogical/обёртки), синхронизация sequences, мониторинг застрявшей подписки. - **Causal consistency на уровне приложения.** LSN-tracking, `pg_wal_replay_wait` (PG 17), session-token/cookie с LSN для маршрутизации чтений — умеете ли спроектировать без глобального sticky-to-primary, который убивает смысл реплик. - **Анатомия failover.** Роль DCS и Raft, TTL lock, fencing/STONITH, watchdog в Patroni, timeline divergence, `pg_rewind` vs полный rebuild, как приложение узнаёт о новом primary (HAProxy + Patroni REST, `target_session_attrs`, service discovery), оценка RTO/RPO. - **Производительность replay.** Почему replay однопоточный и становится бутылочным горлышком, `recovery_prefetch` (PG 15+), влияние тяжёлых операций (массовые UPDATE, индексация) на lag, cascading replication для разгрузки primary. - **Пулинг и реплики.** Взаимодействие pgBouncer (transaction pooling) с read/write split, почему prepared statements и `target_session_attrs` ломаются в transaction-pooling режиме, мониторинг и circuit breaker по lag в Go-сервисе.