DevOps

Горизонтальное масштабирование Go-сервисов в Kubernetes: от конфигурации HPA до кастомных метрик

Ruslan Ismailov Опубликовано 14 мин чтения
Г

Введение: зачем Go-сервисам нужно горизонтальное масштабирование

Горизонтальное масштабирование (scale out) означает увеличение числа экземпляров сервиса, тогда как вертикальное (scale up) — наращивание ресурсов одного узла. Для stateless-микросервисов на Go горизонтальное масштабирование предпочтительнее по нескольким причинам: оно не требует простоя, позволяет линейно наращивать пропускную способность и обеспечивает отказоустойчивость за счёт избыточности реплик.

Вертикальное масштабирование упирается в физические ограничения узла и нередко создаёт единую точку отказа. Kubernetes с его HPA (Horizontal Pod Autoscaler) даёт декларативный механизм автоматического управления числом реплик на основе метрик — CPU, памяти или произвольных бизнес-показателей.

Особенности Go как языка для масштабируемых сервисов

Go изначально проектировался для высоконагруженных систем. Несколько ключевых характеристик делают его отличным выбором для горизонтально масштабируемых микросервисов в Kubernetes:

  • Горутины и планировщик M:N. Тысячи конкурентных задач укладываются в небольшой объём памяти. Горутина стартует с ~2 КБ стека против ~1 МБ у системного потока.
  • Минимальный footprint. Статически скомпилированный бинарник весит единицы мегабайт; контейнер на базе scratch или distroless запускается за секунды.
  • Stateless по умолчанию. Отсутствие глобального изменяемого состояния (при правильной архитектуре) упрощает добавление реплик без синхронизации.
  • Встроенный net/http и быстрый старт. Go-сервис готов принимать трафик почти мгновенно после запуска пода — критично при агрессивном масштабировании.

Подготовка Go-сервиса к масштабированию

Graceful Shutdown

При уменьшении числа реплик Kubernetes отправляет поду сигнал SIGTERM. Go-сервис обязан завершить текущие запросы, закрыть соединения с БД и очистить ресурсы до истечения terminationGracePeriodSeconds.

package main

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

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }
    log.Println("Server exited cleanly")
}

Readiness и Liveness Probes

Readiness probe сообщает Kubernetes, что под готов принимать трафик. Без корректного эндпоинта новые реплики получат запросы раньше, чем успеют инициализироваться — это источник ошибок при масштабировании. Liveness probe позволяет перезапустить зависший под.

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  failureThreshold: 3
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10

Корректная работа с соединениями

При горизонтальном масштабировании число соединений к PostgreSQL или Redis растёт пропорционально репликам. Используйте пулы с разумными лимитами (SetMaxOpenConns, SetMaxIdleConns для database/sql) и PgBouncer / Redis Cluster на уровне инфраструктуры, чтобы не исчерпать лимиты СУБД.

HPA на основе CPU и Memory: базовая конфигурация

HPA — встроенный контроллер Kubernetes, который периодически (по умолчанию каждые 15 секунд) опрашивает Metrics Server и корректирует число реплик Deployment.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: go-api-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: go-api
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 75
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 120
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30

Подводные камни:

  • Без корректно выставленных requests HPA не сможет вычислить utilization — метрика будет unknown.
  • Go активно использует GC, что создаёт спайки CPU. Целевое значение 60–70% оставляет буфер под сборку мусора.
  • Масштабирование по памяти опасно для Go: рантайм удерживает освобождённую память для переиспользования (MADV_FREE). Используйте GOGC и GOMEMLIMIT для управления поведением GC.

Кастомные метрики для HPA: Prometheus и Prometheus Adapter

Экспорт метрик из Go-сервиса

Используйте официальный клиент github.com/prometheus/client_golang для регистрации и публикации метрик.

package metrics

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    RequestsInFlight = promauto.NewGauge(prometheus.GaugeOpts{
        Name: "go_api_requests_in_flight",
        Help: "Current number of HTTP requests being processed",
    })

    QueueDepth = promauto.NewGaugeVec(prometheus.GaugeOpts{
        Name: "go_worker_queue_depth",
        Help: "Number of tasks waiting in the processing queue",
    }, []string{"queue_name"})

    RequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
        Name:    "go_api_request_duration_seconds",
        Help:    "HTTP request duration",
        Buckets: prometheus.DefBuckets,
    }, []string{"method", "path", "status"})
)

Настройка Prometheus Adapter

Prometheus Adapter реализует Kubernetes Custom Metrics API, позволяя HPA использовать произвольные метрики из Prometheus. Устанавливается через Helm:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus-adapter prometheus-community/prometheus-adapter \
  --namespace monitoring \
  --set prometheus.url=http://prometheus.monitoring.svc \
  --set prometheus.port=9090

Конфигурация правил в values.yaml:

rules:
  custom:
    - seriesQuery: 'go_worker_queue_depth{namespace!="",pod!=""}'
      resources:
        overrides:
          namespace: {resource: "namespace"}
          pod: {resource: "pod"}
      name:
        matches: "go_worker_queue_depth"
        as: "worker_queue_depth"
      metricsQuery: 'avg(go_worker_queue_depth{<<.LabelMatchers>>}) by (<<.GroupBy>>)'

Масштабирование по бизнес-метрикам: длина очереди как триггер

Представим Go-воркер, обрабатывающий задачи из внутренней очереди. Длина очереди — прямой индикатор нагрузки: если задач становится больше, нужно добавить воркеры.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: go-worker-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: go-worker
  minReplicas: 2
  maxReplicas: 30
  metrics:
    - type: Pods
      pods:
        metric:
          name: worker_queue_depth
        target:
          type: AverageValue
          averageValue: "10"

Смысл: HPA будет держать среднее значение worker_queue_depth на уровне 10 задач на реплику. Если очередь выросла до 100 задач при 2 репликах (50 на реплику), контроллер масштабирует деплоймент до 10 реплик.

KEDA как альтернатива: масштабирование по Redis и Kafka

KEDA (Kubernetes Event-Driven Autoscaling) расширяет возможности HPA, добавляя готовые скейлеры для десятков источников событий. Для Go-воркеров, потребляющих сообщения из Redis List или Kafka, KEDA — наиболее удобное решение.

ScaledObject для Redis-очереди

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: go-worker-redis-scaler
  namespace: production
spec:
  scaleTargetRef:
    name: go-worker
  minReplicaCount: 1
  maxReplicaCount: 50
  pollingInterval: 10
  cooldownPeriod: 60
  triggers:
    - type: redis
      metadata:
        address: redis.production.svc:6379
        listName: task_queue
        listLength: "5"
        enableTLS: "false"
      authenticationRef:
        name: redis-auth

ScaledObject для Kafka

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: go-worker-kafka-scaler
  namespace: production
spec:
  scaleTargetRef:
    name: go-kafka-worker
  minReplicaCount: 2
  maxReplicaCount: 40
  triggers:
    - type: kafka
      metadata:
        bootstrapServers: kafka.production.svc:9092
        consumerGroup: go-worker-group
        topic: events
        lagThreshold: "20"
        offsetResetPolicy: latest

KEDA может масштабировать деплоймент до нуля реплик при пустой очереди — полезно для batch-воркеров, работающих по расписанию.

Управление ресурсами: requests/limits, VPA и Cluster Autoscaler

Корректные requests и limits — фундамент правильной работы HPA и планировщика Kubernetes.

resources:
  requests:
    cpu: "250m"
    memory: "128Mi"
  limits:
    cpu: "1000m"
    memory: "512Mi"

Для Go-сервисов рекомендуется:

  • Устанавливать GOMEMLIMIT в 80–90% от limits.memory, чтобы GC агрессивнее освобождал память до того, как контейнер будет убит OOMKiller.
  • Не устанавливать CPU limits слишком низко: CPU throttling создаёт латентность, неотличимую от перегрузки сервиса.
  • VPA (Vertical Pod Autoscaler) в режиме Off полезен для получения рекомендаций по requests/limits без автоматического применения.

Cluster Autoscaler автоматически добавляет узлы в кластер, когда поды не могут быть запланированы из-за нехватки ресурсов. Убедитесь, что PodDisruptionBudget настроен для предотвращения одновременного вытеснения слишком многих реплик при scale-down узлов.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: go-api-pdb
  namespace: production
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: go-api

Тестирование масштабирования: нагрузочные тесты с k6

Перед выходом в продакшн необходимо верифицировать поведение HPA под нагрузкой. k6 — легковесный инструмент для нагрузочного тестирования, написанный на Go, с JavaScript API для сценариев.

import http from 'k6/http';
import { sleep, check } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // ramp-up
    { duration: '5m', target: 200 },  // sustained load
    { duration: '2m', target: 500 },  // stress
    { duration: '2m', target: 0 },    // ramp-down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  const res = http.get('http://go-api.production.svc/api/v1/items');
  check(res, { 'status 200': (r) => r.status === 200 });
  sleep(0.1);
}

Во время теста наблюдайте за HPA командой kubectl get hpa -n production -w и следите за временем реакции: от пика нагрузки до появления новых реплик обычно проходит 1–3 минуты (время scrape Prometheus + цикл HPA + pull образа + инициализация пода).

Типичные проблемы и способы их решения

Cold Start

Несмотря на быстрый запуск Go-бинарника, новый под тратит время на pull образа, инициализацию соединений с БД и прогрев кэшей. В этот период readiness probe не проходит, и под не получает трафик. Решения: использовать pre-pulled образы (DaemonSet или image cache), уменьшить initialDelaySeconds у readiness probe, разогревать пул соединений в фоне до поднятия readiness.

Thundering Herd

При резком росте нагрузки все новые реплики одновременно пытаются подключиться к БД, Redis или внешним API, создавая шторм соединений. Используйте exponential backoff с jitter при инициализации соединений и ограничивайте maxReplicaCount в сочетании с rate limiting на уровне сервиса.

// Jitter при переподключении
func retryWithJitter(attempt int) time.Duration {
    base := time.Duration(attempt) * 100 * time.Millisecond
    jitter := time.Duration(rand.Int63n(int64(base)))
    return base + jitter
}

Flapping (нестабильное масштабирование)

HPA постоянно добавляет и убирает реплики — признак некорректно выбранных порогов или слишком короткого окна стабилизации. Решение: увеличить stabilizationWindowSeconds для scale-down (рекомендуется 120–300 секунд), поднять целевой utilization, использовать behavior.scaleDown.policies для ограничения скорости уменьшения реплик.

Метрика unknown в HPA

Если kubectl describe hpa показывает unknown для CPU/memory — скорее всего не установлены requests в манифесте пода или не запущен Metrics Server. Для кастомных метрик проверьте доступность Custom Metrics API: kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1.

Итог и рекомендации

Горизонтальное масштабирование Go-микросервисов в Kubernetes — это не просто применение манифеста HPA. Это комплексная практика, включающая подготовку самого сервиса (graceful shutdown, probe-эндпоинты, управление соединениями), правильную настройку ресурсов и выбор подходящего источника метрик.

  • Начните с HPA по CPU, выставив целевой utilization 60–70% с учётом спайков GC.
  • Добавьте кастомные метрики через Prometheus Adapter для масштабирования по RPS или latency.
  • Для event-driven воркеров на основе Redis или Kafka используйте KEDA — он значительно проще в настройке и поддерживает масштабирование до нуля.
  • Всегда тестируйте поведение под нагрузкой с k6 или аналогами перед продакшн-деплоем.
  • Настройте PodDisruptionBudget и terminationGracePeriodSeconds для безопасного scale-down.
  • Используйте GOMEMLIMIT для управления GC-давлением и предотвращения OOM.

Комбинация статически типизированного, быстро стартующего Go, декларативного Kubernetes и гибкого HPA с кастомными метриками даёт инфраструктуру, способную обрабатывать непредсказуемые пики нагрузки без ручного вмешательства — именно то, что нужно современным высоконагруженным микросервисам в 2026 году.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →