15 вопросов
Сервисы - небольшие, развертываются независимо, владеют своими данными, общаются по сети (HTTP/gRPC, очереди). В Go каждый сервис - отдельное приложение (бинарник), свой репозиторий или монорепо. Конфигурация и обнаружение сервисов выносятся наружу (Consul, etcd, K8s). Тестирование - контракты, моки, интеграционные тесты.
Монолит проще разрабатывать и деплоить на старте. Микросервисы дают независимый масштаб и деплой, технологическое разнообразие. Дробить имеет смысл при разных нагрузках на части, разных командах или при необходимости изоляции сбоев. В Go монолит часто строят модульно (пакеты, интерфейсы), чтобы потом вынести сервис без переписывания.
Синхронная: HTTP/gRPC вызовы, ожидание ответа. Простота, но связность и каскадные сбои. Асинхронная: очереди (Kafka, RabbitMQ), события; слабая связность, буферизация. В Go синхронно - http.Client или gRPC client; асинхронно - продюсер/консьюмер Kafka или AMQP. Для надежности и снятия пиков часто комбинируют: команды по HTTP, события по очереди.
resp, err := httpClient.Post(ctx, "http://orders/create", "application/json", body)
// или
producer.Produce(ctx, &kafka.Message{TopicPartition: tp, Value: payload})Saga - распределенная транзакция через цепочку локальных транзакций; при сбое выполняют компенсирующие действия (откат). Хореография: каждый сервис публикует события, следующие реагируют и при ошибке публикуют компенсацию. Оркестрация: центральный оркестратор вызывает сервисы и при ошибке запускает компенсации. В Go реализуют как конечный автомат или слой оркестратора с вызовами сервисов и откатом.
Circuit breaker предотвращает каскадные сбои: при большом числе ошибок переходит в состояние "open" и не вызывает бэкенд (сразу возвращает ошибку или fallback); после таймаута пробует снова (half-open). В Go: sony/gobreaker, hashicorp/go-circuitbreaker. Оборачивают вызовы внешнего сервиса; при открытом контуре не делают запрос. Метрики (число вызовов, ошибок) для мониторинга.
cb := gobreaker.NewCircuitBreaker(st)
result, err := cb.Execute(func() (interface{}, error) {
return client.Call(ctx, req)
})Outbox - запись бизнес-события и сообщения для публикации в одну транзакцию в БД; отдельный процесс читает outbox и публикует в очередь. Гарантирует доставку при сбое после commit. В Go: таблица outbox (id, aggregate_id, payload, created_at, published); в той же транзакции INSERT в outbox; воркер SELECT неопубликованных, публикует в Kafka/RabbitMQ, помечает опубликованными.
tx.ExecContext(ctx, "INSERT INTO orders ...")
tx.ExecContext(ctx, "INSERT INTO outbox (topic, payload) VALUES ($1, $2)", "order.created", json)Event Sourcing - состояние хранится как лог событий; текущее состояние восстанавливается применением событий. В Go: хранилище событий (таблица или Kafka), агрегат применяет события (Apply(event)); при запросе загружают события и пересчитывают или хранят снапшоты. Подходит для аудита и воспроизведения. Сложнее запросы и консистентность; часто комбинируют с CQRS (отдельная read-модель).
Сервисы регистрируются при старте и получают адреса других сервисов по имени. Consul/etcd: регистрация с TTL, здоровье через HTTP check; клиент резолвит имя в список инстансов. В K8s - DNS (имя сервиса резолвится в ClusterIP). В Go: клиент Consul (hashicorp/consul), etcd (etcd-client); при вызове другого сервиса берут адрес из discovery, балансируют (round-robin).
// K8s: http.Get("http://orders-service:8080/health")
// Consul: services, _ := consul.Client().Health().Service("orders", "", true, nil)API Gateway - единая точка входа: маршрутизация по пути/хосту к бэкендам, аутентификация, rate limiting, агрегация ответов. В Go можно реализовать gateway как HTTP-сервер с reverse proxy (httputil.ReverseProxy) и middleware (auth, limit). Или использовать готовые (Kong, Envoy) и конфигурировать. Для простых сценариев достаточно Nginx/Envoy; кастом на Go - при сложной логике маршрутизации и агрегации.
proxy := httputil.NewSingleHostReverseProxy(backendURL)
proxy.Director = func(r *http.Request) {
r.URL.Host = backendURL.Host
r.URL.Scheme = backendURL.Scheme
}
http.Handle("/api/", authMiddleware(proxy))BFF - отдельный бэкенд под конкретный клиент (веб, мобильное приложение), агрегирует вызовы к микросервисам и адаптирует ответы. В Go BFF - HTTP-сервер: принимает запрос от клиента, вызывает несколько сервисов (параллельно где возможно), собирает ответ в нужном формате. Уменьшает число запросов с клиента и скрывает детали внутренних API.
func (b *BFF) GetDashboard(ctx context.Context, userID string) (*Dashboard, error) {
var orders *Orders
var profile *Profile
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { orders, _ = b.orders.GetByUser(ctx, userID); return nil })
g.Go(func() error { profile, _ = b.profile.Get(ctx, userID); return nil })
if err := g.Wait(); err != nil { return nil, err }
return &Dashboard{Orders: orders, Profile: profile}, nil
}Backpressure - замедление производителя при перегрузке потребителя, чтобы не накапливать необработанные сообщения. В Go: ограничение числа горутин-воркеров (пул), каналы с буфером ограниченного размера, семантики с блокировкой при полном буфере. При чтении из Kafka ограничивают параллелизм обработки и commit только после обработки. Паттерн: worker pool с N воркерами, задачами по каналу.
sem := make(chan struct{}, 10)
for msg := range kafkaMessages {
sem <- struct{}{}
go func(m Message) {
defer func() { <-sem }()
process(m)
}(msg)
}При недоступности части системы приложение снижает функциональность, но продолжает работать. В Go: проверка доступности зависимостей (health), при сбое возвращать заглушки или кешированные данные; circuit breaker не вызывать сломанный сервис. Критичные операции - быстрый отказ с понятной ошибкой; некритичные - fallback (пустой список, кеш). Мониторинг и алерты по деградации.
if !cache.IsHealthy() {
return nil, ErrServiceUnavailable
}
data, err := cache.Get(ctx, key)
if err != nil {
return getFromDB(ctx, key) // fallback
}При временной ошибке (сеть, 5xx) повторять запрос с задержкой. Exponential backoff: задержка растет (1s, 2s, 4s, ...) до лимита. В Go: цикл с проверкой контекста и ошибки (retryable или нет), time.Sleep(backoff), ограничение числа попыток. Библиотеки: cenkalti/backoff, hashicorp/go-retryablehttp. Не ретраить идемпотентность-критичные операции без идемпотентного ключа или только безопасные методы (GET).
for i := 0; i < maxRetries; i++ {
resp, err := client.Do(req)
if err == nil && resp.StatusCode < 500 { return resp, nil }
select {
case <-ctx.Done(): return nil, ctx.Err()
case <-time.After(time.Duration(1<Liveness - процесс жив (простой endpoint или отсутствие дедлока). Readiness - готов принимать трафик (БД доступна, зависимости отвечают). В Go: /health возвращает 200 или 503; проверки - пинг БД, вызов зависимостей с таймаутом. В K8s liveness/readiness probes указывают на эти endpoint. Отдельный endpoint для зависимостей (dependency health) для отладки.
func (h *Health) Readiness(w http.ResponseWriter, r *http.Request) {
if err := h.db.PingContext(r.Context()); err != nil {
w.WriteHeader(503)
return
}
w.WriteHeader(200)
}Сообщения, которые не удалось обработать после N попыток, отправляют в DLQ для разбора и ручной обработки. В Go: при ошибке обработки не коммитить offset (или nack); после N ретраев публиковать сообщение в отдельный топик/очередь (DLQ). Отдельный воркер или оператор обрабатывает DLQ (логи, алерты, повторная публикация после исправления).
if err := process(msg); err != nil {
if attempt >= maxAttempts {
dlqProducer.Produce(ctx, msg)
return
}
return err
}
consumer.Commit(msg)