Простой HTTP-балансировщик нагрузки, написанный на Go. Проект был сделан в рамках тестового задания для Cloud.ru и затем оформлен как pet-project.
В основе — обратный прокси, round-robin-стратегия выбора backend'ов, периодический health-check и rate limiting с хранением состояния в PostgreSQL.
- распределение HTTP-запросов между несколькими backend-серверами;
- стратегия балансировки
round-robin; - автоматическая проверка доступности backend'ов;
- исключение недоступных backend'ов из маршрутизации;
- rate limiting по IP-адресу клиента;
- хранение счётчиков rate limit в PostgreSQL;
- очистка устаревших токен-бакетов по расписанию;
- запуск через Docker Compose;
- graceful shutdown по
SIGINTиSIGTERM.
- Клиент отправляет запрос на балансировщик.
- Middleware rate limit определяет ключ клиента по IP-адресу.
- Проверяется token bucket для этого IP:
- если лимит исчерпан, возвращается
429 Too Many Requests; - если лимит доступен, запрос идёт дальше.
- если лимит исчерпан, возвращается
- Балансировщик выбирает следующий живой backend по round-robin.
- Запрос проксируется на выбранный backend через
httputil.ReverseProxy. - Параллельно работает health-check, который периодически проверяет backend'ы по TCP.
- Если backend становится недоступен, он помечается как
deadи временно исключается из ротации.
cmd/balancer/main.go — точка входа приложения
internal/config — загрузка и хранение конфигурации
internal/server — HTTP-сервер и middleware
internal/balancer — балансировщик, backend pool, стратегии, health-check
internal/ratelimit — token bucket rate limiter и PostgreSQL storage
config/config.yaml — пример конфигурации
test/backend1, test/backend2 — простые backend'ы для локального запуска
Балансировщик обрабатывает все запросы на / и проксирует их на один из backend'ов.
200 OK— запрос успешно проксирован на backend;429 Too Many Requests— превышен лимит запросов;502 Bad Gateway— ошибка при проксировании на backend;503 Service Unavailable— нет доступных backend'ов;500 Internal Server Error— ошибка rate limiter'а или внутренний сбой сервера.
Для запуска понадобятся:
- Go
1.24.xили совместимая версия; - Docker и Docker Compose;
- PostgreSQL (если запускать не через compose).
Конфигурация читается из файла, путь к которому задаётся через переменную окружения CONFIG_PATH.
Пример:
server:
port: "8080"
database:
host: "postgres"
port: "5432"
user: "postgres"
password: "12345"
name: "balancer"
sslmode: "disable"
connect_timeout: "5s"
backends:
- "http://backend1:80"
- "http://backend2:80"
rate_limit:
default_capacity: 100
default_rate: "1s"Порт, на котором поднимается HTTP-сервер балансировщика.
Параметры подключения к PostgreSQL.
host— хост базы данных;port— порт PostgreSQL;user— пользователь;password— пароль;name— имя базы;sslmode— режим SSL для подключения;connect_timeout— таймаут подключения.
Список backend-адресов, между которыми распределяются запросы.
Максимальное число токенов в bucket'е для нового клиента.
Интервал, через который восстанавливается 1 токен.
Пример интерпретации: если default_capacity = 100 и default_rate = "1s", то у клиента есть 100 запросов в запасе, а затем токены будут восстанавливаться по одному раз в секунду.
Rate limit реализован по схеме token bucket.
- каждому клиенту соответствует отдельный bucket;
- ключ bucket'а — IP-адрес клиента;
- при каждом запросе из bucket'а забирается 1 токен;
- если токенов нет — запрос блокируется;
- bucket автоматически пополняется со временем.
Состояние rate limit хранится в PostgreSQL в таблице ratelimit.
Структура таблицы создаётся автоматически при старте приложения:
CREATE TABLE IF NOT EXISTS ratelimit (
key TEXT PRIMARY KEY,
capacity INTEGER NOT NULL,
tokens INTEGER NOT NULL,
rate TEXT NOT NULL,
last_refill TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);Фоновый worker раз в 6 часов удаляет записи, у которых last_refill старше 24 часов. Это помогает не накапливать бесконечное число записей для редко используемых IP.
Используется стратегия round-robin.
- backend'ы перебираются по очереди;
- если backend помечен как недоступный, он пропускается;
- если не осталось ни одного живого backend'а, возвращается
503.
Health-check выполняется каждые 5 секунд и проверяет доступность backend'ов через TCP-подключение с таймаутом 2 секунды.
Важно: проверка на доступность не делает HTTP-запрос GET /, а проверяет именно TCP-доступность хоста и порта. Это означает, что backend может отвечать на TCP, но при этом иметь проблемы на уровне HTTP-приложения.
В config/config.yaml должны быть указаны:
- адрес PostgreSQL;
- список backend'ов;
- параметры rate limit;
- порт сервера.
Для compose-запуска в этом проекте используется:
postgresкак host базы;backend1иbackend2как backend-имена внутри сети Docker;CONFIG_PATH=/config/config.yaml.
docker-compose up --buildcurl http://localhost:8080/Ответы будут приходить от разных backend'ов по очереди.
Сделайте несколько запросов подряд:
for i in {1..6}; do curl -s http://localhost:8080/; echo; doneВ ответе должен меняться backend.
Если лимит небольшой, можно быстро получить 429 Too Many Requests:
ab -n 1000 -c 50 http://localhost:8080/или через curl в цикле.
Если нужно поднять проект вручную:
- Запустите PostgreSQL.
- Укажите корректный
CONFIG_PATH. - Проверьте список backend'ов в конфиге.
- Запустите приложение:
go run ./cmd/balancerexport CONFIG_PATH=./config/config.yaml
go run ./cmd/balancerСобирает все компоненты вместе:
- загружает конфигурацию;
- создаёт pool backend'ов;
- запускает health-check;
- подключается к PostgreSQL;
- создаёт limiter;
- поднимает HTTP-сервер;
- обрабатывает сигналы завершения.
Собирает HTTP middleware-цепочку:
- rate limit middleware;
- handler балансировщика.
Содержит:
Backend— описание одного backend'а;BackendPool— набор backend'ов;RoundRobinStrategy— выбор следующего backend'а;HealthChecker— проверка доступности backend'ов;LoadBalancer— HTTP handler, который проксирует запрос.
Содержит:
TokenBucket— логика токен-бакета;TokenBucketLimiter— обёртка для проверки лимита;Database— хранилище на PostgreSQL;- middleware для защиты HTTP-цепочки.
В проекте есть unit-тесты для:
- балансировщика;
- стратегии round-robin;
- health-check;
- rate limiter'а;
- PostgreSQL storage слоя.
Запуск:
go test ./...- Перезагрузка конфигурации по
SIGHUPреализована, но текущие компоненты после старта продолжают использовать уже загруженные зависимости. То есть изменениеconfig.yamlво время работы не меняет поведение запущенного процесса без дополнительной логики повторной инициализации. - Health-check проверяет доступность backend'а по TCP, а не HTTP-ответ.
- Rate limiting привязан к IP-адресу клиента. Если несколько пользователей выходят в интернет через один NAT, они могут делить один и тот же лимит.
- Ошибка при парсинге backend URL приводит к аварийному завершению при старте, потому что
url.Parseобрабатывается черезlog.Fatal. - Для простоты не реализованы:
- sticky sessions;
- weighted round-robin;
- retries;
- circuit breaker;
- metrics и tracing;
- административный API.
- добавить поддержку нескольких стратегий балансировки;
- сделать HTTP-based health-check;
- вынести rate limit в отдельную БД/Redis-реализацию;
- добавить метрики Prometheus;
- добавить structured logging;
- сделать динамическую перезагрузку конфигурации на практике;
- добавить
/healthzи/metrics.
В репозитории уже лежат два простых backend'а в test/backend1 и test/backend2. Они используются как наглядный пример распределения запросов.
В состав Docker Compose входит SPA, связанное с реальным Go-балансировщиком. Оно показывает фактический backend для каждого запроса, остаток token bucket, TCP health-state и позволяет временно исключать ноды из round-robin.
docker-compose up --buildТакже поддерживается синтаксис Compose Plugin: docker compose up --build.
После запуска откройте http://localhost:3000. Сам балансировщик по-прежнему доступен на http://localhost:8080.