Эффективное программирование TCP-IP

       

По возможности пишите один большой блок вместо нескольких маленьких


| | |

Для этой рекомендации есть несколько причин. Первая очевидна и уже обсуждалась выше: каждое обращение к функциям записи (write, send и т.д.) требует, по меньшей мере, двух контекстных переключений, а это довольно дорогая операция. С другой стороны, многократные операции записи (если не считать случаев типа записи по одному байту) не требуют заметных накладных расходов в прило­жении. Таким образом, совет избегать лишних системных вызовов- это, скорее, «правила хорошего тона», а не острая необходимость.

Есть, однако, и более серьезная причина избегать многократной записи мел­ких блоков - эффект алгоритма Нейгла. Этот алгоритм кратко рассмотрен в совете 15. Теперь изучим его взаимодействие с приложениями более детально. Если не принимать в расчет алгоритм Нейгла, то это может привести к неправильному решению задач, так или иначе связанных с ним, и существенному снижению производительности некоторых приложений.

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

Алгоритм был впервые предложен в 1984 году Джоном Нейглом (RFC 896 [Nagle 1984]) для решения проблем производительности таких программ, как telnet и ей подобных. Обычно эти программы посылают каждое нажатие клавиши в отдельном сегменте, что приводит к засорению сети множеством крохотных датаграмм (tinygrams). Если принять во внимание, что минимальный размер ТСР-сегмента (без данных) равен 40 байт, то накладные расходы при посылке одного байта в сегменте достигают 4000%. Но важнее то, что увеличивается число пакетов в сети. А это приводит к перегрузке и необходимости повторной передачи, из-за чего перегрузка еще более увеличивается. В неблагоприятном случае в сети находится несколько копий каждого сегмента, и пропускная способность резко снижается по сравнению с номинальной.


Соединение считается простаивающим, если в нем нет неподтвержденных данных (то есть хост на другом конце подтвердил все отправленные ему данные). В первоначальном виде алгоритм Нейгла должен был предотвращать описанные выше проблемы. При этом новые данные от приложения не посылаются до тех пор, пока соединение не перейдет в состояние простоя. В результате в соединении не может находиться более одного небольшого неподтвержденного сегмента.
Процедура, описанная в RFC 1122 [Braden 1989] несколько ослабляет это требование, разрешая посылать данные, если их хватает для заполнения целого сегмента. Иными словами, если можно послать не менее MSS байт, то это разрешено, даже если соединение не простаивает. Заметьте, что условие Нейгла при этом по-прежнему выполняется: в соединении находится не более одного небольшого неподтвержденного сегмента.
Многие реализации не следуют этому правилу буквально, применяя алгоритм Нейгла не к сегментам, а к операциям записи. Чтобы понять, в чем разница, предположим, что MSS составляет 1460 байт, приложение записывает 1600 байт, в окнах приема и передачи свободно, по меньшей мере, 2000 байт и соединение простаивает. Если применить алгоритм Нейгла к сегментам, то следует послать 1460 байт, а затем ждать подтверждения перед отправкой следующих 140 байт – алгоритм Нейгла применяется при посылке каждого сегмента. Если же использовать алгоритм Нейгла к операциям записи, то следует послать 1460 байт, а вслед за ними еще 140 байт - алгоритм применяется только тогда, когда приложение передаете TCP новые данные для доставки.
Алгоритм Нейгла работает хорошо и не дает приложениям забить сеть крохотными пакетами. В большинстве случае производительность не хуже, чем в реализации TCP, в которой алгоритм Нейгла отсутствует.
Примечание: Представьте, например, приложение, которое передает ТСP один байт каждые 200 мс. Если период кругового обращения (RTT, для соединения равен одной секунде, то TCP без алгоритма Нейгла будет посылать пять сегментов в секунду с накладными расходами 4000%. При наличии этого алгоритма первый байт отсылается сразу, а следующие четыре байта, поступившие от приложения, будут задержаны, пока не придет подтверждены на первый сегмент. Тогда все четыре байта посылаются сразу. Таким образом, вместо пяти сегментов послано только два, за счет чего накладные расходы уменьшились до 1600% при сохранении той же скорости 5 байт/с.


К сожалению, алгоритм Нейгла может плохо взаимодействовать с другой, добавленной позднее возможностью TCP - отложенным подтверждением.
Когда прибывает сегмент от удаленного хоста, TCP задерживает отправку АСК в надежде, что приложение скоро ответит на только что полученные данные. Поэтому АСК можно будет объединить с данными. Традиционно в системах, производных от BSD, величина задержки составляет 200 мс.
Примечание: В RFC 1122 не говорится о сроке задержки, требуется лишь, чтобы она была не больше 500 мс. Рекомендуется также подтверждать, по крайней мере, каждый второй сегмент.
Отложенное подтверждение служит той же цели, что и алгоритм Нейгла - уменьшить число повторно передаваемых сегментов.


Принцип совместной работы этих механизмов рассмотрим на примере типичного сеанса «запрос/ответ». Как показано на рис. 3.12, клиент посылает короткий запрос серверу, ждет ответа и посылает следующий запрос.
Заметьте, что алгоритм Нейгла не применяется, поскольку клиент не посылает новый сегмент, не дождавшись ответа на предыдущий запрос, вместе с которым приходит и АСК. На стороне сервера задержка подтверждения дает серверу время ответить. Поэтому для каждой пары запрос/ответ нужно всего два сегмента. Если через RTT обозначить период кругового обращения сегмента, а через Тp - время, необходимое серверу для обработки запроса и отправки ответа (в миллисекундах), то на каждую пару запрос/ответ уйдет RTT + Тp мс.
А теперь предположим, что клиент посылает свой запрос в виде двух последовательных операций записи. Часто причина в том, что запрос состоит из заголовка, за которым следуют данные. Например, клиент, который посылает серверу запросы переменной длины, может сначала послать длину запроса, а потом сам запрос.
Примечание: Пример такого типа изображен на рис. 2.17, но там были приняты меры для отправки длины и данных в составе одного сегмента.
На рис. 3.13. показан поток данных.


Рис. 3.12. Поток данных из одиночных сегментов сеанса «запрос.ответ»
Рис. 3.13. Взаимодействие алгоритма
Нейгла и отложенного подтверждения
<


На этот раз алгоритмы взаимодействуют так, что число сегментов, посланных на каждую пару запрос/ответ, удваивается, и это вносит заметную задержку.
Данные из первой части запроса посылаются немедленно, но алгоритм Нейгла не дает послать вторую часть. Когда серверное приложение получает первую часть запроса, оно не может ответить, так как запрос целиком еще не пришел. Это значит, что перед посылкой подтверждения на первую часть должен истечь тайм-аут установленный таймером отложенного подтверждения. Таким образом, алгоритмы Нейгла и отложенного подтверждения блокируют друг друга: алгоритм Нейгла мешает отправке второй части запроса, пока не придет подтверждение на первую а алгоритм отложенного подтверждения не дает послать АСК, пока не сработает таймер, поскольку сервер ждет вторую часть. Теперь для каждой пары запрос/ответ нужно четыре сегмента и 2 X RTT + Тp + 200 мс. В результате за секунду можно обработать не более пяти пар запрос/ответ, даже если забыть о времени обработки запроса сервером и о периоде кругового обращения.
Примечание: Для многих систем это изложение чрезмерно упрощенное. Например, системы, производные от BSD, каждые 200 мс проверяют все соединения, для которых подтверждение было отложено. При этом АСК посылается независимо от того, сколько времен прошло в действительности. Это означает, что реальная задержка может составлять от 0 до 200 мс, в среднем 100 мс. Однако часто задержка достигает 200мс из-за «фазового эффекта состоящего в том, что ожидание прерывается следующим тактом таймера через 200 мс. Первый же ответ синхронизирует ответы с тактовым генератором. Хороший пример такого поведения см. в работе [Minshall et al. 1999]. I
Последний пример показывает причину проблемы: клиент выполняет последовательность операций «запись, запись, чтение». Любая такая последовательность приводит к нежелательной интерференции между алгоритмом Нейгла и алгоритмом отложенного подтверждения, поэтому ее следует избегать. Иными словами приложение, записывающее небольшие блоки, будет страдать всякий раз, когда хост на другом конце сразу не отвечает.
Представьте приложение, которое занимается сбором данных и каждые 50 мс посылает серверу одно целое число. Если сервер не отвечает на эти сообщения, а просто записывает данные в журнал для последующего анализа, то будет наблюдаться такая же интерференция. Клиент, пославший одно целое, блокируется алгоритмом Нейгла, а затем алгоритмом отложенного подтверждения, например на 200 мс, после чего посылает по четыре целых каждые 200 мс.

Содержание раздела