Балансировка и отказоустойчивость

Проектный вопрос: как раздать трафик по нескольким серверам так, чтобы падение одного из них никто не заметил.

Вопрос: «Чем балансировка на L4 отличается от L7? И что произойдёт с запросами, если один из бэкендов внезапно умрёт?»

Что на самом деле проверяет интервьюер

Первая часть — понимаете ли вы, на каком уровне работает ваша инфраструктура и что из этого следует (можно ли маршрутизировать по URL, видит ли балансировщик содержимое запроса, где терминируется TLS). Вторая часть куда важнее: проверяют, знаете ли вы, что сама по себе балансировка отказоустойчивости не даёт. Её дают health checks, таймауты, ретраи и защита от лавины.

L4 против L7

КритерийL4 (транспортный)L7 (прикладной)
ВидитIP и портURL, заголовки, cookie, метод
Маршрутизациятолько по адресу назначенияпо пути, домену, заголовку
TLSпроходит насквозьобычно терминируется на балансировщике
Накладные расходыминимальныевыше: нужно разобрать запрос
ПримерыAWS NLB, IPVS, haproxy в режиме tcpnginx, 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 с деградацией, чтобы не добивать падающий сервис.»

Проверьте себя
1. Что умеет балансировщик L7, чего принципиально не может L4?
AРаспределять нагрузку между несколькими серверами
BМаршрутизировать запросы по URL, домену и заголовкам, потому что видит содержимое HTTP-запроса
CПроверять доступность бэкендов
DРаботать с TCP-соединениями
2. Почему автоматические ретраи опасны при перегрузке сервиса?
AОни нарушают порядок обработки запросов
BОни умножают нагрузку на уже задыхающийся сервис, а синхронные повторы без jitter создают волны запросов
CОни работают только с HTTPS
DОни требуют sticky sessions
3. Health check приложения проверяет доступность базы, кэша и трёх внешних API. Чем это плохо?
AПроверка выполняется слишком быстро
BКратковременный сбой любой зависимости выведет из ротации сразу все бэкенды, превратив частичную деградацию в полный отказ
CБалансировщик не умеет обрабатывать такие ответы
DЭто увеличивает задержку ответа для пользователей