Когда TIME_WAIT становится врагом - QubStore

Когда TIME_WAIT становится врагом

Содержание статьи

Привет, Хабр!

Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.

При построении инфраструктуры высоконагруженных систем можно столкнуться с множеством различных проблем. Так, каждый инженер, работающий с высоконагруженными сервисами на 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, это условие почти никогда не выполняется.

Когда TIME_WAIT становится врагом

Представьте мобильное приложение: тысячи пользователей за одним публичным 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 становится врагом

Этот параметр задаёт максимальное количество сокетов в состоянии TIME_WAIT, которое система готова держать одновременно. По умолчанию он часто равен 262 144 или даже больше. Если его уменьшить до значения меньше размера диапазона портов, система начнёт агрессивно удалять старые сокеты TIME_WAIT, освобождая порты быстрее.

Но здесь важно понимать, что это не решает проблему, а лишь сдвигает порог. При пиковых нагрузках удаление сокетов из TIME_WAIT раньше времени может привести к проблемам с безопасностью и корректностью данных, но в некоторых сценариях это приемлемый компромисс. 

Синхронизация с сетевой инфраструктурой

Кроме портов и TIME_WAIT, есть ещё один критический параметр — net.core.somaxconn. Он определяет максимальную длину очереди ожидающих соединений (listen backlog) для сокетов. По умолчанию он часто равен 128 — критически мало для любого серьёзного сервиса.

Когда приложение не успевает принимать соединения быстрее, чем они поступают, очередь переполняется. Новые SYN‑пакеты либо игнорируются, либо сбрасываются. В результате клиенты получают таймауты, хотя сам сервер может быть не нагружен.

Когда TIME_WAIT становится врагом

Значение 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.

Если эти метрики растут — у вас архитектурная проблема, а не проблема ядра.

Когда TIME_WAIT становится врагом

Когда соединения внезапно обрываются, порты заканчиваются, а сервис теряет доступность, точечной настройки ядра уже недостаточно. Важно уметь находить слабое место в инфраструктуре, восстанавливать систему после отказа и заранее проектировать её так, чтобы единичный сбой не приводил к простою. Разобраться с проблемами на практике помогут бесплатные вебинары от преподавателей Otus:

  • 30 июля, 20:00. «Восстанавливаем RAID5 в Linux». Записаться

  • 18 августа, 19:00. «Готовим инфраструктуру к сбоям: отказоустойчивый кластер на базе VRRP и HAProxy». Записаться

Больше бесплатных уроков и других полезных подборок смотрите в дайджесте.

Ссылка на основную публикацию