Введение
В данной статье описаны причины возникновения и решения проблемы с 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
И вот она проблема – почти 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;
}
Тюнинг ядра. Ещё раз [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.
Используемые источники
- https://blog.cloudflare.com/how-to-stop-running-out-of-ephemeral-ports-and-start-to-love-long-lived-connections/
- https://ma.ttias.be/nginx-cannot-assign-requested-address-for-upstream/
- https://www.nginx.com/blog/overcoming-ephemeral-port-exhaustion-nginx-plus/
- https://habr.com/ru/company/flant/blog/343348/
- https://tools.ietf.org/html/rfc1379
- https://man7.org/linux/man-pages/man7/ip.7.html
- https://gryzli.info/2018/03/05/nginx-cannot-assign-requested-address/
- https://stackoverflow.com/questions/6426253/tcp-tw-reuse-vs-tcp-tw-recycle-which-to-use-or-both