Высокопроизводительное сетевое программирование: Часть 1. Максимально эффективное использование сетевых ресурсов

Если у вас есть опыт программирования под UNIX®, то вас, возможно, уже занимал вопрос увеличения производительности сети. Статья рассказывает о некоторых полезных методах и приемах, позволяющих серьезно улучшить производительность и выжать максимум возможного из пропускной способности вашей сети.

Гириш Венкатачалам, консультант и популяризатор технологий Open Source, консультант

Photo of Girish VenkatachalamГириш Венкатачалам занимается UNIX программированием более десяти лет. Он разработал протокол IPSec для Nucleus - ОС для встроенных систем. Область его интересов включает в себя криптографию, мультимедиа, сети и встроенные системы. Он любит плавать, ездить на велосипеде, заниматься йогой и просто одержим фитнесом. С ним можно связаться по электронной почте girish1729@gmail.com.



16.12.2009

Введение

Наверное, каждый имеющий опыт программирования UNIX®-систем когда-нибудь задавался вопросом увеличения производительности работы с сетью и в некоторых случаях с дисковым вводом/выводом. В этой статье обсуждаются некоторые продвинутые приемы программирования, применяемые при реализации протоколов для оптимального использования пропускной способности системы. (Эта статья не касается вопросов тонкой настройки параметров ОС, конфигурирования ядра и настройки системы в целом).

Хотя пропускная способность любого протокола ограничена законом Шеннона и другими факторами, такими как модель использования сети, чаще всего причиной неоптимального использования сетевых ресурсов является некачественный или наивный код.

Улучшение производительности в равной степени является как наукой, так и искусством. Для получения наилучшей результирующей скорости передачи данных приходится использовать различные инструменты измерения производительности, выявлять узкие места и либо устранять, либо минимизировать их влияние. С помощью простых и прямолинейных научных метолов можно быстро получить огромный прирост производительности.

Конвейерная обработка и постоянные соединения

Конвейерная обработка (pipelining) - это хорошо известная концепция, применяемая в процессорах для уменьшения задержек в циклах выборка-декодирование-выполнение. Выборка каждой инструкции сопряжена с определенной задержкой, которой можно избежать, если выбирать инструкции заранее, сохраняя их для последующего выполнения. Как вы сейчас увидите, эта же концепция применяется и в сетях.

Серверу можно заранее сообщать, чем будет интересоваться клиент после обработки текущего запроса. Сервер будет поддерживать очередь поступивших запросов и выполнять их один за другим вместо того, чтобы выполнять текущий запрос, затем считывать из сети следующий запрос и т.д. Эта методика как никакая другая увеличивает скорость отклика интерактивных приложений и уменьшает задержки.

Но реализовать такую методику не всегда возможно. А когда она работает, иногда происходит переполнение очереди сервера. Правда, такие случаи редки и практически всегда конвейерная обработка работает очень хорошо. Все распространенные протоколы, включая HTTP 1.1, NNTP, X11 и т.д., используют конвейерную обработку в той или иной форме.

Постоянные соединения протокола управления передачей данных (TCP)

Очевидно, что описанный выше метод не будет работать, если вы для каждого запроса или транзакции будете создавать отдельное TCP соединение. Это не единственная причина для повторного использования существующего соединения. Процедура рукопожатия при установлении или завершении TCP соединения - это очень большие издержки, которых по возможности лучше избегать.

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

Рисунок 1. Конвейерная и обычная обработка
Конвейерная и обычная обработка

Как это перенести в программирование? Использовать существующие TCP соединения просто, но реализовать конвейерную обработку - нетривиально. Архитектура протокола должна позволять отслеживать находящиеся в очереди запросы и идентифицировать запрос, которому соответствует каждый конкретный ответ.

В следующем разделе мы рассмотрим другие механизмы, которые могут помочь нам в этом стремлении.

Неблокирующий ввод/вывод, select(2), и poll(2)

Вы, вероятно, знакомы с двумя моделями программирования: синхронной и асинхронной обработкой. Описанная выше методика конвейерной обработки - это пример использования асинхронной обработки для улучшения производительности. Синхронное программирование приводит к простой архитектуре, более простому коду и, зачастую, к плохой производительности, что неприемлемо. Чтобы обойти эту проблему, приходится применять другие приемы, такие как неблокирующий ввод/вывод.

Блокирующие и неблокирующие сокеты можно грубо сравнить с синхронной и асинхронной обработкой, но находятся они не на сетевом уровне, а на уровне операционной системы. В типичных вызовах к блокирующему сокету, таких как socket, write(2) или send(2), пользовательский процесс ждет завершения системного вызова. Ядро берет на себя заботу о переводе процесса в спящее состояние, ожидании готовности сокета к записи, чтении кода состояния TCP и так далее. И, в конце концов, управление возвращается приложению.

В случае неблокирующего сокета программист сам должен удостовериться в том, что сокет готов для записи и что все данные записываются правильно. Очевидно, это ведет к определенным неудобствам при программировании и необходимости изучения новой концепции, но, овладев ею, вы откроете для себя мощный инструмент достижения хорошей производительности любого сетевого кода.

Недостаточно просто использовать read(2) и write(2), или recv(2) и send(2), в момент, когда сокет разблокирован. Необходимо с помощью дополнительных системных вызовов, таких как poll(2) или select(2), определять, можно ли сейчас записывать данные в сокет или считывать их из сети.

Одна из возможностей - это использование poll(2) для определения готовности к записи (так как select(2) не может этого делать) и использование select(2) для определения того, что в сокет поступили данные (с другой стороны). В листинге 1 приведен пример, подробно иллюстрирующий работу с неблокирующим вводом/выводом.

Листинг 1. Пример неблокирующего ввода/вывода
/******************************************
 * Чтение из неблокирующего сокета        * 
 *   с помощью poll(2)                    *
 *****************************************/
void
poll_wait(int fd, int events)
{
    int n;
    struct pollfd pollfds[1];
    memset((char *) &pollfds, 0, sizeof(pollfds));

    pollfds[0].fd = fd;
    pollfds[0].events = events;

    n = poll(pollfds, 1, -1);
    if (n < 0) {
	perror("poll()");
	errx(1, "Poll failed");
    }
}

size_t
readall(int sock, char *buf, size_t n) {
	size_t pos = 0;
	ssize_t res;

	while (n > pos) {
		res = read (sock, buf + pos, n - pos);
		switch ((int)res) {
			case -1:
				if (errno == EINTR || errno == EAGAIN)
					continue;
				return 0;
			case 0:
				errno = EPIPE;
				return pos;
			default:
				pos += (size_t)res;
		}
	}
	return (pos);
}

size_t
readmore(int sock, char *buf, size_t n) {

	fd_set rfds;
	int ret, bytes;



	poll_wait(sock,POLLERR | POLLIN ); 
	bytes = readall(sock, buf, n);

	if (0 == bytes) {
		perror("Connection closed");
		errx(1, "Readmore Connection closure");
		/* NOT REACHED */
	}

	return bytes;
}

/******************************************
 * Запись в неблокирующий сокет           * 
 *   с помощью poll(2)                    *
 *****************************************/


void
poll_wait(int fd, int events)
{
    int n;
    struct pollfd pollfds[1];
    memset((char *) &pollfds, 0, sizeof(pollfds));

    pollfds[0].fd = fd;
    pollfds[0].events = events;

    n = poll(pollfds, 1, -1);
    if (n < 0) {
	perror("poll()");
	errx(1, "Poll problem");
    }
}


size_t
writenw(int fd, char *buf, size_t n)
{
	size_t pos = 0;
	ssize_t res;
	while (n > pos) {
		poll_wait(fd, POLLOUT | POLLERR);
		res = write (fd, buf + pos, n - pos);
		switch ((int)res) {
			case -1:
				if (errno == EINTR || errno == EAGAIN)
					continue;
				return 0;
			case 0:
				errno = EPIPE;
				return pos;
			default:
				pos += (size_t)res;
		}
	}
	return (pos);

}

Фрагментация пакетов IP и другие случайные сетевые факторы

Вызов Sendfile(2) помогает избежать издержек, связанных с копированием буфера, за счет непосредственной двоичной передачи данных из файловой системы в сеть. Однако, к сожалению, этот системный вызов имеет проблемы с переносимостью на современные системы UNIX и вообще недоступен на OpenBSD. Для обеспечения переносимости избегайте его прямого использования.

Sendfile(2) может уменьшать задержки благодаря избыточному вызову memcpy(2). Этот прием можно использовать наряду наряду с неблокирующим вводом/выводом для улучшения производительности сетевого кода на уровне операционной системы.

Однако на производительность могут влиять и факторы, находящиеся глубоко на сетевом уровне. Возможно, вы слышали об IP-фрагментации. Фрагментация и повторная сборка пакетов - это дорогостоящая операция, которая, несмотря на то, что выполняется только промежуточными маршрутизаторами, как правило, заметно ухудшает производительность.

Существует методика определения максимального размера передаваемого по маршруту блока данных (Path Maximum Transfer Unit или PMTU), избавляющая от необходимости фрагментации IP-пакетов. Используя эту методику, вы как минимум можете узнать (или, скорее, оценить), какого размера TCP-сегменты будут скорее всего доставлены без фрагментации на уровне IP. К счастью, TCP-уровень операционной системы сам заботится о разбиении данных на сегменты подходящим образом, чтобы избежать IP-фрагментации. Для TCP, представляющего собой поток байтов без границ сообщений, этот подход работает отлично. Но будьте внимательными с протоколом UDP (User Datagram Protocol) - для него это не работает.

Также нужно проследить, чтобы в вашей сети не злоупотребляли бесполезными и вредоносными пакетами (изолируйте Windoze-машины в отдельную виртуальную локальную сеть). В мире UNIX полезными инструментами для этого являются tcpdump, iftop и средства мониторинга пропускной способности, такие как wmnet.

Заключение

В этой статье было представлено несколько идей по оптимизации использования пропускной способности сети. Одновременное применение различных инструментов позволяет улучшить производительность сети.

Не пропустите вторую статью этой серии, в которой будут представлены другие способы оптимизации использования сети.

Ресурсы

Научиться

Получить продукты и технологии

  • Разработайте ваш следующий проект с помощью ознакомительного ПО от IBM, которое можно загрузить непосредственно с сайта developerWorks.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=457027
ArticleTitle=Высокопроизводительное сетевое программирование: Часть 1. Максимально эффективное использование сетевых ресурсов
publish-date=12162009