Дэвид Кларк предложил запретить принимающей стороне отправлять информацию об однобайтовом размере окна. Вместо этого получатель должен подождать, пока в буфере не накопится значительное количество свободного места. В частности, получатель не должен отправлять сведения о новом размере окна, пока не сможет принять сегмент максимального размера (который он объявлял при установке соединения) или пока его буфер не освободится хотя бы наполовину. Кроме того, увеличению эффективности передачи может способствовать сам отправитель, отказываясь от отправки слишком маленьких сегментов. Вместо этого он должен подождать, пока размер окна не станет достаточно большим для отправки полного сегмента (или хотя бы равного половине размера буфера получателя).
В задаче избавления от синдрома глупого окна алгоритм Нейгла и решение Кларка дополняют друг друга. Нейгл пытался решить проблему приложения, предоставляющего данные TCP-подсистеме посимвольно. Кларк старался разрешить проблему приложения, посимвольно получающего данные у TCP. Оба решения хороши и могут работать одновременно. Суть их заключается в том, чтобы не отправлять и не просить передавать данные слишком малыми порциями.
Для повышения производительности принимающая TCP-подсистема может делать нечто большее, чем просто обновлять информацию о размере окна крупными порциями. Как и отправляющая TCP-подсистема, она может буферизировать данные и блокировать запрос READ (чтение данных), поступающий от приложения, пока у нее не накопится значительный объем данных. Таким образом снижается количество обращений к TCP-подсистеме (и вместе с ними накладные расходы). Конечно, такой подход увеличивает время ответа, но для неинтерактивных приложений, например, при передаче файла, эффективность может быть важнее увеличения времени ответа на отдельные запросы.
Илл. 6.41. Синдром глупого окна
Еще одна проблема получателя состоит в том, что сегменты могут приходить не по порядку. Он будет хранить данные в буфере, пока не сможет передать их приложению в нужной последовательности. В принципе, нет ничего плохого в том, чтобы отвергать пакеты, прибывшие не в свою очередь, ведь они все равно будут повторно переданы отправителем, однако это неэффективно.
Подтверждение может быть выслано, только если все данные, включая подтверждаемый байт, получены. Это накопительное подтверждение. Если до получателя доходят сегменты 0, 1, 2, 4, 5, 6 и 7, он может подтвердить получение данных вплоть до последнего байта сегмента 2. Когда у отправителя истечет время ожидания, он передаст сегмент 3 еще раз. Если к прибытию сегмента 3 получатель сохранит в буфере сегменты с 4-го по 7-й, он сможет подтвердить получение всех байтов, включая последний байт сегмента 7.
6.5.9. Управление таймерами в TCP
В TCP используется множество таймеров (по крайней мере, в теории). Наиболее важным из них является таймер повторной передачи (Retransmission TimeOut, RTO), который запускается при отправке сегмента. Если подтверждение получения сегмента придет раньше окончания заданного интервала, таймер останавливается. И наоборот, если период ожидания истечет раньше, чем прибудет подтверждение, сегмент передается еще раз (а таймер запускается снова). Соответственно, возникает вопрос: насколько долгим должен быть интервал времени ожидания?
На транспортном уровне эта проблема значительно сложнее, чем в протоколах канального уровня. Например, в 802.11 величина ожидаемой задержки измеряется в микросекундах, и ее довольно легко предсказать (у нее небольшой разброс), поэтому таймер можно установить на момент чуть позднее ожидаемого прибытия подтверждения (илл. 6.42 (а)). Поскольку перегрузок нет, подтверждения на канальном уровне задерживаются редко, поэтому их отсутствие в течение установленного временного интервала с большой вероятностью означает потерю фрейма или подтверждения.
TCP вынужден работать в совершенно иных условиях. Функция плотности вероятности для времени, необходимого для доставки подтверждения TCP, выглядит скорее как график на илл. 6.42 (б), чем на илл. 6.42 (а). Она более пологая и вариативная. Поэтому предсказать, сколько времени потребуется для прохождения данных от отправителя к получателю и обратно, весьма непросто. Даже если бы мы знали, каким должно быть это время, есть еще одна сложность — выбор подходящего интервала ожидания. Если он слишком короткий (например, T1 на илл. 6.42 (б)), возникнут излишние повторные передачи, заполняющие интернет бесполезными пакетами. Если же установить слишком большое значение (T2), то из-за увеличения времени ожидания в случае потери пакета пострадает производительность. При этом среднее значение и величина дисперсии времени прибытия подтверждений могут измениться за несколько секунд при возникновении и устранении перегрузки.
Илл. 6.42. Плотность вероятности времени прибытия подтверждения: (а) на канальном уровне; (б) для TCP
Решение состоит в использовании динамического алгоритма, который постоянно меняет период ожидания, основываясь на измерениях производительности сети. Алгоритм, широко применяемый в TCP, разработан Джейкобсоном (Jacobson) в 1988 году и работает следующим образом. Для каждого соединения в TCP предусмотрена переменная SRTT (Smoothed Round-Trip Time — усредненное время в пути туда-обратно), в которой хранится текущее наилучшее ожидаемое время получения подтверждения для данного соединения. При отправке сегмента запускается таймер, который измеряет время получения подтверждения и повторяет передачу, если оно не приходит в срок. Если подтверждение успевает вернуться до истечения периода ожидания, TCP-подсистема подсчитывает время, которое понадобилось для его получения (R). Затем значение переменной SRTT обновляется по следующей формуле:
где α — весовой коэффициент, определяющий, насколько быстро забываются старые значения. Обычно α = 7/8. Это формула вычисления взвешенного скользящего среднего (Exponentially Weighed Moving Average, EWMA), или фильтра низких частот, с помощью которого можно удалять шум.
Даже при известном значении SRTT выбор периода ожидания подтверждения — нетривиальная задача. В первых реализациях TCP это значение вычислялось как 2xRTT, но опыт показал, что постоянный множитель слишком негибкий и не реагирует на увеличение разброса. В частности, модели очередей случайного (то есть пуассоновского) трафика показывают, что когда нагрузка приближается к пропускной способности, задержка растет и становится крайне изменчивой. В результате может сработать таймер повторной передачи, после чего будет отправлена копия пакета, хотя оригинальный пакет все еще будет находиться в сети. Как правило, такие ситуации возникают именно при высокой нагрузке — и это не самое лучшее время для отправки в сеть лишних пакетов.
Чтобы решить эту проблему, Джейкобсон предложил сделать интервал времени ожидания чувствительным к отклонению RTT и к усредненному RTT. Для этого потребовалась еще одна сглаженная переменная — RTTVAR (RoundTrip Time Variation — изменение времени в пути туда-обратно), которая вычисляется по формуле:
Как и в предыдущем случае, это взвешенное скользящее среднее. Обычно β = 3/4. Значение интервала ожидания повторной передачи (RTO) устанавливается по формуле:
Множитель 4 выбран произвольно, однако умножение целого числа на 4 может быть выполнено одной командой сдвига, при этом менее 1 % всех пакетов придет с опозданием, превышающим четыре среднеквадратичных отклонения. Обратите внимание, что RTTVAR является не среднеквадратичным, а средним отклонением, но на практике это довольно близкие значения. В своей работе Джейкобсон приводит множество хитрых способов вычисления значений интервала ожидания с помощью только целочисленного сложения, вычитания и сдвига. Для современных хостов такая экономия не требуется, но она стала элементом культуры повсеместного применения TCP: он должен работать как на суперкомпьютерах, так и на небольших устройствах. Для RFID-чипов его пока еще не реализовали, но ведь все может быть.
Более подробные сведения о том, как вычислять этот интервал ожидания, а также начальные значения переменных, можно найти в RFC 2988. Для таймера повторной передачи минимальное значение также устанавливается равным 1 с, независимо от предварительной оценки. Это значение, выбранное с запасом (и в основном эмпирически), требуется, чтобы избежать выполнения лишних повторных передач на основании измерений (Оллман и Паксон; Allman and Paxson, 1999).
При сборе данных (R) для вычисления RTT возникает вопрос, что делать при повторной передаче сегмента. Когда для него приходит подтверждение, неясно, к какой передаче оно относится, первой или последней. Неверная догадка может серьезно нарушить работу RTO. Эта проблема была обнаружена радиолюбителем Филом Карном (Phil Karn). Его интересовал вопрос передачи TCP/IP-пакетов с помощью коротковолновой любительской радиосвязи, известной своей ненадежностью. Предложение Карна было очень простым: не обновлять оценки для сегментов, переданных повторно. Кроме того, при каждой повторной передаче время ожидания можно удваивать до тех пор, пока сегменты не пройдут с первой попытки. Это исправление получило название алгоритма Карна (Karn’s algorithm) (Карн и Партридж; Karn and Partridge, 1987) и применяется в большинстве реализаций TCP.
В протоколе TCP используется не только таймер повторной передачи, но и таймер настойчивости (persistence timer). Он предназначен для предотвращения следующей тупиковой ситуации. Получатель отправляет подтверждение, в котором указывает окно нулевого размера (это значит, что отправитель должен подождать). Через некоторое время получатель передает пакет с новым размером окна, но этот пакет теряется. Теперь обе стороны ожидают действий друг от друга. Когда срабатывает таймер настойчивости, отправитель высылает получателю пакет с вопросом, не изменилось ли текущее состояние. В ответ получатель сообщает текущий размер окна. Если он все еще равен нулю, таймер настойчивости запускается снова, и весь цикл повторяется. В случае увеличения окна отправитель может передавать данные.