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 - RabbitMQ — это **smart broker / dumb consumer**: брокер хранит логику маршрутизации (exchanges + bindings) и выталкивает (push) сообщения потребителям. Kafka — наоборот: **dumb broker / smart consumer** (pull, потребитель сам держит offset). - Базовая модель AMQP 0-9-1: producer публикует в **exchange**, exchange по **binding** и **routing key** раскладывает сообщение по **очередям**, consumer читает из очереди. Очередь — единственное место, где сообщение реально хранится. - Типы exchange: `direct` (точное совпадение routing key), `topic` (паттерны с `*` и `#`), `fanout` (broadcast, ключ игнорируется), `headers` (матчинг по заголовкам). - Надёжность доставки строится на трёх китах: **manual ack/nack** (подтверждение от consumer), **publisher confirms** (подтверждение от брокера producer-у), **persistent + durable + quorum queue** (переживание рестарта/отказа узла). - **DLQ** (dead-letter exchange) ловит сообщения, которые были отвергнуты (reject/nack без requeue), протухли по TTL, или вытеснены по max-length. - **Prefetch** (`basic.qos`) — главный рычаг производительности: без него один медленный consumer заберёт всю очередь round-robin'ом; с prefetch=1 получается fair dispatch. - **Quorum queues** (на базе Raft) — современный стандарт для надёжности. Classic mirrored queues **deprecated** и удалены в RabbitMQ 4.0. - Выбор: RabbitMQ — для **task/job queue**, сложной маршрутизации, RPC, per-message acknowledgement. Kafka — для **event streaming**, replay, высокого throughput и ordered log. ## Теория ### Модель AMQP 0-9-1 AMQP 0-9-1 (Advanced Message Queuing Protocol) — это **бинарный wire-протокол**, реализуемый RabbitMQ по умолчанию. Не путать с AMQP 1.0 — это совершенно другой протокол (он тоже поддерживается RabbitMQ через плагин, но дефолтная модель — именно 0-9-1). Ключевая идея, которую важно произнести на собеседовании: **producer никогда не публикует напрямую в очередь**. Он публикует в exchange. Exchange — это маршрутизатор без хранения; очередь — буфер с хранением. ``` binding (routing pattern) | Producer --publish--> [ Exchange ] --route--> [ Queue ] --deliver--> Consumer (routing key) \--route--> [ Queue ] --deliver--> Consumer ``` Иерархия сущностей: - **Connection** — TCP-соединение к брокеру (дорогое, держим долгоживущим). - **Channel** — лёгкая виртуальная сессия внутри connection. Вся работа (publish, consume, declare) идёт через channel. Каналы НЕ потокобезопасны — один channel на одну горутину, не шарить между горутинами. - **Exchange** — точка входа, выполняет маршрутизацию. - **Queue** — FIFO-буфер (с оговорками про prefetch/requeue), хранит сообщения. - **Binding** — правило «связать exchange и queue по такому-то паттерну». #### Виртуальные хосты (vhost) vhost — это namespace/изоляция: свои exchanges, queues, permissions. Аналог отдельного логического брокера. Используется для multi-tenancy. ### Exchanges: четыре типа маршрутизации Терминология, которую часто путают: - **routing key** — атрибут *сообщения*, который выставляет **producer** при публикации. - **binding key** — паттерн, указанный при создании **binding** между exchange и queue. Маршрутизация = матчинг routing key (из сообщения) против binding key (из bindings). #### 1. Direct exchange Сообщение попадает в очереди, у которых binding key **точно равен** routing key. ``` binding key="error" --> queue Q1 binding key="info" --> queue Q2 binding key="error" --> queue Q3 (одинаковый ключ -> оба получат) publish(routing_key="error") -> Q1 и Q3 publish(routing_key="info") -> Q2 ``` Дефолтный безымянный exchange (`""`) — это особый direct exchange: каждая очередь автоматически забиндена к нему по своему имени. Поэтому `publish(exchange="", routing_key="my-queue")` кладёт прямо в очередь `my-queue`. Это «упрощённый» режим, который и создаёт иллюзию, что публикуют «в очередь». #### 2. Topic exchange Routing key — это строка из слов, разделённых точками: `logs.error.payment`. Binding key — паттерн с двумя wildcard: - `*` (звёздочка) — заменяет **ровно одно слово**. - `#` (решётка) — заменяет **ноль или более слов**. ``` binding key="logs.*.payment" matches "logs.error.payment" (yes) matches "logs.info.payment" (yes) NOT "logs.error.db.payment" (no, * = одно слово) binding key="logs.#" matches "logs" (yes, # = ноль слов) matches "logs.error.payment.db" (yes) binding key="*.error.*" matches "app.error.db" (yes) NOT "error.db" (no) binding key="#" matches всё (поведение как fanout) ``` Topic — самый гибкий тип; direct и fanout логически выражаются через topic, но специализированные типы быстрее. #### 3. Fanout exchange Игнорирует routing key полностью. Копию сообщения получают **все** забинженные очереди. Классический pub/sub broadcast (рассылка событий, кэш-инвалидация всем нодам). #### 4. Headers exchange Маршрутизация по заголовкам сообщения вместо routing key. В binding указывается аргумент `x-match`: - `x-match=all` — должны совпасть **все** указанные заголовки (AND). - `x-match=any` — достаточно **одного** (OR). Используется редко (медленнее topic), но полезен, когда критерий маршрутизации — несколько независимых атрибутов, плохо ложащихся в иерархический routing key. ### Очереди и их свойства - **durable** — определение очереди переживает рестарт брокера (сама очередь не исчезнет). Это про метаданные очереди, НЕ про сообщения. - **persistent message** (`delivery_mode=2`) — сообщение пишется на диск. Чтобы сообщение пережило рестарт, нужно И durable queue, И persistent message. Любое из двух по отдельности не спасает. - **exclusive** — очередь видна только текущему connection и удаляется при его закрытии. - **auto-delete** — удаляется, когда отвалится последний consumer. Важно: persistent message не гарантирует «уже на диске» в момент возврата publish. Без publisher confirms сообщение может потеряться в буфере брокера до fsync. ### Acknowledgements: ack / nack / reject Consumer должен сообщить брокеру судьбу сообщения. Это контракт **at-least-once** доставки. - **auto-ack** (`autoAck=true`, в протоколе `no-ack`): брокер считает сообщение доставленным сразу после отправки в сокет. Если consumer упадёт, не обработав — **сообщение потеряно**. Быстро, но небезопасно. Использовать только когда потеря допустима (метрики, логи). - **manual ack** (`autoAck=false`): consumer явно вызывает `Ack`. Пока ack не пришёл, сообщение остаётся «unacked» (в полёте). Если connection/channel закроется — брокер вернёт unacked-сообщения обратно в очередь (requeue). Это и даёт at-least-once. Три способа подтвердить: - **basic.ack** — успешно обработано, удалить. - **basic.nack** — отвергнуть; поддерживает `multiple` и `requeue`. Расширение RabbitMQ (в чистом AMQP только reject). - **basic.reject** — отвергнуть **одно** сообщение, с флагом `requeue`. Флаги: - **requeue=true** — вернуть сообщение в голову очереди для повторной попытки. Опасность: «poison message» зациклится навсегда (получили -> nack(requeue) -> снова получили -> ...). Для отравленных сообщений requeue=false + DLQ. - **requeue=false** — не возвращать; сообщение либо отправится в DLX (если настроен), либо будет отброшено. - **multiple=true** — батч-ack: подтвердить это сообщение И все предыдущие неподтверждённые с меньшим delivery tag на этом канале. Сильно повышает throughput, но требует аккуратности при out-of-order обработке. **delivery tag** — это монотонный per-channel идентификатор доставки (не свойство сообщения). Поэтому ack валиден только на том же канале, через который пришла доставка. ### Prefetch (basic.qos): fair dispatch vs round-robin По умолчанию брокер раздаёт сообщения потребителям **round-robin**, проталкивая их сразу, не глядя на занятость consumer. Проблема: если один consumer медленный, ему всё равно набросают сообщений в локальный буфер, пока быстрые простаивают. `basic.qos(prefetch_count=N)` ограничивает число **неподтверждённых** сообщений у consumer: брокер не пошлёт (N+1)-е, пока не получит ack за одно из N. ``` prefetch=1 (fair dispatch): Consumer A (медленный): держит 1, пока не ack-нет — больше не получает Consumer B (быстрый): быстро ack-ает -> получает следующее => работа течёт к тому, кто свободен ``` - **prefetch=1** — максимальная справедливость, но round-trip ack на каждое сообщение = меньше throughput. - **prefetch=больше** — выше throughput за счёт пайплайнинга, но риск, что одна нода нахватает много и при падении вернёт большой requeue-батч + хуже балансировка. QoS бывает per-channel и global (на весь канал vs на каждого consumer канала). Тюнинг prefetch — классический senior-вопрос про производительность. ### Dead Letter Exchange (DLX) и причины dead-letter Очередь можно настроить с аргументами: - `x-dead-letter-exchange` — куда отправлять «мёртвые» сообщения. - `x-dead-letter-routing-key` — заменить routing key (если не задан — сохраняется исходный). Сообщение становится dead-lettered по одной из причин (фиксируется в заголовке `x-death` с reason): 1. **rejected** — consumer сделал `basic.reject` или `basic.nack` с **requeue=false**. 2. **expired** — истёк message TTL (per-message или queue-level). 3. **maxlen** — очередь достигла `x-max-length` / `x-max-length-bytes`, сообщение вытеснено. 4. **delivery_limit** — превышен лимит доставок (актуально для quorum queues с `x-delivery-limit`). Заголовок `x-death` накапливает историю прохождений — по нему делают retry с backoff и счётчиком попыток, отправляя в DLQ окончательно после N ретраев. Типовой паттерн retry с задержкой: основная очередь -> DLX -> «retry»-очередь с TTL -> по истечении TTL её DLX возвращает обратно в основную. Так делают delayed retry без отдельного scheduler (либо плагин `rabbitmq_delayed_message_exchange`). ### Message TTL - **per-queue TTL** (`x-message-ttl`) — TTL для всех сообщений очереди. - **per-message TTL** (`expiration` при публикации) — для конкретного сообщения. - При обоих действует минимальный. Нюанс: per-queue TTL проверяется только у **головы** очереди — протухшее сообщение в середине будет удалено лишь когда дойдёт до головы (для классических очередей). Это влияет на момент попадания в DLX. `x-expires` — TTL на саму очередь (удалить неиспользуемую очередь). ### Publisher confirms Без подтверждений `publish` — fire-and-forget: producer не знает, дошло ли сообщение до очереди. Сообщение может потеряться (брокер упал до fsync, ошибка маршрутизации). **Publisher confirms** — асинхронный механизм: producer переводит канал в confirm-режим (`confirm.select`), и брокер присылает `basic.ack` за каждое сообщение (по его sequence number) после того, как оно надёжно принято (для persistent — записано на диск; для зеркалируемых/quorum — реплицировано в кворуме). При проблеме приходит `basic.nack`. Это даёт producer-у at-least-once на стороне публикации. Confirms поддерживают `multiple` (батч-подтверждение по sequence number), как и acks. Не путать с **transactions** (`tx.select`) — AMQP-транзакции синхронны и в ~250 раз медленнее, на практике почти всегда выбирают confirms. ### Mandatory flag + basic.return `publish(mandatory=true)`: если сообщение **не удалось замаршрутизировать ни в одну очередь** (нет подходящего binding), брокер вернёт его producer-у через `basic.return` (с reason code, напр. `NO_ROUTE`). Без mandatory такое сообщение **молча отбрасывается**. Это защита от «чёрной дыры»: публикуете в exchange, но binding-а нет — сообщение исчезает бесследно. Senior должен совмещать publisher confirms (дошло до брокера) + mandatory/return (нашлась ли очередь). > Был ещё флаг `immediate` (доставить, только если есть готовый consumer прямо сейчас) — он **удалён** в RabbitMQ начиная с версии 3.0. На собеседовании это хороший «детальный» факт. ### Quorum queues (Raft) vs classic mirrored queues (deprecated) **Classic mirrored queues** (HA-queues, политика `ha-mode`) — старый способ репликации: один master + зеркала. Проблемы: split-brain, потеря сообщений при определённых сетевых разделениях, сложная синхронизация зеркал. Они **deprecated** с 3.x и **полностью удалены в RabbitMQ 4.0**. **Quorum queues** — современная замена, построены на **Raft** (consensus-алгоритм). Каждая quorum queue — это реплицированная state machine: лидер + фолловеры, запись считается зафиксированной после подтверждения **большинством** (кворумом) реплик. Свойства: - Предсказуемое поведение при сетевых разделениях (CP-выбор: при потере кворума очередь становится недоступной для записи, но не теряет/не дублирует подтверждённые данные). - `x-delivery-limit` — встроенная защита от poison message (после N доставок -> dead-letter). - Всегда durable и реплицируемы; рекомендуются для всего, где важна сохранность. - Минусы: больше overhead памяти/диска, число реплик обычно нечётное (3/5) для кворума, не подходят для очень коротких/временных очередей и не поддерживают некоторые фичи (например, per-message priority, хотя многое уже добавлено). Также существуют **streams** (RabbitMQ Streams) — append-only лог в духе Kafka, для high-throughput и replay; но это отдельная тема. ### Сравнение с Kafka | Критерий | RabbitMQ (AMQP) | Kafka | |---|---|---| | Модель доставки | **Push** (брокер проталкивает consumer-у) | **Pull** (consumer тянет по offset) | | Где «ум» | **Smart broker / dumb consumer** (маршрутизация в брокере) | **Dumb broker / smart consumer** (consumer держит offset/логику) | | Хранение | Очередь = буфер; сообщение **удаляется после ack** | Лог с **retention** (по времени/размеру); сообщение НЕ удаляется после чтения | | Маршрутизация | Богатая: direct/topic/fanout/headers, bindings | Только topic + partition (по ключу); фильтрация на стороне consumer | | Replay (перечитать) | Нет (ack удалил) — либо отдельная DLQ/streams | **Да** — seek по offset, перечитать с любой точки | | Ordering | FIFO в пределах очереди, но prefetch/requeue/несколько consumer ломают строгий порядок | Строгий порядок **в пределах партиции** | | Throughput | Десятки–сотни тыс. msg/s; ниже из-за per-message ack/routing | Очень высокий (млн msg/s) — последовательная запись в лог, батчинг | | Consumer scaling | Несколько consumer на очередь (competing consumers) | Партиции; параллелизм ограничен числом партиций в группе | | Acknowledgement | Per-message ack/nack/reject, requeue | Commit offset (обычно батчами); нет per-message reject | | Durability/репликация | Quorum queues (Raft) | Партиции реплицируются (ISR), acks=all | | Backpressure | Естественный через prefetch | Consumer сам регулирует pull rate | | Типичный use case | **Task/job queue**, RPC, сложная маршрутизация, отложенные задачи, командный поток | **Event streaming**, аналитика, лог событий, event sourcing, CDC, fan-out на много независимых читателей | Главный нарратив для собеседования: RabbitMQ оптимизирован под **«сделай эту работу один раз и убери из очереди»**, Kafka — под **«сохрани поток событий, чтобы много разных потребителей могли читать и перечитывать его независимо»**. ### Пример на Go (github.com/rabbitmq/amqp091-go) `amqp091-go` — официальный форк/преемник `streadway/amqp`. Производитель с publisher confirms и mandatory: ```go package main import ( "context" "log" "time" amqp "github.com/rabbitmq/amqp091-go" ) func publisher() { conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/") if err != nil { log.Fatal(err) } defer conn.Close() ch, err := conn.Channel() if err != nil { log.Fatal(err) } defer ch.Close() // Topic exchange, durable if err := ch.ExchangeDeclare( "logs.topic", "topic", true, // durable false, // autoDelete false, // internal false, // noWait nil, ); err != nil { log.Fatal(err) } // Включаем publisher confirms на канале. if err := ch.Confirm(false /* noWait */); err != nil { log.Fatal(err) } confirms := ch.NotifyPublish(make(chan amqp.Confirmation, 1)) // Возвраты по mandatory (NO_ROUTE). returns := ch.NotifyReturn(make(chan amqp.Return, 1)) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err = ch.PublishWithContext(ctx, "logs.topic", // exchange "logs.error.payment", // routing key true, // mandatory -> вернётся через NotifyReturn, если нет binding false, // immediate (удалён, всегда false) amqp.Publishing{ ContentType: "application/json", DeliveryMode: amqp.Persistent, // delivery_mode=2, пишется на диск Body: []byte(`{"event":"payment_failed"}`), Timestamp: time.Now(), }, ) if err != nil { log.Fatal(err) } select { case c := <-confirms: if c.Ack { log.Printf("broker confirmed delivery tag=%d", c.DeliveryTag) } else { log.Printf("broker NACKed tag=%d (не сохранено)", c.DeliveryTag) } case r := <-returns: log.Printf("returned: %s (нет очереди для routing key)", r.ReplyText) case <-ctx.Done(): log.Printf("timeout ожидания confirm") } } ``` Потребитель с manual ack, prefetch и DLX-обработкой: ```go func consumer() { conn, _ := amqp.Dial("amqp://guest:guest@localhost:5672/") defer conn.Close() ch, _ := conn.Channel() defer ch.Close() // Очередь с DLX и лимитом длины. args := amqp.Table{ "x-dead-letter-exchange": "dlx.exchange", "x-dead-letter-routing-key": "failed", "x-message-ttl": int32(60000), // 60s TTL "x-max-length": int32(10000), // вытеснение -> DLX (maxlen) "x-queue-type": "quorum", // quorum queue (Raft) "x-delivery-limit": int32(5), // poison protection } q, err := ch.QueueDeclare("payments", true, false, false, false, args) if err != nil { log.Fatal(err) } _ = ch.QueueBind(q.Name, "logs.*.payment", "logs.topic", false, nil) // Fair dispatch: не более 10 неподтверждённых сообщений у этого consumer. if err := ch.Qos(10 /* prefetchCount */, 0, false /* global */); err != nil { log.Fatal(err) } msgs, err := ch.Consume( q.Name, "worker-1", false, // autoAck=false -> manual ack (at-least-once) false, false, false, nil, ) if err != nil { log.Fatal(err) } for d := range msgs { if err := process(d.Body); err != nil { // requeue=false -> уйдёт в DLX (reason: rejected). // requeue=true зациклил бы poison message. _ = d.Nack(false /* multiple */, false /* requeue */) continue } _ = d.Ack(false /* multiple */) } } func process(b []byte) error { /* ... */ return nil } ``` Ключевые методы для senior-словаря: `Confirm`, `NotifyPublish`, `NotifyReturn`, `Qos`, `Nack(multiple, requeue)`, `Ack(multiple)`, аргументы очереди `x-dead-letter-exchange`, `x-queue-type=quorum`. ## Подводные камни / gotchas - **Channel НЕ потокобезопасен.** Шаринг одного канала между горутинами публикует/читает с гонками и ломает delivery tags. Один channel на горутину; connection можно шарить. - **delivery tag привязан к каналу.** Ack/Nack валидны только на том канале, где пришла доставка. После реконнекта старые теги невалидны. - **durable queue ≠ persistent message.** Нужны оба, иначе сообщения теряются при рестарте брокера. - **publish без confirms — fire-and-forget.** Возврат из `Publish` не означает, что брокер сохранил/замаршрутизировал сообщение. - **mandatory забывают.** Без него сообщение без binding исчезает молча. Confirms подтвердят «дошло до брокера», но не «попало в очередь». - **requeue=true + poison message = бесконечный цикл.** Сообщение, которое всегда падает, будет крутиться вечно. Нужен delivery-limit (quorum) или счётчик через DLX/x-death. - **auto-ack теряет данные.** Брокер ack-ает в момент отправки в сокет; падение consumer = потеря. - **prefetch по умолчанию неограничен** (для старого поведения) — один consumer может нахватать всю очередь, ломая балансировку. Всегда ставьте Qos. - **Большой prefetch = большой requeue при падении** и неравномерная нагрузка. - **Очередь как «общая шина» переполняется.** Без max-length/TTL медленный consumer приводит к росту очереди, swap, деградации брокера (flow control / memory alarm заблокирует publishers). - **Classic mirrored queues удалены в 4.0.** Код/конфиги с `ha-policy` сломаются — мигрируйте на quorum queues. - **Per-queue TTL чистится только с головы очереди** (classic) — протухшее в середине дойдёт до DLX с задержкой. - **Memory/disk alarms** блокируют publishers (TCP backpressure) — приложение «зависает» на publish, хотя это by design. ## Вопросы на собеседовании **В:** В чём разница между routing key и binding key? **О:** Routing key — атрибут сообщения, который выставляет producer при публикации. Binding key — паттерн, заданный при создании binding между exchange и queue. Маршрутизация — это матчинг routing key против binding key по правилам типа exchange (точное равенство для direct, паттерн с `*`/`#` для topic). **В:** Что произойдёт с `*` и `#` в topic-биндинге `logs.*` для routing key `logs.error.db`? **О:** Не совпадёт. `*` заменяет ровно одно слово, а тут после `logs.` два слова. Совпало бы `logs.#` (`#` = ноль или более слов) или `logs.*.*`. **В:** Чем publisher confirms отличаются от consumer ack? **О:** Это разные концы пути. Confirms — подтверждение от брокера producer-у, что сообщение надёжно принято (записано/реплицировано). Consumer ack — подтверждение от consumer брокеру, что сообщение обработано и можно удалять. Для end-to-end at-least-once нужны оба, плюс mandatory для проверки наличия очереди. **В:** Зачем нужен prefetch и что даёт prefetch=1? **О:** Prefetch (`basic.qos`) ограничивает число неподтверждённых сообщений у consumer. Без него брокер раздаёт round-robin, не глядя на занятость, и медленный consumer забивается, пока быстрые простаивают. prefetch=1 даёт fair dispatch: следующее сообщение приходит только после ack предыдущего — работа течёт к свободным. Цена — больше ack round-trip и ниже throughput. **В:** Назовите причины, по которым сообщение попадает в DLQ. **О:** (1) reject/nack с requeue=false; (2) истёк message TTL; (3) превышен max-length/max-length-bytes (вытеснение); (4) превышен delivery-limit (quorum queues). Причина пишется в заголовок `x-death`. **В:** Как сделать retry с задержкой без внешнего планировщика? **О:** Через DLX + TTL: основная очередь reject-ит в retry-очередь, у которой message-TTL и DLX обратно на основную. По истечении TTL сообщение «оживает» и возвращается. Либо плагин delayed-message-exchange. Счётчик попыток ведём по `x-death`, после N ретраев — в постоянную DLQ. **В:** Чем quorum queues лучше classic mirrored queues? **О:** Quorum queues на Raft: запись фиксируется кворумом реплик, предсказуемое поведение при сетевых разделениях (нет split-brain, не теряют подтверждённые данные), встроенный delivery-limit для poison messages. Mirrored queues страдали от split-brain и потери сообщений и удалены в RabbitMQ 4.0. **В:** Когда выбрать Kafka вместо RabbitMQ? **О:** Когда нужен replay (перечитать историю по offset), очень высокий throughput, строгий ordered лог, много независимых потребителей одного потока, event sourcing/streaming/аналитика. RabbitMQ — для task queue, сложной маршрутизации, RPC, per-message ack/reject, отложенных задач, где сообщение удаляется после обработки. **В:** Что делает флаг mandatory и что без него? **О:** При mandatory=true, если сообщение нельзя замаршрутизировать ни в одну очередь (нет binding), брокер вернёт его producer-у через basic.return (NO_ROUTE). Без mandatory такое сообщение молча отбрасывается — «чёрная дыра». **В:** Сообщение помечено persistent, очередь durable. Гарантирована ли сохранность сразу после publish? **О:** Нет. Без publisher confirms producer не знает, что брокер успел сделать fsync. Брокер может упасть с сообщением в буфере до записи на диск. Гарантию даёт только confirm (для persistent он приходит после записи на диск / репликации кворумом). ## На что копают на senior+ - **Точная семантика доставки.** AMQP даёт at-least-once; exactly-once на уровне брокера нет — нужна идемпотентность consumer (dedup по message-id/business key). Умейте объяснить, где именно появляются дубликаты (requeue после падения до ack, реконнект с unacked). - **Ordering под нагрузкой.** Строгий FIFO ломается при нескольких competing consumers, prefetch>1 и requeue (сообщение возвращается, но другие уже ушли вперёд). Если нужен порядок — single consumer / single active consumer / партиционирование по ключу (или Kafka). - **Backpressure и flow control.** Memory/disk alarms блокируют publishers через TCP; `connection.blocked`. Senior понимает, что «зависший publish» — это защита брокера, и проектирует с max-length/TTL/quorum, а не безразмерные очереди. - **Connection/channel модель и реконнект.** Топология (exchanges/queues/bindings) и consumers должны пересоздаваться после реконнекта; delivery tags инвалидируются. Часто используют обёртки с авто-recovery, но важно понимать, что именно теряется. - **Quorum queues внутри = Raft.** Кворум, нечётное число реплик, поведение при потере большинства (становится недоступной на запись — CP). Связка с темой консенсуса. - **Сравнение архитектур.** Чётко разделять smart-broker/push vs dumb-broker/pull и вытекающие последствия: replay, retention, throughput, масштабирование, ordering. Это любимый «системный» вопрос. - **Идемпотентный producer + transactional outbox.** Как не потерять и не задвоить событие между БД и брокером (outbox pattern, dedup на стороне consumer) — частый follow-up.