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/ > Модуль: System Design · Уровень: Senior ## TL;DR Чат-система — это набор stateful WebSocket-коннектов поверх stateless-логики доставки. Главная боль: коннекты живут на конкретных серверах, а сообщение приходит «не туда». Решается через **session registry** (Redis: `user_id → ws_node`) и брокер для маршрутизации между нодами. Доставка — **at-least-once + idempotency key + ACK**, порядок — **монотонный seq per channel** (а не глобальный). Хранение горячего потока — **wide-column (Cassandra/ScyllaDB)** с ключом `(channel_id, bucket) / seq`. Offline — fallback на **push (APNs/FCM)** + полная синхронизация по seq при reconnect. Узкое место senior-уровня — не throughput сообщений, а **миллионы одновременных stateful-коннектов**, presence-стадо и hot groups. ## Требования ### Functional - 1:1 и групповые чаты (до ~1000 участников в группе для базового дизайна; «каналы»/broadcast — отдельная история). - Доставка сообщений в (near) real-time, индикатор доставки/прочтения (delivered/read receipts). - Presence: online / offline / last seen, typing-индикаторы. - История сообщений, пагинация, синхронизация между устройствами одного пользователя (multi-device). - Offline-доставка: push-уведомление + догон сообщений при возврате online. - Гарантии: сообщение не теряется (durable), не дублируется на UI (dedup), порядок в рамках одного чата сохраняется. ### Non-functional - Latency p99 доставки online↔online < 200 ms. - Доступность 99.99% на сервисе доставки. - Durability сообщений: после ACK сервера сообщение не теряется (репликация RF=3). - Масштаб: десятки миллионов одновременных коннектов. - Горизонтальное масштабирование gateway и storage. - Безопасность: TLS, auth токены, опционально E2E-шифрование (меняет дизайн — сервер не видит контент, нет server-side поиска). ## Оценки нагрузки Возьмём средний по размеру мессенджер. | Параметр | Значение | Расчёт | |---|---|---| | DAU | 50M | дано | | Concurrent connections | ~10M | ~20% DAU онлайн одновременно | | Сообщений на пользователя/день | 40 | дано | | Messages/day | 2B | 50M × 40 | | Avg write QPS | ~23k | 2B / 86400 | | Peak write QPS | ~115k | ×5 пик | | Fan-out factor (avg) | ~3 | 1:1 и средние группы | | Delivery QPS (peak) | ~350k | 115k × 3 | | Размер сообщения (метаданные + текст) | ~1 KB | payload+headers | | Storage/day | ~2 TB | 2B × 1 KB | | Storage/год | ~730 TB | до репликации; ×3 RF ≈ 2.2 PB | | Память на коннект | ~10–50 KB | буферы рида/райта + структура сессии | | RAM на 1M коннектов | ~10–50 GB | + overhead Go runtime/GC | | Коннектов на WS-ноду | ~250k–500k | при тюнинге (ulimit, эфемерные порты, buffers) | | Число WS-нод | ~20–40 | 10M / 300k + запас на failover | Вывод по числам: write-нагрузка (23k–115k QPS) для wide-column store — рутина. Боль — **10M stateful TCP-коннектов**: это про лимиты ОС, GC-давление от миллионов горутин/буферов и про маршрутизацию. ## Архитектура ``` ┌─────────────────────────┐ ┌──────────┐ WSS (TLS) │ Session Registry │ │ Clients │◄─────────────┐ │ (Redis Cluster) │ │ mobile/ │ │ │ user_id → {node, conn} │ │ web │ │ │ presence + heartbeat │ └──────────┘ │ └───────────▲─────────────┘ │ │ register/lookup ┌──────┴──────┐ │ │ L4 LB / │ ┌─────┴──────────────┐ │ consistent │──►│ WS Gateway nodes │ │ hashing │ │ (Go: hub + │ └─────────────┘ │ goroutine/conn) │ └───┬──────────▲──────┘ │ publish │ deliver ▼ │ (pull/push) ┌─────────────────────────┐ │ Message Broker / Bus │ │ (Kafka / Redis Streams │ │ / NATS) topic per node │ │ or per channel shard │ └───┬───────────────┬──────┘ │ │ ┌────────────▼──┐ ┌─────▼──────────┐ │ Chat Service │ │ Push Service │ │ - validate │ │ APNs / FCM │ │ - assign seq │ │ for offline │ │ - fan-out │ └────────────────┘ │ - persist │ └───┬───────────┘ │ write ┌───────────▼────────────┐ ┌──────────────────┐ │ Message Store │ │ Metadata DB │ │ Cassandra/ScyllaDB │ │ (Postgres): │ │ (channel_id,bucket)/seq │ │ users, channels, │ │ RF=3 │ │ membership │ └─────────────────────────┘ └──────────────────┘ ``` ### Компоненты - **WS Gateway** — терминирует WSS, держит коннекты, на каждый коннект горутина (или пара read/write горутин) + центральный hub. При коннекте регистрирует `user_id → node_id` в session registry. Stateless по бизнес-логике, stateful по коннектам. - **Session Registry (Redis)** — карта «где сидит пользователь». Любая нода, получив сообщение для `user_id`, ищет его ноду и роутит туда. Также хранит presence (heartbeat с TTL). - **Message Broker** — транспорт между нодами и сервисами. Варианты: topic-per-node (нода подписана на свой topic, отправитель публикует в topic ноды-получателя) или topic-per-channel-shard. Развязывает приём и доставку, даёт буфер на пиках. - **Chat Service** — валидация, проверка membership, **присвоение seq** (монотонного per-channel), fan-out, запись в store, постановка push для offline-получателей. Stateless, горизонтально масштабируется. - **Message Store (Cassandra/ScyllaDB)** — горячий поток сообщений, оптимизирован под запись и под чтение «диапазон по каналу». - **Metadata DB (Postgres)** — пользователи, чаты, состав групп, настройки. Сильная консистентность, транзакции. - **Push Service** — для offline-получателей шлёт APNs/FCM. ## Ключевые решения и trade-offs ### Транспорт: WebSocket vs long-polling vs SSE - **WebSocket** — полнодуплекс, один TCP-коннект, низкий overhead на сообщение, нативный push с сервера. Минус: stateful коннект, балансировка/деплой сложнее, нужны heartbeat (ping/pong) и reconnect-логика. - **Long-polling** — работает везде, но дорого (новый HTTP-запрос на каждое сообщение), высокая latency и нагрузка на LB. Годится как fallback. - **SSE** — только сервер→клиент, для отправки нужен отдельный HTTP. Проще WS, но половинчатый. Подходит для feed/notifications, не для чата. Выбор: **WebSocket** как основной, long-polling как degraded fallback за корп. прокси. ### Connection state и маршрутизация Коннект пользователя B живёт на ноде N2. Сообщение от A приходит на ноду N1. Нужно доставить B. 1. N1 (через Chat Service) персистит сообщение и присваивает seq. 2. Lookup в session registry: `B → N2`. 3. Публикация в broker (topic ноды N2 или прямой gRPC N1→N2). 4. N2 находит локальный коннект B и пишет в его write-горутину. Session registry — Redis с TTL; при разрыве коннекта TTL чистит запись (или explicit deregister). Multi-device: `user_id` → **set** активных {device, node}. ### Доставка: at-least-once + dedup + ACK - Сервер при приёме генерит/принимает `client_msg_id` (UUID от клиента) → дедуп на записи (idempotency). Присваивает серверный `seq`. - Доставка получателю → клиент шлёт **ACK**; нет ACK в таймаут → ретрай (возможны дубли, поэтому клиент дедупит по `client_msg_id`/`seq`). - Exactly-once на практике = at-least-once + идемпотентность на обоих концах. «Честный» exactly-once в распределёнке слишком дорог. ### Ordering — seq per channel - **Не глобальный** ordering (нереалистично и не нужен). Монотонный `seq` **в рамках канала**: единый аллокатор на канал (шард Chat Service, владеющий каналом, или атомарный counter в Redis/Cassandra LWT). - Клиент сортирует/детектит дыры по seq → если seq не подряд, делает sync-запрос недостающего диапазона. - Альтернатива для слабых требований: server timestamp + tie-break, но при clock skew ломается; seq надёжнее. ### Presence - Heartbeat: клиент шлёт ping каждые ~30 c → Redis key `presence:{user}` с TTL ~45 c. Нет ping → ключ протух → offline. **Eventually consistent** (лаг до TTL). - Подписка на presence контактов: дорого делать на каждый контакт push. Обычно — pull при открытии чата + редкие апдейты, либо presence-каналы только для активных диалогов. - Typing — эфемерно, через broker без персиста, с коротким TTL. ### Групповые чаты: fan-out write vs read - **Fan-out on write** (push в «почтовый ящик» каждого участника при отправке): быстрое чтение, но дорого для больших групп (1 сообщение в группе 1000 → 1000 записей). Хорошо для маленьких/средних групп и 1:1. - **Fan-out on read** (одна копия в канал, читатели подтягивают): дёшево на запись, дороже на чтение, идеально для больших групп/каналов. - Гибрид: 1:1 и группы до ~N (например 256) — write fan-out (доставка + per-user mailbox для синка); большие группы/broadcast — read fan-out (одна копия в `(channel_id,...)`, клиенты тянут по seq). ### Offline + sync при reconnect - Получатель offline → сообщение всё равно персистится; ставится задача в Push Service (APNs/FCM) с превью. - При reconnect клиент шлёт `last_seen_seq` по каждому каналу → сервер отдаёт diff `(seq > last_seen_seq)`. Это и есть синхронизация и dedup-граница. - Per-device `last_seen_seq` для корректного multi-device. ### Хранение - **Cassandra/ScyllaDB**, partition key `(channel_id, time_bucket)`, clustering key `seq DESC`. Time-bucket (например по дню/часу) ограничивает размер партиции и распределяет нагрузку. Чтение «последние N» и «диапазон seq» — эффективно. - RF=3, запись `LOCAL_QUORUM`. TTL для эфемерных данных. Холодные данные → дешёвый объектный storage/архив. - Metadata (membership, настройки) → Postgres (транзакции, FK). ## Масштабирование и узкие места - **Stateful WS-коннекты** — главный масштаб. Sticky-сессии не обязательны (нода найдётся через registry), но reconnect должен переинициализировать сессию быстро. Деплой нод = массовый разрыв коннектов → нужен graceful drain (медленный сдвиг трафика, клиентский reconnect с jitter-backoff, иначе **thundering herd** на reconnect). - **Consistent hashing на LB** по `user_id` снижает перетасовку при добавлении/удалении нод, но при WS чаще достаточно random LB + registry. Consistent hashing полезен для channel-sharding в Chat Service (владелец канала = аллокатор seq). - **Hot groups / hot channels** — паблик-канал с миллионами подписчиков ломает write fan-out. Решение: read fan-out + кэш «хвоста» сообщений + батчинг доставки + rate limit на канал. - **Presence-стадо (thundering herd)** — при массовом реконнекте (деплой, сетевой сбой) лавина heartbeat/lookup в Redis. Митигация: jitter на heartbeat, батч-апдейты presence, отдельный Redis-кластер под presence, локальный кэш на нодах. - **GC-давление в Go** — миллионы горутин и буферов. Тюнинг: пул буферов (`sync.Pool`), `GOGC`/`GOMEMLIMIT`, объединение мелких write через буфер, уменьшение per-conn аллокаций, рассмотреть epoll-based libs (gnet, nbio) при экстремальном числе коннектов вместо goroutine-per-conn. - **Лимиты ОС** — `ulimit -n`, `net.ipv4.ip_local_port_range`, conntrack, размеры сокет-буферов, `SO_REUSEPORT`. - **Broker как bottleneck** — topic-per-node не масштабируется по числу нод линейно; channel-sharded topics + партиционирование Kafka по `channel_id` сохраняют порядок внутри партиции бесплатно. ### Go: hub + goroutine-per-connection (gorilla/websocket) ```go // Client — одна WS-сессия. Две горутины: readPump и writePump. type Client struct { hub *Hub conn *websocket.Conn send chan []byte // буфер исходящих; close => writePump завершается user string } type Hub struct { register chan *Client unregister chan *Client // локальные коннекты этой ноды: user_id -> множество устройств clients map[string]map[*Client]struct{} deliver chan Envelope // входящие из брокера для доставки mu sync.RWMutex } func (h *Hub) Run() { for { select { case c := <-h.register: h.mu.Lock() if h.clients[c.user] == nil { h.clients[c.user] = map[*Client]struct{}{} } h.clients[c.user][c] = struct{}{} h.mu.Unlock() // регистрируем в Redis: user -> этот node_id, с TTL registerSession(c.user, nodeID) case c := <-h.unregister: h.mu.Lock() if set, ok := h.clients[c.user]; ok { if _, ok := set[c]; ok { delete(set, c) close(c.send) if len(set) == 0 { delete(h.clients, c.user) deregisterSession(c.user, nodeID) } } } h.mu.Unlock() case env := <-h.deliver: // пришло из брокера: доставить локально h.mu.RLock() for c := range h.clients[env.ToUser] { select { case c.send <- env.Payload: default: // буфер переполнен -> медленный клиент: дропаем/закрываем // (sync произойдёт по last_seen_seq при reconnect) } } h.mu.RUnlock() } } } func (c *Client) writePump() { ticker := time.NewTicker(30 * time.Second) // ping defer func() { ticker.Stop(); c.conn.Close() }() for { select { case msg, ok := <-c.send: c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if !ok { // hub закрыл канал c.conn.WriteMessage(websocket.CloseMessage, nil) return } if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil { return } case <-ticker.C: c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { return // мёртвый коннект } } } } func (c *Client) readPump() { defer func() { c.hub.unregister <- c }() c.conn.SetReadDeadline(time.Now().Add(45 * time.Second)) c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(45 * time.Second)) refreshSessionTTL(c.user) // presence heartbeat return nil }) for { _, data, err := c.conn.ReadMessage() if err != nil { return // разрыв -> unregister в defer } // приходящее сообщение: dedup по client_msg_id, в Chat Service, // присвоение seq, fan-out через брокер c.hub.onInbound(c.user, data) } } ``` Ключевые приёмы: write только из `writePump` (нельзя писать в один `*websocket.Conn` из нескольких горутин — gorilla не потокобезопасен на запись), `send` — буферизированный канал с дропом при переполнении (медленный клиент не должен блокировать hub), ping/pong как heartbeat и для presence TTL. ## Вопросы на собеседовании **В:** Как доставить сообщение пользователю, чей коннект живёт на другой ноде? **О:** Session registry (Redis) хранит `user_id → node_id`. Отправляющая нода делает lookup и роутит сообщение на нужную ноду через брокер (topic-per-node) или прямой gRPC. Целевая нода находит локальный коннект в hub и пишет в его send-канал. Для multi-device — множество {device,node} на user. **В:** Какие гарантии доставки вы дадите и как достигнете «не теряем / не дублируем»? **О:** At-least-once на проводе + идемпотентность по `client_msg_id` на записи и на клиенте (dedup по seq). Durability — персист до ACK получателю с RF=3 `LOCAL_QUORUM`. Exactly-once «честный» не делаем — слишком дорого; эквивалент = at-least-once + dedup на обоих концах. **В:** Как обеспечить порядок сообщений в чате? **О:** Монотонный `seq` per-channel от единого аллокатора (шард-владелец канала или атомарный counter). Глобальный порядок не нужен и нереалистичен. Клиент сортирует по seq, детектит дыры и дозапрашивает недостающий диапазон. Kafka-партиционирование по channel_id даёт порядок внутри партиции бесплатно. **В:** Fan-out on write или on read для групп? **О:** Гибрид. 1:1 и небольшие/средние группы — write fan-out (быстрое чтение, per-user mailbox для синка). Большие группы/каналы — read fan-out (одна копия в канал, клиенты тянут по seq), иначе hot group убивает запись. Порог переключения по размеру группы. **В:** Как работает presence и почему он eventually consistent? **О:** Heartbeat (ping каждые ~30 c) обновляет Redis-ключ с TTL ~45 c. Нет ping → ключ протух → offline. Лаг до TTL — отсюда eventual consistency. Подписка на presence контактов — pull при открытии чата + точечные апдейты, чтобы не делать fan-out presence на всех. **В:** Что происходит при деплое WS-нод и массовом реконнекте? **О:** Graceful drain: медленно уводим трафик, клиенты реконнектятся с jitter+exponential backoff, чтобы избежать thundering herd. При reconnect клиент шлёт last_seen_seq → сервер отдаёт diff. Registry чистит старые сессии по TTL/explicit deregister. **В:** Где боттлнек при 10M коннектов и как масштабироваться? **О:** Не throughput сообщений, а stateful-коннекты: память/GC на горутины и буферы, лимиты ОС, presence-стадо. Масштаб: ~300k коннектов/нода → 30–40 нод; sync.Pool для буферов, GOMEMLIMIT, батчинг записей, отдельный Redis под presence, при экстриме — epoll-based (gnet/nbio) вместо goroutine-per-conn. **В:** Как хранить сообщения и почему wide-column? **О:** Cassandra/Scylla, partition `(channel_id, time_bucket)`, clustering `seq DESC`. Запись-оптимизирована, чтение «последние N / диапазон seq» эффективно, линейно масштабируется, RF=3. Time-bucket ограничивает рост партиции. Metadata (membership) — в Postgres ради транзакций. **В:** Что меняется при E2E-шифровании? **О:** Сервер не видит контент: нет server-side поиска/модерации/превью в push, fan-out усложняется (ключи на участника), история восстанавливается только при наличии ключей у клиента. Сервер становится «слепым» транспортом + key directory (например протокол Signal/X3DH + Double Ratchet). ## На что копают на senior+ - Чёткое различие «масштаб коннектов» vs «масштаб сообщений» — кандидат должен сразу назвать stateful-коннекты главной болью, а не CRUD сообщений. - Реалистичность гарантий: не обещать exactly-once и глобальный ordering; уметь обосновать at-least-once + dedup и per-channel seq. - Маршрутизация между нодами: понимание session registry, его TTL/консистентности, что происходит при гонке reconnect (старая нода ещё держит, новая уже зарегалась). - Backpressure на медленного клиента: что делать с переполненным send-каналом (дроп + sync по seq, а не блокировка hub). - Thundering herd при реконнектах и presence-стадо — конкретные митигации (jitter, backoff, батчинг, отдельный кластер). - Hot partition / hot group: как не убить Cassandra одной горячей партицией и как обслужить мега-канал. - Go-специфика: потоконебезопасность записи в один Conn, GC-давление от миллионов горутин, sync.Pool, GOMEMLIMIT, выбор goroutine-per-conn vs epoll. - Multi-device sync и per-device read state, корректный last_seen_seq. - Деплой stateful-сервиса без потери коннектов (graceful drain, connection draining на LB). - Трейд-офф выбора брокера (Kafka per-channel ordering vs NATS/Redis Streams latency) и почему именно он.