Ресурсоемкий Node.js: часть 2

Используя Kue и Redis, мы разрабатываем и можем фактически бесконечно масштабировать решения для запуска ресурсоемкого кода на Node.js, в частности на веб-сервере.

Это вторая (и последняя) часть серии, начатой с CPU Intensive Node.js: часть 1.

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

Kue - это приоритетная очередь задач, поддерживаемая redis, построенная для node.js.
— Kue Team

Сначала немного о том, что за Redis и почему он, а также что такое Kue и почему он.

  • Кластеризованные процессы Node.js не шарят глобальный скоуп, поэтому в этом случае очередь данных должна быть в другом процессе по-любому
  • Redis является широко используемым, многофункциональным и высоко оптимизированным хранилищем данных в памяти
  • Kue обеспечивает надежную очередь с приоритетми, используя структуру raw/value для Redis.

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

Hello

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

index.js

/* eslint-disable no-console */
const kue = require('kue');
const queue = kue.createQueue();
console.log('INDEX CONNECTED');
const job = queue.create('mytype', {
  title: 'mytitle',
  letter: 'a',
})
  .removeOnComplete(true)
  .save((err) => {
    if (err) {
      console.log('INDEX JOB SAVE FAILED');
      process.exit(0);
      return;
    }
    job.on('complete', (result) => {
      console.log('INDEX JOB COMPLETE');
      console.log(result);
      process.exit(0);
    });
    job.on('failed', (errorMessage) => {
      console.log('INDEX JOB FAILED');
      console.log(errorMessage);
      process.exit(0);
    });
  });

Мы запускаем index.js. На этом этапе мы можем использовать утилиту, предоставленную Kue для контроля очереди. Она показывает одиночное задание, ожидающее обработки.

node_modules/kue/bin/kue-dashboard -p 3050 -r redis://127.0.0.1:6379

Затем мы запустим worker.js. Он подключается к очереди, обрабатывает задание, а затем ждет следующего задания.

worker.js

/* eslint-disable no-console */
const kue = require('kue');
const { sleep } = require('sleep');
const queue = kue.createQueue();
console.log('WORKER CONNECTED');
queue.process('mytype', (job, done) => {
  sleep(5);
  console.log('WORKER JOB COMPLETE');
  switch (job.data.letter) {
    case 'a':
      done(null, 'apple');
      break;
    default:
      done(null, 'unknown');
  }
});

Hello-Web

В этом примере мы рефакторим предыдущий пример с использованием Kue (полный пример).

index.js

/* eslint-disable no-console */
const express = require('express');
const kue = require('kue');
const app = express();
const queue = kue.createQueue();
app.get('/', (req, res) => res.send('Hello World!'));
app.get('/intense', (req, res) => {
  const job = queue.create('mytype', {
    title: 'mytitle',
    letter: 'a',
  })
    .removeOnComplete(true)
    .save((err) => {
      if (err) {
        res.send('error');
        return;
      }
      job.on('complete', (result) => {
        res.send(`Hello Intense ${result}`);
      });
      job.on('failed', () => {
        res.send('error');
      });
    });
});
app.listen(3000, () => console.log('Example app listening on port 3000!’));

Первое наблюдние заключается в том, что если задание не обрабатывается (до завершения или отказа) за две минуты (по умолчанию), Node.js автоматически закрывает response.

Запустив worker.js, как в последнем примере, у нас есть лучшее решение нашей проблемы с ресурсоемким кодом. С одним workerом это решение может обслуживать до 24 одновременных вызовов на эндпоинт, грузящий ЦПУ (120 секунд / 5 секунд) до того, пока вызовы не зафейлятся, но каждый последующий вызов занимает более длительное время. Node.js просто закроет response, который занимает более 120 секунд.

Предложенное решение

  • Не ресурсоемкие эндпоинты работают без прерываний.
  • Решение стабильно (не будет крашить сервер) независимо от количества одновременных вызовов.
  • Однако, высоконагруженные эндпоинты все еще имеют проблемы масштабирования; фактически падают после примерно 6 одновременных подключений (люди не хотят ждать более 30 секунд для получения ответа).

Масштабирование

Мы завершаем эту серию, рассуждая о том, как мы можем увеличить число одновременно обрабатываемых вызовов.

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

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

Есть несколько других взглядов на эту проблему:

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

В случае использования сопоставления числа воркеров с числом процессоров вы можете использовать функцию дочерних процессов Node.js для автоматизации (полный пример). Нам нужно только добавить файл workers.js и запустить его вместо worker.js.

workers.js

const { cpus } = require('os');
const { fork } = require('child_process');
const numWorkers = cpus().length;
for (let i = 0; i < numWorkers; i += 1) {
  fork('./worker');
}

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

Завершая

Несмотря на то, что есть некоторые дополнительные документированные рабочие вопросы, например, обслуживание очереди, для создания более надежного решения, мы продемонстрировали, как можно сравнительно легко масштабировать высоконагруженные сетевые приложения Node.js.

Если вам интересна web-разработка, присоединяйтесь к нашему Telegram!

Оригинал статьи - CPU Intensive Node.js: Part 2