Задержка и пропускная способность сети
Для оптимального использования сети связаны три основных вопроса:
- Задержка в сети
- Насыщенность сети
- Влияние на обработку пакетов
В этом разделе описывается задача программирования, требующая использования RPC, а затем проектирует два решения: одно плохо написанное и одно хорошо написанное. Затем оба решения тщательно изучаются, и обсуждается их влияние на производительность сети.
Перед обсуждением этих двух решений в следующих разделах обсуждаются и разъясняется проблемы производительности сети.
Задержка сети
Пропускная способность сети и задержка сети — это отдельные термины. Сети с высокой пропускной способностью не гарантируют низкую задержку. Например, сетевой путь, пересекающий спутниковую связь, часто имеет высокую задержку, даже если пропускная способность очень высока. Нередки случаи, когда сетевой круговой путь, пересекающий спутниковую связь, имеет задержку в пять или более секунд. Такая задержка подразумевает следующее: приложение, предназначенное для отправки запроса, ожидания ответа, отправки другого запроса, ожидания другого ответа и т. д., будет ожидать по крайней мере пять секунд для каждого обмена пакетами, независимо от скорости сервера. Несмотря на растущую скорость компьютеров, спутниковые передачи и сетевые носители основаны на скорости света, которая, как правило, остается постоянной. Таким образом, повышение задержки для существующих спутниковых сетей вряд ли произойдет.
Насыщенность сети
Некоторая насыщенность происходит во многих сетях. Самыми простыми сетями для насыщения являются медленные модемы, такие как стандартные аналоговые модемы 56 кб. Однако соединения Ethernet с несколькими компьютерами в одном сегменте также могут стать насыщенными. То же самое относится и к широкополосным сетям с низкой пропускной способностью или перегружаемой сетью, например маршрутизатором или коммутатором, которые могут обрабатывать ограниченный объем трафика. В таких случаях, если сеть отправляет больше пакетов, чем может обработать самый слабый канал, она удаляет пакеты. Чтобы избежать перегрузки, стек TCP Windows масштабируется при обнаружении удаленных пакетов, что может привести к значительным задержкам.
Последствия обработки пакетов
При разработке программ для сред более высокого уровня, таких как RPC, COM и даже сокеты Windows, разработчики, как правило, забывают, сколько работы выполняется за кулисами для каждого отправленного или полученного пакета. Когда пакет поступает из сети, компьютер обслуживает прерывание из сетевого карта. Затем вызов отложенной процедуры (DPC) помещается в очередь и должен пройти через драйверы. Если используется какая-либо форма безопасности, может потребоваться расшифровать пакет или проверить криптографический хэш. Кроме того, в каждом состоянии необходимо выполнить ряд проверок допустимости. Только после этого пакет поступает в конечное место назначения: код сервера. Отправка большого количества небольших фрагментов данных приводит к дополнительным затратам на обработку пакетов для каждого небольшого блока данных. Отправка одного большого блока данных, как правило, потребляет значительно меньше времени ЦП в системе, хотя стоимость выполнения для многих небольших блоков по сравнению с одним большим блоком может быть одинаковой для серверного приложения.
Пример 1. Плохо спроектированный RPC-сервер
Представьте себе приложение, которое должно получать доступ к удаленным файлам, и задача состоит в том, чтобы разработать интерфейс RPC для управления удаленным файлом. Самым простым решением является зеркало процедуры файлов студии для локальных файлов. Это может привести к обманчиво чистому и знакомому интерфейсу. Ниже приведен сокращенный IDL-файл:
typedef [context_handle] void *remote_file;
... .
interface remote_file
{
remote_file remote_fopen(file_name);
void remote_fclose(remote_file ...);
size_t remote_fread(void *, size_t, size_t, remote_file ...);
size_t remote_fwrite(const void *, size_t, size_t, remote_file ...);
size_t remote_fseek(remote_file ..., long, int);
}
Это кажется достаточно элегантным, но на самом деле, это заслуженный временем рецепт производительности катастрофы. Вопреки распространенному мнению, удаленный вызов процедуры — это не просто вызов локальной процедуры с проводом между вызывающим и вызываемого.
Чтобы увидеть, как этот рецепт обеспечивает производительность, рассмотрим файл размером 2K, в котором 20 байт считываются с начала, а затем 20 байт с конца, и посмотрите, как это выполняется. На стороне клиента выполняются следующие вызовы (многие пути кода опущены для краткости):
rfp = remote_fopen("c:\\sample.txt");
remote_read(...);
remote_fseek(...);
remote_read(...);
remote_fclose(rfp);
Теперь представьте, что сервер отделен от клиента вспомогательной связью с пятисекундным временем кругового пути. Каждый из этих вызовов должен дождаться ответа, прежде чем он сможет продолжить, что означает абсолютный минимум для выполнения этой последовательности в 25 секунд. Учитывая, что мы извлекаем только 40 байт, это ужасно низкая производительность. Клиенты этого приложения будут в ярости.
Теперь представьте, что сеть перегружена, так как емкость маршрутизатора где-то в сетевом пути перегружена. Такая конструкция заставляет маршрутизатор обрабатывать по крайней мере 10 пакетов, если у нас нет безопасности (по одному для каждого запроса и по одному для каждого ответа). Это тоже не хорошо.
Эта конструкция также заставляет сервер получать пять пакетов и отправлять пять пакетов. Опять же, не очень хорошая реализация.
Пример 2. Более совершенный RPC-сервер
Давайте перепроектируем интерфейс, рассмотренный в примере 1, и посмотрим, можно ли улучшить его. Важно отметить, что для того, чтобы этот сервер был действительно хорош, требуется знание шаблона использования для заданных файлов: такие знания не предполагается в данном примере. Таким образом, это лучше спроектированный RPC-сервер, но не оптимально спроектированный RPC-сервер.
Идея в этом примере заключается в том, чтобы свернуть как можно больше удаленных операций в одну операцию. Первая попытка заключается в следующем:
typedef [context_handle] void *remote_file;
typedef struct
{
long position;
int origin;
} remote_seek_instruction;
... .
interface remote_file
{
remote_fread(file_name, void *, size_t, size_t, [in, out] remote_file ..., BOOL CloseWhenDone, remote_seek_instruction *...);
size_t remote_fwrite(file_name, const void *, size_t, size_t, [in, out] remote_file ..., BOOL CloseWhenDone, remote_seek_instruction *...);
}
В этом примере все операции сворачиваются в операции чтения и записи, что позволяет при необходимости открыть одну и ту же операцию, а также необязательный параметр close и seek.
Эта же последовательность операций, написанная в сокращенном виде, выглядит следующим образом:
remote_read("c:\\sample.txt", ..., &rfp, FALSE, NULL);
remote_read(NULL, ..., &rfp, TRUE, seek_to_20_bytes_before_end);
При рассмотрении более эффективно спроектированного RPC-сервера при втором вызове сервер проверяет, что file_name имеет значение NULL, и использует сохраненный открытый файл в rfp. Затем он увидит, что есть инструкции поиска, и разместит указатель файла за 20 байт до конца перед чтением. По завершении он распознает, что флаг CloseWhenDone имеет значение TRUE, а также закроет файл и закроет rfp.
В сети с высокой задержкой выполнение этой версии занимает 10 секунд (в 2,5 раза быстрее) и требует обработки только четырех пакетов; два получает от сервера, а два — с сервера. Дополнительные значения ifs и отмена перемежей, которые выполняет сервер, являются незначительными по сравнению со всеми остальными.
Если причинно-следственный порядок указан правильно, интерфейс можно даже сделать асинхронным, а два вызова можно отправить параллельно. При использовании причинно-следственного упорядочения вызовы по-прежнему отправляются по порядку. Это означает, что в сети с высокой задержкой сохраняется только пятисекундная задержка, даже если количество отправленных и полученных пакетов одинаково.
Мы можем свернуть это еще больше, создав один метод, который принимает массив структур, каждый из которых описывает определенную операцию с файлами. удаленный вариант точечной и сборной ввода-вывода. Подход окупается до тех пор, пока результат каждой операции не требует дальнейшей обработки на клиенте; другими словами, приложение будет считывать 20 байт в конце независимо от того, что первые 20 байт прочитаны.
Однако если для определения следующей операции необходимо выполнить обработку на первых 20 байтах после их считывания, сворачивание всего в одну операцию не работает (по крайней мере, не во всех случаях). Элегантность RPC заключается в том, что приложение может иметь оба метода в интерфейсе и вызывать любой из них в зависимости от потребности.
Как правило, при задействовании сети лучше объединить как можно больше вызовов в один вызов. Если приложение имеет два независимых действия, используйте асинхронные операции и разрешите им выполняться параллельно. По сути, оставьте конвейер полным.