Жажда скорости: GRID-вычисления

Окончание, начало в №24

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

Компоненты системы

Какой бы ни была система GRID-вычислений, в нее обязательно будет вовлечено несколько разнородных технологий, каждая из которых обеспечивает тот или иной компонент программы, так что сперва определим основные компоненты системы, а затем дадим им более подробные комментарии:
1. Способ описания задач. Хотя известные системы распределенных вычислений работают с исполняемыми файлами, для упрощения задачи в примере будут использованы динамически загружаемые библиотеки. Также, чтобы не концентрировать внимание на поддержке нескольких операционных систем, отдадим честь Linux как системе, которая обслуживает многие суперкомпьютеры, и выберем ее для наших экспериментов;
2. Ресурсы. Реализация совместных для вычислительных устройств (компьютеров) ресурсов разнится от системы к системе. Некоторые используют для этого базу данных, некоторые — открытые по NFS файлы. Можно воспользоваться именованными (в рамках вычислительной системы) ресурсами, что мы и сделаем;
3. Коммуникация между вычислительными устройствами. Поскольку в данной статье мы не рассматриваем специализированные библиотеки, обходясь стандартными библиотеками, в качестве средства коммуникации между компьютерами используется интерфейс под названием "сокет";
4. Запуск новых задач. Поскольку серверное приложение будет представлять собой демон, отдельно потребуется средство взаимодействия сервера с пользователем. Воспользуемся для этого технологией межпроцессного взаимодействия под названием System-V IPC.

Обмен сообщениями

Ввиду того, что серверу и клиентам постоянно нужно обмениваться между собой сообщениями, следует рассмотреть вопрос проектирования передачи данных по сети. Существуют задачи, для которых потребуется фиксированная длина сообщения (например, сообщение о присоединении клиента, которое будет включать идентификатор сообщения и некоторую служебную информацию, размер которой известен заранее). Однако некоторые сообщения могут нести в себе информацию, размер которой может изменяться от задачи к задаче: например, пересылка файла библиотеки клиенту, тогда как заранее размер файла не известен. Таким образом, информация, которая является общей для всех сообщений — это идентификатор сообщения и длина передаваемых с сообщением данных для того, чтобы сообщение можно было считать полностью. Из этих двух параметров можно сформировать заголовок сообщения.

Очевидно, что если сообщения с фиксированной длиной можно организовать в структуры, чтобы можно было далее с помощью одного вызова функции передачи данных отослать на другой компьютер, то с сообщениями с не определенной на этапе проектирования программы длиной так сделать нельзя. Потому можно выделить два варианта передачи таких "составных" сообщений: 1) передача сообщения по частям: сначала передается само сообщение и размер данных, а после этого передаются сами данные. В этом случае придется использовать средства синхронизации, чтобы избежать ситуации смешивания фрагментов разных сообщений, если одновременно два или более потоков передают сообщения в один и тот же сокет; 2) формирование участка памяти, который содержит сообщение вместе с сопутствующими данными. Это избавит от необходимости синхронизировать потоки при необходимости доступа к сокету, но может быть несколько расточительно по отношению к памяти.

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

Чтение сообщения получателем сопряжено с той проблемой, что получатель не знает, какое сообщение должно прийти в данный момент и,
соответственно, не знает, какой размер оно будет иметь. Здесь придет на выручку заголовок сообщения: он имеет фиксированный размер, так что его можно считать, а затем, в зависимости от идентификатора сообщения и/или объявленного размера данных, считать остаток сообщения, после чего перейти к ожиданию нового сообщения.

Поскольку сообщения задаются отправителем преимущественно с помощью структур, существует также вероятность, что правильно составленная и отправленная структура может считаться неправильно из-за различия выравнивания полей структуры у отправителя и получателя. Для того чтобы избежать такой ошибки, в C++ применяется директива компилятора #pragma pack. В данном случае нужно выравнивание по одному байту, так что директива будет иметь вид #pragma pack(1). Объявляться она должна перед определением структур. Рекомендуется после этого вернуться к прежнему выравниванию с помощью директивы #pragma pack(4).

Сам обмен сообщениями реализовывается с помощью сокетов, для чего в исходном коде нужно присоединить файл "sys/socket.h". Технология работы с сокетами позволяет серверу зарезервировать для соединения порт, а клиентам — обратиться к заданному IP адресу и порту. Прослушивая сокет, можно получать сообщения от только что запустившегося на другом компьютере в сети клиента. Сокеты создаются с помощью функции socket, с помощью функции bind сокет присоединяется к определенному порту и IP-адресу; пара порт-IP описывается в структуре sockaddr_in, причем преобразовать IP- адрес из текстового формата в число, которое будет соответствовать данному адресу и которое можно будет указать в структуре sockaddr_in, можно с помощью функции inet_aton. Для того чтобы начать прослушивание сокета, используется функция listen, а непосредственно за прием данных серверным приложением отвечает функция accept. Сообщения читаются с помощью функции recv. В качестве его параметров указывается сокет, указатель на область памяти, куда будут считываться данные, размер считываемых данных и флаги. Хочется обратить особое внимание на флаг MSG_WAITALL, который предписывает дождаться весь объем данных и только после этого завершать чтение сокета. Если не использовать его, то сообщение может быть передано не полностью.

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

Ресурсы

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

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

Любая система распределенных вычислений выполняет также задачу по синхронизации ресурсов. Может сложиться такая ситуация, когда двум клиентам понадобится один и тот же ресурс одновременно. Поскольку копии ресурсов на клиентских компьютерах независимы, один из клиентов начнет выполнять задачу относительно имеющихся в ресурсе данных. Второй клиент тоже начнет выполнять задачу относительно старых данных, вследствие чего в лучшем случае работа будет выполнена не эффективно, а в худшем — ошибочно. Потому каждый клиент должен иметь возможность зарезервировать ресурс на время проведения какой-то операции. Если второй клиент попытается зарезервировать ресурс тогда, когда он уже используется, он будет вынужден ждать освобождения ресурса. Такого рода синхронизация выполняется на сервере.

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

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

Задачи

Все, ради чего нужна система распределенных вычислений, — это выполнение задач. Задача включает в себя непосредственно выполняемый код и ресурсы, принадлежащие этой задаче. Поскольку система должна быть способна обработать несколько задач одновременно, каждой задаче удобно присваивать уникальный идентификатор. Поскольку в разных задачах могут встречаться ресурсы с одинаковым именем, ресурсы также должны быть привязаны к какому-то идентификатору задач.

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

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

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

Загрузка задач

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

Система взаимодействия обернута в достаточно удобный для программиста набор функций, который позволяет простейшим образом отсылать сообщения отправителем, а получателю предоставляет возможность ожидания сообщений. Для того чтобы иметь возможность работать с System V IPC, нужно присоединить файл "sys/ipc.h". Создается канал обмена данными с помощью функции ftok и msgget, последняя из которых возвращает идентификатор канала. Отправка и получение сообщений осуществляется соответственно с помощью функций msgsnd и msgrcv. Удаляется канал с помощью функции управления msgctl с аргументом IPC_RMID. Удалять канал должен только получатель.

Ввиду того, что нет предела совершенству, предлагаю еще несколько интересных ссылок по теме распределенных вычислений.
. сайт (MPICH2) и сайт (OpenMPI) – два сайта, на которых размещены реализации стандарта Message Passing Interface, который используется при проектировании систем распределенных вычислений. В отличие от OpenMPI, есть версия MPICH для Windows.
. сайт – сайт проекта Globus Toolkit, который реализует в себе множество стандартов для GRID-вычислений
. сайт – сайт о GRID-вычислениях. Давно не обновляется, хотя на нем размещено много интересной информации по данной теме.
. сайт – сайт о параллельных вычислениях в целом. Содержит массу полезной документации по стандартам, включая MPI.

Влад Маслаков dreamer.mas@gmail.com


Компьютерная газета. Статья была опубликована в номере 26 за 2009 год в рубрике программирование

©1997-2022 Компьютерная газета