65 535 портов — миф о потолке: сколько исходящих соединений вы откроете на самом деле
Рано или поздно в любом обсуждении нагрузки всплывает фраза «у сервера всего 65 535 портов, больше соединений не откроешь». Она звучит убедительно и даже подкрепляется реальными авариями с TIME_WAIT — но описывает не тот предел, который вы думаете. Разберём, что именно ограничивает число 65 535, почему сервер под нагрузкой в 100 000 клиентов не нарушает никаких законов физики, и какой потолок исходящих соединений у вас есть на самом деле.
Содержание
- Откуда взялось число 65 535 и что оно ограничивает на самом деле
- Сервер, принимающий соединения, использует один и тот же порт для всех клиентов
- Четвёрка, которая определяет соединение — и почему один порт можно занимать многократно
- TIME_WAIT — реальный источник проблем с исходящими портами
- Другие реальные пределы: дескрипторы, память, conntrack, NAT
- Как посчитать свой реальный потолок исходящих соединений
Откуда взялось число 65 535 и что оно ограничивает на самом деле
Поле номера порта в заголовке TCP и UDP — 16 бит. Это даёт 65 536 значений, от 0 до 65 535. Порт 0 зарезервирован и обычно не используется приложениями напрямую, отсюда и округление до «65 535 портов» в разговорах.
Дальше важный нюанс, который обычно теряется: этот диапазон — не общесистемный лимит соединений, а диапазон номеров портов на одном локальном IP-адресе. Порты 1–1023 исторически считаются привилегированными (на Linux и других Unix-подобных системах для биндинга на них нужны права root или capability CAP_NET_BIND_SERVICE), 1024–49151 — «зарегистрированные», а верхний диапазон IANA относит к динамическим/эфемерным — именно из него ядро берёт номер локального порта, когда ваш процесс сам инициирует исходящее соединение.
На Linux реальный эфемерный диапазон настраивается и отличается от IANA-рекомендации. Посмотреть свой:
cat /proc/sys/net/ipv4/ip_local_port_range
# например: 32768 60999
Это уже не 65 535, а обычно около 28 000 значений — именно эта цифра фигурирует в историях вида «через несколько минут кончились исходящие порты». Но это диапазон для соединений с одного локального IP к одному конкретному удалённому адресу — не абсолютный потолок сервера.
Сервер, принимающий соединения, использует один и тот же порт для всех клиентов
Здесь и рождается главная путаница. Когда nginx слушает 443, он не «раздаёт» клиентам разные порты одного за другим, пока не кончатся. Он слушает один порт — 443 — и принимает на него сколько угодно параллельных TCP-соединений. Проверить:
ss -tlnp | grep :443
# LISTEN 0 511 0.0.0.0:443 0.0.0.0:*
Один листенер, один порт, тысячи одновременных клиентов. Число открытых соединений на стороне сервера, принимающего трафик, порты вообще не ограничивают — ограничивают файловые дескрипторы, память под сокеты и буферы, размер очереди backlog, настройки таблицы conntrack, если трафик проходит через netfilter/NAT. Про упор именно в эти ресурсы на конкретных цифрах разговор отдельный — здесь важно только зафиксировать: входящая сторона в 65 535 не упирается никогда, потому что порт на ней один и тот же.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверЧетвёрка, которая определяет соединение — и почему один порт можно занимать многократно
Ядро идентифицирует TCP-соединение не номером порта, а четвёркой значений: (локальный IP, локальный порт, удалённый IP, удалённый порт). Уникальным должно быть именно сочетание всех четырёх, а не порт сам по себе.
Отсюда прямое следствие для исходящих соединений: если ваш сервер подключается к одному и тому же удалённому адресу и порту (скажем, к одной базе данных на 5432 или к одному upstream API), то на каждое такое соединение ядро обязано выделить уникальный локальный порт — потому что три остальных элемента четвёрки (локальный IP, удалённый IP, удалённый порт) у всех этих соединений одинаковые, различаться может только локальный порт. В этом случае вы действительно упираетесь в размер эфемерного диапазона на этом локальном IP — те самые ~28 000–64 000 значений.
Но если сервер подключается к разным удалённым адресам — к тысячам разных API, разным клиентским вебхукам, разным сайтам при обходе — то один и тот же локальный порт может использоваться одновременно в разных соединениях, потому что четвёрка в каждом случае разная за счёт разного удалённого IP или порта. Именно поэтому краулер или прокси-сервер, обслуживающий множество разных направлений, способен держать заметно больше 65 535 параллельных исходящих TCP-сессий с одного локального IP — предел здесь определяется не портами, а тем, сколько записей о соединениях вообще способна удержать система (память, дескрипторы, conntrack).
TIME_WAIT — реальный источник проблем с исходящими портами
Даже когда соединение закрыто, занятый им локальный порт не освобождается мгновенно. Сторона, инициировавшая закрытие, переводит сокет в состояние TIME_WAIT и держит его там в течение удвоенного MSL (Maximum Segment Lifetime) — на практике это нередко порядка 60 секунд, конкретное значение платформозависимо и на части систем настраивается. Пока сокет в TIME_WAIT, связанная с ним четвёрка (а с ней и локальный порт для этого направления) считается занятой.
Если сервер открывает много коротких исходящих соединений к одному и тому же адресу — типичный случай: без keep-alive дергаете один и тот же upstream API или один и тот же Redis новым соединением на каждый запрос — вы можете исчерпать доступные локальные порты для этого конкретного направления задолго до того, как упрётесь в память или дескрипторы. Именно этот сценарий подробно разбирается в материале про то, как заканчиваются исходящие порты и накапливаются десятки тысяч TIME_WAIT — там показано, как считать точный запас портов для одного направления и куда он утекает. Смежный разбор — откуда вообще берутся тысячи соединений в TIME_WAIT на практике одного сервиса.
Посмотреть, сколько у вас сейчас соединений висит в TIME_WAIT именно по одному направлению:
ss -tan state time-wait '( dst 10.0.0.5 )' | wc -l
Если число близко к размеру вашего ip_local_port_range — вы упёрлись не в абстрактный лимит порта, а в скорость, с которой освобождаются занятые TIME_WAIT-соединения к этому конкретному адресу.
Другие реальные пределы: дескрипторы, память, conntrack, NAT
Даже разобравшись с портами, полезно держать в голове, что именно раньше даст сбой на практике:
| Ресурс | Что проверить | Типичный симптом при исчерпании |
|---|---|---|
| Файловые дескрипторы процесса | ulimit -n, cat /proc/<pid>/limits | EMFILE, ошибки accept()/connect() |
| Файловые дескрипторы системы | cat /proc/sys/fs/file-max | ENFILE по всей системе |
| Таблица conntrack (если трафик через netfilter) | sysctl net.netfilter.nf_conntrack_max, sysctl net.netfilter.nf_conntrack_count | новые соединения молча роняются или рвутся |
| Память под сокеты | cat /proc/net/sockstat, ss -m | рост RSS, своп, OOM при массе долгоживущих соединений |
| Эфемерный диапазон для ОДНОГО направления | cat /proc/sys/net/ipv4/ip_local_port_range | EADDRNOTAVAIL при connect() к одному и тому же адресу |
Отдельно стоит случай, когда ваш сервер выходит наружу не напрямую, а через NAT (например, все контейнеры на хосте используют один и тот же внешний IP через SNAT, или весь дата-центр отдаёт наружу через шлюз с ограниченным пулом публичных адресов). В этой ситуации настоящий потолок в районе 64 тысяч портов на пару (внешний IP, направление) — уже не миф, а буквальная реальность: NAT-устройство обязано транслировать каждое исходящее соединение в уникальный внешний порт для данного удалённого адреса, и если внешних IP мало, а направление одно, вы упрётесь именно в эфемерный диапазон на уровне NAT, причём раньше, чем в любой из лимитов на самом сервере. Это отдельно разбирается в материале про то, как один клиент открыл шестьдесят тысяч соединений и убил NAT — механика та же, что и с обычным TIME_WAIT, только упор происходит на уровне трансляции адресов, а не на конечном сервере.
Таблица conntrack заслуживает отдельного внимания: на многих дистрибутивах значение nf_conntrack_max по умолчанию исторически тоже стоит в районе 65536 — это совпадение с числом портов чисто историческое, значения независимы и настраиваются отдельно. Если у вас включён netfilter (а он включён почти всегда, как только в игру вступают Docker, iptables или nftables), переполнение именно этой таблицы часто наступает раньше, чем исчерпание портов. Подробный разбор — в материале о том, как переполняется таблица conntrack и соединения начинают рваться.
Как посчитать свой реальный потолок исходящих соединений
Абстрактной «единой цифры» не существует — потолок зависит от направления трафика (одно направление или много) и от того, какой ресурс кончится первым на вашей конкретной машине. Порядок проверки:
- Диапазон эфемерных портов — актуален только для соединений к одному и тому же удалённому адресу:
cat /proc/sys/net/ipv4/ip_local_port_range
Ширина диапазона — это верхняя граница числа одновременных соединений к ОДНОМУ адресу с ОДНОГО локального IP, при условии что TIME_WAIT не съедает их быстрее, чем они закрываются.
- tcp_tw_reuse — позволяет ядру повторно использовать сокет в TIME_WAIT для нового исходящего соединения при определённых условиях, что ослабляет упор в TIME_WAIT для одного направления:
sysctl net.ipv4.tcp_tw_reuse
Включайте осознанно и проверяйте поведение под своей нагрузкой — это не универсальное решение всех проблем с портами, а сужение конкретной причины.
- Лимит дескрипторов процесса и системы:
ulimit -n
cat /proc/sys/fs/file-max
Для сервисов, поднятых через systemd, лимит процесса дополнительно проверьте в unit-файле (LimitNOFILE=), так как ulimit в интерактивной сессии может отличаться от лимита демона.
- Размер и заполненность таблицы conntrack, если между сервером и внешним миром есть netfilter/NAT:
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_count
- Фактическая картина по соединениям прямо сейчас:
ss -s
ss -tan state established | wc -l
ss -tan state time-wait | wc -l
- Число уникальных удалённых адресов, если сомневаетесь, одно у вас направление или много:
ss -tan state established | awk '{print $5}' | cut -d: -f1 | sort -u | wc -l
Если это число большое — вы в сценарии «много направлений», где эфемерный диапазон одного локального порта не является лимитирующим фактором, и внимание нужно сместить на дескрипторы, память и conntrack.
Если после всех проверок именно эфемерный диапазон на одно направление оказался узким местом, а расширить его через tcp_tw_reuse и сокращение tcp_fin_timeout недостаточно, рабочий вариант — добавить серверу дополнительные локальные IP-адреса и явно биндить исходящие соединения на разные из них (через bind() перед connect() в приложении или через настройки исходящего IP в клиентской библиотеке). Каждый дополнительный локальный IP даёт свой собственный независимый эфемерный диапазон для того же удалённого адреса — де-факто умножая доступный пул портов на число IP.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если я вижу в мониторинге 40 000 установленных TCP-соединений на сервере — это нормально?
Само число не говорит ни о чём плохом. Если это входящие соединения на порт 443 — сервер спокойно живёт с гораздо большими цифрами, пока хватает памяти и дескрипторов. Если это исходящие соединения к одному и тому же адресу — стоит проверить, не приближается ли число к ширине вашего ip_local_port_range, и не копится ли параллельно TIME_WAIT.
Почему тогда ошибка EADDRNOTAVAIL вообще существует, если порты не ограничены?
Она существует именно для случая исходящего соединения к одному и тому же удалённому адресу с одного локального IP — там реальный конечный диапазон портов есть, и он может закончиться, особенно при коротких соединениях без keep-alive и переиспользования. Ошибка не про сервер в целом, а про конкретную пару (локальный IP, удалённый адрес).
Помогает ли IPv6 решить проблему с портами?
Косвенно — да, но не потому, что в IPv6 больше портов (поле порта в TCP/UDP то же самое, 16 бит, независимо от версии IP). Помогает то, что IPv6 обычно даёт вам куда больше доступных адресов, а значит проще выделить серверу несколько локальных IPv6-адресов и разложить исходящие соединения по ним, каждый со своим независимым диапазоном портов.
SO_REUSEPORT решает эту проблему?
Нет, это про другое — SO_REUSEPORT позволяет нескольким процессам слушать один и тот же входящий порт для балансировки принимаемых соединений между воркерами. К эфемерному диапазону для исходящих соединений это не относится.
Есть ли смысл вручную сужать ip_local_port_range, чтобы освободить порты под что-то другое?
Обычно нет: сужение диапазона уменьшает доступный пул для исходящих соединений, ничего не «освобождая» для входящих — те и так не используют эфемерный диапазон. Единственная типичная причина сузить его — зарезервировать конкретные порты под конкретные сервисы, слушающие локально.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →