Современные технологии разработки распределенных вычислительных систем. Учебное пособие
.pdfПорождение нескольких дочерних процессов производится с помощью метода 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
