Nginx 99: Cannot assign requested address to upstream

Введение

В данной статье описаны причины возникновения и решения проблемы с Nginx, когда периодически возникает ошибка вида [crit] 12889#0: *32401195 connect() to 127.0.0.1:80 failed (99: Cannot assign requested address) while connecting to upstream.

В моем случае сложности добавляло следующее: используется Nginx Ingress Controller в kubernetes, был установлен сложный релиз с множеством нового функционала и маршрутизацией для микросервисов. Поэтому было не сразу понятно, в чем проблема и куда смотреть, и прежде чем я пришёл к решению проблемы, было перелопачено много access-логов и конфигов в k8s, т.к. основными симптомами являлись периодические 502 ошибки в одной из частей приложения, а основная часть работала без проблем.

Причины возникновения

Перед тем, как рассказать о решении проблемы, необходимо иметь минимальный багаж знаний о работе TCP. В частности, как происходит сеанс установки TCP-соединения (в данной статье описывается лишь частично в рамках исследуемой проблемы):

  • На стороне клиента создается сокет
  • По аналогии сокет создается и на сервере, к которому обращается клиент
  • Оба сокета соединяются для создания пары сокетов, которая называется quadruplet
  • quadruplet состоит из локального IP-адреса и порта, а также удаленного IP-адреса и порта клиента. В RFC quadruplet описывается так: {A, B, Port.A, Port.B} и по сути представляет собой кортеж из значений IP и портов.

Чтобы сразу понимать, как это выглядит в Linux, можно запустить команду netstat или ss для просмотра сетевых соединений на порту, который прослушивает Nginx:

# netstat -nt | grep ":443 " | grep EST

tcp        0      0 10.89.108.182:443       10.89.108.137:1879      ESTABLISHED

Из команды выше адрес 10.89.108.137 – клиент, а 1879 – порт, связанный с клиентом только на время соединения и поэтому называется эфемерным . Когда соединение будет разорвано, временный порт будет доступен для повторного использования уже в рамках другого соединения.

Клиентский порт назначается автоматически из определенного диапазона, который указан в ядре по пути:

# cat /proc/sys/net/ipv4/ip_local_port_range

32768	60999

По умолчанию размер диапазона составляет 60999-32768=28231 (в моем случае на RHEL 7), что не так уж и много для высоконагруженных систем с большим количеством клиентов.

Диагностирование

Итак, определившись с текущим объемом ip_local_port_range – 28231, необходимо диагностировать, сколько соединений используется. Для этого можно воспользоваться также netstat или ss:

# ss -s
Total: 8253 (kernel 8381)
TCP:   47773 (estab 5439, closed 42021, orphaned 92, synrecv 0, timewait 41675/0), ports 0

Transport Total     IP        IPv6
*         8381      -         -
RAW       0         0         0
UDP       69        18        51
TCP       5752      5705      47
INET      5821      5723      98
FRAG      0         0         0

В примере выше, общее кол-во соединений составляет 47773. Соединений в состоянии established не так и много, а вот на соединения timewait стоит обратить внимание, т.к. они являются активными потребителями диапазона в данном случае (к тому же их кол-во постоянно меняется). Можно сразу смотреть их кол-во без лишнего вывода:

# ss -a | grep TIME-WAIT | wc -l
36169
# netstat -nt | grep WAIT | wc -l
36376

Information

netstat в моём случае отработал быстрее, а ss с ощутимой задержкой

И вот она проблема – почти 37 тысяч соединений, а в лимите разрешено всего 28231. У такой проблемы есть даже название “Ephemeral Port Exhaustion”, т.е. эфемерное истощение портов, если перевести дословно.

Решение

Тюнинг ядра

Первым делом можно сразу же увеличить лимит по умолчанию – это будет самым простым и верным решением. Для этого:

echo "net.ipv4.ip_local_port_range = 15000    64000" >>/etc/sysctl.d/99-sysctl.conf
sysctl -p

Главное помнить, что лимит не резиновый и не стоит ставить меньше 4096 (порты для системных нужд) и выше 65000 + на различных портах могут слушать различные демоны прикладного ПО. Я выставил 49000 для начала. Хотя допустимо минимальным значением диапазона выставить 1024, но нужно помнить, что могут быть проблемы с другими службами и фаерволом, если сделать бездумно.

Изменение адреса назначения

Помните, что каждое соединение состоит из 4 частей (называемых quadruplet) с исходным IP и исходным портом, целевым IP и целевым портом? Так вот если нет возможности изменить исходный порт или IP-адрес, можно изменить IP-адреса назначения и тем самым избежать ограничений по лимиту из ip_local_port_range.

Наглядно это будет выглядеть так: например, конечный endpoint слушает на всех портах сетевых интерфейсов, т.е. на 0.0.0.0. А значит к нему можно обратиться по разным адресам – локальному, внешнему, loopback или добавить алиасы на интерфейс. Для этого создается апстрим с именем backend:

upstream backend {
  server 127.0.0.1:80;
  server 192.168.10.1:80;
  server ${PUBLIC_IP}:80;
}

И далее можно проксировать трафик в вышеописанный upstream:

server {
  listen 443;
  ...
  location / {
    proxy_pass http://backend;
    ...
  }
}

Тем самым будет достигнута экономия локальных портов из диапазона ip_local_port_range, т.к. вместо одного адреса уже будет два или более – в зависимости от настроек.

Настройка keepalive

Есть ещё одно решение – это настройка keepalive между Nginx и серверами в upstream. Keepalive-соединение будет поддерживаться открытым, а потому его можно использовать повторно уже другими клиентами, не создавая новых и опять же не расходуя лимиты ip_local_port_range.

Для настройки нужно указать директиву keepalive и её значение в блоке upstream:

upstream backend {
  server 127.0.0.1:80;
  server 192.168.10.1:80;
  server ${PUBLIC_IP}:80;
  keepalive 128;
}

Warning

При использовании keepalive, необходимо также использовать следующие директивы:
proxy_http_version 1.1;
proxy_set_header Connection "";
И учитывать, что для протокола WebSocket это может быть критично.

Тюнинг ядра. Ещё раз [2]

После того, как лимиты в ip_local_port_range увеличены, количество соединений всё равно может увеличиваться. Особенно это актуально для веб-сервера, к которому происходят множественные обращения. Как правило, основным потребителем являются соединения в состоянии TIME_WAIT, и занимают большую часть диапазона ip_local_port_range. В таком случае ошибка Cannot assign requested address может снова повториться.

Сгладить ситуацию поможет параметр ядра net.ipv4.tcp_tw_reuse=1. Как видно из названия, он позволяет использовать повторно соединения TW, не создавая новых. Проблем с применением этого параметра на работающем сервере быть не должно.

Есть также другой параметр tcp_tw_recycle, который позволяет сократить время нахождения соединения в TIME-WAIT, но известно, что при его использовании могут быть проблемы у клиентов за NAT. Не рекомендуется использовать данный параметр, а в новых версиях ядра он вообще удален.

Заключение

В итоге получилось, что для решения ошибки Nginx Cannot assign requested address while connecting to upstream можно использовать целый комплекс мер – увеличить лимиты в ядре, настроить keepalive и сконфигурировать upstream. Но также не стоит забывать, что нужно искать проблему не там, где кажется на первый взгляд, т.к. могут получаться такие совпадения, связанные с выкаткой нового функционала. Грешил на одно, а проблема оказалась в итоге совсем в другом месте. В идеале же нужно применять всё в зависимости от ситуации, а также не могу не упомянуть про мониторинг.

В моем случае после решения данной проблемы, я настроил в Zabbix простой мониторинг кол-ва TCP-соединений через netstat -nt для 80 и 443 на нужном мне порту, где запущен Nginx Ingress Controller, чтобы понимать динамику загрузки и оперативно принять меры.

Для более глубокого понимания проблемы рекомендую изучить ссылки в используемых источниках ниже, особенно из блога Nginx.

Используемые источники

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: