Содержание


Высокопроизводительное сетевое программирование

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

Comments

Серия контента:

Этот контент является частью # из серии # статей: Высокопроизводительное сетевое программирование

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Высокопроизводительное сетевое программирование

Следите за выходом новых статей этой серии.

Наверное, каждый имеющий опыт программирования 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.

Заключение

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

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

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