Deep Engineering
Средний·Опубликовано·25 МИН

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.

Что здесь на самом деле спрашивают

Лестница выглядит так:

  1. «Что такое TIME_WAIT — разминка про состояние после закрытия.
  2. «У кого он возникает — у клиента или у сервера?» — вопрос, на котором видно, знает ли собеседник правило про активное закрытие.
  3. «У нас 30 тысяч сокетов в TIME_WAIT, что делать?» — главный вопрос темы и тот, где ответ настройками виден сразу как неверный.
  4. «Что означает EADDRNOTAVAIL — вопрос-ловушка: это не про память и не про дескрипторы.
  5. «Сколько новых соединений в секунду выдержит клиент?» — вопрос про арифметику, а не про «зависит».
  6. «Зачем это состояние вообще нужно?» — вопрос про то, что оно защищает.

Числа получены прогоном bench/timewait/ports.py и bench/timewait/practice.py на loopback. Длительность состояния здесь не взята из документации, а снята по часам.

База: у соединения есть след, который остаётся после закрытия

Прежде чем говорить про состояния и номера портов, стоит назвать обычными словами, что происходит с соединением от начала до конца. Сокращённо его жизнь такая:

  1. подключились — стороны договорились, что разговаривают друг с другом;
  2. соединение работает — по нему ходят данные в обе стороны;
  3. закрыли — одна сторона говорит «у меня всё», другая отвечает тем же;
  4. состояние ожидания — разговора уже нет, а след от него ещё держится.

Весь урок — про четвёртый шаг. У него есть имя: 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
наблюдение замераbench/timewait/ports.py. Обе стороны соединения живут в одном процессе на loopback, поэтому по выводу ss видно, на какой из них возник TIME_WAIT: у одной стороны порт сервера стоит локальным адресом, у другой — адресом собеседника.

Двадцать одинаковых обменов в обоих случаях. Сервер тот же, клиент тот же, данные те же. Отличается только то, кто вызвал 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 (максимального времени жизни сегмента).

RFC 9293

Пассивной стороне ждать нечего: подтверждать ей уже нечего.

контракт языкаКому достаётся состояние, задано RFC 9293. Замер показывает, что так и происходит, но правило пришло из стандарта, а не из прогона.

И здесь стоит сказать правило точно, потому что ходячая формулировка «достаётся тому, кто закрыл первым» верна не всегда. Стандарт говорит про активное закрытие, и при обычном закрытии — одна сторона шлёт 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
наблюдение замераbench/timewait/ports.py. Длительность не взята из документации: скрипт закрывает соединение и опрашивает ss, пока сокет не исчезнет. Отсюда и дробное число — это показание часов.

Шестьдесят секунд по часам. А дальше — ловушка: рядом стоит 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, прежде чем сокет будет принудительно закрыт.

tcp(7), tcp_fin_timeout

Ожидание чужого 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(7), tcp_max_tw_buckets

Это предел числа, а не длительности — цитата сама договаривает, что бывает при превышении: If this number is exceeded, the socket is closed and a warning is printed (Если это число превышено, сокет закрывается и печатается предупреждение). Проблему «портов не хватает» такой предел не решает: он меняет одну неприятность на другую.

Третью настройку, tcp_tw_reuse, разбираем ниже, в разговоре про SO_REUSEADDR: она не про длительность, а про то, можно ли занять порт раньше срока. Значение 2 в прогоне — не «включено дважды»: на этой машине переиспользование разрешено только для loopback, где замер и идёт.

контракт языкаЧего касается каждая из трёх настроек — сказано в tcp(7), а не выведено из прогона.
деталь реализации · Linux 6.18.44Сама длительность на Linux задана константой в ядре, а не настройкой: замер показывает её значение на этой машине. Стандарт задаёт её иначе — как удвоенное максимальное время жизни сегмента.

Практический вывод: правильный ответ на «как сократить 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
наблюдение замераbench/timewait/ports.py. Исчерпание сделано не нагрузкой, а сужением ip_local_port_range на время блока: отказ наступает за секунды и не зависит от того, сколько соединений открыто в системе помимо замера. Исходный диапазон возвращается сразу после.

Пятьдесят номеров — пятьдесят соединений. Не сорок восемь и не пятьдесят два: ровно столько, сколько номеров. Пятьдесят первое соединение брать неоткуда, потому что все предыдущие номера ещё заняты тем самым состоянием ожидания.

И обратите внимание на саму ошибку. Что она означает, 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.

перевод

При попытке привязать его к эфемерному порту выяснилось, что все номера портов эфемерного диапазона сейчас заняты.

connect(2), EADDRNOTAVAIL

Ни памяти, ни дескрипторов, ни отказа сервера в этом определении нет: кончились именно номера. Диагностика отсюда однозначная — увидев эту ошибку в логе клиента, искать надо не на сервере.

контракт языкаСмысл EADDRNOTAVAIL задан connect(2). Замер показывает, когда ядро её возвращает; чем именно она вызвана — сказано в документе.

Механизм 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
наблюдение замераbench/timewait/ports.py. Диапазон прочитан из /proc, длительность снята по часам; сама последняя строка — деление одного на другое, а не отдельное наблюдение.

Около четырёхсот семидесяти новых соединений в секунду — вот и весь запас обычной машины по умолчанию, если каждый запрос открывает новое соединение к одному и тому же адресату.

Число это скромнее, чем кажется на слух, и в этом вся его ценность: сервис на пять тысяч запросов в секунду выходит за него в десять раз, и никакая ёмкость машины этого не отменит — кончаются номера, а не ресурсы.

И сразу — докуда это число доходит, потому что законом природы оно не является. Это грубая оценка для повторных соединений к одному и тому же адресу и порту назначения в этой конфигурации, а не общий потолок машины. Границы у неё три. Первая: ядро различает соединения по четвёрке — адрес и порт с обеих сторон, — поэтому у второго адресата свой набор номеров, и общий счёт выше. Вторая: обе величины в делении сняты здесь, на этой машине; при другом диапазоне портов число другое, и считать его надо заново, а не помнить как константу. Третья: это оценка для худшего случая — клиента, который не переиспользует ничего.

контракт языкаЧто соединение опознаётся парой сокетов — адресом и портом с обеих сторон — определение стандарта, а не наблюдение этого замера: второго адресата в прогоне нет.

Именно поэтому оценка и полезна: не как число в паспорте машины, а как порядок величины, на котором привычка «соединение на запрос» перестаёт работать.

Механизм 5: переиспользование убирает всю арифметику

Последний механизм — самый короткий, потому что тут нечего объяснять.

5. A REUSED CONNECTION SPENDS ONE PORT, WHATEVER THE LOAD
---------------------------------------------------------
  requests sent                                  400
  local ports spent                              1
  TIME_WAIT sockets left behind                  1
наблюдение замераbench/timewait/ports.py. Та же машина. Сервер здесь держит соединение открытым, а диапазон портов — исходный, не суженный, как в третьем блоке. Считается одно: сколько локальных портов потрачено на 400 запросов.

Четыреста запросов, один порт, один сокет в 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 новых соединения в секунду. Но это и не потолок машины: грубая оценка для повторных соединений к одному и тому же адресу и порту назначения в этой конфигурации — у второго адресата свой набор номеров, а на машине с другим диапазоном число другое. Порядок величины при этом остаётся: сервис на пять тысяч запросов в секунду выходит за него в десять раз, и ёмкость машины тут ни при чём.

Утверждение

Раз на машине приложения порты свободны, дело не в портах

На самом деле

Свободны они там, где вы смотрите. Наружу соединения часто уходят через шлюз, подменяющий адреса, и различать разговоры ему приходится своими номерами портов — из своего ограниченного диапазона, общего на всю сеть машин за ним. Тратят их все вместе, и кончаются они у всех сразу. Замера этого в уроке нет: это следствие того, что номер порта со стороны шлюза — единственное, чем он различает соединения.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ печатает сам скрипт замера.

Практика · что напечатает

Двадцать одинаковых обменов, первым закрывает клиент. Печатаются три вещи: сколько сокетов в TIME_WAIT оказалось на стороне сервера, сколько на стороне клиента, и чем кончилась попытка открыть двести соединений при диапазоне в двадцать номеров портов. Что напечатает этот код?
server_side, client_side = who_holds(closes_first=False)
made, stopped = until_ports_run_out()
print(server_side)
print(client_side)
print(stopped)

Практика · оцените

Диапазон эфемерных портов — 28 232 номера, состояние TIME_WAIT держит порт около минуты. Сколько новых соединений в секунду к одному адресу выдержит клиент, который не переиспользует соединения?
соединений в секунду

Проверка знаний

Вопрос 1 из 5

На чьей стороне возникает TIME_WAIT?

Источники и что читать дальше

3 ИСТОЧНИКА

  1. 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
  2. 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
  3. 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