Learning Platform
Глоссарий Troubleshooting
Урок 14.01 · 18 мин
Начальный
Load BalancerScalabilityHigh AvailabilityHealth Checks

Зачем нужен load balancer — scalability, redundancy, health checks

Вы написали отличный HTTP-сервер на Python. Он принимает запросы, дёргает базу, возвращает JSON. Один пользователь — 50 мс на запрос, всё летает. Десять пользователей — тоже норм. Сто одновременных — сервер начинает потеть. Тысяча — падает с timeout-ами, потому что Python-процесс просто не успевает.

Что вы сделаете? Скорее всего, инстинктивный ответ — «возьмём сервер помощнее». Это называется vertical scaling (вертикальное масштабирование): больше RAM, больше CPU, быстрее диск. Подход работает, но у него есть потолок: самый мощный сервер у AWS (m7i.metal-48xl) стоит $20+ в час и упирается в физику кремния — больше 192 vCPU вы не получите.

Альтернатива — horizontal scaling: вместо одного жирного сервера запустим десять обычных. Каждый обрабатывает свою долю запросов. Если нагрузка вырастет — добавим ещё. Если упадёт — уберём лишние. Но как клиент узнает, на какой из десяти серверов идти? Вот тут и появляется load balancer — сущность, которая стоит между клиентами и пулом серверов и распределяет запросы.


Три задачи, которые решает load balancer

В production load balancer всегда делает три вещи одновременно: распределяет нагрузку, обеспечивает отказоустойчивость и проверяет здоровье backend-ов. Любая одна задача без двух других — это пол-решения.

LB в архитектуре: клиенты -> LB -> пул backend-ов
Клиент 1Браузер пользователя или мобильное приложение. Знает только публичный адрес LB, ничего не знает про backend-ы
Клиент 2Ещё один клиент. Может быть в другой стране -- LB обработает запрос от любого
Клиент NТысячи клиентов одновременно. Все стучатся на один и тот же IP/DNS
HTTPS
Load BalancerПубличный endpoint api.example.com. Принимает все входящие соединения и решает, на какой backend отправить каждый запрос
internal
Backend 1Реальный application-сервер. Не виден напрямую из интернета -- только LB может с ним говорить. Один из N идентичных инстансов
Backend 2Идентичная копия первого. Та же кодовая база, та же конфигурация. Может быть на той же физической машине или на другой
Backend NN может меняться динамически: autoscaling group в AWS поднимает новые инстансы при росте нагрузки и убивает при падении

Заметьте: клиенты вообще не подозревают о существовании backend-ов. Для них единственная видимая сущность — адрес LB. Это даёт огромную гибкость: завтра вы можете заменить все десять серверов на двадцать, написать их на Go вместо Python, переехать в другой data-center — клиенты ничего не заметят.


Задача 1: Scalability (масштабирование)

Главная видимая фича LB — это распределение нагрузки. Если у вас десять серверов и тысяча запросов в секунду, каждому достанется около 100 RPS. Сервер с такой нагрузкой не страдает.

Важно понимать: LB сам по себе не делает ваше приложение быстрее. Один запрос всё равно обрабатывается одним backend-ом за те же 50 мс. LB меняет другое — пропускную способность системы в целом (throughput). Если один сервер тянет 200 RPS, то десять серверов за LB тянут ~2000 RPS, при условии что bottleneck не в общей БД или другой shared-ресурсе.

Один сервер:    1000 req/s -> 50% запросов timeout-ят, latency p99 = 8000ms
Один сервер:     200 req/s -> 0% timeout, latency p99 = 80ms (нормально)

С LB + 5 backend-ов: 1000 req/s -> по 200 каждому -> 0% timeout, p99 = 80ms

Это horizontal scaling, и его главное преимущество перед vertical — линейность. Чтобы удвоить throughput, удваиваем число backend-ов. Чтобы утроить — утраиваем. С vertical scaling вам пришлось бы покупать новый, в два раза более мощный сервер — и в какой-то момент таких просто не существует.

NOTE

Linearity ломается, когда bottleneck не в backend-ах, а в общем ресурсе. Если все 10 серверов ходят в одну Postgres и БД захлёбывается — увеличение числа application-серверов проблему не решит. LB снимает нагрузку только с application tier. Дальше думаем про read replicas, sharding, кэширование — но это уже не про LB.


Задача 2: Redundancy (отказоустойчивость)

Второй мотив для LB — избавиться от single point of failure. Если у вас один сервер, и он упал в три часа ночи — весь сервис недоступен, пока вы его не поднимете. Это плохо: ваши пользователи злятся, SLA горит, дежурный инженер выгорает.

С LB и пулом серверов отказ одного backend-а — не катастрофа. LB просто перестанет на него отправлять трафик, а остальные подхватят. При этом мощность пула должна быть рассчитана с запасом (N+1 redundancy: если нужно 9 серверов для нагрузки, держим 10 — один может упасть без деградации).

Что происходит при падении backend-а
LBLB периодически делает health check каждого backend-а -- обычно GET на /health endpoint. Видит, что Backend 2 не отвечает
health check
Backend 1Отвечает 200 OK на /health. LB считает его здоровым и шлёт трафик
Backend 2Не отвечает (timeout) или отвечает 5xx. LB помечает unhealthy и временно убирает из ротации
Backend 3Отвечает 200 OK. Подхватывает часть трафика, которая раньше шла на Backend 2

Кроме планового отказа, LB защищает от менее очевидных проблем:

  • Rolling deployment. Вы выкатываете новую версию приложения. Если в ней баг — упадёт не весь пул сразу, а тот инстанс, на который вы её только что задеплоили. LB его выкинет, остальные продолжат работать, дежурный спокойно откатит.
  • Тяжёлый запрос. Один клиент прислал запрос, который на 5 минут занял Python-воркер. С одним сервером новые запросы стояли бы в очереди. С пулом из 10 серверов остальные 9 продолжают обслуживать как обычно.
  • Memory leak. Один из инстансов медленно течёт по памяти, OOM-killer прибивает его. LB замечает, выкидывает, supervisord перезапускает процесс — клиенты ничего не видят.

Задача 3: Health checks (проверка здоровья)

Чтобы LB знал, какой backend живой, а какой нет, он периодически делает health check — проверочный запрос. Это не магия, а обычный HTTP-запрос на специальный endpoint (обычно /health или /healthz), который backend должен реализовать.

Простой health check на Python (FastAPI):

from fastapi import FastAPI

app = FastAPI()

@app.get('/healthz')
def healthz():
    return {'status': 'ok'}

LB настраивается так: каждые 5 секунд стучаться на /healthz. Если 3 подряд проверки failed — помечаем backend unhealthy. Если 2 подряд OK — возвращаем в ротацию.

Есть два уровня health check:

  1. Liveness (живой ли процесс). Самый простой — если HTTP-сервер вообще отвечает, значит процесс живой. Тупо возвращаем 200 OK без всякой логики. Если процесс мёртв или зависший — TCP-connect упадёт или будет timeout.
  2. Readiness (готов ли обслуживать запросы). Более глубокий — проверяем, что backend подключён к БД, что миграции прошли, что внешние зависимости доступны. Если БД упала — backend жив, но запросы обслужить не сможет, поэтому из ротации лучше убрать.
import psycopg2

@app.get('/readyz')
def readyz():
    try:
        # Проверяем, что БД доступна
        conn = psycopg2.connect(DSN)
        conn.cursor().execute('SELECT 1')
        conn.close()
        return {'status': 'ready'}
    except Exception as e:
        # 503 говорит LB: не шли мне трафик, я не готов
        return Response(status_code=503, content=f'db down: {e}')
WARNING

Не делайте readiness check слишком тяжёлым. Если он делает запрос в БД — это лишняя нагрузка каждые 5 секунд от каждого LB. С десятью LB и пулом из 50 инстансов — это 5000 запросов в секунду на БД только из-за health check. Лучше кэшировать результат на 1-2 секунды или проверять простой ping.


Где живёт LB в реальности

LB бывают двух типов: hardware и software. Hardware LB (F5 BIG-IP, Citrix NetScaler) — это физические железки, которые стоят в дата-центре и обрабатывают трафик на уровне специализированных чипов. Дорого, надёжно, исторически используется в банках и telecom.

Software LB — это программы на обычных серверах. Самые популярные: NGINX, HAProxy, Envoy, Traefik. Для большинства задач — именно их вы и будете использовать. Они достаточно быстрые (один NGINX легко тянет 50000 RPS на одном CPU), бесплатные и гибкие.

В облаках есть managed LB-сервисы:

  • AWS: Application Load Balancer (ALB, L7), Network Load Balancer (NLB, L4), Classic Load Balancer (legacy).
  • GCP: Cloud Load Balancing — глобальный (anycast IP), регионный, internal.
  • Cloudflare: Load Balancing — умеет geo-routing, failover между регионами.

Их вы платите по запросам/трафику, не настраиваете сервера, не следите за ними сами. На сервисах побольше всё равно используют свой NGINX/Envoy за облачным LB — слой за слоем.

Internet
   |
   v
Cloudflare (DDoS, WAF, кэширование)
   |
   v
AWS ALB (TLS termination, маршрутизация по path)
   |
   v
NGINX в Kubernetes (rate limiting, internal routing)
   |
   v
Application pods

Это типичная архитектура для production. На каждом уровне LB решает свои задачи.


DNS round-robin — LB бедного человека

Прежде чем появились нормальные LB, люди делали load balancing через DNS. Идея простая: в DNS-записи api.example.com указываете несколько IP-адресов, и резолвер каждый раз отдаёт их в разном порядке.

dig api.example.com +short
# 10.0.0.1
# 10.0.0.2
# 10.0.0.3

# Через 30 секунд:
dig api.example.com +short
# 10.0.0.2
# 10.0.0.3
# 10.0.0.1

Клиент берёт первый IP в списке и идёт туда. Получается распределение. Но у этого подхода куча проблем:

  • DNS кэшируется. ОС, браузер, локальный резолвер — все кэшируют ответ на TTL (часто 60+ секунд). Распределение получается неравномерное.
  • Нет health check. Если один из IP мёртв, DNS об этом не знает. Клиент будет получать его как ответ и падать с timeout-ами.
  • Нет TLS termination. Каждый сервер сам должен иметь сертификат и принимать HTTPS.
  • Нет sticky sessions. Если приложение хранит state в памяти — следующий запрос пользователя может попасть на другой сервер, и сессия потеряется.

Несмотря на это, DNS round-robin до сих пор используют — но не как основной LB, а как глобальное распределение между регионами. Например, у GitHub api.github.com резолвится в разные IP в зависимости от региона — это DNS-based geo-routing.


Что важно запомнить

Load balancer — это не «волшебная штука для масштабирования», а конкретное решение трёх задач:

  1. Scalability: распределить нагрузку по N серверам, чтобы каждому досталось 1/N.
  2. Redundancy: если один сервер упал — остальные продолжают работать. Single point of failure ликвидирован.
  3. Health checks: автоматически детектить, какие backend-ы живы, и роутить трафик только на здоровые.
Kubernetes Service — встроенный L4 load balancer кластера Docker networking: bridge, overlay и встроенный LB в Swarm

Без всех трёх — это не LB, а пол-LB. DNS round-robin делает только первую задачу, и поэтому в production его одного недостаточно.

В следующих уроках разберём, чем отличаются LB на уровне L4 (transport, работает с TCP/UDP) и L7 (application, понимает HTTP), как настраивается reverse proxy на примере NGINX, и какие бывают алгоритмы выбора backend-а (round-robin — лишь один из многих).


Попробуй сам

Давайте посмотрим, как выглядит распределение нагрузки руками. Запустим два простых HTTP-сервера на разных портах и накидаем им запросов.

# Терминал 1: backend на порту 8001
python3 -m http.server 8001

# Терминал 2: backend на порту 8002
python3 -m http.server 8002

# Терминал 3: имитируем round-robin через скрипт
for i in {1..10}; do
  if (( i % 2 == 0 )); then
    curl -s http://localhost:8001/ -w 'backend=8001 status=%{http_code}\n' -o /dev/null
  else
    curl -s http://localhost:8002/ -w 'backend=8002 status=%{http_code}\n' -o /dev/null
  fi
done

А теперь сделаем настоящий LB. Установите NGINX (на macOS — brew install nginx, на Ubuntu — apt install nginx) и создайте простой конфиг:

http {
    upstream backend_pool {
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
    }

    server {
        listen 8080;
        location / {
            proxy_pass http://backend_pool;
        }
    }
}
events {}

Запустите NGINX с этим конфигом и стрельните 10 раз в localhost:8080:

nginx -c /path/to/your/nginx.conf

for i in {1..10}; do
  curl -s http://localhost:8080/ -o /dev/null -w 'request %{response_code}\n'
done

# Посмотрите логи: nginx будет чередовать backend-ы автоматически
tail -f /usr/local/var/log/nginx/access.log

Теперь убейте один из backend-ов (Ctrl+C в терминале 1 или 2) и сделайте ещё 10 запросов. Заметите: NGINX поймёт, что один upstream мёртв, и перестанет отправлять на него запросы. Это и есть базовый health check «по факту» — NGINX видит connection refused и временно выкидывает backend.


Проверка знанийKnowledge check
Junior спрашивает: 'Если у меня всего один сервер и он не падает, зачем мне вообще ставить LB? Это же лишний слой, лишняя задержка и лишняя точка отказа'
ОтветAnswer
Аргумент про 'лишний слой' резонный -- LB действительно добавляет 1-5 мс задержки, и сам по себе может упасть. Но 'мой сервер не падает' -- иллюзия. Падает буквально всё: процесс может зависнуть, OOM-killer прибить, диск кончиться, deploy сломать. Без LB любая из этих причин -- полный downtime сервиса. С LB и хотя бы двумя backend-ами -- незаметная для пользователей деградация. Второй контраргумент: даже одиночный backend часто ставят за LB заранее. Причина -- TLS termination (LB занимается HTTPS, backend живёт по HTTP внутри сети), плюс rate limiting, плюс единая точка для логов и метрик. Когда нагрузка вырастет и понадобится добавить второй сервер -- это будет одна строчка в конфиге LB, без переписывания клиентов. И третий: 'LB сам -- single point of failure'. Решается тем, что LB тоже делают избыточным. В AWS ALB -- это managed-сервис с встроенной репликацией по AZ. В self-hosted -- ставят два HAProxy с keepalived и floating VIP. Получается N-уровневая отказоустойчивость.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Назовите три фундаментальные задачи, которые решает любой production load balancer одновременно.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4