Перейти к содержанию

Architecture overview

Короткий справочник о том, как устроена backend-часть проекта и какие паттерны мы используем. Прочитать за 10–15 минут — чтобы ориентироваться в коде и в разговорах команды.

Структура документа повторяет каталог паттернов с microservices.io/patterns. Для каждой категории — таблица со статусом «Используем / Не используем / Планируется» и коротким объяснением, как именно у нас реализован паттерн.

Карта системы

Верхнеуровневый срез: откуда трафик приходит, какие сервисы есть, чем они обмениваются.

flowchart LR
    Client[Клиент<br/>web / mobile]
    GW[API Gateway<br/>Traefik<br/>TLS, JWT verify,<br/>rate-limit]
    subgraph K8s["Kubernetes кластер"]
        USR[user-service<br/>auth + profiles]
        REV[review-service<br/>отзывы + реакции]
        MED[media-service<br/>загрузка + storage]
        NOT[notification-service<br/>push/email/sms]
        subgraph Infra["общая инфра"]
            PG[(Postgres<br/>schema per service)]
            RDS[(Redis<br/>cache, rate-limit,<br/>dedup)]
            KFK[(Kafka<br/>kazmaps.*)]
            S3[(S3 / MinIO<br/>user uploads)]
        end
    end
    OBS[Observability<br/>Prometheus, Loki,<br/>Tempo, Grafana]

    Client -->|HTTPS| GW
    GW -->|/v1/*<br/>signed HMAC headers| USR
    GW -->|/v1/*| REV
    GW -->|/v1/*| MED
    GW -->|/v1/*| NOT

    USR <-->|/internal/*| REV
    REV <-->|/internal/*| MED

    USR -->|outbox| KFK
    REV -->|outbox| KFK
    MED -->|outbox| KFK
    KFK --> NOT
    KFK --> REV
    KFK --> MED

    USR --> PG
    REV --> PG
    MED --> PG
    NOT --> PG
    USR --> RDS
    REV --> RDS
    NOT --> RDS
    MED --> S3

    USR -.otlp.-> OBS
    REV -.otlp.-> OBS
    MED -.otlp.-> OBS
    NOT -.otlp.-> OBS

Правила на диаграмме:

  • Внешний трафик — всегда через gateway. Прямого доступа к сервисам извне нет.
  • Между сервисами: синхронные запросы — /internal/* (HTTP, HMAC внутренний токен), асинхронные факты — через Kafka и outbox.
  • Каждый сервис владеет своей схемой в Postgres. Cross-service JOIN запрещены.

Тип системы

Microservices architecture. Каждый сервис — отдельный git-репозиторий со своим жизненным циклом, pull request'ами, релизами и независимым деплоем. Межсервисное взаимодействие двумя способами:

  • Event-driven через Kafka — основной способ сигнализировать о фактах между сервисами (review.created, user.banned, photo.uploaded).
  • Синхронные HTTP-вызовы через внутренние REST-endpoint'ы — когда одному сервису нужны данные другого «прямо сейчас» для ответа на запрос пользователя. Внешний трафик всегда приходит через API Gateway.

Каждый сервис владеет своей БД (Postgres), своей схемой и своим outbox'ом.

Decomposition

Границы сервисов = границы владения данными. Один сервис владеет одной доменной областью и её таблицами; всё, что нужно от другой области, получается через событие или internal HTTP-вызов.

Паттерн Статус Как
Decompose by business capability Используем Сервисы нарезаны по capability'ям: auth+профили (user), отзывы (review), медиа (media), уведомления (notification).
Decompose by subdomain / DDD Используем Subdomain'ы совпадают с сервисами; внутри — стандартный DDD-слой domain/ с entity и sentinel-ошибками.
Strangler application Не используем Greenfield-проект, нет legacy-монолита, который надо душить.
Self-contained service Используем частично По возможности сервис отвечает за запрос полностью; для cross-service list-view используем API composition.

Практическое правило: если две capability'и начинают постоянно запрашивать данные друг у друга синхронно — проверяй, правильно ли проведена граница. Событийная связь допустима, плотная RPC-связь — повод пересобрать границы.

Deployment

Unit деплоя — Docker-образ одного сервиса. Образ собирается в CI репозитория сервиса, пушится в registry, Helm/kustomize раскатывает в Kubernetes. Каждый сервис деплоится своим пайплайном, независимо от остальных.

Паттерн Статус Как
Service per container Используем Один сервис = один Docker-образ (multistage, non-root, CGO_ENABLED=0, -ldflags="-s -w").
Single service instance per host Используем В Kubernetes каждый pod — один контейнер сервиса.
Multiple services per host Не используем
Serverless deployment Не используем
Service deployment platform Используем Kubernetes как платформа деплоя; Helm/kustomize для манифестов.
Microservice chassis Планируется Шаблон-генератор будущего сервис-репо (kazmaps-service-template) со стандартными cmd/server/main.go, middleware stack, Makefile, Dockerfile.
Service template Планируется То же, что chassis — единый скелет нового сервиса.
Sidecar Не используем

Cross-cutting concerns

Горизонтальные вопросы, которые касаются всех сервисов одинаково: конфигурация, секреты, общий скелет.

Паттерн Статус Как
Externalized configuration Используем Вся конфигурация через env; секреты — через SOPS / Vault, в образ не попадают. Загрузка — internal/config/ + валидация на старте (fail-fast, если обязательное поле пустое).
Microservice chassis / service template Планируется См. Deployment.

.env.example с placeholder'ами коммитится в сервис-репо; реальный .env — в .gitignore. Продовые секреты в контейнер попадают как env-переменные из Kubernetes Secret'ов, заполняемых из Vault.

Communication style

Правило по умолчанию: асинхронно через события. HTTP используется только тогда, когда абоненту нужен ответ внутри одного запроса пользователя.

Паттерн Статус Как
Messaging Используем Kafka + Watermill. Publisher/Subscriber, Router, DLQ — всё через watermill. Напрямую sarama/kafka-go не используем. Сериализация — JSON.
Remote Procedure Invocation — HTTP REST Используем Для internal endpoint'ов между сервисами (/internal/*). Chi-router на сервере, стандартный net/http на клиенте.
Remote Procedure Invocation — gRPC Не используем
Domain-specific protocol Не используем
Idempotent Consumer Используем Watermill Deduplicator middleware (Redis SETNX + TTL по Message.UUID/Event-Id) плюс идемпотентность на БД-уровне (unique constraint / upsert).

Имена Kafka-топиков — kazmaps.<service>.<entity>.<action>. Envelope каждого сообщения содержит Event-Type, Schema-Version, Correlation-Id, Source-Service, Published-At, traceparent. Подробности — в conventions/events.md.

External API

Внешний трафик всегда приходит через API Gateway. Напрямую до сервиса с интернета ходить нельзя — порт сервиса не выставляется наружу.

Паттерн Статус Как
API Gateway Используем Traefik на edge: TLS termination, routing по host/path, rate-limit, JWT verification перед проксированием в сервис.
Backend for Frontend Не используем Один общий REST API для web и mobile; разведение нагрузок делаем на уровне endpoint'ов, не отдельным BFF-слоем.

Сервисы различают два типа endpoint'ов: /v1/* — публичные (идут через gateway), /internal/* — только для других сервисов (блокируются на gateway, защищены middleware InternalToken).

Service discovery

Паттерн Статус Как
Client-side discovery Не используем
Server-side discovery Используем Kubernetes DNS: сервис доступен по имени <svc>.<ns>.svc.cluster.local. Клиент ничего не знает про instance'ы — маршрутизацией занимается kube-proxy.
Service registry Не используем отдельный Роль registry играет Kubernetes API / etcd.
Self-registration Не используем
3rd party registration Не используем

Reliability

Сервис должен уметь корректно переживать падение соседа: не залипать на retry бесконечно, не превращать одну точечную ошибку в каскад.

Паттерн Статус Как
Circuit breaker Используем Для internal HTTP-вызовов — per-pod gobreaker в клиентской обёртке. Подробно — patterns/retry-and-circuit-breaker.md. Для платных внешних провайдеров (SMS, push) — shared state через Redis, чтобы все pod'ы видели один счётчик.
Retry Используем Watermill Retry middleware на Kafka-handler'ах: 5 попыток, exponential backoff 500ms×2 с jitter. Для HTTP — cenkalti/backoff/v4 с MaxElapsedTime ≤ 5s. Полные правила — patterns/retry-and-circuit-breaker.md.
Timeout Используем Все I/O-вызовы под context.WithTimeout. HTTP-сервер имеет read/write/idle timeout. БД-пул имеет ConnTimeout.
Bulkhead Используем Ограничение concurrency в upload pipeline: семафор на параллельную обработку chunks, чтобы media-сервис не съел весь pool соединений БД.
Fail fast Используем На старте сервис валидирует конфиг, пингует БД/Kafka. Redis-ping — только в /readyz сервисов с fail-closed-операциями (см. how-to/handle-redis-outage.md). Что-то обязательное не отвечает — os.Exit(1).
Graceful degradation Используем Cache — fail-open при падении Redis (continue to DB). Rate-limit — fail-open для default-endpoint'ов, fail-closed для критичных. Stale cache как fallback при открытом CB. Детали — how-to/handle-redis-outage.md.

Security

Паттерн Статус Как
Access token Используем JWT. На edge Traefik проверяет подпись и прокидывает user-id в подписанных internal-заголовках в сервис. Внутри сервиса middleware GatewayAuth читает их и кладёт user-id/role в context.Value.

Сервис не проверяет JWT сам для каждого запроса — доверяет подписанным заголовкам от gateway. Прямой запрос в сервис, минуя gateway, не пройдёт сетевую политику кластера. Internal endpoint'ы дополнительно защищены статическим токеном (InternalToken middleware).

Observability

Три основных сигнала — логи, метрики, трейсы — идут из каждого сервиса и связываются по correlation_id / trace_id. Чтобы отладить запрос, который прошёл через 3 сервиса, достаточно одного id.

Паттерн Статус Как
Health check API Используем Каждый сервис выставляет /healthz (liveness — без проверки зависимостей) и /readyz (readiness — проверяет БД, Redis, Kafka).
Log aggregation Используем log/slog с JSON handler в stdout. Сбор — Loki, запросы — через Grafana. Все логи структурированные, request-id/correlation-id во всех записях запроса.
Distributed tracing Используем OpenTelemetry SDK, экспорт в Tempo. W3C trace context пробрасывается в Kafka metadata (traceparent) и в HTTP заголовки.
Exception tracking Планируется Единый exception tracker (Sentry/эквивалент) для группировки ошибок из prod — подключается отдельно от логов.
Application metrics Используем Prometheus endpoint /metrics. Регистрируются counters, histograms, gauges с low-cardinality лейблами. Алерты — на уровне Prometheus Alertmanager.
SLO / error budget Используем Availability + latency SLI per endpoint, multi-window burn-rate alerts (fast 14.4× / slow 6×). Formulas и шаблоны — conventions/slo-and-budget.md.
Audit logging Используем Каждый сервис имеет таблицу audit_log в своей схеме; пишется в бизнес-транзакции рядом с основной записью. Отдельный retention — 180 дней обычных событий, 1 год security-событий (см. conventions/data-retention.md).

Data management

Каждый сервис — единственный владелец своей БД. Другие сервисы видят данные только через API или через события. Никаких совместных записей в одну таблицу двумя сервисами.

Паттерн Статус Как
Database per service Используем У каждого сервиса своя схема в Postgres. Cross-schema JOIN между сервисами — запрещены.
Shared database Не используем Запрещено. Внешний ID хранится как BIGINT без REFERENCES.
Event sourcing Не используем Store-of-truth — обычные реляционные таблицы. События производные, а не первичный журнал.
CQRS Используем частично Через watermill/components/cqrs — подключаем, когда в сервисе становится >3 команд/событий на одну доменную модель и inline-switch становится каша. Пока большинство сервисов обходятся прямыми handler'ами.
Saga Не используем Планируется к подключению, когда появятся cross-service операции с откатом. Пока таких операций нет.
API composition Используем Для list-view, которые требуют данных из нескольких сервисов (например, «отзыв + автор + аватар»): фронт/gateway параллельно запрашивает нужные сервисы и склеивает ответы. Никакой cross-service join в БД.
CQRS read model Не используем Для list-view используем API composition, не отдельную read-model-БД.
Transactional outbox Используем Таблица outbox в схеме сервиса, запись в outbox идёт в той же транзакции, что и бизнес-операция. Публикация — через watermill-sql + components/forwarder. Retention acked-строк — 7 дней (см. conventions/data-retention.md).
Polling publisher Не используем Роль publisher'а выполняет components/forwarder.
Data retention Используем Soft-delete (deleted_at) для бизнес-сущностей, TTL + hard-delete через k8s CronJob для технических таблиц (outbox, sessions, audit). Детали — conventions/data-retention.md.
Transaction log tailing (CDC) Не используем Debezium/аналоги не подключены.
Domain event Используем Первичный способ сигнализации между сервисами. Имя события — факт в прошедшем времени (review.created, photo.uploaded).
Event-driven architecture Используем Все cross-service эффекты (уведомления, обновление денормализованных счётчиков и т.п.) — реакция на события, а не прямой RPC-вызов.

Testing

Пирамида тестов: много unit-тестов service-слоя, умеренно integration-тестов через testcontainers (реальные Postgres и Kafka), единичные end-to-end прогонки. Unit Watermill-handler'ов — через gochannel (in-memory pub-sub).

Паттерн Статус Как
Service component test Планируется Тестирование сервиса целиком, поднятого in-process с in-memory зависимостями. Пока каждый сервис покрыт unit-тестами + integration через testcontainers.
Consumer-driven contract test Планируется Pact или Schemathesis для проверки, что producer не ломает консьюмеров (актуально для event payload и internal REST).
Consumer-side contract test Планируется Параллельно с CDC — валидация, что консьюмер умеет читать все версии схем producer'а.

UI patterns

Не применимо — handbook описывает backend. UI-паттерны с microservices.io здесь не рассматриваются.

Что мы ЯВНО не делаем

Список вещей, которых у нас нет — специально, чтобы новички, пришедшие из других стеков, не искали их в коде:

  • Shared database между сервисами. У каждого своя схема, cross-schema JOIN запрещены. Внешний ID хранится как BIGINT без REFERENCES — FK через границу сервиса не делаются.
  • Event sourcing как store-of-truth. События — производные от состояния в реляционных таблицах, а не первичный журнал. Восстановление состояния из Kafka не поддерживается и не гарантируется.
  • gRPC для internal communication. Только HTTP REST + Kafka. .proto-файлов и codegen-пайплайнов в проекте нет.
  • Service mesh (Istio, Linkerd). Маршрутизация — Kubernetes + Traefik, наблюдаемость — OpenTelemetry-SDK напрямую из кода, retry и timeout — в клиентской библиотеке сервиса.
  • BFF слой. Один REST API обслуживает и web, и mobile. Разница в потребностях решается параметрами endpoint'а, а не отдельным сервисом на каждую клиентскую платформу.
  • Monolith. Никаких «сложим пока в один сервис, потом разделим». Каждая новая capability = новый сервис-репо с начала.
  • Client-side service discovery. Клиенты ходят по DNS-именам сервисов, balancing — Kubernetes kube-proxy. Никаких Consul/Eureka-клиентов в коде.
  • 2PC (two-phase commit) / distributed transactions. Для cross-service согласованности будем подключать Saga, когда появится такая задача. Пока cross-service write-операций с откатом нет — все изменения локальны в пределах одного сервиса и публикуются через outbox.
  • ORM. Работа с Postgres — напрямую через pgx/v5 + pgxpool. Никакого gorm, ent, sqlx или собственной обёртки, скрывающей SQL.
  • Бизнес-логика в SQL (триггеры, stored procedures, БД-функции с бизнес-правилами). Всё бизнес-правило — в Go, в internal/service/.
  • Kafka напрямую из handler'а HTTP. Публикация события — только через таблицу outbox внутри той же транзакции, что и основная запись.
  • Sync request-reply через Kafka. Для RPC используем HTTP internal endpoint, не round-trip через топики.

Связанные разделы handbook

Базовые conventions:

Deep-dive паттерны:

Рецепты и runbook'и:

Ссылки

Публичные материалы, к которым отсылает этот документ: