Раздел 24 · Микросервисы (Go)

Неделя 3: Postgres для Order и Inventory, JSONB, миграции

middle~60 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Неделя 3: Postgres для Order и Inventory, JSONB, миграции

In-memory было нужно чтобы сосредоточиться на контрактах и архитектуре. Теперь добавляем реальные хранилища: оба бизнес-сервиса (Order и Inventory) переезжают на PostgreSQL. У каталога запчастей появляются enum-категория и JSONB-атрибуты. Плюс миграции и docker-compose, чтобы локальный запуск был одной командой.

Цели недели

  1. Поднять два независимых инстанса PostgreSQL в docker-compose с volume-ами и healthcheck-ами (по инстансу на сервис).
  2. Заменить in-memory репозиторий OrderService на Postgres.
  3. Заменить in-memory репозиторий InventoryService на Postgres с осмысленной схемой каталога.
  4. Подключить миграции, обеспечить авто-накатку при старте.
  5. Сохранить тесты зелёными (unit с моками работают как и работали).

Один инстанс на сервис, не shared

Соблазн, поднять одну Postgres-машину и держать там схемы order_db и inventory_db, экономия RAM. Это работает в раннем dev, но мы намеренно делаем по инстансу:

  • изоляция миграций, миграции одного сервиса не упираются в lock-таблицу другого,
  • blast radius, упавший Postgres не валит сразу два сервиса,
  • скейлинг, у каталога 99% read, у Order смешанная нагрузка с outbox-write, разные настройки shared_buffers и work_mem,
  • подготовка к k8s, в проде это всё равно отдельные StatefulSet-ы.

Это и есть тема ADR-0002 на эту неделю.

Структура deploy/compose/

deploy/
└── compose/
    ├── core/docker-compose.yml         # общая сеть microservices_net
    ├── order/docker-compose.yml        # postgres-order
    └── inventory/docker-compose.yml    # postgres-inventory

Раскладываем по разным compose-файлам, потому что:

  • каждый сервис может подниматься независимо (docker compose -f deploy/compose/order/docker-compose.yml up -d),
  • общая сеть переиспользуется,
  • упрощается переход на k8s (один compose, один манифест),
  • легко управлять отдельными зависимостями (например, поднял только Postgres для миграций).

core/docker-compose.yml

name: microservices-core
networks:
  microservices_net:
    name: microservices_net
    driver: bridge

order/docker-compose.yml

name: microservices-order
services:
  postgres-order:
    image: postgres:17-alpine
    container_name: postgres-order
    environment:
      POSTGRES_USER: ${ORDER_PG_USER:-postgres}
      POSTGRES_PASSWORD: ${ORDER_PG_PASSWORD:-postgres}
      POSTGRES_DB: ${ORDER_PG_DB:-order}
    ports:
      - "${ORDER_PG_PORT:-5432}:5432"
    volumes:
      - postgres-order-data:/var/lib/postgresql/data
    networks:
      - microservices_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${ORDER_PG_USER:-postgres} -d ${ORDER_PG_DB:-order}"]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  postgres-order-data:

networks:
  microservices_net:
    external: true

networks: external: true означает, что сеть должна быть создана заранее (через core compose). Это форсит правильный порядок поднятия.

inventory/docker-compose.yml

name: microservices-inventory
services:
  postgres-inventory:
    image: postgres:17-alpine
    container_name: postgres-inventory
    environment:
      POSTGRES_USER: ${INVENTORY_PG_USER:-postgres}
      POSTGRES_PASSWORD: ${INVENTORY_PG_PASSWORD:-postgres}
      POSTGRES_DB: ${INVENTORY_PG_DB:-inventory}
    ports:
      - "${INVENTORY_PG_PORT:-5433}:5432"   # хост-порт другой, чтобы не пересекаться с order
    volumes:
      - postgres-inventory-data:/var/lib/postgresql/data
    networks:
      - microservices_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${INVENTORY_PG_USER:-postgres} -d ${INVENTORY_PG_DB:-inventory}"]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  postgres-inventory-data:

networks:
  microservices_net:
    external: true

Внутри docker-сети сервисы видят друг друга по имени (postgres-order:5432, postgres-inventory:5432), различные хост-порты нужны только если ты хочешь подключиться с ноута через psql.

Postgres, pgx или database/sql

Для production-Go рекомендую pgx. Причины:

  • нативный Postgres-протокол (быстрее database/sql),
  • богаче по фичам (LISTEN/NOTIFY, COPY, batch),
  • умеет в JSONB, arrays, intervals, enum без танцев с rendering-ом,
  • встроенный pool (pgxpool).

Можно использовать через database/sql-обёртку (stdlib.OpenDB), если хочется совместимости с другими тулзами (sqlx, sqlc), но в этом курсе берём напрямую.

Соединение

// order/internal/repository/order/repository.go
package orderrepo

import (
    "context"
    "github.com/jackc/pgx/v5/pgxpool"
)

type Repository struct {
    pool *pgxpool.Pool
}

func New(ctx context.Context, dsn string) (*Repository, error) {
    pool, err := pgxpool.New(ctx, dsn)
    if err != nil {
        return nil, err
    }
    if err := pool.Ping(ctx); err != nil {
        pool.Close()
        return nil, err
    }
    return &Repository{pool: pool}, nil
}

func (r *Repository) Close() { r.pool.Close() }

Запросы Order

// order/internal/repository/order/create.go
func (r *Repository) Create(ctx context.Context, o *model.Order) error {
    _, err := r.pool.Exec(ctx, `
        INSERT INTO orders (uuid, user_uuid, total_cents, status, part_uuids, created_at)
        VALUES ($1, $2, $3, $4, $5, $6)
    `, o.UUID, o.UserUUID, o.TotalCents, o.Status, o.PartUUIDs, o.CreatedAt)
    return err
}

Конвертер domain <-> repo-model живёт в repository/converter/order.go. Если у domain-Order поле Total time.Duration, а в БД total_seconds INT, конвертация одна точка изменения.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.