Балансировка и отказоустойчивость
Проектный вопрос: как раздать трафик по нескольким серверам так, чтобы падение одного из них никто не заметил.
Вопрос: «Чем балансировка на L4 отличается от L7? И что произойдёт с запросами, если один из бэкендов внезапно умрёт?»
Что на самом деле проверяет интервьюер
Первая часть — понимаете ли вы, на каком уровне работает ваша инфраструктура и что из этого следует (можно ли маршрутизировать по URL, видит ли балансировщик содержимое запроса, где терминируется TLS). Вторая часть куда важнее: проверяют, знаете ли вы, что сама по себе балансировка отказоустойчивости не даёт. Её дают health checks, таймауты, ретраи и защита от лавины.
L4 против L7
| Критерий | L4 (транспортный) | L7 (прикладной) |
| Видит | IP и порт | URL, заголовки, cookie, метод |
| Маршрутизация | только по адресу назначения | по пути, домену, заголовку |
| TLS | проходит насквозь | обычно терминируется на балансировщике |
| Накладные расходы | минимальные | выше: нужно разобрать запрос |
| Примеры | AWS NLB, IPVS, haproxy в режиме tcp | nginx, Envoy, AWS ALB, Ingress Controller |
Практический вывод: L4 берут, когда нужны предельная пропускная способность или произвольный протокол (базы, брокеры, gRPC-стримы). L7 — когда нужны маршрутизация по путям, канареечные веса, переписывание заголовков, единая точка терминации TLS. В Kubernetes Service type: LoadBalancer — это, как правило, L4, а Ingress Controller — L7 поверх него.
Алгоритмы распределения
- round-robin — по кругу. Просто и подходит для однородных бэкендов.
- least connections — новому запросу достаётся наименее загруженный бэкенд. Лучше при разной длительности запросов.
- ip_hash / consistent hashing — клиент всегда попадает на один и тот же бэкенд. Нужен для кэшей и sticky sessions; при добавлении бэкенда consistent hashing переносит минимум ключей.
- weighted — с весами, если серверы разной мощности или идёт canary.
Про sticky sessions спросят отдельно. Правильный ответ: это костыль, который стоит применять осознанно. Привязка ломает равномерность нагрузки и мешает выкатке — при удалении бэкенда его пользователи всё равно теряют сессию. Здоровое решение — сделать приложение stateless и держать сессии в общем хранилище (Redis) или в подписанном токене.
Что происходит, когда бэкенд умирает
Ответ распадается на четыре механизма, и назвать нужно все.
1. Health checks
Балансировщик периодически опрашивает бэкенды и выводит из ротации те, что не отвечают. Разделяют активные проверки (балансировщик сам стучится на /health) и пассивные (бэкенд помечается «плохим» после N ошибок на реальном трафике). Важная тонкость: endpoint проверки должен отражать способность обслуживать запросы, но не тянуть за собой все внешние зависимости — иначе один сбой БД разом выведет из ротации весь пул.
upstream api {
least_conn;
server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.13:8080 backup;
keepalive 32;
}
server {
listen 443 ssl;
server_name app.example.com;
location / {
proxy_pass http://api;
proxy_connect_timeout 2s;
proxy_read_timeout 10s;
proxy_next_upstream error timeout http_502;
proxy_next_upstream_tries 2;
}
}
2. Таймауты
Отсутствие таймаутов — самая частая причина каскадных отказов. Если бэкенд отвечает бесконечно, воркеры прокси заняты, очередь растёт, и падает весь сервис, а не один бэкенд. Таймауты выставляются на каждом стыке: connect, read, write, а также на клиентские вызовы внутри приложения.
3. Ретраи — осторожно
Повтор запроса помогает при единичной сетевой ошибке и вредит при перегрузке: сервис уже не справляется, а вы утраиваете нагрузку. Правила: повторять только идемпотентные запросы (GET, PUT, DELETE, но не «списать деньги»), ограничивать число попыток, использовать экспоненциальную задержку с jitter, иначе все клиенты синхронно ударят снова в одну и ту же секунду.
4. Circuit breaker и деградация
Если бэкенд стабильно отдаёт ошибки, «предохранитель» размыкает цепь: запросы перестают отправляться и сразу получают отказ, давая упавшему сервису шанс восстановиться. Через таймаут пропускается пробный запрос, и при успехе цепь замыкается. Рядом стоит graceful degradation: не показывать рекомендации, если сервис рекомендаций лежит, но отдать страницу товара. Пользователь видит урезанный сервис вместо ошибки.
Ещё пара вещей, которые стоит упомянуть
- Балансировщик сам не должен быть единой точкой отказа: минимум две ноды с общим виртуальным адресом (keepalived, VRRP) или облачный managed-балансировщик; на уровне DNS — несколько A-записей.
- Зональность. Реплики размещают по разным зонам доступности; в Kubernetes это делается через
topologySpreadConstraints. - Rate limiting на входе защищает от того, что один клиент выест всю мощность.
Типичные ошибки кандидатов
- Считают, что балансировщик сам по себе обеспечивает отказоустойчивость — без health checks он будет исправно слать трафик в мёртвый бэкенд.
- Не ставят таймауты и получают каскадный отказ из-за одного медленного сервиса.
- Включают ретраи без ограничения попыток и без jitter, устраивая шторм запросов.
- Повторяют неидемпотентные запросы — и получают двойные списания.
- Проверяют в health check всю цепочку зависимостей и выводят из ротации весь пул при мигании базы.
- Держат сессии в памяти процесса и лечат это sticky sessions вместо вынесения состояния наружу.
Как ответить кратко (20–30 секунд)
«L4 балансирует по IP и порту, не разбирая содержимое: быстро и годится для любых протоколов. L7 видит URL, заголовки и cookie, поэтому умеет маршрутизацию по путям, веса для canary и терминацию TLS. Если бэкенд умирает, его выводит из ротации health check — активный или пассивный по числу ошибок. Но одной балансировки мало: нужны таймауты на каждом стыке, иначе один зависший бэкенд утянет весь сервис; ретраи только для идемпотентных запросов, с ограничением попыток и экспоненциальной задержкой с jitter; и circuit breaker с деградацией, чтобы не добивать падающий сервис.»