Skip to content

Repository files navigation

reload-stand

Стенд к двум статьям:

Собирает 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.txtresults/logs/, если что-то не собралось). Для второй части — results/PART2.txt.

Что он делает

Апстрим на 127.0.0.1:9001 отвечает сразу на / и через шесть секунд на /slow. Каждый прокси проксирует на него всё, добавляя заголовок X-Config: v1. Дальше для каждой цели:

  1. открыть keep-alive соединение KA, сделать в нём запрос → ждём v1;
  2. в фоне начать запрос в /slow — он будет «в полёте» через весь reload;
  3. сменить конфиг на v2 и перезагрузить;
  4. снять дерево процессов;
  5. повторить запрос в том же сокете KA — здесь реализации расходятся;
  6. запрос в новом сокете — контроль, ждём v2;
  7. дождаться запроса из шага 2 — на каком конфиге он доработал;
  8. снять дерево процессов ещё раз, через три секунды.

Перезагрузка у каждого своя: 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         старый конфиг     живёт, НОВЫЙ конфиг новый конфиг

Двенадцать клеток, различается одна. Почему — в статье.

Вторая часть: пять опытов на nginx

Первый сценарий отвечает на вопрос «что с соединением». Вторая часть проверяет пять отдельных утверждений про сам 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 в сокет, в конфиге nginx listen без 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 появится строка [!!] с причиной, а прогон продолжится.

About

No description or website provided.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Used by

Contributors

Languages