Привет, Хабр!
Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.
При построении инфраструктуры высоконагруженных систем можно столкнуться с множеством различных проблем. Так, каждый инженер, работающий с высоконагруженными сервисами на Linux, рано или поздно сталкивается с загадочной ошибкой: Cannot assign requested address. Или внезапными обрывами соединений без видимых причин. Проблема почти всегда кроется в сетевых настройках ядра — той части инфраструктуры, которую принято настраивать по принципу «работает — не трогай». В высоконагруженных системах этот подход может быть смертельно опасен.
Почему порты заканчиваются
Для начала давайте разберемся в одном важном вопросе: почему сетевые порты имеют свойство заканчиваться.
Здесь сразу стоит отметить, что TCP‑соединение не закрывается мгновенно. После того как одна из сторон инициирует закрытие, сокет переходит в состояние TIME_WAIT и остаётся там по умолчанию 60 секунд.
Это необходимо для того, чтобы все пакеты, задержавшиеся в сети, успели дойти или были отброшены. Также это делается для того, чтобы случайно не переиспользовать тот же порт и IP‑адрес для нового соединения, которое получит «призрачные» пакеты от старого. Казалось бы, все вполне логично и правильно.
Но в высоконагруженных системах, где клиентское приложение создаёт множество короткоживущих соединений (например, PHP‑FPM, подключающийся к Redis или внешнему API через connect() без пулинга), эти TIME_WAIT‑сокеты накапливаются с огромной скоростью. Каждое закрытое соединение занимает порт из диапазона ip_local_port_range, который по умолчанию часто составляет всего около 28 тысяч портов.

Когда приложение создаёт 1000 новых соединений в секунду, а порт освобождается только через 60 секунд, в системе одновременно может находиться до 60 тысяч сокетов в состоянии TIME_WAIT. Это неизбежно приводит к исчерпанию доступных портов и ошибке Cannot assign requested address.
Про tcp_tw_recycle и tcp_tw_reuse
Первое желание инженера, увидевшего горы TIME_WAIT, — включить «быстрое переиспользование» портов через параметры tcp_tw_recycle и tcp_tw_reuse. Многие руководства по настройке производительности до сих пор содержат эти параметры в примерах, и это огромная ловушка.
В Linux ядрах версии 4.12 и выше параметр tcp_tw_recycle был полностью удалён. И не случайно. При включённом tcp_tw_recycle и активных tcp_timestamps ядро полагается на то, что временные метки в пакетах от одного источника строго возрастают. В реальном мире, особенно в облачных средах и за NAT, это условие почти никогда не выполняется.

Представьте мобильное приложение: тысячи пользователей за одним публичным IP‑адресом оператора. NAT‑шлюз назначает порты динамически. При включённом tcp_tw_recycle сервер, увидев пакет от одного IP с временной меткой меньше, чем у предыдущего пакета от того же IP (а это нормально при NAT), интерпретирует это как атаку с повторением старых пакетов и молча сбрасывает соединение.
Результат — внезапные обрывы соединений с клиентами, которые невозможно диагностировать стандартными средствами. Именно поэтому даже в инструкциях по настройке некоторых продуктов прямо указано: не использовать
tcp_tw_recycleпри работе с NAT.
С параметром tcp_tw_reuse ситуация чуть лучше, но он работает только для исходящих соединений (когда ваше приложение выступает в роли клиента) и только если установлены временные метки. Это позволяет переиспользовать TIME_WAIT‑сокеты для новых соединений к тому же адресату. Однако для входящих соединений на серверном порту он бесполезен.
Избавляемся от коротких соединений
Прежде чем трогать параметры ядра, стоит взглянуть на архитектуру приложения. Ошибка Cannot assign requested address — это симптом, а не болезнь. Коренная причина — архитектурный антипаттерн «создавать новое TCP‑соединение на каждый запрос».
Правильное решение — использовать пулы соединений или постоянные соединения (persistent connections).
Для популярных библиотек это тривиально:
PHP + phpredis: заменить
$redis->connect()на$redis->pconnect(). Это переключает библиотеку на использование постоянного соединения, которое не закрывается после завершения скрипта, а возвращается в пул.Python + redis‑py: использовать
ConnectionPoolи переиспользовать его экземпляр.Node.js + ioredis: создать один экземпляр клиента с опцией
keepAliveи использовать его во всех запросах.
Переход на постоянные соединения решает проблему TIME_WAIT кардинально: соединения не закрываются — порты не попадают в TIME_WAIT — порты не заканчиваются.
Когда ядро всё же нужно трогать
Если по каким‑то причинам переписать код приложения невозможно (устаревший монолит, закрытые компоненты), можно применить «костыль» на уровне ядра — уменьшить tcp_max_tw_buckets.

Этот параметр задаёт максимальное количество сокетов в состоянии TIME_WAIT, которое система готова держать одновременно. По умолчанию он часто равен 262 144 или даже больше. Если его уменьшить до значения меньше размера диапазона портов, система начнёт агрессивно удалять старые сокеты TIME_WAIT, освобождая порты быстрее.
Но здесь важно понимать, что это не решает проблему, а лишь сдвигает порог. При пиковых нагрузках удаление сокетов из TIME_WAIT раньше времени может привести к проблемам с безопасностью и корректностью данных, но в некоторых сценариях это приемлемый компромисс.
Синхронизация с сетевой инфраструктурой
Кроме портов и TIME_WAIT, есть ещё один критический параметр — net.core.somaxconn. Он определяет максимальную длину очереди ожидающих соединений (listen backlog) для сокетов. По умолчанию он часто равен 128 — критически мало для любого серьёзного сервиса.
Когда приложение не успевает принимать соединения быстрее, чем они поступают, очередь переполняется. Новые SYN‑пакеты либо игнорируются, либо сбрасываются. В результате клиенты получают таймауты, хотя сам сервер может быть не нагружен.

Значение 65535, часто рекомендуемое в документации, для продуктива избыточно. Практический ориентир — 4096 или 8192, но с обязательным мониторингом переполнения очереди (метрика netstat -s | grep "listen queue"). Для сервисов с высоким RPS (более 10000) значение может потребовать увеличения до 32768, но это уже крайность.
Чего не стоит делать: устаревшие рецепты
В интернете до сих пор можно встретить рецепты с включением tcp_tw_recycle и агрессивным уменьшением tcp_fin_timeout. Эти практики опасны и устарели.
Так, tcp_tw_recycle удалён из ядра начиная с версии 4.12. Любая рекомендация его использовать — признак того, что автор не следит за актуальностью. А tcp_tw_reuse имеет смысл только для клиентских исходящих соединений. Для серверного порта он бесполезен.
В свою очередь уменьшение tcp_fin_timeout до 5–10 секунд может привести к ситуации, когда соединение ещё не полностью закрыто удалённой стороной, а локальный порт уже используется для нового соединения, что вызывает сброс пакетов и ошибки.
Проверить, насколько уверенно вы ориентируетесь в инфраструктуре высоконагруженных систем, можно во вступительном тесте. Он поможет определить темы, которые стоит повторить или разобрать глубже.
В заключение
Высоконагруженные системы требуют осознанной настройки сетевого стека.
Первый шаг — всегда архитектура: пулы соединений вместо короткоживущих сокетов устраняют большинство проблем на корню.
Второй шаг — здравый смысл: не использовать параметры, которых нет в актуальных ядрах, и не копировать настройки слепо из интернета.
Третий шаг — мониторинг: следите за количеством
TIME_WAIT‑сокетов, переполнением очередиsomaxconnи ошибкамиCannot assign requested address.
Если эти метрики растут — у вас архитектурная проблема, а не проблема ядра.

Когда соединения внезапно обрываются, порты заканчиваются, а сервис теряет доступность, точечной настройки ядра уже недостаточно. Важно уметь находить слабое место в инфраструктуре, восстанавливать систему после отказа и заранее проектировать её так, чтобы единичный сбой не приводил к простою. Разобраться с проблемами на практике помогут бесплатные вебинары от преподавателей Otus:
30 июля, 20:00. «Восстанавливаем RAID5 в Linux». Записаться
18 августа, 19:00. «Готовим инфраструктуру к сбоям: отказоустойчивый кластер на базе VRRP и HAProxy». Записаться
Больше бесплатных уроков и других полезных подборок смотрите в дайджесте.
Комментарии (2)

poige
22.07.2026 19:091) tcp_max_tw_buckets. Автор предлагает занижать как допустимый «костыль», хотя документация ядра прямо требует этого не делать: лимит защищает от DoS, а при его превышении уничтожается очередной TIME_WAIT-сокет — это не «агрессивная очистка старых».
2) tcp_fin_timeout здесь тоже не к месту: он ограничивает зависшие осиротевшие соединения в FIN_WAIT_2, когда наш FIN уже подтверждён, но удалённая сторона ещё не закрылась; на длительность TIME_WAIT параметр не влияет.
3) somaxconn — та же картина: это потолок для backlog, заданного приложением в listen(), причём речь об уже установленных соединениях, ожидающих accept(). Незавершённые SYN-handshake учитываются отдельно через tcp_max_syn_backlog, поэтому объяснение про «новые SYN либо игнорируются, либо сбрасываются» смешивает разные механизмы. И default 128 устарел: с Linux 5.4 это 4096.
В итоге — обычный для корпоративного Хабра проходной промо-пересказ старых sysctl-шпаргалок, местами с неверной механикой и претензией на технический разбор. Комментарии могут оказаться полезнее. ;)
Patrick139
надо просто нормальные ОС использовать, а не смузи хлебать