HAProxy и keepalived: балансировка и отказоустойчивый IP
Один веб-сервер рано или поздно становится проблемой: то обновление, то перезагрузка, то просто нагрузка, которую он не тянет. Классическое решение — поставить впереди балансировщик, который раздаёт запросы нескольким серверам и перестаёт отправлять трафик на упавшие. Но тогда единой точкой отказа становится сам балансировщик. Чтобы убрать и её, используют пару узлов и плавающий 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 — это набор связанных навыков, вот соседние темы серии: