Зачем вообще разбираться с ephemeral ports
Ephemeral ports (эфемерные порты) — это «временные» локальные порты, которые ядро Linux выделяет клиентской стороне TCP-соединения. Когда ваш сервер делает исходящие запросы (к API, в БД, к почтовому шлюзу, к прокси, в микросервисы), он открывает сокет вида local_ip:ephemeral_port → remote_ip:remote_port.
Проблемы начинаются, когда соединений много и они часто создаются/закрываются, либо есть NAT/SNAT (локально через iptables/nftables или на внешнем шлюзе), либо приложение генерирует шквал коротких запросов без пула/keepalive. Типичный симптом — ошибка приложения: EADDRNOTAVAIL / Cannot assign requested address. Обычно это означает, что ядро не смогло выделить новый локальный порт (и/или подходящую комбинацию адреса/порта) для исходящего соединения.
Важно: речь не про «серверный порт» (80/443), а про исходящие клиентские подключения вашего сервера. Даже веб-сервер может упираться в ephemeral ports, если он активно ходит во внешние сервисы.
Как ядро выбирает локальный порт и почему он может закончиться
Linux выбирает порт из диапазона net.ipv4.ip_local_port_range. По умолчанию в большинстве дистрибутивов это что-то вроде 32768 60999 или близко к 49152 65535 — зависит от версии/настроек. Точный диапазон смотрим так:
sysctl net.ipv4.ip_local_port_range
Количество доступных портов — это размер диапазона. Например, 32768..60999 — около 28232 портов. Кажется много, но при высоком RPS на коротких соединениях вы легко «съедите» этот запас, особенно если порты долго не возвращаются в пул из-за TIME_WAIT или из-за особенностей NAT.
Ключевое ограничение: один и тот же локальный порт можно использовать повторно только если не конфликтует 4-тупл: (src_ip, src_port, dst_ip, dst_port). На практике конфликт чаще всего именно по src_port, потому что dst_ip:dst_port для ваших запросов часто одинаковый (например, все стучатся в один API-эндпоинт).
Если вы уперлись в лимиты из-за исходящих подключений и приложению нужно больше ресурсов, удобнее переносить такие нагрузки на VDS, где вы контролируете sysctl и сетевую обвязку без ограничений общего окружения.

TIME_WAIT: почему «закрыл соединение» не значит «порт свободен»
Состояние TIME_WAIT — нормальная часть TCP. Обычно в него попадает сторона, которая активно закрыла соединение (послала FIN первой). В TIME_WAIT сокет держится, чтобы дождаться возможных «запоздалых» сегментов в сети и не перепутать их с новым соединением, а также корректно завершить TCP-обмен, если финальные пакеты потерялись.
Пока сокет в TIME_WAIT, комбинация адрес/порт может быть недоступна для нового соединения (в зависимости от условий и настроек). Если у вас много коротких исходящих коннектов к одному и тому же адресу/порту, TIME_WAIT быстро становится главным «пожирателем» ephemeral ports.
Посмотреть статистику по состояниям можно через ss:
ss -s
А список именно TIME_WAIT (TCP):
ss -tan state time-wait | head
Если TIME_WAIT — десятки/сотни тысяч, и параллельно растут ошибки EADDRNOTAVAIL, вы почти у цели.
Чем опасно «лечить» TIME_WAIT не разбираясь
TIME_WAIT — это не «мусор», который можно бездумно выключить. Агрессивное переиспользование портов может приводить к редким и трудноуловимым сетевым сбоям (ошибки запросов, странные ретраи, некорректные ответы при совпадениях потоков).
Надежный подход: уменьшить количество коротких соединений и правильно подобрать диапазоны/тайминги. Sysctl-тюнинг — второй шаг, а не первый.
Почему NAT/SNAT усугубляет исчерпание портов
Когда на пути есть NAT (часто в контейнерных сетях, Kubernetes, при выходе через общий шлюз, либо при SNAT на самом сервере), появляются дополнительные ограничения:
- Портовая емкость ограничена на «внешней» стороне. Если много внутренних клиентов «выходят» через один публичный IP, все конкурируют за один пул портов.
- conntrack хранит состояние соединений. Даже после завершения TCP запись может жить какое-то время и влиять на повторное использование портов, а также создавать нагрузку на таблицу.
- Коллизии по 5-туплу в NAT-таблице. В NAT добавляется трансляция, и порт может быть занят не только локальным TCP-стеком, но и состояниями NAT.
Отдельный класс проблем — переполнение conntrack: это уже не только EADDRNOTAVAIL, но и дропы новых соединений на уровне firewall. На практике эти истории часто идут рядом: много коротких коннектов → много записей → много TIME_WAIT и нагрузка на conntrack.
Мини-диагностика conntrack и NAT-состояний
Проверяем текущую загрузку conntrack:
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
Если nf_conntrack_count близок к nf_conntrack_max, NAT/фильтрация начинают «сыпаться» первыми. Симптомы бывают разные: от таймаутов до нестабильного падения исходящих запросов.
Типичные симптомы: EADDRNOTAVAIL и Cannot assign requested address
Эти сообщения часто всплывают в логах приложений и прокси. На уровне POSIX это означает: не удалось привязать локальный адрес/порт для нового соединения. Самые частые причины:
- исчерпан диапазон ephemeral ports из
ip_local_port_range; - очень много
TIME_WAITна одном направлении (к одномуdst_ip:dst_port); - проблемы с SNAT: много внутренних потоков через один внешний IP;
- переполнение conntrack (в NAT-сценариях);
- приложение делает слишком много соединений без keepalive/пула.
Критически важно: сначала понять, кто генерирует поток соединений (какой процесс) и куда (какой remote).
Практическая диагностика: ss и tcpdump
1) Сколько TIME_WAIT и какие состояния доминируют
Быстро оценить масштаб по TCP-состояниям:
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -nr | head
Посмотреть топ назначений для TIME_WAIT (полезно, когда все упирается в один внешний API):
ss -tan state time-wait | awk 'NR>1 {print $5}' | sed 's/:.*//' | sort | uniq -c | sort -nr | head
Если «виновник» — один IP (или пара IP), дальше почти всегда помогает keepalive/пул соединений. Расширение диапазона портов — страховка, но не лечение первопричины.
2) Кто порождает соединения: поиск по PID
ss умеет показывать процессы:
ss -tanp | head
В продакшене вывод может быть большим, поэтому фильтруйте по направлению (например, по remote-порту):
ss -tanp '( dport = :443 )' | head
Если вы отлаживаете таймауты на стыке веб-сервер ↔ апстрим, полезно сверить картину с настройками шлюза. В этом контексте может пригодиться разбор таймаутов Nginx и PHP-FPM (gateway timeouts), чтобы не провоцировать лишние ретраи и churn соединений.
3) Подтвердить картину на уровне пакетов: tcpdump
Когда нужно убедиться, что действительно идет «шквал» коротких TCP-сессий (SYN → data → FIN/RST), снимите короткий трейс:
tcpdump -nn -i any 'tcp and (tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0)' -c 200
Дальше смотрим, много ли SYN/FIN, нет ли массовых RST, нет ли ретраев SYN (что уже похоже на сетевую проблему или фильтрацию).

ip_local_port_range: расширяем «емкость» правильно
Самое безопасное и предсказуемое действие — увеличить диапазон ephemeral ports. Обычно рекомендуют не залезать в низкие порты и оставить место под локальные сервисы, но расширить верхнюю границу до 65535.
Например (подберите диапазон под свою среду):
sysctl -w net.ipv4.ip_local_port_range='20000 65535'
Для постоянного применения — через конфиги sysctl в вашей ОС (обычно в /etc/sysctl.d/), затем:
sysctl --system
Что это дает: больше уникальных исходящих портов, а значит выше потолок по числу параллельных и/или частых соединений к одному назначению.
Что это не решает: если приложение продолжит создавать десятки тысяч новых TCP-сессий в секунду без reuse/keepalive, вы лишь отодвинете проблему.
TIME_WAIT и sysctl: tcp_tw_reuse и tcp_fin_timeout
Частый совет из интернета: «включить tcp_tw_reuse и уменьшить tcp_fin_timeout». В реальности это разные рычаги с разными рисками.
tcp_tw_reuse: осторожно и только понимая последствия
net.ipv4.tcp_tw_reuse исторически позволял переиспользовать сокеты в TIME_WAIT для новых исходящих соединений при определенных условиях. Это может уменьшить вероятность EADDRNOTAVAIL на клиентах с очень большим количеством коротких коннектов.
Но имейте в виду:
- поведение зависит от версии ядра и особенностей сетевого стека;
- в некоторых сценариях (нестабильные сети, асимметричная маршрутизация, NAT, балансировщики) агрессивный reuse повышает риск редких и странных ошибок;
- это не замена нормальному connection pooling и keepalive.
Если вы рассматриваете эту настройку, делайте это после расширения ip_local_port_range и после исправления приложения, и обязательно проверяйте на стенде или на части трафика.
tcp_fin_timeout: не про TIME_WAIT, но влияет на «хвосты» закрытия
net.ipv4.tcp_fin_timeout — таймаут для состояний закрытия соединения (например, FIN_WAIT_2) в ряде ситуаций. Его часто путают с таймером TIME_WAIT. Уменьшение tcp_fin_timeout может помочь, если у вас зависают «полузакрытые» соединения из-за особенностей приложений/пиров, но это не магическая кнопка для уборки TIME_WAIT.
Проверить текущее значение:
sysctl net.ipv4.tcp_fin_timeout
Если вы видите много FIN_WAIT_2 и это подтверждено трассировками или логами, тогда точечное уменьшение может быть оправдано. Но сначала убедитесь, что проблема именно в этом состоянии, а не в TIME_WAIT.
NAT/SNAT: что делать, если упираетесь в портовую емкость
Если узкое место — общий SNAT (например, много контейнеров/подов или несколько внутренних сервисов «выходят» через один внешний IP), типовые варианты:
- Добавить внешние адреса и распределить исходящие потоки. Больше внешних IP → больше суммарная емкость портов (диапазон умножается на количество адресов).
- Разнести трафик по разным SNAT-адресам по сервисам/подсетям. Это снижает конкуренцию за порты.
- Увеличить лимиты conntrack и пересмотреть таймауты. Это лечит «таблица переполнена», но не всегда лечит именно
EADDRNOTAVAIL. - Снизить создание новых TCP-сессий на уровне приложений. HTTP keepalive, gRPC/HTTP2, пулы соединений к БД/кешу, адекватные таймауты и ретраи.
Главная мысль: при NAT вы упираетесь не только в локальный стек, но и в ресурс трансляции. Поэтому «просто увеличить ip_local_port_range» на одной машине может не помочь, если бутылочное горлышко — внешний SNAT.
Чек-лист: когда все падает прямо сейчас
Подтвердить симптом: ошибки
EADDRNOTAVAIL/Cannot assign requested addressв логах приложения/прокси.Посмотреть масштаб по состояниям TCP:
ss -sи долюTIME_WAIT.Понять направление: к каким remote IP/портам больше всего коннектов в
TIME_WAIT.Найти процесс/сервис-источник через
ss -tanp(по возможности фильтруя по dport).Если есть NAT: проверить
nf_conntrack_countиnf_conntrack_max.Быстрый безопасный тюнинг: расширить
ip_local_port_range(если диапазон небольшой).Долгосрочно: включить keepalive/pooling, уменьшить churn соединений, выровнять таймауты, при SNAT — добавить IP или разнести исходящий трафик.
Пара практических сценариев и что обычно помогает
Сценарий A: сервис делает много HTTPS-запросов к одному API
Признаки: огромный TIME_WAIT на dport 443 к одному IP/подсети; периодический EADDRNOTAVAIL при пиках.
- Включить HTTP keepalive в клиенте, ограничить одновременные коннекты, настроить пул.
- Увеличить
ip_local_port_range. - Проверить, что ретраи не слишком агрессивные и не создают «бурст» соединений.
Сценарий B: много контейнеров выходят наружу через один SNAT
Признаки: проблема появляется «волнами» при нагрузке всего кластера; conntrack близок к максимуму; ошибки могут быть на разных сервисах одновременно.
- Увеличить емкость conntrack и мониторить
nf_conntrack_count. - Разнести SNAT по нескольким адресам (если возможно).
- Сократить churn соединений (keepalive/HTTP2), иначе любые лимиты будут «съедены».
Сценарий C: много FIN_WAIT_2 и «подвисшие» закрытия
Признаки: в ss доминируют не TIME_WAIT, а состояния закрытия; часто связано с некорректными таймаутами приложений или с проблемным пэром.
- Разобраться с таймаутами приложения и сервера-назначения.
- Только после подтверждения — аккуратно настраивать
net.ipv4.tcp_fin_timeout.
Что мониторить, чтобы не ловить это ночью
- количество TCP-сокетов по состояниям (особенно
TIME_WAIT); - ошибки приложений вида
EADDRNOTAVAIL; - значение
net.ipv4.ip_local_port_rangeкак часть «паспортных данных» хоста; net.netfilter.nf_conntrack_countи долю отnet.netfilter.nf_conntrack_max(если есть NAT/фильтрация);- RPS на исходящие запросы и число одновременных коннектов в клиентах (пулы/агенты).
Если вы используете Nginx как TCP/UDP-прокси и видите похожие симптомы на сетевом стыке, может быть полезен материал про балансировку TCP/UDP через Nginx stream — там часто всплывают вопросы таймаутов и поведения соединений.
Итоги
Исчерпание ephemeral ports — это не «мистика ядра», а предсказуемый ресурсный потолок. В простых случаях его достаточно поднять расширением ip_local_port_range. В более сложных — проблему создают хвосты TIME_WAIT из-за коротких соединений или NAT/SNAT с conntrack, где узким местом становится общая портовая емкость на внешнем адресе.
Самый надежный путь: сначала диагностика через ss и, при необходимости, tcpdump; затем расширение диапазона портов; после — исправление модели соединений в приложениях (keepalive/pooling); и только потом аккуратный sysctl-тюнинг вроде tcp_tw_reuse и точечная настройка net.ipv4.tcp_fin_timeout при подтвержденной необходимости.


