HAProxy на VPS: round robin, проверки /health, веса, страница статистики. Убили бэкенд — трафик ушёл на остальные. Ubuntu 24.04.
Балансировщик нагрузки — программа, которая стоит перед несколькими копиями вашего приложения и каждый входящий запрос отдаёт одной из них. Если одна копия умерла, трафик туда больше не идёт, и пользователи ничего не замечают. HAProxy — бесплатный балансировщик с открытым кодом, который хорошо делает эту работу и помещается на самый маленький VPS. Ниже мы ставим его, подключаем три бэкенда, нарочно убиваем один и смотрим, как HAProxy обходит упавший.
Коротко.
sudo apt install haproxyставит HAProxy 2.8 на Ubuntu 24.04. В/etc/haproxy/haproxy.cfgдобавьтеfrontend(куда приходят запросы) иbackend(серверы за ним). Включитеoption httpchkиcheckу каждого сервера — тогда мёртвые выводятся из работы сами. Передsystemctl reload haproxyвсегда запускайтеhaproxy -c -f /etc/haproxy/haproxy.cfg. Один балансировщик сам по себе единая точка отказа: решите, насколько это важно именно вам.
Всё проверено на тестовом VPS: Ubuntu 24.04.5, HAProxy 2.8.30 из штатного репозитория apt. Чтобы уместить стенд на одной машине, три «сервера приложения» — маленькие скрипты на Python на портах 9001, 9002 и 9003 адреса 127.0.0.1. В жизни это отдельные серверы или контейнеры. Каждая заглушка отвечает на любой путь своим именем и возвращает 200 на /health (обычный python3 -m http.server не подойдёт: там ответ 404 и сервер будет помечен как DOWN). Выводы ниже взяты из этого прогона. TLS и методы балансировки кроме round robin мы здесь не тестировали и в нужных местах так и пишем.
Причин две, и это разные задачи.
Если вам нужно лишь «чтобы сайт не лежал минуту при выкладке», балансировщик, возможно, не нужен: хватит обратного прокси и скрипта перезапуска. Начинайте с балансировщика, когда копий приложения уже больше одной или скоро будет.
Ubuntu поставляет HAProxy в своём репозитории, лишние источники не нужны. Вторая команда ставит пакет, третья показывает версию — чтобы вы знали, по какой документации сверяться:
sudo apt updatesudo apt install -y haproxyhaproxy -vНа нашем сервере строка вывода начиналась с HAProxy version 2.8.30-0ubuntu0.24.04.2. Пакет сразу запускает службу, но конфиг по умолчанию ничего не слушает, поэтому вреда нет. systemctl is-active haproxy выдал active.
HAProxy читает один файл — /etc/haproxy/haproxy.cfg. Сверху пакет кладёт секции global и defaults; не трогайте их, свои секции дописывайте ниже. Мы добавили вот что:
frontend web bind 127.0.0.1:8080 mode http option forwardfor default_backend appbackend app mode http balance roundrobin option httpchk GET /health http-check expect status 200 default-server inter 2s fall 2 rise 2 server app1 127.0.0.1:9001 check server app2 127.0.0.1:9002 check server app3 127.0.0.1:9003 checkПострочно:
frontend web — «дверь». bind 127.0.0.1:8080 — адрес и порт, которые он слушает. На настоящем сервере вы напишете bind *:80. Мы привязались к localhost, чтобы лабораторию не было видно снаружи.mode http — HAProxy понимает HTTP-запросы, а не просто поток байтов TCP. Нужно для forwardfor, заголовков и маршрутизации по HTTP.option forwardfor — добавляет заголовок X-Forwarded-For с адресом клиента: иначе каждый бэкенд видит только адрес балансировщика.default_backend app — всё отправлять в группу app.balance roundrobin — серверы берутся по очереди: 1, 2, 3, 1, 2, 3.option httpchk GET /health вместе с http-check expect status 200 — раз в несколько секунд HAProxy запрашивает у каждого сервера /health. Любой ответ, кроме 200, считается сбоем. Если у приложения виртуальные хосты, проверке может понадобиться заголовок Host: см. http-check send в документации. Сделайте в приложении дешёвую страницу /health, которая проверяет действительно важное, например живое ли соединение с базой.default-server inter 2s fall 2 rise 2 — проверка каждые 2 секунды; два сбоя подряд выводят сервер из работы, две удачи подряд возвращают. inter 2s и rise 2 — значения по умолчанию; по умолчанию fall равен 3, мы поставили 2, чтобы демонстрация шла быстрее. Для нагруженного приложения интервал можно увеличить.check в конце каждой строки server — без этого слова сервер не проверяется, и HAProxy продолжает слать запросы мёртвому. Это частое упущение.Прежде чем что-то перезапускать, попросите HAProxy проверить файл. Работающую службу это не трогает:
haproxy -c -f /etc/haproxy/haproxy.cfgНужна строка Configuration file is valid. Затем применяем:
sudo systemctl restart haproxyss -ltnp | grep haproxyВ выводе ss был 127.0.0.1:8080 от процесса haproxy. Балансировщик работает.
Каждый бэкенд отвечает своим именем. Мы отправили шесть запросов:
for i in 1 2 3 4 5 6; do curl -s http://127.0.0.1:8080/; doneОтветы пришли строго в порядке app1, app2, app3, app1, app2, app3. Round robin устроен просто: не помнит, кто клиент, просто берёт следующий сервер из списка. Если приложение хранит сессию входа в памяти одного процесса, это проблема: следующий запрос может попасть на другую копию. Либо храните сессии в общем месте (база, Redis или Valkey), либо смотрите в документации HAProxy варианты balance source и привязку по cookie. Мы их не тестировали.
Это главное в упражнении. Мы остановили второй процесс приложения и подождали шесть секунд — втрое больше интервала проверки. Затем ещё шесть запросов (в каждой строке ответа для краткости убрали хвост , X-Forwarded-For=…):
hello from app1hello from app3hello from app1hello from app3hello from app1hello from app3Ни одного неудавшегося запроса, ни одного ответа от app2. HAProxy вывел его из работы сам. Почему, видно в статистике. Мы взяли её в формате CSV командой curl -s 'http://127.0.0.1:8404/stats;csv' (секция listen stats из нашего конфига, ниже) и оставили пять столбцов (строку самого backend опустили):
app app1 UP L7OK 0app app2 DOWN L4CON 2app app3 UP L7OK 0UP / DOWN — текущее состояние.L7OK — последняя проверка прошла на уровне приложения (HTTP 200). L4CON — проверка даже не смогла открыть соединение; здесь это «в соединении отказано».fall 2: после второй неудачи сервер ушёл в DOWN.То же самое есть в системном журнале. В journalctl -u haproxy появляется строка вроде Server app/app2 is DOWN, reason: Layer4 connection problem, info: "Connection refused". Повесьте оповещение на эту строку или на страницу статистики.
Затем мы снова запустили app2. После двух успешных проверок он вернулся в работу, и в шести ответах снова были все три имени.
Допишите эту секцию в тот же файл, затем выполните проверку и перезагрузку, описанные ниже:
listen stats bind 127.0.0.1:8404 mode http stats enable stats uri /stats stats refresh 5sОна отдаёт живую таблицу по каждому серверу: состояние, запросы, ошибки, время ответа. Мы читали только CSV через curl. HTML-версия открывается в браузере по адресу /stats; раз мы привязали её к localhost, заходите с компьютера через SSH-туннель (ssh -L 8404:127.0.0.1:8404 user@ваш-сервер). Не привязывайте её к публичному адресу без пароля: страница рассказывает посторонним, как устроен ваш бэкенд.
Серверы редко одинаковы. Мы задали app1 weight 3 (по умолчанию 1) и перезагрузили конфиг. На десять запросов вышло:
6 hello from app1 2 hello from app2 2 hello from app3Это 3 : 1 : 1, как и просили. Важна команда перезагрузки:
haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxyreload запускает новый процесс с новым конфигом и дает старому завершить открытые соединения, так что пользователей не обрывает. Знак && означает: перезагрузка выполнится, только если проверка прошла. Ещё один метод, balance leastconn (отправлять на сервер с наименьшим числом активных соединений), подходит для долгих соединений; он описан в документации HAProxy, мы его не тестировали.
Мы заменили в копии файла roundrobin на roundrobinn и проверили её:
[ALERT] (36471) : config : parsing [/tmp/hap_bad.cfg:44] : balance only supports 'roundrobin', 'static-rr', 'leastconn', 'source', 'uri', 'url_param', 'hdr(name)' and 'rdp-cookie(name)' options.[ALERT] (36471) : config : Error(s) found in configuration file : /tmp/hap_bad.cfg[ALERT] (36471) : config : Fatal errors found in configuration.Код возврата был 1, в сообщении есть имя файла и номер строки, а живая служба осталась нетронутой — мы проверяли копию. Поэтому мы всегда запускаем -c первым.
Мы остановили все три. Балансировщику спрашивать некого, и клиент получает:
HTTP/1.0 503 Service Unavailableа в журнале появляется backend 'app' has no server available!. Сам HAProxy жив, но ответ — ошибка. Показать страницу поприветливее можно через errorfile 503 (см. документацию), а слово backup у сервера оставит его запасным: он получит трафик, только когда остальных не станет.
С option forwardfor бэкенд получает адрес клиента в X-Forwarded-For. В лаборатории все клиенты были локальные, поэтому в заголовке стоял 127.0.0.1. Затем мы отправили из curl собственный заголовок X-Forwarded-For: 203.0.113.7, и приложение первым вывело подделанное значение. Значит, клиент может выдумать этот заголовок. Настройте приложение или веб-сервер перед ним так, чтобы доверять адресу, добавленному вашим балансировщиком, а не первому значению в списке. На внешнем балансировщике можно сначала убрать собственный заголовок клиента: http-request del-header X-Forwarded-For.
Документация описывает завершение TLS строкой вроде bind *:443 ssl crt /etc/haproxy/certs/site.pem, где в файле лежат вместе сертификат и ключ. Сертификат в этой лаборатории мы не настраивали, поэтому готовый проверенный рецепт не заявляем. Если не хочется возиться с сертификатами вручную, проще может оказаться Caddy или Nginx впереди, а HAProxy за ним или вместо него.
Если упадёт VPS с HAProxy, до живых бэкендов не дойдёт ничего. Три способа с этим жить: принять (небольшой проект), держать конфиг балансировщика настолько простым, чтобы собрать его за десять минут на свежем сервере, либо запустить два балансировщика и переключать адрес в DNS или общим IP. Последнее мы здесь не проверяли. Для большинства проектов в одиночку достаточно маленького, редко меняемого сервера под балансировщик: обычно он надёжнее приложения за ним: в нём мало движущихся частей.
HAProxy экономен. Для обычной пересылки HTTP обычно хватает самого маленького тарифа (нагрузку мы не мерили); нагрузка растёт с числом соединений и с работой TLS, так что при добавлении HTTPS следите за процессором. Держите балансировщик и бэкенды в одной локации, чтобы запрос не пересекал континент дважды; лишний переход мы не измеряли.
Справятся оба. Nginx — прежде всего веб-сервер, а потом балансировщик; открытая версия проверяет серверы только пассивно, замечая неудавшиеся запросы. HAProxy создан для балансировки, проверяет серверы активно (как мы показали) и умеет отдавать страницу статистики. Если Nginx у вас уже стоит и нужна простая балансировка, он подойдёт.
Выкладывайте на один бэкенд, пока остальные работают, убедитесь, что его /health отвечает 200, и переходите к следующему. При включённых проверках HAProxy сам вернёт сервер после нужного числа успешных проверок (rise). Смежный рецепт — в статье про несколько сервисов вместе в Docker Compose.
Нет. Память зависит от числа одновременных соединений; прежде чем выбирать размер, проверьте на своём трафике.
Да, в режиме mode tcp он пересылает любой TCP-трафик, например базу данных или любую другую TCP-службу. Мы разобрали только mode http и ничего другого не тестировали.
DNS round robin раздаёт адреса, не зная, какие серверы живы, а клиенты кэшируют ответ. Мёртвый сервер продолжает получать долю запросов, пока вы не правите DNS и не истекут старые записи. Балансировщик с проверками при наших настройках (в тесте) реагирует за секунды.
apt install haproxy; на Ubuntu 24.04 это версия 2.8.frontend, backend, balance roundrobin и check у каждого сервера.option httpchk GET /health, чтобы выводился не только выключенный сервер, но и плохо отвечающий. В нашем тесте остановленный бэкенд ушёл из ротации за секунды и вернулся сам.haproxy -c -f ...; на живой службе используйте reload, а не restart.X-Forwarded-For слепо и не открывайте страницу статистики наружу.Если бэкенды — контейнеры, Docker Compose покажет, как запускать несколько сервисов вместе, а это руководство поможет, когда образы и журналы начнут съедать диск бэкенда.