1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 2 из 5

Keepalive и выкладка без простоя

Коротко

Два вопроса эксплуатации группы: не тратить время на установку соединений и выкладывать новую версию так, чтобы пользователь не увидел ни одной ошибки.

  • Keepalive к бэкенду с 1.29.7 включён сам, но только в группе: у литерального адреса в proxy_pass каждое соединение новое.
  • Имена серверов группы резолвятся (превращаются в IP-адрес) при старте. Пересоздал контейнер - nginx ходит на чужой адрес, пока не сделаешь reload.
  • Хуже: если имя не резолвится, nginx не стартует и не перезагружается. Один мёртвый контейнер блокирует правку любого другого сайта.
  • Лечит zone плюс resolver внутри группы плюс resolve у сервера - в открытой версии это работает с ветки 1.27.

Если знаешь, что имена группы резолвятся при старте, и видел параметр resolve - листай до «Теперь сам».

Сначала ответь сам

Docker, группа из двух серверов по именам. Ты пересоздал контейнер a1, и он получил другой адрес. Контейнеры живы, из соседнего контейнера всё открывается. Что покажет лог nginx?

↳  выводaccess_log после пересоздания
/after -> 172.25.0.2:8000, 172.25.0.4:8000  status=200  ustatus=502, 200
/after -> 172.25.0.4:8000  status=200
/after -> 172.25.0.4:8000  status=200

nginx ходит на 172.25.0.2 - адрес, которого у a1 давно нет (в замере его успел занять посторонний контейнер). Клиенты получают 200, потому что выручает повтор, а половина мощности при этом выключена. Реальный адрес a1 в этот момент - 172.25.0.8.

На что это похоже

Записанный на бумажке телефон коллеги. Пока номер не менялся, бумажка работает и кажется надёжнее телефонной книги. В день, когда коллега сменил номер, ты продолжаешь звонить по старому - и попадаешь к незнакомому человеку, который на этот номер только что переехал.

Именно так и выглядит худший вариант: если по старому адресу окажется чужой живой сервис, nginx начнёт отправлять к нему пользовательские запросы.

Механизм: когда именно резолвится имя

server a1:8000; старт: имя превратилось в адрес дальше адрес не меняется никогда до следующего reload server a1:8000 resolve; спрашиваем DNS заново по истечении TTL
Обычная запись - один вопрос к DNS за всю жизнь процесса. С resolve - по TTL записи.

Правило

/etc/nginx/nginx.confhttp
upstream app {
    zone app 64k;                    # общая память обязательна
    resolver 127.0.0.11 valid=5s;    # DNS указывают ВНУТРИ группы
    server a1:8000 resolve;
    server a2:8000 resolve;
    keepalive 32;
}

Три условия, и все обязательны. Без zone конфиг не соберётся:

↳  выводnginx -t
nginx: [emerg] resolving names at run time requires upstream "app" in /etc/nginx/nginx.conf:11 to be in shared memory
nginx: configuration file /etc/nginx/nginx.conf test failed

resolver именно внутри группы - в server его не видно:

↳  выводnginx -t
nginx: [emerg] no resolver defined to resolve names at run time in upstream "app" in /etc/nginx/nginx.conf:11
nginx: configuration file /etc/nginx/nginx.conf test failed

Проверено на стенде: после смены адреса контейнера группа с resolve сама перешла на новый адрес, а соседняя группа без него продолжала стучаться в старый. Ветка 1.27 это умеет; 1.24 и 1.26 отвечают "resolver" directive is not allowed here.

Разбор: мёртвый контейнер блокирует весь nginx

Это хуже устаревшего адреса, и на это натыкаются реже, потому что беда приходит в неудачный момент.

↳  выводnginx -t при остановленном контейнере a1
nginx: [emerg] host not found in upstream "a1:8000" in /etc/nginx/nginx.conf:11
nginx: configuration file /etc/nginx/nginx.conf test failed

Конфиг не проходит проверку. Значит:

  • nginx -s reload не сработает - и правка любого другого сайта на этой машине не применится, пока контейнер не поднимут;
  • при перезапуске службы nginx не стартует вовсе. Один остановленный бэкенд роняет весь фронт.

Проверено на стенде обеими сторонами: с обычной записью старт падает, с resolve тот же конфиг с несуществующим именем стартует нормально и просто считает сервер недоступным.

Разбор: keepalive работает не везде

Замер портов на приёмной стороне, шесть запросов подряд:

как записан бэкенд порты
proxy_pass http://app:8000 36494, 36498, 36508, 36518, 36524, 36538
тот же адрес внутри upstream 34576, 34576, 34576, 34576, 34576, 36542

С 1.29.7 кеш соединений включён по умолчанию (keepalive 32), версия к бэкенду 1.1, свой Connection nginx не шлёт. Но группой литеральный адрес не является, и переиспользование его не касается: каждый запрос - новое TCP-соединение, а к HTTPS-бэкенду ещё и рукопожатие.

keepalive задаёт не общее число соединений, а размер кеша простаивающих: сколько держать про запас на каждый воркер. Слишком большое значение просто держит лишние сокеты на бэкенде.

Что ломается без этого

Три строки из старых руководств - keepalive в группе, proxy_http_version 1.1 и proxy_set_header Connection "" в location - на свежих версиях необязательны. Вредными они не стали, и в чужом конфиге их не надо вычищать.

Одно место, где явный Connection "" по-прежнему встречается, - блок, где в тот же location проксируется WebSocket. Там Connection выставляется через map, и для обычных запросов map отдаёт close. У этого есть цена: Connection: close просит бэкенд закрыть соединение после ответа, то есть отменяет keepalive к нему - замер это подтверждает, с close у каждого запроса новый порт. На WebSocket-маршруте это не беда: долгие соединения живут сами по себе. Но общий трафик под keepalive в такой блок заводить не стоит - для него Connection к бэкенду оставляют пустым.

Зачем это в работе

Выкладка без потерянных запросов в открытой версии - это цикл по одному серверу за раз:

$  команда
# 1. вывести из ротации (строка в конфиге записана ровно как `server a1:8000;`)
sed -i 's|server a1:8000;|server a1:8000 down;|' /etc/nginx/nginx.conf
nginx -t && nginx -s reload

sed -i правит файл на месте, s|что|на что| - замена (вертикальная черта вместо обычного слеша, чтобы не сталкиваться со слешами в путях), && запускает reload только если nginx -t прошёл. Шаблон должен совпасть дословно: если в конфиге стоит server a1:8000 resolve;, строку server a1:8000; sed не найдёт и молча ничего не поменяет - правь шаблон под свою строку.

Дальше: подождать, пока старые воркеры дотянут начатые запросы (границу ставит worker_shutdown_timeout), обновить приложение, дождаться, пока его собственный /health ответит 200, вернуть строку без down и снова reload. Потом то же со вторым сервером.

Проверять /health до возврата в ротацию обязательно: slow_start в открытой версии нет, вернувшийся сервер сразу получит полную долю трафика, и роль проверки готовности выполнят первые пользователи.

Если у группы стоит sticky, вместо down удобнее drain: закреплённые клиенты дообслуживаются на своём сервере, новые туда не приходят.

Теперь сам

В docker-compose пять сайтов на одном nginx. Разработчик остановил контейнер приложения одного из них, чтобы «не мешался», и через час другой инженер поправил конфиг соседнего сайта и сделал reload. Что произошло?

Reload не применился: nginx -t упал на host not found in upstream из-за остановленного контейнера. Правка соседнего сайта не доехала, а инженер этого мог не заметить - старый конфиг продолжает работать. Чинится либо поднятием контейнера, либо переводом групп на zone плюс resolver плюс resolve, чтобы имена перестали быть условием старта.

Главное

Keepalive к бэкенду с 1.29.7 включён сам, но работает только внутри upstream: литеральный адрес в proxy_pass открывает соединение на каждый запрос. Имена серверов группы резолвятся при старте, поэтому пересозданный контейнер получает трафик по старому адресу, а имя, которое не резолвится, не даёт nginx ни перезагрузиться, ни стартовать. Лечится тройкой zone плюс resolver внутри группы плюс resolve у сервера - в открытой версии с ветки 1.27. Выкладка без простоя - цикл «пометил down → reload → обновил → дождался /health → вернул → reload» по одному серверу, а при sticky вместо down берут drain.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.