API Gateway своими руками на Go: маршрутизация, аутентификация и rate limiting
Введение: зачем писать 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 на Go | Kong / Traefik |
|---|---|---|
| Время до production | 2–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. Подробнее обо мне →