FreeBSD : lingering close или close + time_wait

Когда программа выкачивает один файл с удаленного сервера с использованием протокола TCP, а после этого сразу же "отваливается", то проблем, скорее всего, не возникнет никаких. Допустим, для определенности, что эта программа использует HTTP для передачи данных. В этом случае вся программа сводится к тому, что открывается сокет, указывается адрес удаленной машины и порт, туда передается, например, "GET /", после чего программа выкачивает все, что ей в этот сокет кинули. Это очень просто и каждый программист, даже никогда до этих пор не работавший с протоколами семейства TCP/IP, прочитав содержимое man-страниц, сможет написать подобную программу.

Тем не менее, редко когда программа ограничивается выкачиванием одного лишь файла. Через некоторое время становится нужно выкачать с удаленной машины еще что-нибудь, потом обработать редиректы, потом еще кое-что появится... вы и оглянуться не успеете, как будете анализировать содержимое файла robots.txt. Тем не менее, программа, которая выкачивает один файл, и программа, которая выкачивает несколько файлов, совсем не одно и то же.


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

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

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

Происходит это потому, что в операционной системе имеется естественное ограничение на количество одновременно открытых сокетов. Связано это с тем, что на каждый сокет выделяется буфер, используемый для операций чтения и записи, который занимает некоторое место. Обычно этот буфер выделяется из некоторого пула и, хотя я не знаю как обстоит с ним дело в, например, Windows, но могу с некоторой долей уверенности сказать, что и там он не резиновый. В BSD, к примеру, его размер можно указать в конфигурации ядра. Тем не менее, количество открытых сокетов в вашей программе не будет велико, так в чем же дело?

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

Существуют два решения этой проблемы. Первое заключается в увеличении количества памяти, выделяемой операционной системой под сокеты. Второе --- в использовании того, что в англоязычной литературе называется "lingering close".

Существует атрибут у сокета, называемый SO_LINGER. При его помощи можно изменить поведение close() на такое, при котором вызывающий процесс будет переведен в состояние ожидания реального закрытия сокета. Выставив этот атрибут, вам потребуется также указать время ожидания для данного сокета. Второе решение рассматриваемой проблемы заключается в том, что бы "включить" SO_LINGER для сокетов и установить время ожидания в 0. В этом случае все структуры, связанные с сокетами, будут освобождены сразу. Делается это следующим образом:

struct linger l = { 1, 0 };
setsockopt(sock, SOL_SOCKET, SO_LINGER, &l, sizeof(struct linger));
close(sock);


Тем не менее, "второе решение" чревато новыми проблемами: через некоторое время вы обнаружите странные ситуации обрыва связи с удаленной машиной при одновременном выкачивании с нее нескольких файлов (в случае одного файла все будет в порядке).

Тут надо четко понимать, откуда взялось время между вызовом close() и реальным освобождением сокета. Все дело в том, что пакеты в сети не идут напрямую от одного адреса к другому, а "блуждают" по сети в поисках своего адресата. При использовании TCP, пакеты характеризуются четырьмя параметрами: IP-адресом отправителя, портом отправителя, IP-адресом получателя и портом получателя. Поэтому, если закрыть сокет сразу, а потом случайно открыть соединение с той же удаленной машиной по тому же локальному порту, то пакеты от старого соединения, которые все еще "блуждают", будут восприняты как реальные данные нового соединения! В частности, очень просто получить потверждающий FIN, который пришел в качестве реакции на предыдущий close(), в результате которого произойдет разрыв соединения.

И еще раз. При повторном HTTP запросе на сервер у вас три из четырех параметров в TCP пакете будут точно такими же, как и при первом HTTP запросе, то есть IP-адреса компьютеров и порт сервера (80). Вероятность же того, что на локальной машине вы получите тот же порт, что и в предыдущий раз, не нулевая и вполне реальная. Таким образом, данные могут быть повреждены.

Существует еще одно объяснение появления времени между вызовом close() и освобождением структур, связанных с сокетами, которое, правда, не имеет прямого отношения к рассматриваемой гипотетической программе. Все дело в том, что при записи в сокет, вызов close() может последовать до того, как данные будут реально переданы на удаленную машину. В этом случае, реализация TCP должна будет "подождать" потверждение о приеме данных удаленной машиной.

Таким образом, возвращаясь к проблеме с сокетами в состоянии TIME_WAIT. Самым правильным будет увеличить размер памяти под сокеты: в этом случае работа вашей программы сразу же станет более стабильной. Lingering close можно применять только в том случае, когда вы уверены в том, что больше соединений с удаленной машиной по данному IP-адресу или порту не будет. Тогда это допустимо. Иначе --- надо подождать.

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

Lingering close с нулевым временем ожидания можно применять только в тех случаях, когда доподлинно известно, что "блуждающие" пакеты ничем больше повредить не могут.
Вы только посетили наш сайт, КОММЕНТИРОВАНИЕ будет доступно через несколько минут.
возможно у Вас отключен javascript, если включен - просто обновите страницу