Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Современные технологии разработки распределенных вычислительных систем. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
15.08.2026
Размер:
1 Мб
Скачать

Порождение нескольких дочерних процессов производится с помощью метода fork(), реализация которого в модуле Cluster отличается от системного вызова fork в Unix-подобных ОС, хотя использует сходную семантику: в

родительском и в дочернем процессе node исполняется одна и та же JavaScript-

программа, но в родительском процессе истинно свойство cluster.isMaster, а

в дочернем – свойство cluster.isWorker. Таким образом программа может разделять две ветви алгоритма: главную, управляющую (Master) и рабочую,

несущую вычислительную нагрузку (Worker).

Количество порождаемых рабочих процессов обычно соответствует числу процессоров в мультипроцессорной системе. NodeJS предоставляет модуль OS,

у которого предусмотрен метод cpus(), возвращающий информацию о доступных процессорах.

Модуль Cluster открывает канал межпроцессного взаимодействия (IPC)

между родительским и дочерним процессами. Дочерний процесс может посылать сообщения родительскому процессу с помощью метода process.send(). В качестве аргумента принимается объект JavaScript.

Родительский процесс может получать эти сообщения, установив обработчик событий ‘message’: cluster.on(‘message’, <handler>).

Явное завершение процесса осуществляется с помощью метода process.exit(). Оповещения о завершении дочерних процессов родительский процесс может получать, установив обработчик событий ‘exit’: cluster.on(‘exit’, <handler>).

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

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

21

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

Начало файла “node-cluster.js” const cluster = require('cluster'); const http = require('http');

const os = require('os'); const cpus = os.cpus(); const cpu = cpus[0];

const numCPUs = cpus.length; const pid = process.pid;

const N = 100000000; // 100 millions if(cluster.isMaster)

{

console.log(`Master ${pid} on ${cpu.model}, ${cpu.speed} MHz, ${numCPUs} logical CPUs`);

for (var i = 0; i < numCPUs; ++i)

{

cluster.fork();

}

const startTime = Date.now(); cluster.on('exit', (worker, code, signal) => {

console.log(`worker ${worker.process.pid} exited.`);

});

var answersCount = 0; var answers = {};

cluster.on('message', (worker, message, handle) => { const id = worker.process.pid; console.log(`${id} : ${message}`);

answers[id] = message; answersCount++; if(answersCount === numCPUs)

{

var average = 0;

22

for(key in answers){average += answers[key];} average /= numCPUs;

const runTime =

Math.round((Date.now() - startTime) / 10) / 100; const gigaPoints =

Math.round(N * numCPUs / runTime / 1e6) / 1e3; console.log(`Monte-Carlo Pi = ${average} (run time: ${runTime} s, ${gigaPoints} Gpts/s)`);

} // end if(answersCount === numCPUs) }); // end cluster.on('message')

}

else // if(cluster.isWorker)

{

console.log(`Slave ${pid} running.`); process.send(calcMonteCarloPi(N)); process.exit();

}

Конец файла “node-cluster.js”

3.4. Использование сетевых хранилищ типа “ключ-значение” для организации обмена данными в распределенной среде

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

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

23

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

некоторое значение (value), возможно, нулевое (null) или неопределенное

(undefined).

Хранилище, организованное по такому принципу, обычно называется KV-

хранилищем (Key-Value Storage). Для единообразного доступа к данным распределенная программа может использовать два KV-хранилища – локальное, размещаемое в оперативной памяти собственного узла, и

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

разработки.

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

Эффективное KV-хранилище должно предоставлять встроенные средства для

работы с очередями в распределенной среде.

Одним из распространенных и перспективных сетевых KV-хранилищ с развитыми функциональными возможностями является система Redis (redis.io, [2]). Redis – это сетевой словарь (dictionary), поддерживающий хранение разных типов данных, в том числе, списков (очередей). Все данные Redis размещает в

оперативной памяти, что обеспечивает исключительно высокую

24

производительность. Разработаны клиентские компоненты Redis для различных систем программирования, включая NodeJS (см. redis.io/clients).

С точки зрения прикладного программиста, сервер Redis принимает и выполняет переданные клиентами команды по управлению данными, такие как get(key) – получить значение по ключу, set(key, value) – записать значение по ключу, del(key) – удалить именованную ячейку, и многие другие

(см. полный список по ссылке redis.io/commands). Важной функцией Redis

является поддержка блокирующих операций извлечения из списков (команды blpop, brpop и др.). Эти операции приводят к автоматическому блокированию клиентского потока в тех случаях, когда указанный в командах список пуст, и

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

while(true)

{

task = redis.blpop(tasksQueue);

result = process(task);

redis.rpush(resultsQueue, result);

}

Здесь команда blpop пытается извлечь элемент из головы очереди заданий.

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

На основе NodeJS и Redis можно простыми средствами создавать распределенные системы, в которых мастер-узел помещает задания в очередь

25

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

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

Redis можно установить на одном из компьютеров локальной сети, в

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

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

Одним из облачных провайдеров Redis является компания Redislabs

(redislabs.com). В настоящее время на уровне бесплатного обслуживания этот провайдер предоставляет хранилище объемом до 30 Мб, за $5/месяц можно получить до 100 Мб (оперативная память традиционно является дорогостоящим ресурсом; см. redislabs.com/pricing/redis-cloud). Бесплатный объем в 30 Мб может быть достаточен для концептуального тестирования распределенных систем, разрабатываемых в учебных и ознакомительных целях. Следует иметь в виду, что в рассматриваемых вариантах использования хранилище Redis

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

(в настоящее время на уровне бесплатного обслуживания – 30, за $5/месяц – до 256).

26

Далее рассмотрим распределенную систему, предназначенную для вычисления оценки числа π методом Монте-Карло. Программный код системы будет состоять из двух компонентов: программа управления мастер-узлом

(“dsmaster.js”) и программа управления узлом-исполнителем (“dsnode.js”).

Для программы узла-исполнителя будем использовать модифицированный код из предыдущего примера. Модификация будет заключаться в подключении дочерних процессов к серверу Redis. Заметим, что мастер-часть кластера

NodeJS теперь не будет заниматься агрегацией результатов (эта задача возлагается на отдельный мастер-узел распределенной системы). Единственная роль мастер-части кластера NodeJS будет состоять в порождении дочерних потоков.

Начало файла “dsnode.js”

//run this script with <unique token> to start node for <unique token> processing

const token = process.argv[2] ? process.argv[2] : 'PI'; const cluster = require('cluster');

const cpus = require('os').cpus(); const cpu = cpus[0];

const numCPUs = cpus.length; const pid = process.pid;

//use your own credentials for Redis server access:

const redisHost = 'xxxx.redislabs.com'; const redisPort = '12345';

const redisConfig = {host: redisHost, port: redisPort}; const jobQueueKey = 'job#' + token;

const resultsQueueKey = 'results#' + token;

if(cluster.isMaster){runManager();} else {runWorker();}

27

function runManager()

{

console.log(`Active node ${pid} on ${cpu.model}, ${cpu.speed} MHz, ${numCPUs} logical CPUs for \'${token}\' processing`); for (var i = 0; i < numCPUs; ++i)

{ cluster.fork(); }

cluster.on('online', (worker) =>

{

console.log(`worker ${worker.process.pid} is running.`);

});

cluster.on('exit', (worker) =>

{

console.log(`worker ${worker.process.pid} exited; forking new one.`);

cluster.fork();

});

cluster.on('message', (worker, message) =>

{

if(message.error)

{

console.log(`worker ${worker.process.pid} error: ${message.error}`);

}

else if(message.jobResult)

{

const result = message.jobResult; console.log(`job result: id=${result.id}, value=${result.value}`);

}

}); // end cluster.on('message') } // end runManager

function runWorker()

{

28

const redisClient = require('redis').createClient(redisConfig);

redisClient.on('error', (err) =>

{

process.send({error: err});

});

function doWork(jobItem)

{

const N = jobItem.count; return {

id: jobItem.id,

value: {count: N, estimation: calcMonteCarloPi(N)}

};

}

function getJob(err, res)

{

const job = JSON.parse(res[1]); const result = doWork(job); process.send

({

jobResult:

{id: result.id, value: result.value.estimation}

});

redisClient.lpush

(

resultsQueueKey,

JSON.stringify(result)

);

redisClient.brpop(jobQueueKey, 0, getJob);

}

redisClient.brpop(jobQueueKey, 0, getJob); } // end runManager

Конец файла “dsnode.js”

29

Начало файла “dsmaster.js”

//run this script with <unique token> to start Master for <unique token> processing

//use second argument to specify work items count

const token = process.argv[2] ? process.argv[2] : 'PI'; const workItemsCount =

process.argv[3] ? Number.parseInt(process.argv[3]) : 4;

// use your own credentials for Redis server access: const redisHost = 'xxxx.redislabs.com';

const redisPort = '12345';

const redisConfig = {host: redisHost, port: redisPort}; const jobQueueKey = 'job#' + token;

const resultsQueueKey = 'results#' + token; const N = 100000000; // 100 millions

const redisClient = require('redis').createClient(redisConfig); redisClient.on('error', console.log);

console.log(`Master for ${workItemsCount} \'${token}\' items processing`);

pushWorkItems(workItemsCount); const startTime = Date.now(); var results = []; gatherResults((result) =>

{

results.push(result); console.log(`result #${result.id}: ${result.value.estimation}`); if(results.length === workItemsCount)

{

const runTime =

Math.round((Date.now() - startTime) / 10) / 100; console.log(`All ${workItemsCount} items done`); showStats(results, runTime);

return false;

30

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]