Операция записи с точки зрения TCP
Как отмечалось выше, операция записи отвечает лишь за копирование данных из буфера приложения в память ядра и уведомление TCP о том, что появились данные для передачи. А теперь рассмотрим некоторые из критериев, которыми руководствуется TCP, «принимая решение» о том, можно ли передать новые данные незамедлительно и в каком количестве. Я не задаюсь целью полностью объяснить логику отправки данных в TCP, а хочу лишь помочь вам составить представление о факторах, влияющих на эту логику. Тогда вы сможете лучше понять принципы работы своих программ.
Одна из основных целей стратегии отправки данных в TCP - максимально эффективное использование имеющейся полосы пропускания. TCP посылает данные блоками, размер которых равен MSS (maximum segment size - максимальный размер сегмента).
Примечание: В процессе установления соединения TCP на каждом конце может указать приемлемый для него MSS. TCP на другом конце обязан удовлетворить это пожелание и не посылать сегменты большего размера. MSS вычисляется на основе MTU (maximum transmission unit - максимальный размер передаваемого блока),как описано в совете 7.
В то же время TCP не может переполнять буферы на принимающем конце. Как вы видели в совете 1, это определяется окном передачи.
Если бы эти два условия были единственными, то стратегия отправки была бы проста: немедленно послать все имеющиеся данные, упаковав их в сегменты размером MSS, но не более чем разрешено окном передачи. К сожалению, есть и другие факторы.
Прежде всего, очень важно не допускать перегрузки сети. Если TCP неожиданно пошлет в сеть большое число сегментов, может исчерпаться память маршрутизатора, что повлечет за собой отбрасывание датаграмм. А из-за этого начнутся повторные передачи, что еще больше загрузит сеть. В худшем случае сеть будет загружена настолько, что датаграммы вообще нельзя будет доставить. Это называется затором (congestion collapse). Чтобы избежать перегрузки, TCP не посылает по простаивающему соединению все сегменты сразу. Сначала он посылает один сегмент и постепенно увеличивает число неподтвержденных сегментов в сети, пока не будет достигнуто равновесие.
Примечание: Эту проблему можно наглядно проиллюстрировать таким примером. Предположим, что в комнате, полной народу, кто-то закричал: «Пожар!» Все одновременно бросаются к дверям, возникает давка, и в результате никто не может выйти. Если же люди будут выходить по одному, то пробки не возникнет, и все благополучно покинут помещение.
Для предотвращения перегрузки TCP применяет два алгоритма, в которых используется еще одно окно, называемое окном перегрузки. Максимальное число байтов, которое TCP может послать в любой момент, - это минимальная из двух величин: размер окна передачи и размер окна перегрузки. Обратите внимание, что эти окна отвечают за разные аспекты управления потоком. Окно передачи, декларируемое TCP на другом конце, предохраняет от переполнения его буферов. Окно перегрузки, отслеживаемое TCP на вашем конце, не дает превысить пропускную способность сети. Ограничив объем передачи минимальным из этих двух окон, вы удовлетворяете обоим требованиям управления потоком.
Первый алгоритм управления перегрузкой называется «медленный старт». Он постепенно увеличивает частоту передачи сегментов в сеть до пороговой величины.
Примечание: Слово «медленный» взято в кавычки, поскольку на самом деле нарастание частоты экспоненциально. При медленном старте окно перегрузки открывается на один сегмент при получении каждого АСК. Если вы начали с одного сегмента, то последовательные размеры окна будут составлять 1,2, 4, 8 и т.д.
Когда размер окна перегрузки достигает порога, который называется порогом медленного старта, этот алгоритм прекращает работу, и в дело вступает алгоритм избежания перегрузки. Его работа предполагает, что соединение достигло равновесного состояния, и сеть постоянно зондируется - не увеличилась ли пропускная способность. На этой стадии окно перегрузки открывается линейно — по одному сегменту за период кругового обращения.
В стратегии отправки TCP окно перегрузки в принципе может запретить посылать данные, которые в его отсутствие можно было бы послать. Если происходит перегрузка (о чем свидетельствует потерянный сегмент) или сеть некоторое время простаивает, то окно перегрузки сужается, возможно, даже до размера одного сегмента. В зависимости от того, сколько данных находится в очереди, и сколько их пытается послать приложение, это может препятствовать отправке всех данных.
Авторитетным источником информации об алгоритмах избежания перегрузки является работа Jacobson 1988], в которой они впервые были предложены. Джекобсон привел результаты нескольких экспериментов, демонстрирующие заметное повышение производительности сети после внедрения управления перегрузкой. В книге [Stevens-1994] содержится подробное объяснение этих алгоритмов и результаты трассировки в локальной сети. В настоящее время эти алгоритмы следует включать в любую реализацию, согласующуюся со стандартом (RFC 1122 [Braden 1989]).
Примечание: Несмотря на впечатляющие результаты, реализация этих алгоритмов очень проста— всего две переменные состояния и несколько строчек кода. Детали можно найти в книге [Wright and Stevens 1995].
Еще один фактор, влияющий на стратегию отправки TCP, - алгоритм Нейгла. Этот алгоритм впервые предложен в RFC 896 [Nagle 1984]. Он требует, чтобы никогда не было более одного неподтвержденного маленького сегмента, то есть сегмента размером менее MSS. Цель алгоритма Нейгла — не дать TCP забить сеть последовательностью мелких сегментов. Вместо этого TCP сохраняет в своих буферах небольшие блоки данных, пока не получит подтверждение на предыдущий маленький сегмент, после чего посылает сразу все накопившиеся данные. В совете 24 вы увидите, что отключение алгоритма Нейгла может заметно сказаться на производительности приложения.
Если приложение записывает данные небольшими порциями, то эффект от алгоритма Нейгла очевиден. Предположим, что есть простаивающее соединение, окна передачи и перегрузки достаточно велики, а выполняются подряд две небольшие операции записи. Данные, записанные вначале, передаются немедленно, поскольку окна это позволяют, а алгоритм Нейгла не препятствует, так как неподтвержденных данных нет (соединение простаивало). Но, когда до TCP доходят данные, полученные при второй операции, они не передаются, хотя в окнах передачи и перегрузки есть место. Поскольку уже есть один неподтвержденный маленький сегмент, и алгоритм Нейгла требует оставить данные в очереди, пока не придет АСК.
Обычно при реализации алгоритма Нейгла не посылают маленький сегмент, если есть неподтвержденные данные. Такая процедура рекомендована RFC 1122. Но реализация в BSD (и некоторые другие) несколько отходит от этого правила и отправляет маленький сегмент, если это последний фрагмент большой одновременно записанной части данных, а соединение простаивает. Например, MSS для простаивающего соединения равен 1460 байт, а приложение записывает 1600 байт. При этом TCP пошлет (при условии, что это разрешено окнами передачи и перегрузки) сначала сегмент размером 1460, а сразу вслед за ним, не дожидаясь подтверждения, сегмент размером 140. При строгой интерпретации алгоритма Нейгла следовало бы отложить отправку второго сегмента либо до подтверждения первого, либо до того, как приложение запишет достаточно данных для формирования полного сегмента.
Алгоритм Нейгла - это лишь один из двух алгоритмов, позволяющих избежать синдрома безумного окна (SWS - silly window syndrome). Смысл этой тактики в том, чтобы не допустить отправки небольших объемов данных. Синдром SWS и его отрицательное влияние на производительность обсуждаются в RFC 813 [Clark 1982]. Как вы видели, алгоритм Нейгла пытается избежать синдрома SWS со стороны отправителя. Но требуются и усилия со стороны получателя, который не должен декларировать слишком маленькие окна.
Напомним, что окно передачи дает оценку свободного места в буферах хоста на другом конце соединения. Этот хост объявляет о том, сколько в нем имеется места, включая в каждый посылаемый сегмент информацию об обновлении окна. Чтобы избежать SWS, получатель не должен объявлять о небольших изменениях.
Следует пояснить это на примере. Предположим, у получателя есть 14600 свободных байт, a MSS составляет 1460 байт. Допустим также, что приложением на Конце получателя читается за один раз всего по 100 байт. Отправив получателю 10 сегментов, окно передачи закроется. И вы будете вынуждены приостановить отправку данных. Но вот приложение прочитало 100 байт, в буфере приема 100 байт освободилось. Если бы получатель объявил об этих 100 байтах, то вы тут же послали бы ему маленький сегмент, поскольку TCP временно отменяет алгоритм Нейгла, если из-за него длительное время невозможно отправить маленький сегмент. Вы и дальше продолжали бы посылать стобайтные пакеты, так как всякий раз, когда приложение на конце получателя читает очередные 100 байт, получатель объявляет освобождении этих 100 байт, посылая информацию об обновлении окна.
Алгоритм избежания синдрома SWS на получающем конце не позволяет объявлять об обновлении окна, если объем буферной памяти значительно не увеличился. В RFC 1122 «значительно» - это на размер полного сегмента или более чем на половину максимального размера окна. В реализациях, производных от BSD, требуется увеличение на два полных сегмента или на половину максимального размера окна.
Может показаться, что избежание SWS со стороны получателя излишне (поскольку отправителю не разрешено посылать маленькие сегменты), но в действительности это защита от тех стеков TCP/IP, в которых алгоритм Нейгла не реализован или отключен приложением (совет 24). RFC 1122 требует от реализаций TCP, удовлетворяющих стандарту, осуществлять избежание SWS на обоих концах.
На основе этой информации теперь можно сформулировать стратегию отправки, принятую в реализациях TCP, производных от BSD. В других реализациях стратегия может быть несколько иной, но основные принципы сохраняются.
При каждом вызове процедуры вывода TCP вычисляет объем данных, которые можно послать. Это минимальное значение количества данных в буфере передачи, размера окон передачи и перегрузки и MSS. Данные отправляются при выполнении хотя бы одного из следующих условий:
Примечание: Если у TCP есть маленький сегмент, который запрещено посылать, то он взводит таймер на то время, которое потребовалось бы для ожидания АСК перед повторной передачей (но в пределах 5-60 с). Иными словами, устанавливается тайм-аут ретрансмиссии (RТО). Если этот таймер, называемый таймером терпения (persist timer), срабатывает, то TCP все-таки посылает сегмент при условии, что это не противоречит ограничениям, которые накладывают окна передачи и перегрузки. Даже если получатель объявляет окно размером нуль байт, TCP все равно попытается послать один байт. Это делается для того, чтобы потерянное обновление окна не привело к тупиковой ситуации.