Команда StatusDude обслуживает тысячи мониторинговых проверок в минуту, работает в трёх регионах и выкатывает деплои по несколько раз в день — и всё это на Docker Compose и HAProxy. Ни одного упавшего запроса, ни одного простоя. Никакого etcd, за которым надо следить в три часа ночи.
Сначала они попробовали Traefik. Продержалось это около четырёх часов. Первая же попытка rolling-деплоя с двумя сервисами (backend и backend_new), имеющими одинаковые Traefik-лейблы, закончилась ошибкой "Service defined multiple times" и 404 на все запросы. Traefik просто отказался маршрутизировать трафик. Когда перешли на схему с docker compose --scale, проявилась вторая проблема: таблица маршрутизации Traefik обновлялась с задержкой в несколько секунд, и во время скейла вниз запросы улетали в уже умирающие контейнеры. Выходили 502-е ошибки.
Но главный убийца был впереди. В Traefik нет возможности ретраить упавший запрос на другом бэкенде. Его встроенный retry middleware просто повторяет попытку на том же самом умирающем сервере. Если docker stop отправляет SIGTERM процессу Uvicorn, а запрос уже летит в этот контейнер — соединение рвётся. Клиент получает ошибку. Traefik с этим ничего не делает. В тот же день Traefik вынесли и поставили HAProxy.
Для zero-downtime деплоя нужно всего три вещи: несколько инстансов бэкенда, балансировщик, который умеет ретраить на другой инстанс, и скрипт, заменяющий контейнеры по одному. В HAProxy это делается через option redispatch 1. Когда запрос падает с ошибкой (502, 503, 504, пустой ответ), HAProxy молча ретраит его на здоровом сервере. Клиент ничего не замечает.
Кроме редиспатча, у HAProxy настроено три независимых уровня проверок здоровья: ретрай для одиночных запросов (миллисекунды), пассивное наблюдение за 5xx ошибками (observe layer7) и активные health check'и каждую секунду (inter 1s). Discovery работает через встроенный DNS Docker (127.0.0.11:53), без монтирования сокета или дёргания label'ов. Конфиг HAProxy статический — около 60 строк.
Сам скрипт деплоя — 10 строк make. Он останавливает и удаляет один контейнер, ждёт, пока HAProxy подхватит изменения по DNS (максимум 2 секунды), и запускает новый с --wait до прохождения healthcheck'а. Потом — следующий. Ни одного упавшего запроса.
Иногда скучный, проверенный годами инструмент оказывается правильным выбором. Nginx бы тоже подошёл, но автору захотелось поднять именно HAProxy.