TIME_WAIT и эфемерные порты: клиент упирается раньше сервера
Десятки тысяч сокетов в TIME_WAIT выглядят как утечка, а это нормальное состояние — и лечат его обычно не тем. Измерено: при обычном закрытии сокеты достаются стороне, которая закрывает активно; состояние живёт минуту; а упирается клиент не в память, а в номера портов, которых конечное число. Настройки, которые крутят вместо лечения, описаны в tcp(7) — и ни одна из них не про длительность.
Полное техническое изложение
TL;DR
Закрытое соединение исчезает не сразу: сторона, закрывшая его, ещё некоторое
время держит свою пару адрес-порт. Это TIME_WAIT, и нужен он затем, чтобы
задержавшиеся в сети сегменты старого соединения не были приняты новым
соединением с теми же адресами и портами. Нормальный конец разговора, а не
утечка и не поломка.
Отсюда главное следствие: упирается в это не сервер и не память, а клиент — в
номера портов. Измерено: при диапазоне в 50 номеров получилось ровно 50
соединений, пятьдесят первое дало EADDRNOTAVAIL. И картина на сервере читается
так же: тридцать тысяч сокетов там говорят о том, кто закрывает соединения, а не
о нагрузке, — измерено, двадцать одинаковых обменов дали 20 сокетов на той
стороне, которая закрывала, и 0 на другой.
Дальше — то, что отличает знающего от читавшего. Точное правило — про
активное закрытие: при обычном закрытии через FIN через TIME_WAIT
проходит сторона, закрывающая активно (бывает ещё одновременное закрытие и сброс
соединения — там картина другая). Состояние живёт около минуты — измерено, 60,4
секунды по часам, — и настройки его не сокращают: tcp_fin_timeout относится к
другому состоянию, tcp_max_tw_buckets ограничивает число сокетов, а не
длительность. Диапазон портов, делённый на длительность состояния, даёт 467,7
соединения в секунду — но это грубая оценка для повторных соединений к одному
и тому же адресу и порту назначения в этой конфигурации, а не общий потолок
машины. И лечится всё это не настройками: измерено, 400 запросов через одно
соединение потратили один порт и оставили один сокет в TIME_WAIT.
- клиент подключается к серверу, обменивается с ним данными и закрывает соединение;
- у машины есть адрес, а у соединения — номер порта с каждой стороны;
- сеть не мгновенна и не идеальна: пакет может задержаться в пути или потеряться.
- состояния TCP по именам, порядок пакетов при закрытии, что такое
FIN; - эфемерные порты,
ip_local_port_range,EADDRNOTAVAIL,tcp_fin_timeout,SO_REUSEADDR.
Что здесь на самом деле спрашивают
Лестница выглядит так:
- «Что такое
TIME_WAIT?» — разминка про состояние после закрытия. - «У кого он возникает — у клиента или у сервера?» — вопрос, на котором видно, знает ли собеседник правило про активное закрытие.
- «У нас 30 тысяч сокетов в
TIME_WAIT, что делать?» — главный вопрос темы и тот, где ответ настройками виден сразу как неверный. - «Что означает
EADDRNOTAVAIL?» — вопрос-ловушка: это не про память и не про дескрипторы. - «Сколько новых соединений в секунду выдержит клиент?» — вопрос про арифметику, а не про «зависит».
- «Зачем это состояние вообще нужно?» — вопрос про то, что оно защищает.
Числа получены прогоном bench/timewait/ports.py и
bench/timewait/practice.py на loopback. Длительность состояния здесь не взята
из документации, а снята по часам.
База: у соединения есть след, который остаётся после закрытия
Прежде чем говорить про состояния и номера портов, стоит назвать обычными словами, что происходит с соединением от начала до конца. Сокращённо его жизнь такая:
- подключились — стороны договорились, что разговаривают друг с другом;
- соединение работает — по нему ходят данные в обе стороны;
- закрыли — одна сторона говорит «у меня всё», другая отвечает тем же;
- состояние ожидания — разговора уже нет, а след от него ещё держится.
Весь урок — про четвёртый шаг. У него есть имя: TIME_WAIT.
Зачем этот шаг существует. Сеть не обещает, что все пакеты дошли и что ни один не задержался по дороге: копия сегмента может блуждать по маршрутам и прибыть с опозданием. При этом соединение опознаётся не именем, а четвёркой чисел — адрес и порт с одной стороны, адрес и порт с другой. Значит, если закрыть соединение и тут же открыть новое с той же самой четвёркой, опоздавший сегмент старого разговора прибудет в новый — и будет принят за свой, потому что отличить их не по чему.
Состояние ожидания и есть защита от этого: закрывшая сторона некоторое время не отдаёт свою половину четвёрки никому. Она ждёт, пока в сети не останется ничего от прошлого разговора.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования:
TIME_WAIT — не утечка и не поломка, а нормальный конец соединения. Дальше —
про то, кому именно достаётся ожидание, сколько оно длится, почему у клиента при
этом кончаются номера портов и на каком счёте соединений это превращается в
отказ.
Механизм 1: ожидание достаётся стороне, которая закрывает активно
Первое, что снимает половину вопросов темы: TIME_WAIT не «возникает в
системе», а достаётся конкретной стороне соединения. Какой именно — видно из
замера, где меняется ровно одно.
1. TIME_WAIT BELONGS TO WHOEVER CLOSED FIRST
--------------------------------------------
the client closes first server side 0, client side 20
the server closes first server side 20, client side 0
Двадцать одинаковых обменов в обоих случаях. Сервер тот же, клиент тот же,
данные те же. Отличается только то, кто вызвал close первым — и сокеты
целиком уходят на эту сторону.
Почему именно этой стороне. Это не произвол реализации, а требование стандарта:
When a connection is closed actively, it MUST linger in the TIME-WAIT state for
a time 2xMSL (Maximum Segment Lifetime)
Когда соединение закрывается активно, оно ОБЯЗАНО задержаться в состоянии TIME-WAIT на время 2xMSL (максимального времени жизни сегмента).
Пассивной стороне ждать нечего: подтверждать ей уже нечего.
И здесь стоит сказать правило точно, потому что ходячая формулировка
«достаётся тому, кто закрыл первым» верна не всегда. Стандарт говорит про
активное закрытие, и при обычном закрытии — одна сторона шлёт FIN, другая
отвечает своим — это и есть «закрыл первым»: замер выше сделан ровно так. Но
обычным закрытием дело не исчерпывается. Бывает одновременное закрытие, когда
FIN уходят навстречу друг другу, — тогда активно закрывают обе стороны, и
ждать приходится обеим. Бывает и сброс соединения — резкий обрыв вместо
FIN, — и через ожидание такое соединение не проходит вовсе. Ни того, ни
другого в этом прогоне нет: он показывает обычный случай. Поэтому правило
звучит так: через TIME_WAIT проходит сторона, выполняющая активное
закрытие, а «кто закрыл первым» — его частный случай для обычного закрытия
через FIN.
Отсюда практический вывод, за который этот механизм и спрашивают: тридцать
тысяч TIME_WAIT на сервере — это не жалоба на клиентов, а утверждение о том,
кто закрывает соединения. Сервер, который отвечает и сразу закрывает, копит их у
себя по построению. Сервер, который держит соединение открытым и даёт закрыть
клиенту, не копит вовсе.
И обратное тоже верно: если сокеты копятся у клиента, значит активно закрывает он, — и дальше вопрос уже не «как их убрать», а «почему на каждый запрос новое соединение».
Механизм 2: длительность не настраивается
Второй механизм — про то, что делают вместо лечения. Сколько состояние живёт на самом деле:
2. HOW LONG IT LASTS
--------------------
in TIME_WAIT right after close True
seconds until it disappeared 60.4
tcp_fin_timeout on this machine 60
tcp_max_tw_buckets on this machine 32768
tcp_tw_reuse on this machine 2
Шестьдесят секунд по часам. А дальше — ловушка: рядом стоит tcp_fin_timeout
со значением 60, и близость выглядит как объяснение. Она им не является — tcp(7) описывает эту настройку иначе:
This specifies how many seconds to wait for a final FIN packet before the socket
is forcibly closed.
Задаёт, сколько секунд ждать финального пакета FIN, прежде чем сокет будет принудительно закрыт.
Ожидание чужого FIN — это другое состояние и другая ситуация. К минуте,
которую мы измерили, эта настройка отношения не имеет.
Две другие настройки, которые крутят в этой теме, тоже делают не то. Про
tcp_max_tw_buckets:
The maximum number of sockets in TIME_WAIT state allowed in the system. This
limit exists only to prevent simple denial-of-service attacks.
Максимальное число сокетов в состоянии TIME_WAIT, допустимое в системе. Этот предел существует только для защиты от простых атак на отказ в обслуживании.
Это предел числа, а не длительности — цитата сама договаривает, что бывает
при превышении: If this number is exceeded, the socket is closed and a warning is printed
(Если это число превышено, сокет закрывается и печатается предупреждение). Проблему «портов не хватает» такой предел не
решает: он меняет одну неприятность на другую.
Третью настройку, tcp_tw_reuse, разбираем ниже, в разговоре про
SO_REUSEADDR: она не про длительность, а про то, можно ли занять порт раньше
срока. Значение 2 в прогоне — не «включено дважды»: на этой машине
переиспользование разрешено только для loopback, где замер и идёт.
Практический вывод: правильный ответ на «как сократить
TIME_WAIT» — «никак, и не нужно». Нужно другое: перестать создавать столько
соединений или перестать быть стороной, которая закрывает их активно.
Механизм 3: клиент упирается в номера портов
Теперь то, ради чего урок написан. Что именно кончается у клиента, который открывает соединение на каждый запрос:
3. WHAT THE CLIENT RUNS OUT OF IS PORT NUMBERS
----------------------------------------------
port range for this block 50000 50049, that is 50 numbers
connections made 50
what stopped the loop EADDRNOTAVAIL
Пятьдесят номеров — пятьдесят соединений. Не сорок восемь и не пятьдесят два: ровно столько, сколько номеров. Пятьдесят первое соединение брать неоткуда, потому что все предыдущие номера ещё заняты тем самым состоянием ожидания.
И обратите внимание на саму ошибку. Что она означает, connect(2) описывает
дословно:
upon attempting to bind it to an ephemeral port, it was determined that all port
numbers in the ephemeral port range are currently in use.
При попытке привязать его к эфемерному порту выяснилось, что все номера портов эфемерного диапазона сейчас заняты.
Ни памяти, ни дескрипторов, ни отказа сервера в этом определении нет: кончились именно номера. Диагностика отсюда однозначная — увидев эту ошибку в логе клиента, искать надо не на сервере.
Механизм 4: потолок считается, а не угадывается — но это оценка
Из двух измеренных величин — размера диапазона и длительности состояния — получается число, которое стоит держать в голове:
4. THE CEILING IS ARITHMETIC OVER THE TWO NUMBERS ABOVE
-------------------------------------------------------
ip_local_port_range on this machine 32768 60999
port numbers in it 28232
seconds a port stays in TIME_WAIT 60.4
new connections per second, ceiling 467.7
Около четырёхсот семидесяти новых соединений в секунду — вот и весь запас обычной машины по умолчанию, если каждый запрос открывает новое соединение к одному и тому же адресату.
Число это скромнее, чем кажется на слух, и в этом вся его ценность: сервис на пять тысяч запросов в секунду выходит за него в десять раз, и никакая ёмкость машины этого не отменит — кончаются номера, а не ресурсы.
И сразу — докуда это число доходит, потому что законом природы оно не является. Это грубая оценка для повторных соединений к одному и тому же адресу и порту назначения в этой конфигурации, а не общий потолок машины. Границы у неё три. Первая: ядро различает соединения по четвёрке — адрес и порт с обеих сторон, — поэтому у второго адресата свой набор номеров, и общий счёт выше. Вторая: обе величины в делении сняты здесь, на этой машине; при другом диапазоне портов число другое, и считать его надо заново, а не помнить как константу. Третья: это оценка для худшего случая — клиента, который не переиспользует ничего.
Именно поэтому оценка и полезна: не как число в паспорте машины, а как порядок величины, на котором привычка «соединение на запрос» перестаёт работать.
Механизм 5: переиспользование убирает всю арифметику
Последний механизм — самый короткий, потому что тут нечего объяснять.
5. A REUSED CONNECTION SPENDS ONE PORT, WHATEVER THE LOAD
---------------------------------------------------------
requests sent 400
local ports spent 1
TIME_WAIT sockets left behind 1
Четыреста запросов, один порт, один сокет в TIME_WAIT в конце. Третий блок на
той же машине кончился отказом на пятидесятом соединении.
Машина и ядро те же. В третьем блоке диапазон был нарочно сужен до пятидесяти номеров — оттуда и отказ на пятидесятом. Но переиспользованию размер диапазона безразличен: один порт при любом его размере. Изменилось одно решение — держать соединение или закрывать. И это решение снимает и арифметику потолка, и вопрос про настройки, и половину вопросов темы.
Заодно оно убирает и то, за что платили в двух прошлых уроках: разрешение имени и рукопожатие TLS. Одно соединение — один раз всё.
Глубже: порты кончаются не там, где стоит приложение
Вся арифметика выше считалась для одной машины: свой диапазон номеров, свой счёт соединений, своя ошибка в своём логе. В настоящей сети это условие часто не выполняется — и тогда порты кончаются не у того, кто их тратил.
Между машинами и внешним миром обычно стоит шлюз, подменяющий адреса: наружу соединения уходят не с адреса машины приложения, а с адреса шлюза. Чтобы потом разложить ответы обратно по машинам, шлюз обязан различать эти разговоры между собой — и различает он их так же, как ядро: номером порта со своей стороны. Значит, номера он раздаёт из своего диапазона, из того же ограниченного поля номеров.
Отсюда главное: за таким шлюзом номера портов общие на всю сеть машин. Их тратят все вместе, и кончаются они у всех сразу. Рассуждение это, а не замер: второй машины и шлюза в прогоне нет.
Два следствия, которые стоит держать в голове. Первое: отсутствие
EADDRNOTAVAIL на машине приложения ничего не доказывает — там номера могут
быть свободны, а соединения всё равно не устанавливаются, потому что они
кончились на шлюзе, и симптом придёт оттуда. Второе: расчёт по одной машине
занижает риск, потому что за общим шлюзом нагрузка машин складывается в один
счёт, а не делится на независимые.
Лечение, впрочем, то же самое, и это хорошая новость: соединение, которое живёт и переиспользуется, не тратит номер ни на машине, ни на шлюзе.
Как отвечать на собеседовании
Короткий ответ: TIME_WAIT — нормальный конец соединения, а не утечка.
Закрывшая сторона ещё некоторое время держит свою пару адрес-порт, чтобы
задержавшиеся сегменты старого соединения не были приняты новым соединением с
теми же адресами и портами. Проблемой это становится у клиента, который
открывает соединение на каждый запрос: у него кончаются не память и не
дескрипторы, а номера портов. Измерено: при диапазоне в 50 номеров ровно 50
соединений и EADDRNOTAVAIL на пятьдесят первом.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
Хороший ответ отличают три вещи. Первая — вы называете порядок величины, а не
слово «зависит»: диапазон портов, делённый на длительность состояния, даёт около
470 новых соединений в секунду, и тут же оговариваете границу — это грубая
оценка для повторных соединений к одному и тому же адресу и порту назначения в
конкретной конфигурации, а не общий потолок машины. Вторая — вы не предлагаете
крутить настройки, а объясняете, почему они не о том: tcp_fin_timeout
относится к другому состоянию, tcp_max_tw_buckets ограничивает число сокетов,
а не длительность. Третья — вы говорите, что TIME_WAIT на сервере
означает, что закрывает сервер, и это отдельный разговор про то, почему он не
держит соединения.
И одна формулировка, которую стоит поправить у себя заранее. «Достаётся тому,
кто закрыл первым» — верно для обычного закрытия через FIN, но правило
стандарта звучит иначе: через TIME_WAIT проходит сторона, выполняющая
активное закрытие. При одновременном закрытии активно закрывают обе, а
соединение, оборванное сбросом, через это состояние не проходит вовсе.
Чего говорить не стоит: «мы уменьшили tcp_fin_timeout, и стало лучше». Если
стало лучше, дело было не в этом.
Дальше спросят
Зачем это состояние вообще нужно?
Чтобы пара «адрес и порт» не была переиспользована, пока в сети могут ходить пакеты от старого соединения. Иначе задержавшийся пакет прибудет в новое соединение с теми же номерами, и его примут за свой.
Стандарт говорит это прямо: TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP peer received the acknowledgment of its connection termination request and to avoid new connections being impacted by delayed segments from previous connections
(TIME-WAIT — означает ожидание, достаточное для уверенности, что удалённый узел TCP получил подтверждение своего запроса на завершение соединения, и для того, чтобы задержавшиеся сегменты прошлых соединений не задели новые).
Отсюда и длительность: стандарт задаёт её как удвоенное максимальное время жизни сегмента. Linux не считает это по стандарту, а держит фиксированную минуту — её мы и измерили. Сократить нельзя не потому, что настройка спрятана, а потому, что настройки такой нет.
Помогает ли SO_REUSEADDR?
Не от этой беды. socket(7) описывает его так: Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses
(Указывает, что правила проверки адресов, передаваемых в вызов bind(2), должны допускать переиспользование локальных адресов). То есть он про bind — про то, как сервер снова
занимает слушающий порт после перезапуска, пока старые соединения ещё в
TIME_WAIT. К исходящим соединениям клиента он отношения не имеет: у них номер
порта выбирает ядро, и выбирать ему по-прежнему не из чего.
Настройка, которая действительно про это, — tcp_tw_reuse, и tcp(7)
описывает её осторожно: разрешить переиспользование, «когда это безопасно с
точки зрения протокола», и не менять без совета специалистов. Это обход, а не
решение.
Что если расширить диапазон портов?
Это работает и иногда оправдано: прочитанный из /proc диапазон — 28 232
номера из 65 535 возможных, то есть меньше половины поля номера порта, и
расширение поднимает потолок пропорционально. Но
поднимает — а не убирает: вдвое больше портов дают вдвое больший потолок, и на
этом всё.
Сравните с прошлым блоком: переиспользование соединения меняет счёт не вдвое, а на порядки — четыреста запросов через один порт. Расширять диапазон стоит тогда, когда переиспользовать нечего: например, у прокси, который по природе открывает исходящее соединение на каждое входящее.
Как это выглядит со стороны балансировщика?
Так же, только у него это происходит с обеих сторон сразу. Балансировщик — клиент для серверов за ним, поэтому исчерпание портов у него наступает первым и проявляется как загадочные отказы под нагрузкой, которых нет ни на одном сервере.
Отсюда и первое, что стоит проверить в такой ситуации: держит ли балансировщик соединения к серверам открытыми. Если на каждое входящее он открывает исходящее — его потолок оценён четвёртым механизмом, и он ниже, чем ожидают.
Частые заблуждения
Много сокетов в TIME_WAIT — это утечка
Это нормальное состояние закрытого соединения, и достаётся оно стороне, выполняющей активное закрытие. Измерено: двадцать одинаковых обменов дали 20 сокетов на той стороне, которая закрывала, и 0 на другой. Число говорит не об утечке, а о том, сколько соединений вы закрываете и как быстро.
Длительность TIME_WAIT сокращается настройкой tcp_fin_timeout
Эта настройка про другое состояние: This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed
(задаёт, сколько секунд ждать финального пакета FIN, прежде чем сокет будет принудительно закрыт). Измерено: состояние жило 60,4 секунды по часам. Совпадение со значением 60 в настройке — совпадение, а не объяснение.
EADDRNOTAVAIL означает нехватку памяти или дескрипторов
Это нехватка номеров портов, и так она и определена в connect(2): upon attempting to bind it to an ephemeral port, it was determined that all port numbers in the ephemeral port range are currently in use
(при попытке привязать его к эфемерному порту выяснилось, что все номера портов эфемерного диапазона сейчас заняты). Измерено: при диапазоне в 50 номеров прошло ровно 50 соединений, а пятьдесят первое получило эту ошибку.
Проблема на сервере, раз TIME_WAIT видно на сервере
Видно там, где закрывают активно. Измерено: когда первым закрывал сервер, все 20 сокетов оказались на его стороне; когда клиент — все 20 у клиента. Сокеты на сервере означают, что соединения закрывает он, и разговор дальше про то, почему он их не держит.
TIME_WAIT всегда достаётся тому, кто закрыл первым
Это частный случай, верный для обычного закрытия через FIN, — и замер сделан именно так. Правило же формулируется от активного закрытия: When a connection is closed actively, it MUST linger in the TIME-WAIT state
(Когда соединение закрывается активно, оно ОБЯЗАНО задержаться в состоянии TIME-WAIT). При одновременном закрытии, когда FIN уходят навстречу друг другу, активно закрывают обе стороны; а соединение, оборванное сбросом, через это состояние не проходит вовсе.
Потолок новых соединений упирается в процессор
Раньше него упирается арифметика портов. Измерено и посчитано: диапазон в 28 232 номера, делённый на 60,4 секунды состояния, даёт 467,7 новых соединения в секунду. Но это и не потолок машины: грубая оценка для повторных соединений к одному и тому же адресу и порту назначения в этой конфигурации — у второго адресата свой набор номеров, а на машине с другим диапазоном число другое. Порядок величины при этом остаётся: сервис на пять тысяч запросов в секунду выходит за него в десять раз, и ёмкость машины тут ни при чём.
Раз на машине приложения порты свободны, дело не в портах
Свободны они там, где вы смотрите. Наружу соединения часто уходят через шлюз, подменяющий адреса, и различать разговоры ему приходится своими номерами портов — из своего ограниченного диапазона, общего на всю сеть машин за ним. Тратят их все вместе, и кончаются они у всех сразу. Замера этого в уроке нет: это следствие того, что номер порта со стороны шлюза — единственное, чем он различает соединения.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ печатает сам скрипт замера.
Практика · что напечатает
server_side, client_side = who_holds(closes_first=False) made, stopped = until_ports_run_out() print(server_side) print(client_side) print(stopped)
Практика · оцените
Проверка знаний
На чьей стороне возникает TIME_WAIT?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Закрытое соединение исчезает не сразу: сторона, закрывшая его, ещё некоторое время держит свою пару адрес-порт. Это
TIME_WAIT, и нужен он затем, чтобы задержавшиеся в сети сегменты старого соединения не были приняты новым соединением с теми же адресами и портами. Нормальный конец разговора, а не утечка и не поломка. - Отсюда главное следствие: упирается в это не сервер и не память, а клиент — в номера портов. Измерено: при диапазоне в 50 номеров получилось ровно 50 соединений, пятьдесят первое дало
EADDRNOTAVAIL. И картина на сервере читается так же: тридцать тысяч сокетов там говорят о том, кто закрывает соединения, а не о нагрузке, — измерено, двадцать одинаковых обменов дали 20 сокетов на той стороне, которая закрывала, и 0 на другой. - Дальше — то, что отличает знающего от читавшего. Точное правило — про активное закрытие: при обычном закрытии через
FINчерезTIME_WAITпроходит сторона, закрывающая активно (бывает ещё одновременное закрытие и сброс соединения — там картина другая). Состояние живёт около минуты — измерено, 60,4 секунды по часам, — и настройки его не сокращают:tcp_fin_timeoutотносится к другому состоянию,tcp_max_tw_bucketsограничивает число сокетов, а не длительность. Диапазон портов, делённый на длительность состояния, даёт 467,7 соединения в секунду — но это грубая оценка для повторных соединений к одному и тому же адресу и порту назначения в этой конфигурации, а не общий потолок машины. И лечится всё это не настройками: измерено, 400 запросов через одно соединение потратили один порт и оставили один сокет вTIME_WAIT.
На самом деле
- Это нормальное состояние закрытого соединения, и достаётся оно стороне, выполняющей активное закрытие. Измерено: двадцать одинаковых обменов дали 20 сокетов на той стороне, которая закрывала, и 0 на другой. Число говорит не об утечке, а о том, сколько соединений вы закрываете и как быстро.
- Эта настройка про другое состояние: This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed. Измерено: состояние жило 60,4 секунды по часам. Совпадение со значением 60 в настройке — совпадение, а не объяснение.
- Это нехватка номеров портов, и так она и определена в
connect(2): upon attempting to bind it to an ephemeral port, it was determined that all port numbers in the ephemeral port range are currently in use. Измерено: при диапазоне в 50 номеров прошло ровно 50 соединений, а пятьдесят первое получило эту ошибку. - Видно там, где закрывают активно. Измерено: когда первым закрывал сервер, все 20 сокетов оказались на его стороне; когда клиент — все 20 у клиента. Сокеты на сервере означают, что соединения закрывает он, и разговор дальше про то, почему он их не держит.
- Это частный случай, верный для обычного закрытия через
FIN, — и замер сделан именно так. Правило же формулируется от активного закрытия: When a connection is closed actively, it MUST linger in the TIME-WAIT state. При одновременном закрытии, когдаFINуходят навстречу друг другу, активно закрывают обе стороны; а соединение, оборванное сбросом, через это состояние не проходит вовсе. - Раньше него упирается арифметика портов. Измерено и посчитано: диапазон в 28 232 номера, делённый на 60,4 секунды состояния, даёт 467,7 новых соединения в секунду. Но это и не потолок машины: грубая оценка для повторных соединений к одному и тому же адресу и порту назначения в этой конфигурации — у второго адресата свой набор номеров, а на машине с другим диапазоном число другое. Порядок величины при этом остаётся: сервис на пять тысяч запросов в секунду выходит за него в десять раз, и ёмкость машины тут ни при чём.
- Свободны они там, где вы смотрите. Наружу соединения часто уходят через шлюз, подменяющий адреса, и различать разговоры ему приходится своими номерами портов — из своего ограниченного диапазона, общего на всю сеть машин за ним. Тратят их все вместе, и кончаются они у всех сразу. Замера этого в уроке нет: это следствие того, что номер порта со стороны шлюза — единственное, чем он различает соединения.
Что разобрано
- Что здесь на самом деле спрашивают
- База: у соединения есть след, который остаётся после закрытия
- Механизм 1: ожидание достаётся стороне, которая закрывает активно
- Механизм 2: длительность не настраивается
- Механизм 3: клиент упирается в номера портов
- Механизм 4: потолок считается, а не угадывается — но это оценка
- Механизм 5: переиспользование убирает всю арифметику
- Глубже: порты кончаются не там, где стоит приложение
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
3 ИСТОЧНИКА
- tcp(7), Linux man-pages 6.7Официальная документация. Про три настройки, которые крутят вместо лечения. `tcp_fin_timeout` — не про TIME_WAIT: «This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed» (Задаёт, сколько секунд ждать финального пакета FIN, прежде чем сокет будет принудительно закрыт). `tcp_max_tw_buckets` ограничивает число, а не длительность: «The maximum number of sockets in TIME_WAIT state allowed in the system. This limit exists only to prevent simple denial-of-service attacks» (Максимальное число сокетов в состоянии TIME_WAIT, допустимое в системе. Этот предел существует только для защиты от простых атак на отказ в обслуживании). `tcp_tw_reuse` разрешает переиспользование, а не сокращает ожидание: «Allow to reuse TIME_WAIT sockets for new connections when it is safe from protocol viewpoint. It should not be changed without advice/request of technical experts» (Разрешить переиспользование сокетов TIME_WAIT для новых соединений, когда это безопасно с точки зрения протокола. Не следует менять без совета или запроса технических специалистов).https://man7.org/linux/man-pages/man7/tcp.7.html
- RFC 9293, Transmission Control Protocol (TCP)Источник. Откуда известно, кому достаётся состояние и зачем оно: «When a connection is closed actively, it MUST linger in the TIME-WAIT state for a time 2xMSL (Maximum Segment Lifetime)» (Когда соединение закрывается активно, оно ОБЯЗАНО задержаться в состоянии TIME-WAIT на время 2xMSL (максимального времени жизни сегмента)). И само определение состояния: «TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP peer received the acknowledgment of its connection termination request and to avoid new connections being impacted by delayed segments from previous connections» (TIME-WAIT — означает ожидание, достаточное для уверенности, что удалённый узел TCP получил подтверждение своего запроса на завершение соединения, и для того, чтобы задержавшиеся сегменты прошлых соединений не задели новые).https://www.rfc-editor.org/rfc/rfc9293.html
- connect(2) и socket(7), Linux man-pages 6.7Официальная документация. Что означает ошибка, в которую упирается клиент. `connect(2)`, EADDRNOTAVAIL: «upon attempting to bind it to an ephemeral port, it was determined that all port numbers in the ephemeral port range are currently in use» (при попытке привязать его к эфемерному порту выяснилось, что все номера портов эфемерного диапазона сейчас заняты). И чего касается настройка, которую предлагают вместо лечения, — `socket(7)`, SO_REUSEADDR: «Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses» (Указывает, что правила проверки адресов, передаваемых в вызов bind(2), должны допускать переиспользование локальных адресов).https://man7.org/linux/man-pages/man2/connect.2.html