Балансировка нагрузки
"The art of progress is to preserve order amid change, and to preserve change amid order." — Alfred North Whitehead
Ни одна серьёзная система не обслуживается одним сервером. Когда приложение получает тысячи, сотни тысяч или миллионы запросов в секунду, один инстанс просто не справляется — ни по CPU, ни по памяти, ни по сетевому I/O. Решение очевидно: добавить больше серверов. Но возникает вопрос: кто решает, какой именно сервер обработает конкретный запрос?
Балансировщик нагрузки (load balancer) — это компонент, который распределяет входящий трафик между группой серверов (backend pool), обеспечивая равномерную утилизацию ресурсов, высокую доступность и предсказуемую латенси. Правильно настроенный балансировщик делает систему устойчивой к отказам отдельных узлов, позволяет масштабироваться горизонтально и обеспечивает бесшовные деплои без даунтайма.
В этой главе мы разберём уровни балансировки (L4 vs L7), ключевые алгоритмы распределения трафика, популярные технологии (Nginx, HAProxy, Envoy), облачные решения (ALB, NLB), глобальную балансировку (GSLB), а также health checks и circuit breakers — механизмы, без которых балансировка теряет смысл.
1 Зачем нужна балансировка нагрузки
Балансировщик решает несколько фундаментальных задач:
Распределение нагрузки
Без балансировщика клиент знает IP-адрес одного сервера и отправляет все запросы на него. С балансировщиком клиент знает только адрес балансировщика, а тот распределяет запросы между backend-серверами.
Без балансировщика: С балансировщиком:
Client ──────► Server Client ──────► Load Balancer
│
┌──────────┼──────────┐
▼ ▼ ▼
Server 1 Server 2 Server 3
Высокая доступность
Если один сервер падает, балансировщик перенаправляет трафик на оставшиеся. Пользователь не замечает отказа — для него система продолжает работать.
Горизонтальное масштабирование
Добавление новых серверов в пул увеличивает пропускную способность системы линейно (в идеале). Балансировщик автоматически включает новые серверы в ротацию.
Zero-downtime деплой
При обновлении приложения серверы обновляются по очереди (rolling update). Балансировщик направляет трафик только на серверы с актуальной версией, исключая обновляемые.
Где балансировщик стоит в архитектуре
Балансировка нагрузки применяется на нескольких уровнях:
Пользователи
│
▼
┌─────────────┐
│ DNS / GSLB │ ← Глобальный уровень (между дата-центрами)
└──────┬──────┘
│
▼
┌─────────────┐
│ L7 LB │ ← Внешний балансировщик (перед API Gateway / web-серверами)
│ (ALB/Nginx) │
└──────┬──────┘
│
▼
┌─────────────┐
│ API Gateway │
└──────┬──────┘
│
▼
┌─────────────┐
│ Internal │ ← Внутренний балансировщик (между микросервисами)
│ L4 LB │
│ (NLB/Envoy) │
└──────┬──────┘
│
┌────┼────┐
▼ ▼ ▼
Svc1 Svc2 Svc3
В крупных системах балансировщики выстраиваются каскадно: DNS → глобальный LB → региональный LB → внутренний LB. Каждый уровень решает свою задачу.
2 L4 vs L7 балансировка
Балансировщики нагрузки работают на разных уровнях модели OSI. Два основных класса — L4 (транспортный уровень) и L7 (уровень приложения) — принципиально различаются по возможностям, производительности и сценариям применения.
2.1 L4 балансировка (Transport Layer)
L4 балансировщик работает на уровне TCP/UDP. Он видит IP-адреса, порты и флаги TCP, но не видит содержимое запроса — не знает, это HTTP, gRPC, WebSocket или что-то другое.
Клиент: SRC=1.2.3.4:54321 DST=10.0.0.1:443
│
▼
L4 Load Balancer
(видит: IP, port, TCP flags)
│
▼
Backend: 10.0.0.5:8080
Как работает:
L4 LB принимает TCP-соединение и пересылает его на один из backend-серверов. Решение о маршрутизации принимается на основе IP-адреса источника, порта назначения и (опционально) начальных байтов соединения. После установки соединения все пакеты в рамках этого соединения идут на один и тот же backend.
Режимы работы:
| Режим | Описание | Производительность |
|---|---|---|
| NAT (DNAT) | LB подменяет destination IP. Ответ идёт через LB. | Средняя |
| DSR (Direct Server Return) | LB перенаправляет запрос, ответ идёт напрямую от backend к клиенту, минуя LB. | Высокая |
| Tunneling (IP-in-IP) | LB инкапсулирует пакет и отправляет backend-серверу. | Высокая |
DSR — наиболее производительный режим: балансировщик обрабатывает только входящий трафик, а ответы (которые обычно значительно больше запросов) идут напрямую от backend к клиенту. Это критично для стриминга видео, скачивания файлов и других сценариев с асимметричным трафиком.
DSR (Direct Server Return):
Client ──request──► L4 LB ──request──► Backend
▲ │
└──────────response (напрямую)─────────┘
Преимущества L4:
- Очень высокая производительность — миллионы соединений в секунду на одном узле
- Низкая латенси — минимальная обработка пакетов
- Универсальность — работает с любым протоколом (HTTP, gRPC, PostgreSQL, Redis, custom TCP)
- Простота — меньше точек отказа
Недостатки L4:
- Не видит содержимое запроса — невозможна маршрутизация по URL, заголовкам, cookies
- Не может терминировать TLS (нужен TLS passthrough или отдельный терминатор)
- Нет возможности модифицировать запрос/ответ
- Session stickiness только по IP (ненадёжно за NAT)
2.2 L7 балансировка (Application Layer)
L7 балансировщик работает на уровне протокола приложения — HTTP, gRPC, WebSocket. Он полностью разбирает запрос: видит URL, заголовки, cookies, тело запроса и может принимать решения на основе любого из этих полей.
Клиент: GET /api/users HTTP/1.1
Host: example.com
Cookie: session=abc123
│
▼
L7 Load Balancer
(видит: URL, headers, cookies, body)
│
┌───────────┼───────────┐
▼ ▼ ▼
/api/users /api/orders /static/*
Users Svc Orders Svc CDN/Static
Что может L7 LB:
- Content-based routing. Маршрутизация по URL path, host header, HTTP method, query parameters, cookies:
# Nginx: маршрутизация по path location /api/users { proxy_pass http://users-backend; } location /api/orders { proxy_pass http://orders-backend; } location /static/ { proxy_pass http://cdn-origin; }
- TLS termination. Принимает HTTPS от клиента, расшифровывает, и отправляет HTTP (plain) на backend. Это снимает нагрузку шифрования с backend-серверов.
Client ──HTTPS──► L7 LB ──HTTP──► Backend
(TLS termination)
- Header manipulation. Добавление, удаление, модификация HTTP-заголовков:
# Добавляем заголовок с реальным IP клиента proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
-
Request/response modification. Сжатие ответов (gzip/brotli), добавление CORS-заголовков, rate limiting.
-
A/B testing и canary deploys. Направление процента трафика на новую версию сервиса:
upstream backend { server app-v1:8080 weight=9; # 90% трафика server app-v2:8080 weight=1; # 10% трафика (canary) }
- WebSocket support. Upgrade HTTP-соединения до WebSocket с маршрутизацией по path.
Преимущества L7:
- Интеллектуальная маршрутизация по содержимому запроса
- TLS termination
- Возможность модификации запросов/ответов
- Session stickiness по cookies (надёжнее, чем по IP)
- Интеграция с WAF, rate limiting, authentication
Недостатки L7:
- Ниже производительность — нужно парсить каждый запрос (сотни тысяч RPS, не миллионы)
- Выше латенси — дополнительная обработка
- Сложнее конфигурация
- Работает только с поддерживаемыми протоколами (HTTP, gRPC)
2.3 Когда что использовать
| Сценарий | L4 | L7 |
|---|---|---|
| Внешний API / Web-приложение | — | Да |
| Маршрутизация между микросервисами по path | — | Да |
| TLS termination | — | Да |
| TCP-сервисы (БД, Redis, Kafka) | Да | — |
| UDP-трафик (DNS, gaming, VoIP) | Да | — |
| Максимальная производительность | Да | — |
| A/B testing, canary deploys | — | Да |
| gRPC балансировка | Частично | Да |
Типичная архитектура: L4 LB на входе (NLB, IPVS) для высокой пропускной способности и TLS passthrough → L7 LB за ним (Nginx, Envoy) для content-based routing. В AWS это выглядит как NLB → ALB, хотя на практике часто достаточно одного ALB.
3 Алгоритмы балансировки
Алгоритм балансировки определяет, какой именно backend-сервер получит следующий запрос. Выбор алгоритма влияет на равномерность распределения нагрузки, латенси и поведение при изменении пула серверов.
3.1 Round Robin
Запросы распределяются по серверам циклически: первый запрос → сервер 1, второй → сервер 2, третий → сервер 3, четвёртый → снова сервер 1.
class RoundRobinBalancer: def __init__(self, servers: list[str]): self.servers = servers self.index = 0 def next_server(self) -> str: server = self.servers[self.index % len(self.servers)] self.index += 1 return server # Использование lb = RoundRobinBalancer(["server1", "server2", "server3"]) # next_server() → server1, server2, server3, server1, server2, ...
Преимущества:
- Простота реализации — одна переменная-счётчик
- Равномерное распределение при одинаковых серверах и одинаковых запросах
- Предсказуемость — легко отладить
Недостатки:
- Не учитывает текущую нагрузку серверов — медленный сервер получает столько же запросов, сколько быстрый
- Не учитывает разную мощность серверов
- Не учитывает «вес» запросов — лёгкий GET и тяжёлый POST с загрузкой файла считаются одинаковыми
Когда использовать: Гомогенный пул серверов (одинаковое железо) с однородной нагрузкой (запросы примерно одинаковой стоимости). Это хороший вариант по умолчанию.
3.2 Weighted Round Robin
Расширение Round Robin, где серверам назначаются веса пропорционально их мощности. Сервер с весом 3 получает в три раза больше запросов, чем сервер с весом 1.
class WeightedRoundRobinBalancer: def __init__(self, servers: list[tuple[str, int]]): # servers = [("server1", 5), ("server2", 3), ("server3", 2)] self.servers = servers self.weights = [w for _, w in servers] self.current_weights = [0] * len(servers) def next_server(self) -> str: # Smooth Weighted Round Robin (Nginx алгоритм) total = sum(self.weights) # Увеличиваем текущие веса for i in range(len(self.servers)): self.current_weights[i] += self.weights[i] # Выбираем сервер с максимальным текущим весом max_idx = self.current_weights.index(max(self.current_weights)) # Уменьшаем вес выбранного сервера self.current_weights[max_idx] -= total return self.servers[max_idx][0]
Nginx использует именно Smooth Weighted Round Robin — алгоритм, который распределяет запросы равномерно во времени, а не блоками. Для серверов с весами (5, 3, 2) последовательность будет: A, A, B, A, C, A, B, A, B, C — а не AAAAABBBCC.
Когда использовать: Гетерогенный пул (серверы разной мощности), canary deploys (новая версия получает малый вес).
3.3 Least Connections
Запрос направляется на сервер с наименьшим числом активных соединений. Это учитывает текущую нагрузку: если сервер обрабатывает тяжёлый запрос, он «занят» и новые запросы пойдут на свободные серверы.
import heapq from threading import Lock class LeastConnectionsBalancer: def __init__(self, servers: list[str]): # (active_connections, server_name) self.pool = [(0, s) for s in servers] heapq.heapify(self.pool) self.lock = Lock() def acquire(self) -> str: with self.lock: conns, server = heapq.heappop(self.pool) heapq.heappush(self.pool, (conns + 1, server)) return server def release(self, server: str): with self.lock: # Уменьшаем счётчик соединений self.pool = [(c - 1 if s == server else c, s) for c, s in self.pool] heapq.heapify(self.pool)
Преимущества:
- Адаптивность — автоматически учитывает текущую нагрузку
- Хорошо работает с запросами разной длительности (short-lived и long-lived)
Недостатки:
- Дороже Round Robin — нужно отслеживать количество соединений
- Не учитывает мощность серверов (решается вариантом Weighted Least Connections)
- При холодном старте (все счётчики = 0) ведёт себя как Round Robin
Когда использовать: Запросы с сильно различающимся временем обработки (API с тяжёлыми запросами к БД), WebSocket-серверы (длительные соединения), gRPC (multiplex на одном соединении).
3.4 Least Response Time
Запрос направляется на сервер с наименьшим временем отклика (и/или наименьшим числом активных соединений). Балансировщик измеряет латенси ответов каждого backend и предпочитает самые быстрые.
class LeastResponseTimeBalancer: def __init__(self, servers: list[str]): self.servers = servers self.response_times = {s: 0.0 for s in servers} self.ewma_alpha = 0.3 # Вес новых измерений def next_server(self) -> str: return min(self.servers, key=lambda s: self.response_times[s]) def record_response(self, server: str, duration: float): # Exponentially Weighted Moving Average old = self.response_times[server] self.response_times[server] = ( self.ewma_alpha * duration + (1 - self.ewma_alpha) * old )
Когда использовать: Гетерогенные backend-ы с разной производительностью, которая может меняться динамически (например, из-за фонового GC, кэш-промахов, переключения на реплику БД).
Nginx Plus и HAProxy поддерживают этот алгоритм. В open-source Nginx он недоступен — используется модуль ngx_http_upstream_least_conn_module.
3.5 IP Hash
Хэш IP-адреса клиента определяет, на какой сервер пойдёт запрос. Один и тот же клиент всегда попадает на один и тот же сервер (при неизменном пуле серверов).
class IPHashBalancer: def __init__(self, servers: list[str]): self.servers = servers def next_server(self, client_ip: str) -> str: idx = hash(client_ip) % len(self.servers) return self.servers[idx]
Преимущества:
- Обеспечивает session affinity без cookies
- Простота реализации
Недостатки:
- Неравномерность: клиенты за одним NAT (корпоративная сеть, мобильный оператор) попадают на один сервер
- При изменении пула серверов (добавление/удаление) большинство клиентов перераспределяются — это ломает сессии
- Не учитывает нагрузку серверов
Когда использовать: Простая session stickiness без возможности использовать cookies (L4 балансировка, не-HTTP протоколы).
3.6 Consistent Hashing
Consistent hashing решает ключевую проблему IP Hash: при изменении пула серверов перераспределяется минимальное количество ключей (≈ K/N, где K — количество ключей, N — количество серверов), а не все.
Хэш-кольцо:
0 / 2^32
│
┌────────┤
│ ServerA (pos: 100)
│ │
│ │ ← key1 (hash: 150) → ServerB
│ │
│ ServerB (pos: 200)
│ │
│ │ ← key2 (hash: 350) → ServerC
│ │
│ ServerC (pos: 500)
│ │
│ │ ← key3 (hash: 800) → ServerA
└────────┘
2^32
Каждый сервер размещается в одной или нескольких точках на хэш-кольце (0 .. 2^32). Ключ хэшируется и попадает на ближайший сервер по часовой стрелке. При добавлении или удалении сервера перераспределяются только ключи между соседними узлами.
import hashlib from bisect import bisect_right class ConsistentHashBalancer: def __init__(self, servers: list[str], virtual_nodes: int = 150): self.ring: list[tuple[int, str]] = [] self.virtual_nodes = virtual_nodes for server in servers: for i in range(virtual_nodes): key = f"{server}:vn{i}" h = self._hash(key) self.ring.append((h, server)) self.ring.sort() self.keys = [h for h, _ in self.ring] def _hash(self, key: str) -> int: return int(hashlib.md5(key.encode()).hexdigest(), 16) def get_server(self, request_key: str) -> str: h = self._hash(request_key) idx = bisect_right(self.keys, h) % len(self.keys) return self.ring[idx][1] def add_server(self, server: str): for i in range(self.virtual_nodes): key = f"{server}:vn{i}" h = self._hash(key) self.ring.append((h, server)) self.ring.sort() self.keys = [h for h, _ in self.ring] def remove_server(self, server: str): self.ring = [(h, s) for h, s in self.ring if s != server] self.keys = [h for h, _ in self.ring]
Virtual nodes (виртуальные узлы): Без виртуальных узлов распределение неравномерно — серверы могут обслуживать сильно различающиеся доли кольца. Virtual nodes размещают каждый физический сервер в нескольких точках кольца (обычно 100–200), что обеспечивает равномерное распределение.
Преимущества:
- Минимальное перераспределение при изменении пула (≈ 1/N ключей)
- Хорошая равномерность с virtual nodes
- Поддерживает weighted distribution (больше virtual nodes = больше нагрузки)
Недостатки:
- Сложнее реализации, чем простой hash
- Необходимость virtual nodes для равномерности
Где используется:
- Amazon DynamoDB — распределение данных по партициям
- Apache Cassandra — назначение данных узлам кластера
- Nginx — директива
hash $request_uri consistent - Akamai CDN — маршрутизация запросов к edge-серверам
- Discord — распределение guild (серверов) между backend-ами
Сравнительная таблица алгоритмов
| Алгоритм | Равномерность | Учёт нагрузки | Session affinity | Сложность | Поведение при изменении пула |
|---|---|---|---|---|---|
| Round Robin | Высокая (одинаковые серверы) | Нет | Нет | O(1) | Полный сброс |
| Weighted RR | Высокая | Статический | Нет | O(1) | Полный сброс |
| Least Connections | Высокая | Динамический | Нет | O(log N) | Плавное перераспределение |
| Least Response Time | Очень высокая | Динамический | Нет | O(N) | Плавное перераспределение |
| IP Hash | Средняя | Нет | По IP | O(1) | Полный сброс |
| Consistent Hashing | Высокая (с vnodes) | Нет | По ключу | O(log N) | Минимальный сброс |
4 Технологии балансировки
4.1 Nginx
Nginx — наиболее популярный web-сервер и reverse proxy, широко используемый как L7 балансировщик. По данным W3Techs, Nginx обслуживает более 30% всех веб-сайтов в интернете.
Архитектура Nginx:
┌────────────────────────────┐
│ Master Process │
│ (чтение конфига, управле- │
│ ние worker-ами) │
└──────────────┬─────────────┘
│
┌─────────────────┼────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Worker 1 │ │ Worker 2 │ │ Worker N │
│ (event loop) │ │ (event loop) │ │ (event loop) │
│ epoll/kqueue│ │ epoll/kqueue│ │ epoll/kqueue│
└──────────────┘ └──────────────┘ └──────────────┘
Nginx использует event-driven, non-blocking архитектуру. Один worker-процесс обрабатывает тысячи соединений одновременно через epoll (Linux) / kqueue (BSD). Это значительно эффективнее модели «один поток на соединение» (Apache prefork).
Типичная конфигурация балансировки:
upstream api_backend { # Алгоритм (по умолчанию Round Robin) least_conn; # или: ip_hash; / hash $request_uri consistent; # Пул серверов server 10.0.1.1:8080 weight=5; # Мощный сервер server 10.0.1.2:8080 weight=3; # Средний сервер server 10.0.1.3:8080 weight=2; # Слабый сервер server 10.0.1.4:8080 backup; # Только при падении основных server 10.0.1.5:8080 down; # Временно отключён # Параметры соединений keepalive 64; # Пул keep-alive соединений к backend } server { listen 443 ssl http2; server_name api.example.com; # TLS ssl_certificate /etc/ssl/cert.pem; ssl_certificate_key /etc/ssl/key.pem; ssl_protocols TLSv1.2 TLSv1.3; # Проксирование location /api/ { proxy_pass http://api_backend; # Заголовки proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Таймауты proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 10s; # Retry при ошибках proxy_next_upstream error timeout http_502 http_503; proxy_next_upstream_tries 2; } # Сжатие gzip on; gzip_types application/json text/plain application/javascript; }
Ключевые параметры:
keepalive— пул постоянных соединений к backend (уменьшает overhead на TCP handshake)proxy_next_upstream— условия, при которых Nginx пробует следующий сервер (retry)max_failsиfail_timeout— параметры health check: если сервер не ответилmax_failsраз заfail_timeout, он временно исключается
Nginx Plus (коммерческая версия) добавляет:
- Активные health checks (HTTP-запросы к backend, а не только пассивный мониторинг)
- Session persistence (sticky sessions по cookie)
- Динамическое обновление upstream без reload
- API для управления конфигурацией
- Least Response Time алгоритм
4.2 HAProxy
HAProxy (High Availability Proxy) — специализированный балансировщик нагрузки, изначально спроектированный именно для этой задачи. Поддерживает L4 и L7 балансировку.
Отличия от Nginx:
| Критерий | Nginx | HAProxy |
|---|---|---|
| Основное назначение | Web-сервер + reverse proxy | Балансировщик нагрузки |
| L4 балансировка | Через stream-модуль | Нативная поддержка |
| Health checks (active) | Только в Nginx Plus | Встроены |
| Sticky sessions | Nginx Plus | Встроены |
| Статистика | Stub status / Plus Dashboard | Детальная stats page |
| Конфигурация | Declarative (nginx.conf) | Declarative (haproxy.cfg) |
| Reload | Graceful reload (новые worker-ы) | Hitless reload (передача сокетов) |
| HTTP/2 к backend | Да | Да (с 2.0+) |
Типичная конфигурация HAProxy:
global
maxconn 50000
log stdout format raw local0
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
option httplog
option forwardfor
frontend http_front
bind *:443 ssl crt /etc/ssl/cert.pem alpn h2,http/1.1
default_backend api_servers
# Content-based routing
acl is_api path_beg /api/
acl is_static path_beg /static/
use_backend api_servers if is_api
use_backend static_servers if is_static
backend api_servers
balance leastconn
option httpchk GET /health
http-check expect status 200
# Cookie-based sticky sessions
cookie SERVERID insert indirect nocache
server api1 10.0.1.1:8080 check weight 5 cookie s1
server api2 10.0.1.2:8080 check weight 3 cookie s2
server api3 10.0.1.3:8080 check weight 2 cookie s3
backend static_servers
balance roundrobin
server static1 10.0.2.1:80 check
server static2 10.0.2.2:80 check
# Страница статистики
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
Когда выбирать HAProxy:
- Нужна продвинутая L4 балансировка (TCP-сервисы: БД, Kafka, Redis)
- Нужны встроенные active health checks без покупки коммерческой лицензии
- Нужна детальная статистика и мониторинг из коробки
- Нужны sticky sessions по cookies
Кто использует: GitHub, Stack Overflow, Reddit, Tumblr, Airbnb.
4.3 Envoy
Envoy — высокопроизводительный L4/L7 proxy, разработанный в Lyft. В отличие от Nginx и HAProxy, Envoy спроектирован для микросервисных архитектур и является ключевым компонентом сервисных mesh-ей (Istio, Linkerd2-proxy использует другой подход, но Envoy доминирует).
Ключевые отличия Envoy:
-
Динамическая конфигурация через API. Nginx и HAProxy требуют reload файла конфигурации. Envoy может получать конфигурацию динамически через xDS API (Listener Discovery Service, Route Discovery Service, Cluster Discovery Service, Endpoint Discovery Service). Это критично для Kubernetes, где pods появляются и исчезают каждую секунду.
-
Observability. Envoy генерирует детальные метрики (Prometheus-compatible), distributed traces (Zipkin/Jaeger), access logs для каждого запроса — без дополнительных модулей.
-
Продвинутая балансировка для gRPC. HTTP/2 мультиплексирует запросы на одном соединении. Nginx при L4 балансировке gRPC отправит все запросы одного соединения на один backend. Envoy понимает HTTP/2 framing и балансирует отдельные gRPC-вызовы.
-
Sidecar proxy. В Service Mesh Envoy деплоится как sidecar рядом с каждым подом — весь входящий и исходящий трафик пода проходит через локальный Envoy.
Service Mesh с Envoy:
┌─────────────────────────┐ ┌─────────────────────────┐
│ Pod A │ │ Pod B │
│ ┌──────┐ ┌───────────┐ │ │ ┌───────────┐ ┌──────┐ │
│ │App A │──│Envoy Proxy│ │───▶│ │Envoy Proxy│──│App B │ │
│ └──────┘ └───────────┘ │ │ └───────────┘ └──────┘ │
└─────────────────────────┘ └─────────────────────────┘
mTLS, retry, circuit breaking, tracing — всё в Envoy
Продвинутые алгоритмы балансировки в Envoy:
- MAGLEV — алгоритм consistent hashing от Google, обеспечивающий более равномерное распределение, чем классический ring-based consistent hashing
- Ring Hash — классический consistent hashing с настраиваемым числом virtual nodes
- Least Request — аналог least connections, но с «power of two random choices» оптимизацией (выбираются два случайных сервера, запрос идёт на менее нагруженный)
- Random — удивительно эффективный при большом числе серверов
Когда выбирать Envoy:
- Микросервисная архитектура с динамическим service discovery
- Kubernetes / Service Mesh (Istio)
- gRPC-сервисы
- Нужна продвинутая observability из коробки
Сравнительная таблица технологий
| Критерий | Nginx (OSS) | HAProxy | Envoy |
|---|---|---|---|
| L4 | Через stream | Нативно | Нативно |
| L7 | Да | Да | Да |
| gRPC (per-request) | Нет (L4 only) | Да | Да |
| Dynamic config | Reload | Reload | xDS API |
| Active health checks | Нет (Plus) | Да | Да |
| Sticky sessions | Нет (Plus) | Да | Да |
| Service Mesh | — | — | Да (Istio) |
| Distributed tracing | Нет | Нет | Да |
| Горячий reload | Graceful | Hitless | Hot restart |
| Лицензия | BSD | GPLv2 | Apache 2.0 |
| Основные пользователи | Большинство web | GitHub, Reddit | Lyft, Google, Dropbox |
5 Облачные балансировщики
Облачные провайдеры предоставляют managed-балансировщики, которые избавляют от необходимости управлять инфраструктурой, автоматически масштабируются и интегрируются с остальными облачными сервисами.
5.1 AWS: ALB и NLB
Application Load Balancer (ALB) — L7 балансировщик:
Интернет
│
▼
┌────────────────────────────────────────────┐
│ ALB │
│ │
│ Listener: HTTPS:443 │
│ │ │
│ ├── Rule: path=/api/* │
│ │ └── Target Group: API servers │
│ │ ├── i-abc123 (10.0.1.1:8080) │
│ │ └── i-def456 (10.0.1.2:8080) │
│ │ │
│ ├── Rule: path=/ws/* │
│ │ └── Target Group: WebSocket servers │
│ │ └── i-ghi789 (10.0.2.1:8080) │
│ │ │
│ └── Default │
│ └── Target Group: Web servers │
│ ├── i-jkl012 (10.0.3.1:80) │
│ └── i-mno345 (10.0.3.2:80) │
└────────────────────────────────────────────┘
Возможности ALB:
- Content-based routing: по path, host header, HTTP method, query string, source IP
- TLS termination с интеграцией ACM (AWS Certificate Manager)
- WebSocket и HTTP/2 поддержка
- gRPC поддержка
- Sticky sessions (cookie-based)
- Интеграция с WAF (AWS WAF) и Cognito (authentication)
- Target types: EC2 instances, IP addresses, Lambda functions
- Автоматическое масштабирование
Network Load Balancer (NLB) — L4 балансировщик:
- Работает на уровне TCP/UDP/TLS
- Миллионы запросов в секунду с ультра-низкой латенси (~100 микросекунд overhead)
- Статический IP-адрес (Elastic IP) — важно для DNS и firewall rules
- Поддержка TLS passthrough и TLS termination
- Source IP preservation — backend видит реальный IP клиента (ALB подменяет на свой)
- Cross-zone load balancing (опционально)
Когда ALB, когда NLB:
| Сценарий | ALB | NLB |
|---|---|---|
| HTTP/HTTPS API | Да | Возможно |
| Content-based routing | Да | Нет |
| gRPC | Да | Нет |
| TCP-сервисы (Redis, Kafka) | Нет | Да |
| UDP-сервисы (DNS, gaming) | Нет | Да |
| Статический IP | Нет | Да |
| Ультра-низкая латенси | Нет | Да |
| WebSocket | Да | Да |
| Перед ALB (NLB → ALB) | — | Да |
Gateway Load Balancer (GWLB) — для прозрачной вставки сетевых appliance-ов (firewall, IDS/IPS). Работает на L3 (IP-уровень) с GENEVE-инкапсуляцией.
5.2 GCP: Cloud Load Balancing
Google Cloud предлагает единую семью балансировщиков с глобальным anycast:
| Тип | Уровень | Регион | Описание |
|---|---|---|---|
| External HTTP(S) LB | L7 | Глобальный | Аналог ALB, но с глобальным anycast (один IP для всего мира) |
| External TCP/UDP LB | L4 | Региональный | Аналог NLB |
| Internal HTTP(S) LB | L7 | Региональный | Для микросервисов внутри VPC |
| Internal TCP/UDP LB | L4 | Региональный | Для внутренних TCP-сервисов |
Ключевое преимущество GCP: глобальный L7 балансировщик с одним anycast IP. Пользователь в Токио, Лондоне и Сан-Паулу обращается к одному IP, а Google Edge автоматически маршрутизирует запрос к ближайшему backend.
5.3 Стоимость и практические советы
Облачные балансировщики тарифицируются по двум параметрам: фиксированная почасовая оплата + плата за обработанные данные (LCU для ALB, NLCU для NLB).
Типичные ошибки:
- Использование ALB для TCP-трафика. ALB не поддерживает TCP passthrough — для Redis, PostgreSQL, Kafka нужен NLB.
- Отключённый cross-zone load balancing. По умолчанию для NLB он выключен — трафик идёт только в backend-ы той же availability zone, что и endpoint NLB. Включайте, если backend-ы неравномерно распределены по зонам.
- Забытый idle timeout. ALB закрывает соединения после 60 секунд бездействия. Для WebSocket и long-polling нужно увеличивать.
- Connection draining (deregistration delay). При удалении backend из target group ALB/NLB ждёт завершения активных запросов. По умолчанию 300 секунд — часто слишком долго для быстрых деплоев. Уменьшайте до 30–60 секунд.
6 Global Server Load Balancing (GSLB)
GSLB распределяет трафик между несколькими дата-центрами или регионами. Если предыдущие разделы описывали балансировку внутри одного дата-центра, GSLB решает вопрос: в какой дата-центр отправить пользователя?
Механизмы GSLB
1. DNS-based GSLB:
Самый распространённый подход. DNS-сервер возвращает IP-адрес ближайшего (или наименее загруженного) дата-центра.
Пользователь в Берлине:
1. DNS query: api.example.com
2. DNS ответ: 185.0.0.1 (дата-центр Франкфурт)
Пользователь в Нью-Йорке:
1. DNS query: api.example.com
2. DNS ответ: 52.1.2.3 (дата-центр Вирджиния)
Стратегии DNS GSLB:
| Стратегия | Описание | Пример |
|---|---|---|
| Geo-proximity | Ближайший дата-центр по географии | Route 53 Geolocation Routing |
| Latency-based | Дата-центр с наименьшей латенси | Route 53 Latency Routing |
| Weighted | Распределение по весам | 80% на US, 20% на EU |
| Failover | Primary-secondary | Primary: US-East, failover: US-West |
| Health-based | Исключение нездоровых дата-центров | Совмещается с любой из вышеперечисленных |
Проблемы DNS GSLB:
- TTL и кэширование. DNS-ответы кэшируются resolver-ами и клиентами. При failover переключение происходит не мгновенно — клиенты продолжают использовать старый IP до истечения TTL (обычно 60–300 секунд).
- DNS resolver location ≠ user location. Пользователь в Токио может использовать DNS-сервер Google (8.8.8.8) в США, и GSLB решит, что пользователь в США. EDNS Client Subnet (ECS) решает эту проблему, передавая подсеть клиента в DNS-запросе.
- Нет учёта нагрузки в реальном времени. DNS GSLB обычно работает на основе статических весов или health checks, а не на основе текущей утилизации серверов.
2. Anycast:
Один и тот же IP-адрес анонсируется через BGP из нескольких дата-центров. Маршрутизаторы интернета автоматически направляют пакеты к ближайшему (по метрике BGP) дата-центру.
Anycast IP: 192.0.2.1
┌── Анонсирован из DC Франкфурт
192.0.2.1 ├── Анонсирован из DC Вирджиния
└── Анонсирован из DC Токио
Пакет от клиента в Берлине → BGP маршрутизация → DC Франкфурт
Пакет от клиента в Бостоне → BGP маршрутизация → DC Вирджиния
Преимущества anycast:
- Мгновенный failover — при отключении дата-центра BGP перенаправляет трафик автоматически (секунды, а не минуты как DNS)
- Защита от DDoS — трафик автоматически распределяется по всем точкам присутствия
- Один IP для всего мира — упрощает конфигурацию
Ограничения anycast:
- Проблемы с TCP при переключении маршрутов — если BGP-маршрут изменился в середине TCP-соединения, соединение разрывается. Для UDP (DNS) это не проблема.
- Ограниченный контроль — маршрутизация определяется BGP, а не логикой приложения
Кто использует anycast:
- Cloudflare — все 200+ дата-центров анонсируют одни IP через anycast
- Google — 8.8.8.8 (DNS), Cloud CDN
- AWS CloudFront — edge-серверы
3. Гибрид: Anycast + DNS:
Cloudflare и подобные сервисы комбинируют оба подхода: DNS-запрос идёт через anycast на ближайший DNS-сервер, который возвращает IP ближайшего edge через anycast. Получается двойной anycast.
7 Health Checks
Балансировщик должен знать, какие backend-серверы работоспособны, а какие нет. Отправка трафика на «мёртвый» сервер приводит к ошибкам и деградации пользовательского опыта. Health checks — механизм проверки состояния серверов.
7.1 Типы health checks
Пассивные health checks (passive / reactive):
Балансировщик анализирует ответы от backend. Если сервер возвращает ошибки (5xx) или не отвечает (timeout), он временно исключается из ротации.
# Nginx: пассивный health check upstream backend { server 10.0.1.1:8080 max_fails=3 fail_timeout=30s; server 10.0.1.2:8080 max_fails=3 fail_timeout=30s; } # Если 3 ошибки за 30 секунд — сервер исключается на 30 секунд
Проблема пассивных health checks: Они реагируют только после того, как реальные пользовательские запросы завершились ошибкой. Первые несколько запросов на нездоровый сервер будут неудачными.
Активные health checks (active / proactive):
Балансировщик периодически отправляет специальные запросы (health probes) к каждому backend. Нездоровые серверы исключаются до того, как реальный трафик попадёт на них.
HAProxy: активные health checks
LB ──[GET /health]──► Backend 1: 200 OK ✓ Healthy
LB ──[GET /health]──► Backend 2: 200 OK ✓ Healthy
LB ──[GET /health]──► Backend 3: 503 ✗ Unhealthy → исключён
LB ──[GET /health]──► Backend 4: timeout ✗ Unhealthy → исключён
# HAProxy: активный health check
backend api_servers
option httpchk GET /health
http-check expect status 200
server api1 10.0.1.1:8080 check inter 5s fall 3 rise 2
server api2 10.0.1.2:8080 check inter 5s fall 3 rise 2
Параметры:
inter 5s— проверка каждые 5 секундfall 3— после 3 неудачных проверок сервер считается нездоровымrise 2— после 2 успешных проверок нездоровый сервер возвращается в ротацию
7.2 Проектирование Health Endpoint
Эффективный health endpoint /health должен проверять реальную работоспособность сервиса, а не просто возвращать 200 OK.
Простой health check (liveness):
@app.get("/health") def health(): return {"status": "ok"}
Проверяет только: «процесс жив и принимает HTTP-запросы». Не обнаружит ситуацию, когда приложение висит из-за deadlock, потеряло соединение с БД или заполнило диск.
Глубокий health check (readiness):
@app.get("/health") def health(): checks = {} # Проверяем подключение к БД try: db.execute("SELECT 1") checks["database"] = "ok" except Exception as e: checks["database"] = f"error: {e}" # Проверяем подключение к Redis try: redis.ping() checks["cache"] = "ok" except Exception as e: checks["cache"] = f"error: {e}" # Проверяем свободное место на диске disk_usage = shutil.disk_usage("/") disk_free_pct = disk_usage.free / disk_usage.total * 100 checks["disk"] = f"{disk_free_pct:.1f}% free" if disk_free_pct < 5: checks["disk"] = f"critical: {disk_free_pct:.1f}% free" all_ok = all( "error" not in v and "critical" not in v for v in checks.values() ) status_code = 200 if all_ok else 503 return JSONResponse( content={"status": "ok" if all_ok else "degraded", "checks": checks}, status_code=status_code, )
Liveness vs Readiness (Kubernetes):
Kubernetes разделяет два типа health checks:
| Тип | Вопрос | Действие при неудаче |
|---|---|---|
| Liveness | «Процесс жив?» | Pod перезапускается |
| Readiness | «Готов принимать трафик?» | Pod исключается из Service (но не перезапускается) |
# Kubernetes: liveness и readiness probes containers: - name: api livenessProbe: httpGet: path: /healthz # Простой: процесс жив? port: 8080 initialDelaySeconds: 10 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /ready # Глубокий: БД, кэш доступны? port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 2
Типичная ошибка: Использовать один и тот же глубокий health check для liveness. Если БД временно недоступна, Kubernetes перезапустит все поды (liveness failed), что только усугубит проблему. Liveness должен проверять только критические признаки жизни процесса. Readiness — зависимости.
7.3 Graceful Shutdown
При обновлении или остановке сервера важно корректно завершить обработку текущих запросов. Процесс graceful shutdown:
1. Сервер получает SIGTERM
2. Сервер перестаёт принимать НОВЫЕ соединения
3. Сервер завершает обработку ТЕКУЩИХ запросов
4. Readiness check начинает возвращать 503 → LB исключает сервер
5. После завершения всех запросов (или timeout) — процесс завершается
import signal import asyncio shutdown_event = asyncio.Event() def handle_sigterm(signum, frame): shutdown_event.set() # Сигнал о начале shutdown signal.signal(signal.SIGTERM, handle_sigterm) @app.get("/ready") def readiness(): if shutdown_event.is_set(): return JSONResponse({"status": "shutting_down"}, status_code=503) return {"status": "ok"} async def graceful_shutdown(): shutdown_event.set() # Ждём завершения текущих запросов (максимум 30 секунд) await asyncio.sleep(30) # Завершаем процесс
Connection draining: ALB и NLB поддерживают deregistration delay — при исключении target из target group балансировщик ждёт завершения активных запросов. Настройте этот параметр в соответствии с максимальным временем обработки запроса (обычно 30–60 секунд, не 300 по умолчанию).
8 Circuit Breaker
Circuit breaker — паттерн, защищающий систему от каскадных отказов. Если downstream-сервис начинает отвечать медленно или с ошибками, circuit breaker «размыкает цепь» — запросы к этому сервису прекращаются, а вызывающий сервис получает быстрый отказ (fail fast) вместо долгого ожидания timeout.
Зачем нужен Circuit Breaker
Без circuit breaker: один медленный сервис «заражает» всю систему.
Без circuit breaker:
User → API Gateway → Service A → Service B (медленный, 30s timeout)
│
Все потоки Service A заняты ожиданием Service B
Service A перестаёт отвечать
API Gateway ждёт Service A
Пользователь видит timeout
→ Каскадный отказ: один медленный сервис убивает всю систему
С circuit breaker: Service A быстро отказывает запросы к Service B и может вернуть fallback-ответ.
С circuit breaker:
User → API Gateway → Service A ──[Circuit OPEN]──✗ Service B
│
└── Немедленный fallback: cached data / default / error
(мс вместо 30s)
Три состояния Circuit Breaker
┌─────────────────┐
┌────│ CLOSED │◄─────────────┐
│ │ (нормальная │ │
│ │ работа) │ │
│ └────────┬────────┘ │
│ │ │
│ Порог ошибок Пробный запрос
│ превышен успешен
│ │ │
│ ▼ │
│ ┌─────────────────┐ ┌────────┴────────┐
│ │ OPEN │────▶│ HALF-OPEN │
│ │ (все запросы │ │ (пропускаем │
│ │ отклоняются) │ │ один запрос) │
│ └─────────────────┘ └─────────────────┘
│ timeout │
│ истёк Пробный запрос
│ провалился
└─────────────────────────────────────┘
- CLOSED — нормальное состояние. Все запросы проходят. Circuit breaker считает ошибки.
- OPEN — цепь разомкнута. Все запросы немедленно отклоняются без обращения к downstream. Длится
reset_timeoutсекунд. - HALF-OPEN — после timeout пропускается один (или несколько) пробный запрос. Если он успешен → CLOSED. Если нет → OPEN.
Реализация
import time from enum import Enum from threading import Lock class State(Enum): CLOSED = "closed" OPEN = "open" HALF_OPEN = "half_open" class CircuitBreaker: def __init__( self, failure_threshold: int = 5, # Порог ошибок для перехода в OPEN reset_timeout: float = 30.0, # Секунд в состоянии OPEN half_open_max: int = 1, # Пробных запросов в HALF_OPEN ): self.failure_threshold = failure_threshold self.reset_timeout = reset_timeout self.half_open_max = half_open_max self.state = State.CLOSED self.failure_count = 0 self.last_failure_time = 0.0 self.half_open_calls = 0 self.lock = Lock() def call(self, func, *args, **kwargs): with self.lock: if self.state == State.OPEN: if time.time() - self.last_failure_time > self.reset_timeout: self.state = State.HALF_OPEN self.half_open_calls = 0 else: raise CircuitOpenError("Circuit is OPEN") if self.state == State.HALF_OPEN: if self.half_open_calls >= self.half_open_max: raise CircuitOpenError("Circuit is HALF_OPEN, max probes reached") self.half_open_calls += 1 try: result = func(*args, **kwargs) self._on_success() return result except Exception as e: self._on_failure() raise def _on_success(self): with self.lock: self.failure_count = 0 if self.state == State.HALF_OPEN: self.state = State.CLOSED def _on_failure(self): with self.lock: self.failure_count += 1 self.last_failure_time = time.time() if self.failure_count >= self.failure_threshold: self.state = State.OPEN class CircuitOpenError(Exception): pass
Использование:
# Circuit breaker для вызовов Payment Service payment_circuit = CircuitBreaker(failure_threshold=5, reset_timeout=30) def process_payment(order_id: str, amount: float): try: result = payment_circuit.call( payment_service.charge, order_id, amount ) return result except CircuitOpenError: # Fallback: поставить платёж в очередь для повторной попытки payment_queue.enqueue(order_id, amount) return {"status": "pending", "message": "Payment will be processed shortly"} except PaymentServiceError: # Ошибка прошла через circuit breaker — она посчитана return {"status": "error", "message": "Payment failed"}
Circuit Breaker в production-инструментах
| Инструмент | Описание |
|---|---|
| Resilience4j (Java) | Преемник Netflix Hystrix. Circuit Breaker, Retry, Rate Limiter, Bulkhead, TimeLimiter. |
| Polly (.NET) | Библиотека устойчивости для .NET. Circuit Breaker, Retry, Timeout, Bulkhead. |
| Envoy | Circuit breaking встроен в proxy: ограничение max connections, max pending requests, max retries. |
| Istio | Circuit breaking через Envoy sidecar + DestinationRule CRD. |
Envoy circuit breaking (через Istio):
apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: payment-service spec: host: payment-service trafficPolicy: connectionPool: tcp: maxConnections: 100 http: h2UpgradePolicy: DEFAULT http1MaxPendingRequests: 50 http2MaxRequests: 100 outlierDetection: consecutive5xxErrors: 5 interval: 10s baseEjectionTime: 30s maxEjectionPercent: 50
outlierDetection — это Envoy-реализация circuit breaker: если endpoint возвращает 5 последовательных 5xx-ошибок, он «выбрасывается» (ejected) из пула на 30 секунд.
Bulkhead Pattern
Дополнительный паттерн устойчивости: изоляция пулов ресурсов для разных downstream-сервисов, чтобы отказ одного не исчерпал ресурсы для остальных.
Без bulkhead:
┌──────────────────────────┐
│ Service A: Thread Pool │
│ (100 потоков, общий пул) │
│ │
│ → Payment Service (slow) │ ← 90 потоков заняты ожиданием
│ → User Service │ ← 10 потоков (не хватает!)
│ → Email Service │ ← 0 потоков (недоступен!)
└──────────────────────────┘
С bulkhead:
┌─────────────────────────┐
│ Service A │
│ ┌─────────────────────┐ │
│ │ Payment Pool (30) │ │ ← Все 30 заняты, но...
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ User Pool (50) │ │ ← ...эти работают нормально
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ Email Pool (20) │ │ ← ...и эти тоже
│ └─────────────────────┘ │
└─────────────────────────┘
8.9 Паттерны балансировки в реальных системах
Netflix: Zuul + Eureka + Ribbon
Netflix построил собственный стек балансировки для микросервисной архитектуры:
- Zuul — API Gateway (L7), принимает все входящие запросы, маршрутизирует между микросервисами, выполняет authentication, rate limiting, canary routing
- Eureka — Service Registry. Каждый микросервис при старте регистрируется в Eureka и периодически обновляет heartbeat. Eureka хранит реестр всех инстансов каждого сервиса.
- Ribbon — Client-side load balancer. Вместо серверного балансировщика каждый сервис сам выбирает, на какой инстанс отправить запрос, получая список инстансов из Eureka.
┌─────────────┐
│ Zuul │◄── Внешние запросы
│(API Gateway)│
└──────┬──────┘
│
┌──────▼──────┐
│ Eureka │ ← Реестр сервисов
│ (Service │
│ Registry) │
└──────┬──────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Svc A │ │Svc B │ │Svc C │
│+Ribbon │ │+Ribbon │ │+Ribbon │ ← Client-side LB
└────────┘ └────────┘ └────────┘
Client-side load balancing — подход, при котором балансировка происходит на стороне клиента (вызывающего сервиса), а не на отдельном балансировщике. Каждый клиент получает список backend-ов из service registry и самостоятельно выбирает, куда отправить запрос.
Преимущества client-side LB:
- Нет дополнительного hop через балансировщик (ниже латенси)
- Нет single point of failure (балансировщик)
- Каждый клиент может использовать свой алгоритм
Недостатки:
- Логика балансировки в каждом сервисе (библиотечная зависимость)
- Сложнее обновлять — нужно обновить библиотеку во всех сервисах
- Мультиязычная среда: нужна реализация Ribbon на каждом языке
Современная альтернатива client-side LB — sidecar proxy (Envoy), который совмещает преимущества обоих подходов: балансировка рядом с сервисом (низкая латенси), но в отдельном процессе (не зависит от языка приложения).
Google: Maglev
Google разработал Maglev — software-defined L4 балансировщик, обрабатывающий трафик для Google Search, YouTube, Gmail и всех внешних сервисов Google.
Ключевые особенности:
- Работает на commodity серверах (не на специализированном железе)
- Каждый сервер Maglev обрабатывает до 10 Mpps (миллионов пакетов в секунду)
- Используется Maglev hashing — алгоритм consistent hashing, обеспечивающий более равномерное распределение, чем ring-based подход, при минимальном disruption при изменении пула
- Все серверы Maglev равноправны (нет master/slave) — трафик распределяется через ECMP (Equal-Cost Multi-Path routing)
Anycast IP: 216.58.x.x
│
┌────┴────┐
│ Router │ ← ECMP: распределение по N серверам Maglev
└────┬────┘
┌─────┼─────┐
▼ ▼ ▼
Maglev Maglev Maglev ← Consistent hashing → Backend pool
#1 #2 #3
Cloudflare: Unimog
Cloudflare разработал Unimog — L4 балансировщик, работающий на базе Linux XDP/eBPF. Обрабатывает трафик всех сервисов Cloudflare на 300+ edge-серверах.
Особенности:
- Работает в kernel space (XDP) — обработка пакетов до сетевого стека Linux
- Производительность до 10M пакетов в секунду на ядро
- DSR (Direct Server Return) — ответы идут напрямую, минуя балансировщик
- Маршрутизация на основе consistent hashing с учётом нагрузки серверов
8.10 Практические рекомендации
Чек-лист при проектировании балансировки
-
Определите уровень балансировки. HTTP API → L7 (ALB/Nginx). TCP-сервисы (БД, Kafka) → L4 (NLB/HAProxy). Нужны оба → каскадно: L4 → L7.
-
Выберите алгоритм. Начните с Round Robin. Запросы разной стоимости → Least Connections. Нужен session affinity → IP Hash или cookie-based sticky sessions. Нужна минимальная перебалансировка при изменении пула → Consistent Hashing.
-
Настройте health checks. Активные health checks обязательны для production. Разделяйте liveness и readiness. Health endpoint должен проверять реальные зависимости (БД, кэш), а не просто возвращать 200.
-
Реализуйте graceful shutdown. При остановке сервера: перестать принимать новые запросы → завершить текущие → завершить процесс. Настройте deregistration delay в балансировщике.
-
Добавьте circuit breaker. Для каждого downstream-сервиса. Определите fallback: кэшированные данные, default-ответ, постановка в очередь. Мониторьте состояние circuit breaker (OPEN/CLOSED).
-
Настройте retry с умом. Retry только для идемпотентных запросов (GET, PUT, DELETE). Не retry-ите POST без idempotency key. Ограничьте число retry (2–3). Используйте exponential backoff с jitter.
-
Мониторинг. Ключевые метрики:
- Request rate (RPS) — по backend и в целом
- Error rate — 4xx, 5xx по backend
- Latency (p50, p95, p99) — от балансировщика и от каждого backend
- Active connections — по backend
- Health check status — количество healthy/unhealthy targets
- Spillover count — запросы, отклонённые из-за отсутствия healthy targets
-
TLS. Терминируйте TLS на балансировщике (или API Gateway). Внутренний трафик может быть HTTP — если сеть доверенная. Для zero-trust — mTLS между сервисами (Envoy/Istio).
-
Connection pooling. Настройте keepalive между балансировщиком и backend-ами. Без keepalive каждый запрос создаёт новое TCP-соединение — это дорого (3-way handshake + TLS handshake).
-
Избегайте single point of failure. Балансировщик не должен быть единственной точкой отказа. Облачные LB (ALB, NLB) масштабируются автоматически. Self-hosted (Nginx, HAProxy) — запускайте минимум два инстанса с VRRP/keepalived для automatic failover.
11 Вопросы для самопроверки
-
Объясните разницу между L4 и L7 балансировкой. В каких сценариях вы выберете L4, а в каких — L7? Приведите по два примера для каждого.
-
Вы проектируете API с 200K RPS. Запросы имеют сильно различающееся время обработки: GET /users/{id} — 5ms, POST /reports — 30s. Какой алгоритм балансировки вы выберете и почему?
-
В вашей системе используется IP Hash для session affinity. 40% трафика приходит из корпоративной сети одного крупного клиента (один внешний IP). Какие проблемы это вызовет и как их решить?
-
Объясните, почему при L4 балансировке gRPC все запросы одного клиента попадают на один backend. Как это решить?
-
Ваш health check endpoint
/healthпроверяет подключение к БД. БД кратковременно недоступна (maintenance, 2 минуты). Что произойдёт, если/healthиспользуется как liveness probe в Kubernetes? Как это исправить? -
Вы используете ALB с target group из 10 EC2-инстансов. Один инстанс начал отвечать медленно (GC pressure), но health check проходит. Пользователи жалуются на высокую латенси. Какие механизмы помогут?
-
Сравните client-side load balancing (Ribbon/gRPC) и server-side load balancing (Nginx/ALB). Когда предпочтительнее каждый из подходов?
-
Объясните паттерн circuit breaker. Зачем нужно состояние HALF_OPEN? Что случится без него?
-
Вы деплоите новую версию сервиса (rolling update). Как обеспечить zero-downtime deployment с помощью балансировщика? Опишите процесс по шагам.
-
Ваша система обслуживает пользователей в Европе, Азии и Северной Америке из трёх дата-центров. Как организовать глобальную балансировку? Сравните DNS-based GSLB и anycast.
12 Дополнительные ресурсы
Papers
- Maglev: A Fast and Reliable Software Network Load Balancer (Eisenbud et al., Google, 2016) — архитектура L4 балансировщика Google, обрабатывающего весь внешний трафик. Детальное описание Maglev hashing и его преимуществ перед ring-based consistent hashing.
- Consistent Hashing and Random Trees (Karger et al., 1997) — оригинальная статья, описывающая consistent hashing. Фундаментальная работа, используемая в десятках распределённых систем.
- The Power of Two Random Choices (Mitzenmacher, 2001) — математическое обоснование алгоритма «выбор из двух случайных», используемого в Envoy и других современных балансировщиках.
- Ananta: Cloud Scale Load Balancing (Patel et al., Microsoft, 2013) — архитектура балансировщика Azure, обрабатывающего трафик всех Azure-сервисов.
Блоги
- Cloudflare Blog: Unimog (blog.cloudflare.com) — серия статей об архитектуре L4 балансировщика Cloudflare на базе XDP/eBPF.
- Netflix Tech Blog: Zuul 2 — архитектура API Gateway Netflix с async non-blocking I/O.
- Envoy Proxy Documentation (envoyproxy.io) — исчерпывающая документация по конфигурации Envoy, включая load balancing, circuit breaking, health checking.
- HAProxy Documentation (docs.haproxy.org) — детальное описание всех алгоритмов балансировки, health checks и конфигурации HAProxy.
- Nginx Blog: Load Balancing (nginx.com/blog) — практические руководства по настройке балансировки в Nginx.
Talks
- "Maglev: A Fast and Reliable Software Network Load Balancer" (Daniel Eisenbud, NSDI) — презентация архитектуры Maglev от инженеров Google.
- "Envoy Internals Deep Dive" (Matt Klein, KubeCon) — внутренности Envoy от его создателя, включая алгоритмы балансировки и circuit breaking.
- "Load Balancing at Scale" (Patrick Nommensen, GitHub Engineering) — как GitHub масштабирует HAProxy для обработки миллионов запросов.
- "Building a Billion User Load Balancer" (Patrick Shuff, Facebook, LISA) — архитектура L4 балансировщика Facebook, обрабатывающего весь трафик social network.