Когда требуется опция TCP SO_LINGER (0)?

96

Думаю, я понимаю формальный смысл этого варианта. В некотором устаревшем коде, с которым я сейчас работаю, эта опция используется. Заказчик жалуется на RST как ответ на FIN со своей стороны при закрытии соединения с его стороны.

Я не уверен, что смогу удалить его безопасно, так как не понимаю, когда его следует использовать.

Не могли бы вы привести пример, когда потребуется такая опция?

димба
источник
1
Вы должны удалить это. Его не следует использовать в производственном коде. Единственный раз, когда я когда-либо видел, что он использовался, был результат неверного теста.
Marquis of Lorne

Ответы:

83

Типичная причина установки SO_LINGERнулевого тайм-аута заключается в том, чтобы избежать большого количества подключений, находящихся в TIME_WAITсостоянии, связывающих все доступные ресурсы на сервере.

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

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

кафе
источник
4
Я абсолютно согласен. Я видел приложение для мониторинга, которое инициировало множество (несколько тысяч кратковременных соединений каждые X секунд), и у него была возможность масштабирования (на тысячу соединений больше). Я не знаю почему, но заявка не откликнулась. Кто-то предложил SO_LINGER = true, TIME_WAIT = 0 для быстрого освобождения ресурсов ОС, и после непродолжительного исследования мы действительно попробовали это решение с очень хорошими результатами. TIME_WAIT больше не проблема для этого приложения.
bartosz.r
24
Я не согласен. Протокол прикладного уровня поверх TCP должен быть спроектирован таким образом, чтобы клиент всегда инициировал закрытие соединения. Таким образом, TIME_WAITон не причинит вреда клиенту. Помните, как говорится в третьем издании «Сетевое программирование UNIX» (Стивенс и др.), Стр. 203: «Состояние TIME_WAIT - ваш друг и помогает нам. Вместо того, чтобы пытаться избежать состояния, мы должны его понять (раздел 2.7) . "
MGD
8
Что, если клиент хочет открывать 4000 подключений каждые 30 секунд (это приложение мониторинга является клиентом! Потому что оно инициирует подключение)? Да, мы можем переработать приложение, добавить несколько локальных агентов в инфраструктуру, изменить модель на push. Но если у нас уже есть такое приложение и оно растет, то мы можем заставить его работать, настроив твингер. Вы меняете один параметр, и у вас внезапно появляется работающее приложение, не вкладывая средств в реализацию новой архитектуры.
bartosz.r
4
@ bartosz.r: Я только говорю, что использование SO_LINGER с таймаутом 0 действительно должно быть последним средством. Опять же, в третьем издании «Сетевое программирование UNIX» (Стивенс и др.) Стр. 203 также говорится, что вы рискуете повредить данные. Прочтите RFC 1337, где вы поймете, почему TIME_WAIT - ваш друг.
MGD
7
@caf Нет, классическим решением будет пул соединений, как это видно в каждом мощном TCP API, например HTTP 1.1.
Маркиз Лорн,
191

По поводу моего предложения прочтите последний раздел: «Когда использовать SO_LINGER с таймаутом 0» .

Прежде чем мы перейдем к небольшой лекции о:

  • Обычное завершение TCP
  • TIME_WAIT
  • FIN, ACKиRST

Обычное завершение TCP

Обычная последовательность завершения TCP выглядит так (упрощенно):

У нас есть два партнера: A и B

  1. Звонки close()
    • A отправляет FINB
    • А переходит в FIN_WAIT_1состояние
  2. B получает FIN
    • B отправляет ACKA
    • B переходит в CLOSE_WAITсостояние
  3. Получает ACK
    • А переходит в FIN_WAIT_2состояние
  4. B звонки close()
    • B отправляет FINA
    • B переходит в LAST_ACKсостояние
  5. Получает FIN
    • A отправляет ACKB
    • А переходит в TIME_WAITсостояние
  6. B получает ACK
    • B переходит в CLOSEDсостояние - т.е. удаляется из таблиц сокетов

ВРЕМЯ ЖДЕТ

Таким образом, одноранговый узел, который инициирует завершение - т.е. звонит close()первым, - окажется в TIME_WAITсостоянии.

Чтобы понять, почему TIME_WAITштат - наш друг, прочтите раздел 2.7 в третьем издании «Сетевое программирование UNIX» Стивенса и др. (Стр. 43).

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

Чтобы обойти эту проблему, я видел, как многие предлагали установить параметр сокета SO_LINGER с таймаутом 0 перед вызовом close(). Однако это плохое решение, поскольку оно приводит к завершению TCP-соединения с ошибкой.

Вместо этого разработайте протокол своего приложения таким образом, чтобы завершение соединения всегда инициировалось со стороны клиента. Если клиент всегда знает, когда он прочитал все оставшиеся данные, он может инициировать последовательность завершения. Например, браузер знает из Content-LengthHTTP-заголовка, когда он прочитал все данные и может инициировать закрытие. (Я знаю, что в HTTP 1.1 он будет некоторое время держать его открытым для возможного повторного использования, а затем закроет.)

Если серверу необходимо закрыть соединение, разработайте протокол приложения так, чтобы сервер запрашивал у клиента вызов close().

Когда использовать SO_LINGER с таймаутом 0

Опять же, согласно третьему изданию "UNIX Network Programming", страница 202-203, установка SO_LINGERтайм-аута 0 перед вызовом close()приведет к тому, что нормальная последовательность завершения не будет инициирована.

Вместо этого одноранговый close()узел, устанавливающий эту опцию и вызывающий , отправит RST(сброс соединения), который указывает на состояние ошибки, и именно так это будет восприниматься на другом конце. Обычно вы будете видеть такие ошибки, как «Сброс соединения одноранговым узлом».

Поэтому в нормальной ситуации устанавливать SO_LINGERтайм-аут 0 перед вызовом close()- с этого момента - вызываемым неудачным закрытием - в серверном приложении - это действительно плохая идея .

Тем не менее, определенная ситуация в любом случае требует этого:

  • Если клиент вашего серверного приложения плохо себя ведет (истекает время ожидания, возвращает недопустимые данные и т. Д.), Имеет смысл неудачное закрытие, чтобы избежать застревания CLOSE_WAITили попадания в TIME_WAITсостояние.
  • Если вам необходимо перезапустить серверное приложение, которое в настоящее время имеет тысячи клиентских подключений, вы можете рассмотреть возможность установки этой опции сокета, чтобы избежать тысяч серверных сокетов TIME_WAIT(при вызове close()со стороны сервера), поскольку это может помешать серверу получить доступные порты для новых клиентских подключений. после перезапуска.
  • На странице 202 вышеупомянутой книги конкретно говорится: «Существуют определенные обстоятельства, которые оправдывают использование этой функции для отправки аварийного закрытия. Одним из примеров является сервер терминала RS-232, который может вечно зависать при CLOSE_WAITпопытке доставить данные на застрявший терминал. порт, но правильно сбросил бы застрявший порт, если бы получил RSTотмену ожидающих данных ".

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

мгд
источник
6
TIME_WAITявляется другом только тогда, когда он не начинает вызывать проблемы: stackoverflow.com/questions/1803566/…
Pacerier
2
так что, если вы пишете веб-сервер? как вы «говорите клиенту инициировать закрытие»?
Шон Нил,
2
@ShaunNeal, ты явно не знаешь. Но хорошо написанный клиент / браузер инициирует закрытие. Если клиент ведет себя плохо, к счастью, у нас есть TIME_WAIT убийство, чтобы гарантировать, что у нас не закончатся дескрипторы сокетов и эфемерные порты.
mgd
Это такой отличный ответ. Спасибо!
Юрай Мартинка
17

Когда задержка включена, но таймаут равен нулю, стек TCP не ожидает отправки ожидающих данных перед закрытием соединения. Данные могут быть потеряны из-за этого, но, установив задержку таким образом, вы принимаете это и просите немедленно сбросить соединение, а не закрывать изящно. Это вызывает отправку RST, а не обычного FIN.

Спасибо EJP за его комментарий, подробности смотрите здесь .

Лен Холгейт
источник
1
Это я поняла. я прошу «реалистичного» примера, когда мы хотели бы использовать полный сброс.
dimba
5
Всякий раз, когда вы хотите прервать соединение; поэтому, если ваш протокол не проходит валидацию, и у вас есть клиент, который внезапно говорит вам чушь, вы прерываете соединение с помощью RST и т. д.
Лен Холгейт
5
Вы путаете тайм-аут с нулевой задержкой с отключением задержки. Задержка означает, что close () не блокируется. Задержка с положительным таймаутом означает, что close () блокируется до тайм-аута. Задержка с нулевым таймаутом вызывает RST, и вот в чем вопрос.
Marquis of Lorne
2
Да, ты прав. Я откорректирую ответ, чтобы исправить свою терминологию.
Лен Холгейт
6

Сможете ли вы безопасно удалить задержку в своем коде или нет, зависит от типа вашего приложения: это «клиент» (открытие TCP-соединений и его активное закрытие в первую очередь) или «сервер» (прослушивание TCP открытых и закрытие после того, как другая сторона инициировала закрытие)?

Если ваше приложение имеет вид «клиента» (сначала закрывается), и вы инициируете и закрываете огромное количество подключений к разным серверам (например, когда ваше приложение является приложением для мониторинга, контролирующим доступность огромного количества разных серверов), ваше приложение проблема в том, что все ваши клиентские подключения застряли в состоянии TIME_WAIT. Затем я бы порекомендовал сократить тайм-аут до меньшего значения, чем значение по умолчанию, чтобы по-прежнему корректно завершать работу, но раньше освобождать ресурсы клиентских подключений. Я бы не установил тайм-аут на 0, так как 0 не завершает корректно с FIN, но прерывается с RST.

Если ваше приложение похоже на «клиент» и ему необходимо получить огромное количество небольших файлов с одного и того же сервера, вам не следует инициировать новое TCP-соединение для каждого файла и в конечном итоге получить огромное количество клиентских подключений в TIME_WAIT, но держите соединение открытым и извлекайте все данные через одно и то же соединение. Задерживающийся вариант можно и нужно убрать.

Если ваше приложение является «сервером» (вторая секунда как реакция на закрытие однорангового узла), при close () ваше соединение корректно завершается и ресурсы освобождаются, поскольку вы не входите в состояние TIME_WAIT. Задерживаться не следует. Но если в вашем серверном приложении есть процесс наблюдения, обнаруживающий, что неактивные открытые соединения бездействуют в течение длительного времени (следует определить «долго»), вы можете отключить это неактивное соединение со своей стороны - рассматривая это как своего рода обработку ошибок - с помощью прерывистого завершения. Это делается путем установки таймаута задержки на 0. Затем close () отправит клиенту RST, сообщая ему, что вы злитесь :-)

Grandswiss
источник
1

На серверах вы можете отправлять сообщения RSTвместо FINотключения некорректно работающих клиентов. Это пропускает, FIN-WAITза которым следуют TIME-WAITсостояния сокетов на сервере, что предотвращает истощение ресурсов сервера и, следовательно, защищает от такого типа атак типа «отказ в обслуживании».

Максим Егорушкин
источник
1

Мне нравится замечание Максима о том, что атаки DOS могут истощить ресурсы сервера. Это также происходит без действительно злонамеренного противника.

Некоторым серверам приходится иметь дело с «непреднамеренной атакой DOS», которая происходит, когда клиентское приложение имеет ошибку с утечкой соединения, когда они продолжают создавать новое соединение для каждой новой команды, которую они отправляют на ваш сервер. А затем, возможно, в конечном итоге их соединения закроются, если они столкнутся с давлением сборщика мусора, или, возможно, соединения в конечном итоге истекут.

Другой сценарий - сценарий «все клиенты имеют один и тот же TCP-адрес». Тогда клиентские соединения различимы только по номерам портов (если они соединяются с одним сервером). И если клиенты начинают быстро циклически открывать / закрывать соединения по любой причине, они могут исчерпать (адрес клиента + порт, IP-адрес сервера + порт) пространство кортежа.

Поэтому я думаю, что серверам лучше всего посоветовать переключиться на стратегию Linger-Zero, когда они видят большое количество сокетов в состоянии TIME_WAIT - хотя это не исправляет поведение клиента, это может уменьшить влияние.

Тим Ловелл-Смит
источник
0

Слушающий сокет на сервере может использовать задержку со временем 0, чтобы иметь доступ к немедленной привязке к сокету и сбросить всех клиентов, чьи соединения еще не завершены. TIME_WAIT - это то, что интересно только тогда, когда у вас есть сеть с несколькими путями и может закончиться неправильно упорядоченными пакетами или иным образом иметь дело с нечетным порядком / временем прибытия сетевых пакетов.

Грегг Уандерли
источник