Распределённые системы для сисадмина: от балансировки до шардинга

Исходная ситуация

Стартап «ГроуТех» — SaaS-платформа для автоматизации маркетинга. За год база пользователей выросла с 500 до 50 000, и инфраструктура начала трещать по швам:

  • Один сервер (32 vCPU, 128 GB RAM) — приложение, база PostgreSQL, Redis, Nginx — всё на одной машине
  • Время ответа API: выросло с 50 мс до 2-3 секунд в пиковые часы
  • PostgreSQL: 800 активных подключений, CPU базы на 95%, медленные запросы забивают пул
  • Простой: 3-4 часа в месяц из-за OOM, зависших запросов, деплоев
  • Нет отказоустойчивости: при падении сервера — полный даунтайм

Задача: масштабировать систему для обработки 100 000+ пользователей с SLA 99.9% (максимум 8.7 часов простоя в год).

Балансировка нагрузки: L4 vs L7

Первый шаг — вынести приложение на несколько серверов и поставить перед ними балансировщик. Мы разобрали с «ГроуТех» разницу между L4 и L7 балансировкой:

L4 (Transport layer) — балансировка на уровне TCP-соединений. Быстрая, не анализирует содержимое запросов. Подходит для TCP-сервисов (БД, очереди):

# HAProxy L4 — балансировка PostgreSQL реплик
frontend pg_read
    bind *:5433
    mode tcp
    default_backend pg_replicas

backend pg_replicas
    mode tcp
    balance leastconn
    option tcp-check
    server pg-replica-1 10.0.1.11:5432 check inter 3s fall 3 rise 2
    server pg-replica-2 10.0.1.12:5432 check inter 3s fall 3 rise 2
    server pg-replica-3 10.0.1.13:5432 check inter 3s fall 3 rise 2

L7 (Application layer) — балансировка на уровне HTTP. Может маршрутизировать по URL, заголовкам, cookie. Подходит для веб-приложений:

# Nginx L7 — балансировка приложения
upstream app_backend {
    least_conn;  # выбираем сервер с наименьшим числом соединений
    server 10.0.1.21:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 10.0.1.22:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 10.0.1.23:8080 weight=2 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name api.growtech.ru;

    location /api/ {
        proxy_pass http://app_backend;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
    }

    # Статика идёт напрямую с CDN, не нагружая бэкенд
    location /static/ {
        return 301 https://cdn.growtech.ru$request_uri;
    }
}

Алгоритмы балансировки, которые мы рассмотрели:

  • Round Robin — по очереди, просто и предсказуемо
  • Least Connections — на сервер с наименьшим числом активных соединений (выбрали этот)
  • IP Hash — sticky sessions по IP клиента (нужно для stateful-приложений)
  • Weighted — более мощные серверы получают больше трафика

Репликация базы данных

PostgreSQL на одном сервере не справлялся с нагрузкой чтения. Мы настроили streaming replication с одним мастером и тремя репликами:

# На мастере: postgresql.conf
wal_level = replica
max_wal_senders = 10
max_replication_slots = 4
synchronous_commit = on
synchronous_standby_names = 'FIRST 1 (replica1, replica2, replica3)'

# Создаём пользователя для репликации
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'strong_password';

# pg_hba.conf — разрешаем подключение реплик
host replication replicator 10.0.1.0/24 scram-sha-256
# На реплике: инициализация через pg_basebackup
pg_basebackup -h 10.0.1.10 -D /var/lib/postgresql/16/main \
  -U replicator -Fp -Xs -P -R

# Флаг -R автоматически создаёт standby.signal и добавляет
# primary_conninfo в postgresql.auto.conf

# Проверяем статус репликации на мастере
SELECT client_addr, state, sync_state,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes
FROM pg_stat_replication;

--  client_addr  | state     | sync_state | lag_bytes
-- 10.0.1.11     | streaming | sync       | 0
-- 10.0.1.12     | streaming | async      | 1024
-- 10.0.1.13     | streaming | async      | 2048

Приложение «ГроуТех» мы разделили на read и write paths: все SELECT-запросы идут на реплики через HAProxy (L4), все INSERT/UPDATE/DELETE — на мастер. Это сняло 70% нагрузки с мастера.

Шардинг: когда репликации недостаточно

Через 3 месяца таблица events (маркетинговые события пользователей) выросла до 2 миллиардов строк. Даже с индексами запросы тормозили. Мы внедрили горизонтальный шардинг — разделение данных по нескольким серверам PostgreSQL.

Стратегия шардинга — по tenant_id (ID клиента):

# Хеш-функция для определения шарда
# shard_number = tenant_id % num_shards

# Citus — расширение PostgreSQL для прозрачного шардинга
# Установка на координатор:
CREATE EXTENSION citus;

# Добавляем воркеры (шарды)
SELECT citus_add_node('10.0.2.1', 5432);  -- shard-1
SELECT citus_add_node('10.0.2.2', 5432);  -- shard-2
SELECT citus_add_node('10.0.2.3', 5432);  -- shard-3
SELECT citus_add_node('10.0.2.4', 5432);  -- shard-4

# Распределяем таблицу по tenant_id
SELECT create_distributed_table('events', 'tenant_id');
SELECT create_distributed_table('campaigns', 'tenant_id');

# Теперь запрос автоматически идёт на нужный шард:
SELECT count(*) FROM events
WHERE tenant_id = 42 AND created_at > '2026-01-01';
-- Citus маршрутизирует на shard-2 (42 % 4 = 2)

Ключевые правила шардинга, которые мы усвоили:

  • Выбирайте ключ шардинга по самому частому фильтру в запросах — у нас это tenant_id
  • Co-location: связанные таблицы (events, campaigns, users) должны шардироваться по одному ключу, чтобы JOIN оставался локальным
  • Избегайте cross-shard запросов — они работают на порядок медленнее
  • Начинайте с 4-8 шардов — resharding болезненная операция, лучше заложить запас

CDN и кэширование

30% трафика «ГроуТех» — статика (JS, CSS, изображения, отчёты в PDF). Мы вынесли её на CDN и настроили многоуровневое кэширование:

# Nginx — кэширование на уровне reverse proxy
proxy_cache_path /var/cache/nginx/api
    levels=1:2 keys_zone=api_cache:100m
    max_size=10g inactive=60m use_temp_path=off;

server {
    location /api/v1/reports/ {
        proxy_pass http://app_backend;
        proxy_cache api_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_key "$request_uri|$http_authorization";
        proxy_cache_use_stale error timeout updating http_500 http_502;
        add_header X-Cache-Status $upstream_cache_status;
    }
}

Redis — кэш уровня приложения:

# Redis Cluster для кэширования сессий и горячих данных
# Конфигурация Redis Cluster (6 нод: 3 master + 3 replica)

# /etc/redis/redis.conf на каждой ноде
port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
maxmemory 4gb
maxmemory-policy allkeys-lru

# Создание кластера
redis-cli --cluster create \
  10.0.3.1:6379 10.0.3.2:6379 10.0.3.3:6379 \
  10.0.3.4:6379 10.0.3.5:6379 10.0.3.6:6379 \
  --cluster-replicas 1

# Пример кэширования в приложении (Python)
import redis
r = redis.RedisCluster(startup_nodes=[{"host": "10.0.3.1", "port": 6379}])

def get_campaign(tenant_id, campaign_id):
    key = f"campaign:{tenant_id}:{campaign_id}"
    cached = r.get(key)
    if cached:
        return json.loads(cached)
    data = db.query("SELECT * FROM campaigns WHERE id = %s", campaign_id)
    r.setex(key, 300, json.dumps(data))  # TTL 5 минут
    return data

Уровни кэширования в порядке приоритета:

  1. CDN (CloudFlare) — статика, TTL 24 часа
  2. Nginx proxy_cache — ответы API, TTL 1-10 минут
  3. Redis Cluster — горячие данные приложения, TTL 5-60 минут
  4. PostgreSQL — shared_buffers + OS page cache

Очереди сообщений и асинхронная обработка

«ГроуТех» отправлял email-рассылки синхронно: пользователь нажимал «Отправить», API блокировался на время отправки 10 000 писем. Мы внедрили Apache Kafka как брокер сообщений:

# Kafka Cluster — 3 брокера
# docker-compose.yml (упрощённо)
version: '3'
services:
  kafka-1:
    image: confluentinc/cp-kafka:7.6.0
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zk:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://10.0.4.1:9092
      KAFKA_NUM_PARTITIONS: 12
      KAFKA_DEFAULT_REPLICATION_FACTOR: 3
      KAFKA_MIN_INSYNC_REPLICAS: 2
      KAFKA_LOG_RETENTION_HOURS: 168  # 7 дней

# Создание топика для email-рассылок
kafka-topics.sh --create --bootstrap-server 10.0.4.1:9092 \
  --topic email-campaigns \
  --partitions 12 \
  --replication-factor 3

# Producer (API-сервер) — кладёт задачу в очередь
from confluent_kafka import Producer
p = Producer({'bootstrap.servers': '10.0.4.1:9092,10.0.4.2:9092'})
p.produce('email-campaigns', key=str(tenant_id), value=json.dumps({
    'campaign_id': 123,
    'recipients': [...],
    'template': 'welcome'
}))
p.flush()

# Consumer (Worker) — обрабатывает задачи из очереди
from confluent_kafka import Consumer
c = Consumer({
    'bootstrap.servers': '10.0.4.1:9092',
    'group.id': 'email-workers',
    'auto.offset.reset': 'earliest'
})
c.subscribe(['email-campaigns'])
while True:
    msg = c.poll(1.0)
    if msg:
        task = json.loads(msg.value())
        send_campaign_emails(task)

Результат: API отвечает за 50 мс вместо 30 секунд, рассылка обрабатывается 5 воркерами параллельно. При пиковой нагрузке можно добавить воркеров — Kafka автоматически распределит партиции.

Service Discovery и Health Checking с Consul

С ростом количества сервисов (API, Workers, PostgreSQL, Redis, Kafka) ручное управление адресами стало невозможным. Мы внедрили HashiCorp Consul для автоматического обнаружения сервисов:

# Установка Consul Agent на каждом сервере
curl -fsSL https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
apt install consul

# /etc/consul.d/consul.hcl
datacenter = "dc1"
data_dir   = "/opt/consul"
retry_join = ["10.0.0.1", "10.0.0.2", "10.0.0.3"]

# Регистрация сервиса с health check
# /etc/consul.d/api-service.hcl
service {
  name = "api"
  port = 8080
  tags = ["v2.1", "production"]

  check {
    http     = "http://localhost:8080/health"
    interval = "10s"
    timeout  = "3s"
    deregister_critical_service_after = "60s"
  }
}

# Nginx интеграция через consul-template
# /etc/consul-template.d/upstream.tpl
upstream api_backend {
    {{range service "api"}}
    server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=10s;
    {{end}}
    keepalive 32;
}

# consul-template автоматически перезагружает Nginx при изменении сервисов
consul-template \
  -template "/etc/consul-template.d/upstream.tpl:/etc/nginx/conf.d/upstream.conf:nginx -s reload"

Теперь при добавлении нового сервера API достаточно запустить на нём Consul agent с регистрацией сервиса — он автоматически появится в upstream Nginx.

Circuit Breaker и отказоустойчивость

В распределённой системе один упавший сервис может повалить всю цепочку. Мы реализовали паттерн Circuit Breaker — «автоматический предохранитель», который размыкает цепь при обнаружении проблем:

# Circuit Breaker на уровне Nginx (без модификации кода приложения)
# Используем proxy_next_upstream + limit_req

# Ограничение запросов к проблемному upstream
upstream email_service {
    server 10.0.5.1:8080 max_fails=5 fail_timeout=30s;
    server 10.0.5.2:8080 max_fails=5 fail_timeout=30s;
}

location /api/v1/email/ {
    proxy_pass http://email_service;
    proxy_connect_timeout 3s;
    proxy_read_timeout 10s;
    proxy_next_upstream error timeout http_502 http_503 http_504;
    proxy_next_upstream_tries 2;
    proxy_next_upstream_timeout 5s;

    # Если все upstream недоступны — возвращаем fallback
    error_page 502 503 504 = @email_fallback;
}

location @email_fallback {
    default_type application/json;
    return 202 '{"status": "queued", "message": "Email service temporarily unavailable, request queued for retry"}';
}

Состояния Circuit Breaker:

  • Closed (замкнут) — всё работает, запросы проходят нормально
  • Open (разомкнут) — сервис недоступен, запросы сразу получают fallback-ответ (не ждут таймаут)
  • Half-Open (полуоткрыт) — пробуем пропустить один запрос; если успешно — замыкаем; если нет — размыкаем снова

В Nginx параметр max_fails=5 fail_timeout=30s реализует этот паттерн: после 5 ошибок сервер исключается из upstream на 30 секунд (Open), затем Nginx пробует снова (Half-Open).

Результаты и архитектура

Финальная архитектура «ГроуТех» после 2 месяцев работы:

  • 3 сервера API за Nginx L7 балансировщиком
  • PostgreSQL: 1 мастер + 3 реплики + 4 шарда Citus для таблицы events
  • Redis Cluster: 6 нод (3 master + 3 replica)
  • Kafka: 3 брокера, 5 consumer-воркеров
  • Consul: 3 сервера + agent на каждой ноде
  • CDN: CloudFlare для статики
МетрикаДо (1 сервер)После (распределённая)
Время ответа API (p50)800 мс45 мс
Время ответа API (p99)3200 мс180 мс
Пользователей50 000200 000+
Запросов/сек50015 000
Доступность~99.5%99.95%
Простой/месяц3-4 часа< 15 минут

Распределённые системы — это не только про производительность, но и про отказоустойчивость. Падение одного сервера API, одной реплики PostgreSQL или одного брокера Kafka не вызывает даунтайм. Система самовосстанавливается через health checks и circuit breakers. Если вашему проекту пора масштабироваться — обращайтесь в ITFresh, мы спроектируем архитектуру под вашу нагрузку.

Часто задаваемые вопросы

Три сигнала: CPU/RAM постоянно выше 70%, время ответа API выросло в 3+ раза, и вы не можете деплоить без даунтайма. Не масштабируйте заранее — premature optimization дороже, чем правильно настроенный один сервер. Начинайте с оптимизации (кэш, индексы, пул соединений), и только когда упираетесь в железо — добавляйте серверы.
Партицирование — разделение одной таблицы на части внутри одного сервера PostgreSQL. Шардинг — распределение данных по нескольким серверам. Начинайте с партицирования (бесплатно, просто), и только когда один сервер не справляется — переходите на шардинг (Citus, Vitess).
RabbitMQ — классический брокер сообщений, проще в настройке, лучше для задач с подтверждением обработки. Kafka — распределённый лог, лучше для потоковой обработки, аналитики и высоких throughput (миллионы сообщений в секунду). Для типичного веб-приложения RabbitMQ проще, для Big Data и event-driven архитектуры — Kafka.
Для 5-10 серверов достаточно статической конфигурации в Nginx и /etc/hosts. Consul и подобные инструменты оправданы при 20+ сервисах с динамическим масштабированием, когда серверы добавляются и убираются автоматически.
Chaos Engineering — целенаправленное внесение сбоев. Начните просто: остановите один из серверов API и убедитесь, что трафик переключился на оставшиеся. Затем усложняйте: убейте реплику PostgreSQL, отключите брокер Kafka. Инструменты: Chaos Monkey, Litmus, или просто systemctl stop в cron по расписанию.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#балансировка#репликация#шардинг#cdn#consul#kafka#distributed systems#load balancing
Комментарии 0

Оставить комментарий

загрузка...