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

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

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

Коротко

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

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

Если знаешь, что имена группы резолвятся при старте, и видел параметр 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;    # в группе или в http, но не в server
    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 должен быть виден группе: внутри неё или на уровне http. Из блока server группа его не видит - на nginx 1.31.6 проверены все три места:

↳  вывод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, чтобы имена перестали быть условием старта.

Главное

При замене контейнеров проверь обновление DNS-адресов в группе, а при выкладке выводи и возвращай серверы по одному. После запуска новой версии дождись готовности приложения. Наличие keepalive и успешный reload сами по себе не доказывают доступность обновлённого бэкенда.

Комментарии

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

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

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