Урок 39: Монолит vs микросервисы — практический разбор на Go

Урок 39. Монолит vs микросервисы — практический разбор на Go

🔄 Node.js → Go (особенности архитектуры):

📋 Что изучаем

📁 СТРУКТУРА: Modular Monolith

monolithic-app/ ├── cmd/ │ └── server/ │ └── main.go # Точка входа, DI ├── internal/ │ ├── domain/ # Общие типы и интерфейсы │ │ ├── user.go │ │ └── order.go │ ├── users/ # Модуль пользователей │ │ ├── repository.go # Интерфейс репозитория │ │ ├── postgres.go # Реализация │ │ ├── service.go # Бизнес-логика │ │ └── transport.go # HTTP/gRPC хендлеры │ ├── orders/ # Модуль заказов │ │ ├── repository.go │ │ ├── postgres.go │ │ ├── service.go │ │ └── transport.go │ ├── payments/ # Модуль платежей │ │ ├── repository.go │ │ ├── postgres.go │ │ ├── service.go │ │ └── transport.go │ └── shared/ # Общая инфраструктура │ ├── database/ │ ├── logger/ │ └── middleware/ ├── migrations/ └── go.mod

📁 СТРУКТУРА: Микросервисы

microservices/ ├── api-gateway/ # Gateway (HTTP → gRPC) │ ├── cmd/server/main.go │ └── internal/ ├── user-service/ # Сервис пользователей │ ├── cmd/server/main.go │ ├── internal/ │ └── proto/user.proto ├── order-service/ # Сервис заказов │ ├── cmd/server/main.go │ ├── internal/ │ └── proto/order.proto ├── payment-service/ # Сервис платежей │ ├── cmd/server/main.go │ ├── internal/ │ └── proto/payment.proto ├── notification-service/ # Сервис уведомлений │ ├── cmd/consumer/main.go # Kafka consumer │ └── internal/ ├── docker-compose.yml └── k8s/

💻 Modular Monolith: композиция модулей

// cmd/server/main.go — КОМПОЗИЦИЯ МОДУЛЕЙ
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "monolith/internal/shared/database"
    "monolith/internal/users"
    "monolith/internal/orders"
    "monolith/internal/payments"
)

func main() {
    log.Println("🏗️ Запуск Modular Monolith...")

    // 1. Инициализация общей инфраструктуры
    db := database.MustConnect(os.Getenv("DATABASE_URL"))
    defer db.Close()

    // 2. Инициализация модулей (каждый независим!)
    userModule := users.NewModule(db)
    orderModule := orders.NewModule(db)
    paymentModule := payments.NewModule(db)

    // 3. Связываем модули через ИНТЕРФЕЙСЫ (не прямые вызовы!)
    // OrderService нужен UserService для проверки пользователя
    orderModule.SetUserService(userModule.Service())
    // PaymentService нужен OrderService для получения суммы
    paymentModule.SetOrderService(orderModule.Service())

    // 4. Настраиваем HTTP-роутер
    mux := http.NewServeMux()
    userModule.RegisterRoutes(mux)    // /api/users/*
    orderModule.RegisterRoutes(mux)   // /api/orders/*
    paymentModule.RegisterRoutes(mux) // /api/payments/*

    // 5. Запуск
    server := &http.Server{Addr: ":8080", Handler: mux}
    go server.ListenAndServe()

    // Graceful shutdown
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    server.Shutdown(context.Background())
}

// ╔══════════════════════════════════════════════════════════╗
// ║  МОДУЛЬ (пример: orders/module.go)                      ║
// ╚══════════════════════════════════════════════════════════╝

// Module — самодостаточный модуль
type Module struct {
    repo        OrderRepository
    service     *OrderService
    handler     *OrderHandler
    userService UserService  // Зависимость от другого модуля (ИНТЕРФЕЙС!)
}

// UserService — интерфейс, который нужен от модуля users
type UserService interface {
    Exists(ctx context.Context, userID string) (bool, error)
}

func NewModule(db *database.Pool) *Module {
    repo := NewPostgresOrderRepo(db)
    service := NewOrderService(repo)
    handler := NewOrderHandler(service)

    return &Module{
        repo:    repo,
        service: service,
        handler: handler,
    }
}

func (m *Module) Service() *OrderService {
    return m.service
}

func (m *Module) SetUserService(us UserService) {
    m.service.userService = us
}

func (m *Module) RegisterRoutes(mux *http.ServeMux) {
    mux.HandleFunc("POST /api/orders", m.handler.Create)
    mux.HandleFunc("GET /api/orders/{id}", m.handler.Get)
}

💻 Микросервис: коммуникация через gRPC + Kafka

// ╔══════════════════════════════════════════════════════════╗
// ║  API GATEWAY → gRPC → МИКРОСЕРВИСЫ                      ║
// ╚══════════════════════════════════════════════════════════╝

// api-gateway/internal/handler/order_handler.go
type OrderHandler struct {
    orderClient   pb.OrderServiceClient
    userClient    pb.UserServiceClient
    paymentClient pb.PaymentServiceClient
}

func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    var req CreateOrderRequest
    json.NewDecoder(r.Body).Decode(&req)

    // 1. Проверяем пользователя (gRPC)
    userResp, err := h.userClient.GetUser(r.Context(),
        &pb.GetUserRequest{Id: req.UserID})
    if err != nil {
        writeError(w, 404, "User not found")
        return
    }

    // 2. Создаём заказ (gRPC)
    orderResp, err := h.orderClient.CreateOrder(r.Context(),
        &pb.CreateOrderRequest{
            UserID: req.UserID,
            Items:  convertItems(req.Items),
        })
    if err != nil {
        writeError(w, 500, "Order creation failed")
        return
    }

    // 3. Инициируем платёж (gRPC)
    paymentResp, err := h.paymentClient.CreatePayment(r.Context(),
        &pb.CreatePaymentRequest{
            OrderID: orderResp.Id,
            Amount:  orderResp.TotalPrice,
        })
    if err != nil {
        // Платёж не прошёл — отменяем заказ (компенсация)
        h.orderClient.CancelOrder(r.Context(),
            &pb.CancelOrderRequest{Id: orderResp.Id})
        writeError(w, 402, "Payment failed")
        return
    }

    writeJSON(w, 201, map[string]string{
        "order_id":   orderResp.Id,
        "payment_id": paymentResp.Id,
    })
}

// ╔══════════════════════════════════════════════════════════╗
// ║  КОММУНИКАЦИЯ ЧЕРЕЗ KAFKA (асинхронная)                ║
// ╚══════════════════════════════════════════════════════════╝

// order-service — публикует событие
func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderReq) (*Order, error) {
    order := s.createInDB(ctx, req)

    // Публикуем событие
    s.kafkaProducer.PublishEvent(ctx, "order.created", OrderCreatedEvent{
        OrderID: order.ID,
        UserID:  order.UserID,
        Amount:  order.TotalPrice,
    })

    return order, nil
}

// notification-service — слушает события
func (c *NotificationConsumer) Start(ctx context.Context) {
    consumer := kafka.NewConsumer(kafka.ConsumerConfig{
        Brokers: []string{"kafka:9092"},
        Topic:   "order.created",
        GroupID: "notification-service",
    })

    consumer.Start(ctx, func(msg kafka.Message) error {
        var event OrderCreatedEvent
        json.Unmarshal(msg.Value, &event)

        // Отправляем email
        return c.emailService.SendOrderConfirmation(event.UserID, event.OrderID)
    })
}

📊 Когда что выбирать

КритерийModular MonolithМикросервисы
Команда1-10 разработчиков10-100+ разработчиков
Сложность деплояОдин бинарник5-20+ сервисов
ПроизводительностьПрямые вызовы (0ms)Сеть (0.1-5ms)
Изоляция ошибокПадает всёПадает один сервис
МасштабированиеВсё приложениеПроблемный сервис
ТранзакцииACID легкоSaga, сложно
Стоимость Go-инстанса50m CPU, 32MB RAM5×50m CPU, 5×32MB RAM

🚀 Правило масштабирования

СТАРТАП (день 1): ┌──────────────────────────────────────┐ │ MODULAR MONOLITH │ │ users | orders | payments | … │ │ (один бинарник, 32MB RAM) │ └──────────────────────────────────────┘ ✅ Быстрая разработка ✅ Простой деплой ✅ Легко тестировать

РОСТ (100+ заказов/сек): ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ users │ │ orders │ │payments│ │ notif │ └────────┘ └────────┘ └────────┘ └────────┘ ↑ ↑ ↑ ↑ └──────────┴──────────┴──────────┘ API Gateway ✅ Независимое масштабирование ✅ Изоляция отказов ✅ Разные команды

КОГДА ДРОБИТЬ:

  1. Разные команды владеют разными модулями
  2. Разная нагрузка (orders — 1000 RPS, users — 10 RPS)
  3. Разные требования к надёжности (payments — критично)
  4. Разные циклы деплоя (users — раз в неделю, orders — 5 раз в день)
⚠️ Типичные ошибки:
💡 Практический совет:

💡 Best practices от сеньоров:

🔑 Ключевые концепции

Что нужно запомнить из этого урока:

💡 Для Node.js разработчика:

← Предыдущий урок Следующий урок →