м фильма. Конечно, при этом возникает вопрос, как определить, просматривает ли пользователь видео со всем вниманием или вышел на кухню за стаканом воды, не удосужившись нажать на паузу.
6.7.5. Проектирование хостов для быстрых сетей
Измерение и настройка могут значительно улучшить производительность, но это не заменит качественного дизайна сети. Плохо спроектированная сеть может быть усовершенствована только до определенного уровня. Для дальнейшего увеличения производительности ее придется переделать.
В данном разделе мы рассмотрим несколько эмпирических правил, относящихся к программной реализации сетевых протоколов на хостах. Опыт показывает, что именно эти правила часто являются ограничивающим фактором производительности, даже если сеть сама по себе работает быстро. Так происходит по двум причинам. Во-первых, сетевые карты и маршрутизаторы разрабатываются таким образом, чтобы работать со скоростью физического соединения. Это значит, что они могут обрабатывать пакеты с той скоростью, с которой пакеты поступают. Во-вторых, нас интересует производительность, доступная для приложений. А она вычисляется не на основе мощности канала, а исходя из пропускной способности и задержки, которые являются результатом работы сетевого и транспортного уровней.
Уменьшение накладных расходов из-за программного обеспечения улучшает производительность за счет повышения пропускной способности и снижения задержки. Оно также позволяет снизить затраты энергии, необходимые для передачи данных по сети, что актуально для портативных компьютеров. Большинство этих идей известны разработчикам сетей уже много лет. Впервые они были четко сформулированы Могулом (Mogul, 1993), и мы во многом опираемся на его работу. Другой источник по этой теме — книга Меткалфа (Metcalfe, 1993).
Скорость центрального процессора важнее скорости сети
Многолетний опыт показывает, что почти во всех быстрых сетях накладные расходы операционной системы и протокола составляют основное время задержки. Например, теоретически минимальное время удаленного вызова процедур в сети Ethernet мощностью 1 Гбит/с составляет 1 мкс, что соответствует минимальному запросу (64 байта), на который приходит минимальный (64-байтный) ответ. На практике значительным достижением считается даже небольшое снижение накладных расходов программного обеспечения при вызове удаленной процедуры (что происходит редко).
Аналогично при работе с гигабитной сетью основная задача состоит в достаточно быстрой передаче битов из буфера пользователя на линию, а также в том, чтобы получатель смог обработать их с той скоростью, с какой они приходят. Если удвоить производительность процессора и быстродействие памяти, это нередко может дать практически двойную пропускную способность. При этом удвоение пропускной способности линии часто не дает никакого эффекта, если узким местом являются хосты.
Уменьшайте число пакетов, чтобы снизить накладные расходы
Каждый сегмент содержит определенный объем накладных расходов (например, заголовок) и некоторое количество данных (например, пользовательские данные). Оба этих компонента требуют пропускной способности. Также каждому из них требуется обработка (например, обработка заголовка и вычисление контрольной суммы). При отправке 1 млн байт побайтовые затраты времени процессора на обработку не зависят от размера сегмента. Однако при использовании 128-байтных сегментов затраты на обработку заголовков будут в 32 раза выше, чем для сегментов размером 4 Кбайт. И эти затраты растут очень быстро.
Накладные расходы более низких уровней усиливают этот эффект. Прибытие каждого пакета вызывает новое прерывание, если хост работает в активном режиме. В современных конвейерных процессорах каждое прерывание нарушает работу процессорного конвейера, снижает эффективность работы кэша, требует изменения контекста управления памятью, аннулирует таблицу предсказания переходов и требует сохранения в стеке значительного числа регистров процессора. Таким образом, уменьшение количества отправляемых сегментов на n дает снижение числа прерываний и накладных расходов в целом в n раз.
Можно сказать, что компьютер, как и человек, плохо справляется с многозадачностью. Это объясняет стремление отправлять MTU-пакеты максимального размера и обходиться без фрагментации. Алгоритм Нейгла и метод Кларка также являются попытками избежать отправки маленьких пакетов.
Минимизируйте число операций с данными
Самый простой способ реализовать многоуровневый стек протоколов — использовать один модуль для каждого уровня. К сожалению, это приводит к многократному копированию данных (или, по крайней мере, многократному доступу к ним). К примеру, после того как пакет принимается сетевой картой, он обычно копируется в системный буфер ядра. Затем он должен быть обработан на сетевом и на транспортном уровнях. Поэтому он поочередно копируется в буферы этих уровней и, наконец, передается получающему приложению. Часто входящий пакет копируется три или четыре раза, прежде чем содержащийся в нем сегмент доставляется по назначению.
Такое копирование может сильно снизить производительность, поскольку операции с памятью обычно в десятки раз медленнее, чем операции с использованием только внутренних регистров. К примеру, если 20 % инструкций действительно связаны с обращением к памяти (то есть данных нет в кэше), — а это вполне вероятная цифра при обработке входящих пакетов, — среднее время выполнения инструкции снизится в 2,8 раза (0,8 × 1 + 0,2 × 10). Аппаратные улучшения здесь не помогут — проблема в слишком большом числе операций копирования, выполняемых операционной системой.
Продуманная операционная система постарается уменьшить число операций копирования, объединяя процессы обработки на разных уровнях. К примеру, TCP и IP обычно работают вместе («TCP/IP»), поэтому когда обработка переходит от сетевого уровня к транспортному, копировать пользовательские данные пакета не нужно. Еще один популярный прием состоит в том, чтобы выполнять несколько операций на уровне за один проход. Например, контрольные суммы часто вычисляются во время копирования данных (когда это действительно необходимо), и новое значение добавляется в конец.
Минимизируйте число переключений контекста
Схожее правило заключается в том, чтобы по возможности избегать переключений контекста (например, из режима ядра в режим пользователя), поскольку они объединяют в себе негативные свойства прерываний и копирования. Именно во избежание этих затрат транспортные протоколы часто реализуются в ядре. Как и число пакетов, количество переключений контекста можно снизить с помощью библиотечной процедуры, передающей данные во внутренний буфер до тех пор, пока их не наберется достаточное количество. Аналогично на стороне получателя небольшие сегменты следует собирать вместе и передавать пользователю за один раз, минимизируя число переключений контекста.
В идеале входящий пакет вызывает одно переключение контекста из текущего пользовательского процесса в режим ядра, а затем еще одно — при передаче управления принимающему процессу и предоставлении ему прибывших данных. К сожалению, во многих операционных системах происходит еще одно переключение. Например, если диспетчер сети работает в виде отдельного процесса в пространстве пользователя, поступление пакета вызывает передачу управления от процесса пользователя ядру, затем от ядра диспетчеру сети, затем снова ядру и, наконец, от ядра получающему процессу. Эта последовательность переключений контекста показана на илл. 6.51. Все эти переключения для каждого пакета сильно расходуют время центрального процессора и существенно снижают производительность сети.
Илл. 6.51. Четыре переключения контекста для обработки одного пакета в случае, если сетевой менеджер находится в пространстве пользователя
Лучше избегать перегрузки, чем восстанавливаться после нее
Старая пословица, гласящая, что профилактика лучше лечения, справедлива и в деле борьбы с перегрузками в сетях. Когда сеть перегружена, пакеты теряются, пропускная способность растрачивается впустую, увеличивается задержка и т.п. Все эти издержки нежелательны, а процесс восстановления после перегрузки требует времени и терпения. Гораздо более эффективной стратегией является предотвращение перегрузки, напоминающее прививку от болезни, — неприятно, зато избавляет от более крупных неприятностей.
Избегайте тайм-аутов
Таймеры в сетях необходимы, но их следует применять умеренно и стремиться минимизировать количество тайм-аутов. Когда срабатывает таймер, обычно повторяется какое-то действие. Если его повтор необходим, так и следует поступить, однако повторение действия без особой необходимости является расточительным.
Чтобы избегжать излишней работы, следует устанавливать период ожидания с небольшим запасом. Таймер, срабатывающий слишком поздно, несколько увеличивает задержку в случае (маловероятном) потери сегмента. Преждевременно срабатывающий таймер впустую тратит время процессора и пропускную способность, а также напрасно увеличивает нагрузку на, возможно, десятки маршрутизаторов.
6.7.6. Быстрая обработка сегментов
Теперь, когда мы поговорили об общих правилах, рассмотрим несколько методов, позволяющих ускорить обработку сегментов. Дополнительные сведения по этой теме можно найти в следующих источниках: Кларк и др. (Clark et al., 1989); Чейз и др. (Chase et al., 2001).
Накладные расходы по обработке сегментов состоят из двух компонентов: затраты на сегмент и на каждый байт. Нужно действовать сразу на обоих направлениях. Ключ к быстрой обработке сегментов — отделить стандартный, успешный случай (одностороннюю передачу данных) и разобраться с ним. Многие протоколы придают особое значение действиям в нештатной ситуации (например, при потере пакета). Но для быстрой работы протокола разработчик должен пытаться минимизировать время обработки данных в нормальном режиме. Снижение времени обработки при возникновении ошибок является второстепенной задачей.