Балансировка нагрузки¶
Когда у тебя N инстансов API — кто-то должен решить, на какой отправить конкретный запрос. Этот «кто-то» — load balancer. Его выбор и настройка определяют, выживет ли система при росте, отказе ноды или внезапном пике.
L4 vs L7¶
🧩 Простыми словами. L4 (transport) и L7 (application) — это уровни OSI-модели. Названия пугают, разница простая:
- L4 LB видит TCP/UDP пакеты. Знает «откуда → куда», IP, порт. Не знает HTTP-путь, не понимает заголовков. Очень быстрый, almost zero overhead.
- L7 LB парсит HTTP-запрос. Видит method, path, headers, cookies. Может
роутить
/api/users → service-A,/api/orders → service-B. Дороже по CPU.
graph TB
subgraph L4
TCP[TCP packet] --> L4LB[L4 LB<br/>HAProxy TCP, AWS NLB]
L4LB --> Pod1[Pod 1]
L4LB --> Pod2[Pod 2]
end
subgraph L7
HTTP[HTTP request] --> L7LB[L7 LB<br/>nginx, envoy, ALB]
L7LB -->|/api/users| ServA[Service A]
L7LB -->|/api/orders| ServB[Service B]
L7LB -->|host=admin.x| Admin[Admin]
end
⚙️ Под капотом. L4 чаще DSR (Direct Server Return) или просто NAT — десятки миллионов pps на одной ноде. L7 терминирует TCP, парсит HTTP, иногда TLS — на порядок дороже, но даёт path-routing, sticky sessions, header injection, A/B-routing.
💥 Зачем это нужно. L4 — для raw throughput (gRPC pass-through, WebSocket). L7 — для микросервисной маршрутизации, canary deploys, header-based routing.
| Свойство | L4 | L7 |
|---|---|---|
| Speed | Очень быстро | Медленнее |
| HTTP-aware | Нет | Да |
| TLS termination | Нет (passthrough) | Да |
| Path-based routing | Нет | Да |
| Headers / cookies | Нет | Да |
| WebSocket / gRPC | Прозрачно | Нужна поддержка |
🛠 В Go-проекте. На фронт обычно nginx/Traefik (L7). Перед ним — облачный LB
(AWS NLB, GCP TCP LB) на L4 для DDoS-устойчивости и multi-AZ. Внутри
Kubernetes — Service (L4 через kube-proxy) + Ingress (L7 через nginx-ingress
или envoy).
Алгоритмы балансировки¶
Round-robin¶
Простейший. Каждый следующий запрос — следующему инстансу по кругу.
✅ Дёшево, равномерно при одинаковых инстансах. ❌ Не учитывает текущую нагрузку. Если запрос на A повис на 30 секунд — round-robin всё равно даст ему следующий.
Weighted round-robin¶
Инстансы с разной мощностью получают разный вес. A:3, B:1, C:1 → A получает
3/5 трафика. Используется при разнородных машинах или canary deploy
(canary:1, stable:9).
Least connections¶
LB смотрит текущее число открытых соединений и шлёт на наименее загруженного.
✅ Хорошо для long-lived connections (gRPC streaming, WebSocket). ❌ LB должен помнить состояние. В кластере LB-инстансов — нужен shared state.
Weighted least connections¶
Тот же least-conn, но с весами. Дефолт в HAProxy.
IP-hash¶
server = hash(client_ip) % N. Один клиент всегда на один инстанс. Использовали
до Redis-сессий — для sticky session на нативной L4-балансировке.
Consistent hashing¶
🧩 Простыми словами. Обычный hash-by-key плох при добавлении/удалении ноды: все ключи переcчитываются. Consistent hashing располагает ноды на «кольце» хешей, и при изменении количества нод пересчитывается только малая часть ключей (в среднем 1/N).
graph TD
H((hash ring)) --- N1[Node 1]
H --- N2[Node 2]
H --- N3[Node 3]
K1[key1] -.idёт по часовой стрелке.-> N2
K2[key2] -.-> N3
K3[key3] -.-> N1
Ключевое применение:
- Cache cluster. При добавлении Redis-ноды большинство ключей не двигаются.
- Sharding. То же.
- Sticky service mesh. Linkerd / envoy используют consistent hash для закрепления ключа за инстансом (нужно для in-memory state).
Реализация на Go: github.com/buraksezer/consistent, github.com/lafikl/consistent.
import "github.com/buraksezer/consistent"
cfg := consistent.Config{
PartitionCount: 271, // простое число > nodes*replicas
ReplicationFactor: 20, // virtual nodes на физический
Load: 1.25,
Hasher: hasher{},
}
c := consistent.New(nil, cfg)
c.Add(node1); c.Add(node2); c.Add(node3)
member := c.LocateKey([]byte("user:42"))
Random / random-two-choices¶
«Random» — да, тупо случайный. Удивительно, но почти как round-robin для большого числа однородных инстансов. Power of two choices (выбираем случайных два, берём менее загруженного) — почти как least-conn, но без state. Используется в envoy.
📊 Чек-лист выбора алгоритма.
| Workload | Алгоритм |
|---|---|
| Stateless API, ровные запросы | Round-robin |
| Long-lived (gRPC, WS) | Least connections |
| Caching tier (Redis, Memcached) | Consistent hash |
| Heterogeneous fleet | Weighted RR |
| Canary deployment | Weighted RR / по headers |
| Cloud-native, low-latency | Power of two choices |
Sticky sessions¶
🧩 Простыми словами. «Прибей клиента к одному инстансу». Реализуется через
куку JSESSIONID или header. Нужна, если приложение хранит состояние локально.
❌ Антипаттерн в большинстве случаев: stateless приложения масштабируются горизонтально, состояние выносим в Redis/БД.
✅ Иногда оправдано: WebSocket к конкретной shard, in-memory game state.
⚙️ Failure mode. При падении инстанса все его «прибитые» клиенты разом теряют сессии — большая боль, если состояние было в памяти.
Health checks¶
LB должен знать, какие инстансы живы. Опрашивает каждые N секунд:
- Active. LB сам ходит на
/healthили TCP-конект. Простой и стандартный. - Passive. Смотрит на статусы реальных запросов: 5xx или таймаут — пометил unhealthy.
- Combined. Active как минимум, passive как ускоритель.
// типичный /health в Go-сервисе
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
if !db.Healthy() || !redis.Healthy() {
http.Error(w, "unhealthy", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})
📌 Liveness vs Readiness (Kubernetes).
- Liveness. «Жив ли процесс?» — если нет, перезапустить. Простая проверка.
- Readiness. «Готов ли принимать трафик?» — если нет, исключить из балансировки. Возвращай fail при прогреве кэша, миграции, downstream outage.
Failover и redundancy¶
graph TB
User --> DNS{DNS}
DNS --> LB1[LB primary<br/>AZ-A]
DNS --> LB2[LB standby<br/>AZ-B]
LB1 --> A1[App AZ-A]
LB1 --> A2[App AZ-A]
LB2 --> B1[App AZ-B]
LB2 --> B2[App AZ-B]
A1 --> DB[(DB primary)]
B1 --> DBR[(DB replica)]
DB <-.replication.-> DBR
- Active-active. Оба LB принимают трафик. Higher capacity, но сложнее координация.
- Active-passive. Standby ждёт failover (через keepalived / VRRP / DNS-flip). Проще, но capacity = 1.
Failure modes для LB:
- LB упал — нужен второй LB с floating IP (VRRP) или anycast.
- App упал — health check отметит, LB исключит. Через 1–10 секунд новые запросы туда не пойдут.
- DB primary упал — promote replica. Сложнее, см. databases.md.
Геобалансировка¶
🧩 Простыми словами. Пользователь в Лондоне должен попасть в LB в Лондоне, а не в Сан-Франциско. Решается двумя путями:
DNS-LB (GeoDNS)¶
DNS-сервер видит исходный IP запроса (по EDNS Client Subnet) и отдаёт IP ближайшего LB. Например, AWS Route 53 latency-based routing.
✅ Работает на любом приложении. ❌ DNS кешируется (TTL 60s минимум) — failover медленный.
Anycast¶
Один и тот же IP-адрес объявляется из разных датацентров. Сетевой маршрутизатор
сам выберет ближайший. Так работают CDN, DNS-серверы Google (8.8.8.8).
✅ Очень быстрый failover (BGP withdraw). ❌ Нужна поддержка от провайдера. Сложно.
CDN¶
Edge-кэш для статики и иногда динамики. Cloudflare, Fastly, CloudFront, Yandex CDN. Снимает первый hop с твоей инфры. Дешевле, чем покупать второй датацентр.
🔥 Пример: API сервис в проде¶
graph TB
User --> CDN[Cloudflare<br/>edge cache]
CDN --> ALB[AWS ALB<br/>L7, TLS terminate]
ALB --> Ing[nginx-ingress<br/>L7, K8s]
Ing --> SvcA[Service api]
Ing --> SvcB[Service search]
SvcA --> P1[Pod 1]
SvcA --> P2[Pod 2]
SvcA --> P3[Pod 3]
Двухуровневая балансировка: облачный ALB перед K8s, ingress внутри. ALB на L7 делает TLS-termination + WAF, ingress — path-routing внутрь.
❌ Типичные ошибки¶
- Один LB без HA → SPOF.
- Health check на
/(фронт) — не отражает state DB. Делай/readyотдельно. - Sticky sessions на stateless API → ненужная связанность.
- Не настроен
keepaliveupstream → новые TCP-коннекты на каждый запрос. - DNS TTL 1 час → failover часами.
- Long-lived gRPC через L7 LB без поддержки HTTP/2 → деградация до HTTP/1.
🤖 Что спрашивает AI-ментор¶
- Когда выбирать L4, а когда L7?
- В чём разница round-robin и least-connections? Когда какой лучше?
- Что такое consistent hashing и зачем оно для кэшей?
- Что такое sticky session и почему это часто плохая идея?
- Liveness vs readiness probe в Kubernetes — в чём разница?
📊 Уровни глубины¶
L1. Знаешь, что такое LB, что round-robin делит запросы по кругу. Видел nginx.
L2. Различаешь L4/L7, алгоритмы, написал /health. Понимаешь readiness vs
liveness.
L3. Настраивал HAProxy/envoy в проде. Использовал consistent hashing для кэша. Делал failover (active-passive с keepalived). Знаешь BGP/anycast, TLS termination, WAF, mTLS, debugged sticky session bugs.
📝 Подумай¶
- У тебя gRPC сервис с long-lived connections. Какой алгоритм балансировки выберешь и почему?
- Зачем нужны virtual nodes (replicas) в consistent hashing?
- Health check возвращает 200, но БД мертва. Что не так с проверкой?
Ответ
- Round-robin плох: запрос «прилипает» к одному коннекту → один инстанс перегружен. Least-connections или random-two-choices, чтобы балансировать по числу активных стримов. Альтернатива — client-side LB через service mesh (envoy) с правильным policy.
- Без виртуальных нод одна физическая нода занимает один сектор кольца — при удалении/добавлении пересчёт неравномерный (одни ноды берут много, другие почти ничего). Virtual nodes (100–1000 per phys node) сглаживают распределение.
- Health-проверка не пингует зависимости. Это liveness check, а нужен
readiness: проверять
db.Ping(), доступность downstream-сервисов. Если один из них умер —/readyвозвращает 503, LB исключает под из балансировки.
Дальше: queues.md — асинхронная коммуникация, без которой не выживет ни один большой сервис.