Стенд к двум статьям:
- «nginx не умеет reload. Он умеет fork» — https://habr.com/ru/articles/1067686/
- «
nginx -s reloadможет не применить конфиг» — https://habr.com/ru/articles/1068364/
Собирает nginx, Apache httpd и Traefik из запиненных тегов и прогоняет по ним (плюс по HAProxy из пакета) один и тот же сценарий перезагрузки конфигурации. Отвечает на один вопрос: что происходит с уже открытым соединением, когда вы делаете reload.
| версия | откуда бинарь | |
|---|---|---|
| nginx | release-1.31.3 |
сборка из тега |
| httpd | 2.4.68, MPM event |
сборка из тега |
| traefik | v3.7.10 |
сборка из тега |
| haproxy | из пакета дистрибутива | только наблюдение |
Ко второй части здесь же лежат пять отдельных опытов, уже только по nginx:
каталог part2/.
./run-in-docker.sh # три прокси
EXTRAS=1 ./run-in-docker.sh # плюс haproxy
PART2=1 ./run-in-docker.sh # пять опытов второй частиWindows, PowerShell:
.\run-in-docker.ps1
.\run-in-docker.ps1 -Extras
.\run-in-docker.ps1 -Part2Первый прогон 10–15 минут — собираются httpd и traefik. Повторные — минуты:
каталоги сборки лежат в томе reload-stand-work.
Результат: results/RESULTS.txt (и results/logs/, если что-то не собралось).
Для второй части — results/PART2.txt.
Апстрим на 127.0.0.1:9001 отвечает сразу на / и через шесть секунд на
/slow. Каждый прокси проксирует на него всё, добавляя заголовок
X-Config: v1. Дальше для каждой цели:
- открыть keep-alive соединение KA, сделать в нём запрос → ждём
v1; - в фоне начать запрос в
/slow— он будет «в полёте» через весь reload; - сменить конфиг на
v2и перезагрузить; - снять дерево процессов;
- повторить запрос в том же сокете KA — здесь реализации расходятся;
- запрос в новом сокете — контроль, ждём
v2; - дождаться запроса из шага 2 — на каком конфиге он доработал;
- снять дерево процессов ещё раз, через три секунды.
Перезагрузка у каждого своя: nginx -s reload, httpd -k graceful,
haproxy -sf <старый pid>, а у Traefik команды reload нет вовсе — конфиг
переписывается на диске, файловый провайдер с watch: true подхватывает сам.
worker_processes 1 у nginx и StartServers 1 у httpd — чтобы эффект не
размазался по процессам.
запрос в полёте idle keep-alive новый сокет
nginx старый конфиг закрыт новый конфиг
httpd старый конфиг закрыт новый конфиг
haproxy старый конфиг закрыт новый конфиг
traefik старый конфиг живёт, НОВЫЙ конфиг новый конфиг
Двенадцать клеток, различается одна. Почему — в статье.
Первый сценарий отвечает на вопрос «что с соединением». Вторая часть проверяет пять отдельных утверждений про сам reload — каждое своим опытом, каждый опыт печатает вывод и вердикт.
PART2=1 ./run-in-docker.sh
EXPS="1 2 3" PART2=1 ./run-in-docker.sh # выборочноНомера файлов совпадают с номерами разделов статьи.
| # | опыт | что проверяет | вердикт |
|---|---|---|---|
| 1 | exp1_new_binary.py |
reload во время бинарного апгрейда: -s reload (1а), прямой HUP (1б), выход из состояния (1в) |
подтвердилось: конфиг применяется в лучшем случае к половине процессов |
| 2 | exp2_keepalive_min_timeout.py |
keepalive_min_timeout и старый конфиг |
подтвердилось |
| 3 | exp3_upstream_pool.py |
пул keepalive к апстримам: есть ли он по умолчанию и сбрасывается ли на reload | подтвердилось |
| 4 | exp4_reuseport.py |
ломает ли reuseport бесшовность |
опровергнуто: потерь нет |
| 5 | exp5_accept_latency.py |
пауза в accept и переполнение backlog |
опровергнуто: ни паузы, ни очереди |
Опыт 4 сравнивает inode'ы слушающих сокетов до и после reload — не ss, а
/proc/net/tcp плюс /proc/<pid>/fd, чтобы не спутать слушающий сокет с
socketpair'ом мастера; отдельным шагом проверяет reload, меняющий
worker_processes. Опыт 3 сначала выясняет, есть ли пул вообще (с 1.29.7 он
включён по умолчанию для явного блока upstream), а потом удерживает
уходящего воркера живым медленным запросом: без этого «пул опустел» ничего не
доказывает, воркер и так сменился.
Каждый опыт запускается и отдельно — нужен собранный nginx:
export WORK=$PWD/work OUT=$PWD/results
bash targets/nginx.sh build
export NGINX_BIN=$WORK/build/nginx/sbin/nginx NGINX_PREFIX=$WORK/build/nginx
python3 part2/exp3_upstream_pool.pyОдин файл в targets/. Скопируйте targets/haproxy.sh (самый короткий, 47 строк) и заполните пять функций:
NAME="ENVOY 1.3x — hot restart" # как подписать раздел в выводе
PORT=8085 # свободный порт
PROC_PAT="envoy" # по чему искать процессы в ps
SETTLE=2.0 # сколько ждать после reload
t_build() { ... } # собрать или проверить наличие
t_config() { ... } # $1 = v1|v2 — записать конфиг с заголовком X-Config
t_start() { ... }
t_reload() { ... } # если reload не нужен (как у Traefik) — оставить :;
t_stop() { ... }Дальше:
TARGETS="nginx envoy" ./run-in-docker.shКаждая цель запускается и отдельно, это удобно при отладке:
bash targets/nginx.sh build
bash targets/nginx.sh config v1
bash targets/nginx.sh start
bash targets/nginx.sh reloadБольше всего не хватает Envoy и Caddy. Если прогоните — присылайте вывод, добавлю в таблицу.
- Только HTTP/1.1.
scenario.pyшлёт сыройGET / HTTP/1.1в сокет, в конфиге nginxlistenбезhttp2. Для HTTP/2 средняя клетка у nginx точно другая (GOAWAYвместо закрытия), а у остальных не мерена. Прогнать по h2 — это не «поправить одну строку»: нужен h2-клиент, умеющий держать соединение и слать запросы поштучно. - HAProxy запускается без master-worker и без
-x. В юните Debian стоит-Ws, и дерево процессов там выглядит иначе. На клетки таблицы это не влияет:-xпро передачу listen-сокетов, а не про keep-alive. - Один воркер. С
worker_processes autoконтракт тот же, но повторяемость падает: судьба конкретного соединения зависит от того, в каком воркере оно живёт. - Всё в одном контейнере, петлевой интерфейс, никакой сети между узлами.
claims-*.tsv — реестр всех утверждений о коде из статьи. Каждая строка это
id, само утверждение, файл и якорь — литеральная строка, которая обязана
найтись в этом файле. Якорь, а не номер строки: номера уезжают между релизами.
python3 verify_claims.py claims-nginx.tsv --repo /path/to/nginx # 36 строк
python3 verify_claims.py claims-httpd.tsv --repo /path/to/httpd # 16
python3 verify_claims.py claims-traefik.tsv --repo /path/to/traefik # 17
python3 verify_claims.py claims-part2.tsv --repo /path/to/nginx # 30, вторая часть
python3 verify_claims.py claims-part2-traefik.tsv --repo /path/to/traefik # 6Скрипт печатает текущие номера строк и выходит с ненулевым кодом, если хоть один якорь не нашёлся. Репозитории должны быть на тех же тегах.
| файл | что это |
|---|---|
run-in-docker.sh / .ps1 |
точка входа |
Dockerfile |
golang:1.26-bookworm + C-обвязка (apr, pcre2, autoconf, libtool) |
run-all.sh |
оркестратор: собирает цели, гоняет сценарий, пишет RESULTS.txt |
lib.sh |
общие функции и диспетчер подкоманд для целей |
targets/*.sh |
по файлу на прокси |
scenario.py |
сам сценарий, один на все цели |
backend.py |
апстрим с медленным /slow |
part2/run-all.sh |
оркестратор второй части: собирает nginx, гоняет пять опытов |
part2/common.py |
общее для опытов: конфиг, старт, reload, дерево процессов, запрос |
part2/exp*.py |
по файлу на опыт, нумерация как в статье |
part2/counting_backend.py |
апстрим-счётчик соединений для опыта 3 |
part2/PART2-sample.txt |
полный вывод прогона всех пяти опытов, на который ссылается статья |
claims-*.tsv + verify_claims.py |
реестр проверяемых утверждений о коде и скрипт проверки |
sudo apt-get install -y build-essential autoconf libtool libtool-bin \
libapr1-dev libaprutil1-dev libpcre2-dev zlib1g-dev libssl-dev \
git procps python3
# плюс Go >= 1.26 — traefik v3.7.10 требует его в go.mod
OUT=$PWD/results WORK=$PWD/work ./run-all.shОтказ одной цели не роняет остальные: в RESULTS.txt появится строка [!!]
с причиной, а прогон продолжится.