Один веб-сервер рано или поздно становится проблемой: то обновление, то перезагрузка, то просто нагрузка, которую он не тянет. Классическое решение — поставить впереди балансировщик, который раздаёт запросы нескольким серверам и перестаёт отправлять трафик на упавшие. Но тогда единой точкой отказа становится сам балансировщик. Чтобы убрать и её, используют пару узлов и плавающий IP-адрес, который автоматически переезжает на живой узел. Эту работу делает keepalived по протоколу VRRP.

Соберём схему: srv-lb01 (192.168.10.41) и srv-lb02 (192.168.10.42) в паре, плавающий адрес 192.168.10.40, за ними два веб-сервера srv-linux01 (192.168.10.11) и srv-linux02 (192.168.10.12). Домен itpages.lab.local.

Шаг 1. Подготовка узлов

На обоих балансировщиках ставим пакеты и разрешаем службам привязываться к адресу, которого на узле пока нет. Это важно: на резервном узле плавающего IP нет, а HAProxy должен стартовать всё равно.

sudo apt update
sudo apt install -y haproxy keepalived
echo 'net.ipv4.ip_nonlocal_bind = 1' | sudo tee /etc/sysctl.d/90-haproxy.conf
sudo sysctl --system
haproxy -v

Проверь, что часы синхронны (chrony или systemd-timesyncd) и что узлы видят друг друга: ping -c2 192.168.10.42 с первого. VRRP использует мультикаст на адрес 224.0.0.18, и некоторые виртуальные сети его режут, об этом ниже.

Шаг 2. Конфигурация HAProxy

Файл /etc/haproxy/haproxy.cfg одинаков на обоих узлах. Начнём с http-балансировки двух бэкендов и странички статистики.

global
    log /dev/log local0
    maxconn 4000
    user haproxy
    group haproxy
    daemon

defaults
    mode http
    log global
    option httplog
    option dontlognull
    option forwardfor
    timeout connect 5s
    timeout client  30s
    timeout server  30s
    retries 3

frontend fe_web
    bind 192.168.10.40:80
    default_backend be_web

backend be_web
    balance roundrobin
    option httpchk GET /health
    http-check expect status 200
    server srv-linux01 192.168.10.11:80 check inter 3s fall 3 rise 2
    server srv-linux02 192.168.10.12:80 check inter 3s fall 3 rise 2

listen stats
    bind 192.168.10.41:8404
    stats enable
    stats uri /stats
    stats refresh 10s
    stats auth admin:ChangeMe123

Параметры check, fall и rise задают проверку здоровья: сервер выводится из ротации после трёх неудачных проверок подряд и возвращается после двух удачных. Строку статистики на втором узле поменяй на адрес 192.168.10.42, а пароль замени на свой. Проверяем и запускаем:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl enable --now haproxy
sudo systemctl reload haproxy

Для обновлений без обрыва соединений всегда используй reload, а не restart: новый процесс подхватывает сокеты, а старый дорабатывает текущие запросы.

Шаг 3. Алгоритмы балансировки и сессии

Round robin подходит, если все серверы равны и запросы однотипны. Для неравных машин задай вес, а для долгих соединений выбирай leastconn:

backend be_web
    balance leastconn
    cookie SRV insert indirect nocache
    server srv-linux01 192.168.10.11:80 check weight 100 cookie s1
    server srv-linux02 192.168.10.12:80 check weight 50  cookie s2

Строка с cookie включает липкие сессии: пользователь, которого один раз направили на s1, продолжит ходить туда, пока тот жив. Это костыль для приложений, хранящих сессию в памяти. Если можешь, храни сессии в общем хранилище (Redis, база) и липкость не используй, так балансировка получается равномернее, а отказ сервера не выбрасывает пользователей.

Шаг 4. Настраиваем keepalived на srv-lb01

Теперь сделаем плавающий адрес. Главный узел srv-lb01 получает более высокий приоритет. Файл /etc/keepalived/keepalived.conf:

global_defs {
    router_id srv-lb01
    enable_script_security
    script_user root
}

vrrp_script chk_haproxy {
    script "/usr/bin/pgrep haproxy"
    interval 2
    weight -30
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 110
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass S3cr3tVrp
    }
    unicast_src_ip 192.168.10.41
    unicast_peer {
        192.168.10.42
    }
    virtual_ipaddress {
        192.168.10.40/24 dev eth0
    }
    track_script {
        chk_haproxy
    }
}

Название интерфейса проверь командой ip -br a, у тебя может быть ens18 или enp0s3. Режим unicast мы выбрали сознательно: он работает в облаках и виртуальных сетях, где мультикаст режется. Скрипт chk_haproxy снижает приоритет узла на 30, если процесс HAProxy пропал, и тогда адрес уходит к соседу, даже если сам хост жив.

Шаг 5. Настраиваем keepalived на srv-lb02

Резервный узел почти копия, отличаются роль, приоритет и адреса:

global_defs {
    router_id srv-lb02
    enable_script_security
    script_user root
}

vrrp_script chk_haproxy {
    script "/usr/bin/pgrep haproxy"
    interval 2
    weight -30
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass S3cr3tVrp
    }
    unicast_src_ip 192.168.10.42
    unicast_peer {
        192.168.10.41
    }
    virtual_ipaddress {
        192.168.10.40/24 dev eth0
    }
    track_script {
        chk_haproxy
    }
}

Запускаем службу на обоих узлах и смотрим, у кого оказался адрес:

sudo systemctl enable --now keepalived
ip -br a show eth0
journalctl -u keepalived -n 20 --no-pager

На srv-lb01 в выводе будет 192.168.10.40, на srv-lb02 нет. В журнале ищи строку Entering MASTER STATE. Пароль auth_pass в VRRP ограничен восемью символами и защищает лишь от случайных соседей, а не от злоумышленника, так что считай сеть между балансировщиками доверенной.

Шаг 6. Проверка отказа

Единственный способ убедиться, что отказоустойчивость работает, это сломать её сознательно. Запусти с клиента непрерывные запросы и в другом окне останови службу на главном узле:

while true; do curl -s -o /dev/null -w '%{http_code} %{time_total}\n' http://192.168.10.40/; sleep 0.5; done
sudo systemctl stop haproxy
ip -br a show eth0
sudo systemctl start haproxy

Ожидаемо: пара запросов задержится или упадёт, потом адрес появится на srv-lb02, и ответы пойдут снова. Когда HAProxy на srv-lb01 вернётся, адрес вернётся обратно, потому что у него приоритет выше. Если не хочешь лишнего переключения туда-обратно, поставь на обоих узлах state BACKUP и опцию nopreempt: тогда адрес остаётся там, где оказался. Затем проверь и отказ бэкенда: останови nginx на srv-linux01 и посмотри страницу статистики на 192.168.10.41:8404/stats, где сервер станет красным.

Шаг 7. TCP-режим для остальных сервисов

HAProxy умеет не только HTTP. Для баз данных или SSH-пересылки используй mode tcp, он не разбирает содержимое и работает быстрее:

listen ssh_gw
    bind 192.168.10.40:2222
    mode tcp
    balance source
    server srv-linux01 192.168.10.11:22 check
    server srv-linux02 192.168.10.12:22 check

Режим balance source привязывает клиента к серверу по его адресу, так что соединения одного человека всегда попадают на одну машину.

Шаг 8. HTTPS на балансировщике

Сейчас мы балансируем открытый http. В реальности трафик шифрован, и у тебя два варианта. Первый, passthrough: HAProxy в режиме tcp пропускает TLS-поток к серверам, сертификаты живут на бэкендах, зато балансировщик не видит заголовков. Второй, терминация: HAProxy сам расшифровывает трафик и общается с бэкендами по http, что даёт фильтрацию, перенаправления и единое место для сертификатов. Для второго варианта собери файл из сертификата, цепочки и ключа и подключи его:

cat itpages.crt ca-chain.crt itpages.key | sudo tee /etc/haproxy/certs/itpages.pem > /dev/null
sudo chmod 600 /etc/haproxy/certs/itpages.pem

frontend fe_https
    bind 192.168.10.40:443 ssl crt /etc/haproxy/certs/itpages.pem alpn h2,http/1.1
    http-request set-header X-Forwarded-Proto https
    redirect scheme https code 301 if !{ ssl_fc }
    default_backend be_web

Не забудь, что сертификат нужно разместить на обоих балансировщиках и обновлять синхронно. Удобно хранить его в одном каталоге под контролем git или раскатывать скриптом rsync, а после обновления выполнять sudo systemctl reload haproxy. Параметр alpn включает HTTP/2 для клиентов, что заметно ускоряет страницы с множеством ресурсов.

Шаг 9. Маршрутизация по условиям (ACL)

HAProxy умеет направлять запросы на разные бэкенды по имени хоста, пути или заголовкам. Это превращает его в простой шлюз для нескольких приложений за одним адресом:

frontend fe_web
    bind 192.168.10.40:80
    acl host_api hdr(host) -i api.itpages.lab.local
    acl path_static path_beg /static /images
    use_backend be_api if host_api
    use_backend be_static if path_static
    default_backend be_web

Так можно отправить API на отдельные серверы, статику на другие, а основной сайт оставить на текущих. Ещё одна полезная функция, ограничение частоты запросов через stick-table: она отсекает клиентов, которые шлют больше сотни запросов за десять секунд, и спасает бэкенды от простого флуда.

frontend fe_web
    stick-table type ip size 100k expire 30s store http_req_rate(10s)
    http-request track-sc0 src
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }

Шаг 10. Наблюдение и обслуживание без простоя

Чтобы выводить сервер на обслуживание без обрыва пользователей, используй сокет администратора. Добавь в раздел global строку stats socket /run/haproxy/admin.sock mode 660 level admin и управляй состоянием командами:

echo 'set server be_web/srv-linux01 state drain' | sudo socat stdio /run/haproxy/admin.sock
echo 'show servers state be_web' | sudo socat stdio /run/haproxy/admin.sock
echo 'set server be_web/srv-linux01 state ready' | sudo socat stdio /run/haproxy/admin.sock

Режим drain перестаёт отправлять новых пользователей на сервер, но даёт доработать текущим сессиям, после чего его можно обновлять и возвращать в строй. Для метрик подключи экспортёр Prometheus, встроенный в современные версии, и отслеживай число активных сессий, очередь и ошибки проверок.

Шаг 11. Ловушки при эксплуатации пары

Когда схема заработала, нужно удержать её в рабочем состоянии. Хранить конфигурации лучше в одном git-репозитории и раскатывать на оба узла командой rsync -av /etc/haproxy/ srv-lb02:/etc/haproxy/, а перед перезагрузкой всегда проверять синтаксис. Уведомления о переключении помогают узнать о проблеме раньше пользователей: keepalived умеет вызывать скрипт при смене состояния.

vrrp_instance VI_1 {
    notify /usr/local/bin/vrrp-notify.sh
}

#!/usr/bin/env bash
# /usr/local/bin/vrrp-notify.sh: аргументы TYPE NAME STATE PRIORITY
logger -t keepalived-notify "$1 $2 перешёл в состояние $3 (приоритет $4)"

Такая запись попадёт в журнал, а оттуда её подхватит система мониторинга. Обрати внимание и на ARP: после переезда адреса keepalived рассылает gratuitous ARP, чтобы коммутаторы и соседи обновили таблицы. Если в вашей сети переключение занимает больше нескольких секунд, проверь настройки защиты ARP на коммутаторе и параметр garp_master_delay. Наконец, раз в месяц устраивай учения: отключай главный узел и убеждайся, что всё продолжает работать. Схема, которую ни разу не проверяли в бою, подводит именно тогда, когда нужнее всего.

Частые ошибки и как их избежать

  • Забытый ip_nonlocal_bind. Без него HAProxy на резервном узле не стартует, потому что не может привязаться к 192.168.10.40, и при переключении получаешь пустое место вместо сервиса. Параметр прописывается в sysctl.d и проверяется командой sysctl net.ipv4.ip_nonlocal_bind на обоих узлах.
  • Одинаковые virtual_router_id в разных парах или блокированный VRRP. Если в сети есть другая пара keepalived с тем же идентификатором, получишь странные переключения. Выбирай уникальный номер, используй unicast и открой протокол 112 в файрволе: sudo iptables -A INPUT -p vrrp -s 192.168.10.0/24 -j ACCEPT. Признак проблемы, когда оба узла считают себя MASTER (split brain).
  • Нет проверки здоровья или она неправильная. Если /health возвращает 200, даже когда приложение сломано, балансировщик продолжит слать трафик на мёртвый сервер. Делай проверку, которая реально трогает приложение и зависимости, и отдельно следи за тем, чтобы конфиги на lb01 и lb02 не расходились (храни их в git или раскатывай одной командой).

Что дальше

Администрирование Linux — это набор связанных навыков, вот соседние темы серии: