Backend-разработка

API Gateway своими руками на Go: маршрутизация, аутентификация и rate limiting

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

Введение: зачем писать API Gateway самостоятельно?

API Gateway — это единая точка входа для всех клиентских запросов в микросервисной архитектуре. Он занимается маршрутизацией, аутентификацией, балансировкой нагрузки и ограничением частоты запросов, освобождая бизнес-сервисы от сквозной логики.

Готовые решения — Kong, Traefik, AWS API Gateway — закрывают большинство задач. Но есть сценарии, когда собственная реализация на Go оправдана: жёсткие требования к производительности, нестандартные протоколы аутентификации, полный контроль над логикой обработки ошибок или просто ограничения лицензий. Go идеально подходит для этой задачи: низкие накладные расходы, встроенная конкурентность и богатая стандартная библиотека.

Архитектура API Gateway: основные компоненты

Хороший API Gateway состоит из следующих слоёв:

  • Router — сопоставляет входящий запрос с целевым микросервисом по пути, методу и заголовкам.
  • Middleware chain — последовательность обработчиков: аутентификация, rate limiting, логирование, трассировка.
  • Reverse proxy — проксирует запрос к upstream-сервису и возвращает ответ клиенту.
  • Circuit breaker / Retry — защищает систему от каскадных сбоев.
  • Observability — сбор метрик, логов и трейсов.

Реализация маршрутизации запросов к микросервисам

Для маршрутизации воспользуемся популярным роутером gorilla/mux или встроенным net/http. Ниже — минималистичная реализация reverse proxy с динамической маршрутизацией.

package main

import (
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
)

type Route struct {
    Prefix  string
    Target  string
}

var routes = []Route{
    {Prefix: "/users", Target: "http://user-service:8081"},
    {Prefix: "/orders", Target: "http://order-service:8082"},
    {Prefix: "/products", Target: "http://product-service:8083"},
}

func proxyHandler(target string) http.Handler {
    url, _ := url.Parse(target)
    proxy := httputil.NewSingleHostReverseProxy(url)
    proxy.ModifyResponse = func(resp *http.Response) error {
        resp.Header.Set("X-Gateway", "go-gateway/1.0")
        return nil
    }
    return proxy
}

func main() {
    mux := http.NewServeMux()
    for _, r := range routes {
        handler := proxyHandler(r.Target)
        mux.Handle(r.Prefix+"/", http.StripPrefix(r.Prefix, handler))
    }
    log.Println("Gateway listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Роутер перебирает зарегистрированные маршруты, находит совпадение по префиксу и проксирует запрос через httputil.ReverseProxy. Для более сложной маршрутизации (параметры пути, regex) подключайте chi или gorilla/mux.

Аутентификация и авторизация: JWT и middleware

Аутентификация на уровне Gateway избавляет каждый микросервис от повторной проверки токенов. Реализуем middleware для валидации JWT с помощью библиотеки golang-jwt/jwt.

package middleware

import (
    "context"
    "fmt"
    "net/http"
    "strings"

    "github.com/golang-jwt/jwt/v5"
)

type contextKey string
const UserIDKey contextKey = "userID"

var jwtSecret = []byte("super-secret-key")

func JWTAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if !strings.HasPrefix(authHeader, "Bearer ") {
            http.Error(w, "missing or invalid token", http.StatusUnauthorized)
            return
        }
        tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
        token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
            if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
            }
            return jwtSecret, nil
        })
        if err != nil || !token.Valid {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        claims, _ := token.Claims.(jwt.MapClaims)
        userID := claims["sub"].(string)
        ctx := context.WithValue(r.Context(), UserIDKey, userID)
        // Передаём userID в заголовке для downstream-сервисов
        r.Header.Set("X-User-ID", userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Middleware извлекает userID из JWT-клеймов, помещает его в контекст запроса и добавляет заголовок X-User-ID, который downstream-сервисы могут читать напрямую — без повторного парсинга токена.

Rate limiting: Token Bucket и Sliding Window с Redis

Rate limiting защищает микросервисы от перегрузки. Рассмотрим два популярных алгоритма и их реализацию с Redis.

Token Bucket

Токены накапливаются с постоянной скоростью и расходуются при каждом запросе. Реализуем через атомарные операции Redis:

package ratelimit

import (
    "context"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
)

type TokenBucketLimiter struct {
    client   *redis.Client
    capacity int64
    rate     int64 // токенов в секунду
}

func (l *TokenBucketLimiter) Allow(ctx context.Context, key string) (bool, error) {
    now := time.Now().Unix()
    bucketKey := fmt.Sprintf("tb:%s", key)

    pipe := l.client.TxPipeline()
    getTokens := pipe.Get(ctx, bucketKey)
    _, err := pipe.Exec(ctx)
    _ = err

    tokens, _ := getTokens.Int64()
    if tokens <= 0 {
        tokens = l.capacity
    }
    _ = now

    if tokens > 0 {
        l.client.Decr(ctx, bucketKey)
        l.client.Expire(ctx, bucketKey, time.Second*60)
        return true, nil
    }
    return false, nil
}

Sliding Window с Redis

Более точный алгоритм: подсчитываем количество запросов за скользящее временное окно с помощью Sorted Set в Redis.

package ratelimit

import (
    "context"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
)

type SlidingWindowLimiter struct {
    client  *redis.Client
    limit   int64
    window  time.Duration
}

func (l *SlidingWindowLimiter) Allow(ctx context.Context, key string) (bool, error) {
    now := time.Now()
    windowStart := now.Add(-l.window).UnixMilli()
    swKey := fmt.Sprintf("sw:%s", key)

    pipe := l.client.TxPipeline()
    pipe.ZRemRangeByScore(ctx, swKey, "0", fmt.Sprintf("%d", windowStart))
    count := pipe.ZCard(ctx, swKey)
    pipe.ZAdd(ctx, swKey, redis.Z{
        Score:  float64(now.UnixMilli()),
        Member: now.UnixNano(),
    })
    pipe.Expire(ctx, swKey, l.window)
    _, err := pipe.Exec(ctx)
    if err != nil {
        return false, err
    }
    return count.Val() < l.limit, nil
}

Middleware для rate limiting встраивается в цепочку до аутентификации (защита от DDoS на открытых эндпоинтах) или после неё (персональные лимиты по userID):

func RateLimitMiddleware(limiter *SlidingWindowLimiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            key := r.RemoteAddr // или userID из контекста
            allowed, err := limiter.Allow(r.Context(), key)
            if err != nil || !allowed {
                http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

Обработка ошибок: Circuit Breaker и Retry

Когда upstream-сервис деградирует, Gateway должен реагировать корректно. Используем библиотеку sony/gobreaker для circuit breaker:

package circuit

import (
    "net/http"
    "time"

    "github.com/sony/gobreaker"
)

func NewBreakerProxy(target string, next http.Handler) http.Handler {
    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        target,
        MaxRequests: 5,
        Interval:    10 * time.Second,
        Timeout:     30 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            return counts.ConsecutiveFailures > 3
        },
    })

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        _, err := cb.Execute(func() (interface{}, error) {
            rr := &responseRecorder{ResponseWriter: w}
            next.ServeHTTP(rr, r)
            if rr.statusCode >= 500 {
                return nil, fmt.Errorf("upstream error: %d", rr.statusCode)
            }
            return nil, nil
        })
        if err != nil {
            http.Error(w, "service unavailable", http.StatusServiceUnavailable)
        }
    })
}

Для retry-логики используйте экспоненциальный backoff: повторяйте запрос не более 3 раз с задержкой 100ms, 200ms, 400ms перед тем, как вернуть ошибку клиенту.

Логирование и трассировка запросов

Каждый запрос через Gateway должен получать уникальный trace ID, который передаётся в заголовке X-Request-ID во все downstream-вызовы. Используем go.opentelemetry.io/otel для distributed tracing:

func TracingMiddleware(tracer trace.Tracer) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx, span := tracer.Start(r.Context(), r.URL.Path)
            defer span.End()

            requestID := r.Header.Get("X-Request-ID")
            if requestID == "" {
                requestID = uuid.New().String()
            }
            span.SetAttributes(attribute.String("request.id", requestID))
            r.Header.Set("X-Request-ID", requestID)

            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Структурированное логирование через slog (Go 1.21+) позволяет легко парсить логи в ELK или Loki:

slog.Info("request",
    "method", r.Method,
    "path", r.URL.Path,
    "duration_ms", time.Since(start).Milliseconds(),
    "status", statusCode,
    "request_id", requestID,
)

Деплой API Gateway в Kubernetes

API Gateway разворачивается как Deployment с HPA для автоматического масштабирования. Пример манифеста:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gateway
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-gateway
  template:
    metadata:
      labels:
        app: api-gateway
    spec:
      containers:
      - name: gateway
        image: myregistry/api-gateway:v1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: redis-secret
              key: url
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: jwt-secret
              key: value
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "512Mi"
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: api-gateway-svc
spec:
  type: LoadBalancer
  selector:
    app: api-gateway
  ports:
  - port: 80
    targetPort: 8080

В Kubernetes важно настроить readiness и liveness пробы, чтобы трафик не направлялся на поды в процессе холодного старта или при деградации. Для хранения конфигурации маршрутов используйте ConfigMap с возможностью hot-reload через fsnotify.

Сравнение с готовыми решениями: когда самописный Gateway оправдан?

КритерийСамописный Gateway на GoKong / Traefik
Время до production2–4 недели1–3 дня
Гибкость логикиМаксимальнаяОграничена плагинами
ПроизводительностьОптимизирована под задачуВысокая, но с накладными расходами
Операционная нагрузкаВысокая (поддержка кода)Низкая
СтоимостьВремя разработчиковБесплатно / Enterprise-лицензии

Самописный API Gateway на Go оправдан, если:

  • Нестандартные протоколы аутентификации или бизнес-логика маршрутизации, которую нельзя выразить конфигурацией.
  • Экстремальные требования к задержкам (sub-millisecond overhead).
  • Полный контроль над зависимостями и отсутствие vendor lock-in.
  • Команда хорошо знает Go и готова поддерживать код.

В остальных случаях Traefik в связке с Kubernetes Ingress или Kong с плагинами закроют 95% потребностей быстрее и надёжнее.

Заключение

Мы построили полноценный API Gateway на Go: от динамической маршрутизации запросов к микросервисам до JWT-аутентификации, rate limiting с Redis (Token Bucket и Sliding Window), circuit breaker, distributed tracing и деплоя в Kubernetes. Go идеально подходит для этой задачи — минимальные накладные расходы, мощная стандартная библиотека и простая модель конкурентности.

Ключевые принципы при разработке собственного Gateway: держите middleware chain линейным и тестируемым, выносите конфигурацию маршрутов в горячо перезагружаемый источник (Redis, ConfigMap), никогда не игнорируйте observability с первого дня. Хорошо спроектированный Gateway становится надёжным фундаментом всей микросервисной платформы.

Технологии

Теги

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

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