Как стать автором
Обновить
344.16
FirstVDS
Виртуальные серверы в ДЦ в Москве

7 способов улучшить производительность Node.js в масштабе

Время на прочтение 11 мин
Количество просмотров 9.1K
Автор оригинала: Ayooluwa Isaiah

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

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

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

Профилирование и мониторинг вашего приложения


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

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

Выполнение некоторых или всех вышеперечисленных тестов позволит вам получить несколько важных показателей, таких как:

  • время отклика,
  • средняя задержка,
  • частота появления ошибок,
  • запросы в секунду,
  • пропускная способность,
  • потребление ресурсов процессора и памяти,
  • количество одновременных пользователей

и не только.

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

Также важно использовать инструмент мониторинга производительности приложений (APM), чтобы следить за долгосрочной производительностью системы. Различные решения для мониторинга могут позаботиться об этом за вас. Нам нравится AppSignal :).

Его легко интегрировать в ваше приложение (просто выполните npx @appsignal/cli install), и он будет автоматически отслеживать несколько показателей производительности, таких как время отклика и пропускная способность, а также вести журналы ошибок, отслеживать доступность системы, показывать метрики хоста и многое другое. Вы можете использовать полученные данные для принятия упреждающих мер по повышению производительности системы или для быстрого определения первопричины конкретной проблемы, чтобы оперативно устранить её до того, как её заметят ваши пользователи.


Снижение задержки за счёт кэширования


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

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

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

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

Относительно простым способом реализации кэширования в приложении Node.js является in-process кэширование, такое как node-cache. Оно предполагает размещение активно используемых данных в памяти, откуда они могут быть извлечены быстрее. Основная проблема с внутрипроцессным кэшем заключается в том, что он привязан к процессу приложения, поэтому редко подходит для распределённых рабочих процессов (особенно при кэшировании изменяемых объектов). В таких ситуациях можно использовать распределённое решение для кэширования, например, Redis или Memcached. Они работают независимо от приложения и более практичны при масштабировании приложения на несколько серверов.

Используйте тайм-ауты при работе с операциями ввода-вывода


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

Таймаут — это максимальное время ожидания, установленное для запроса. Он показывает, как долго клиент готов ждать ответа от внешней службы. Если ответ не будет получен в течение указанного времени, соединение прервётся, чтобы приложение не зависло на неопределённое время. Многие популярные библиотеки для выполнения HTTP-запросов в Node.js (например, axios) не устанавливают таймаут по умолчанию, что означает, что любой удалённый API может заставить ваше приложение ждать запрошенный ресурс бесконечно долго. Чтобы этого не произошло, необходимо установить таймаут запроса:

const axios = require("axios");
 
axios.defaults.timeout === 1000; // global timeout of 1s

В приведённом выше фрагменте тайм-аут в 1000 мс (1 с) установлен по умолчанию для всех HTTP-запросов, выполняемых через axios. Это гарантирует, что любой запрос не займёт больше этого времени, даже если API не отвечает. Вы также можете установить значение тайм-аута для отдельных запросов, когда глобальное значение по умолчанию не подходит:

axios
  .get("https://example.com/api", { timeout: 2000 })
  .then((response) => {
    console.log(response);
  })
  .catch((error) => {
    console.log(error);
  });

Обратите внимание, что значение timeout axios — это таймаут чтения, который отличается от таймаута соединения. Последний — это время, в течение которого должно быть установлено TCP-соединение, а первый определяет, как долго клиент будет ждать ответа после установления соединения.

Обычно таймаут соединения намного меньше таймаута чтения. Клиент может попробовать другой сервер или альтернативный API, если одна служба слишком долго принимает соединение. При этом у сервера остаётся достаточно времени, чтобы сгенерировать ответ, как только соединение будет принято.

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

Прежде чем выбрать значение таймаута, вы можете проследить за временем ответа API, к которым вы подключаетесь, с помощью специализированных инструментов или путём протоколирования вызовов API. Это позволит вам принять обоснованное решение по длительности таймаута для всех внешних сервисов, с которыми взаимодействует ваша программа. Кроме того, для важных сервисов следует предусмотреть стратегию повторных попыток, чтобы учесть временные замедления. На графике ниже показано, как в AppSignal можно отслеживать среднее время отклика для конечной точки.


Не передавайте статические ассеты с помощью Node.js


Чтобы обеспечить наилучшую производительность ваших Node.js серверов, воздержитесь от их использования для передачи статических ассетов, таких как JavaScript, CSS или файлы образов из вашего приложения. Node.js не был разработан с учётом этого сценария использования, поэтому передача ассетов из основного приложения потребляет много ценных ресурсов и задерживает важные бизнес-вычисления. Переложите задачу передачи статических файлов на веб-сервер, такой как Nginx, который может выполнять все оптимизации, что не имеет смысла выполнять в Node.js. Этот тест показывает, что Nginx примерно в два раза быстрее передаёт статические ассеты, чем Node.js (используя статическое промежуточное ПО Express).

Другой вариант передачи статических файлов — настроить CDN-прокси, например, Amazon CloudFront, для кэширования статического содержимого и передачи его как можно ближе к конечным пользователям. Это освобождает серверы Node.js для обработки только динамических запросов.

Использование кластеризации для повышения пропускной способности



Кластеризация — это техника, используемая для горизонтального масштабирования сервера Node.js на одной машине путём порождения дочерних процессов — «Воркеров» (Рабочие или Workers), которые выполняются одновременно и используют один порт. Это распространённая тактика для сокращения времени простоя, замедлений и сбоев путём распределения входящих соединений между всеми доступными воркерами, чтобы доступные ядра процессора использовались в полной мере. Поскольку экземпляр Node.js работает в один поток, он не может должным образом использовать преимущества многоядерных систем — отсюда и необходимость в кластеризации.

Вы можете кластеризовать свой сервер Node.js с помощью модуля cluster в стандартной библиотеке. Вот пример, взятый из официальной документации:

const cluster = require("cluster");
const http = require("http");
const process = require("process");
const os = require("os");
 
const cpus = os.cpus;
 
const numCPUs = cpus().length;
 
if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);
 
  // Fork workers.
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
 
  cluster.on("exit", (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`);
  });
} else {
  // Workers can share any TCP connection
  // In this case it is an HTTP server
  http
    .createServer((req, res) => {
      res.writeHead(200);
      res.end("hello world\n");
    })
    .listen(8000);
 
  console.log(`Worker ${process.pid} started`);
}

После запуска этой программы соединения, отправленные на порт 8000, будут распределяться между воркерами. Это приведёт к более эффективному управлению запросами в приложении:

$ node server.js
Primary 15990 is running
Worker 15997 started
Worker 15998 started
Worker 16010 started
Worker 16004 started

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

Для более надёжного управления кластерами Node.js лучше использовать менеджер процессов PM2 для Node.js. Он использует под капотом модуль кластера и заботится о создании воркеров, их остановке или перезапуске и распределении входящей нагрузки между ними. Он также предоставляет некоторые инструменты, которые помогут вам контролировать и настраивать производительность воркеров:


Масштабирование нескольких машин с помощью балансировщика нагрузки


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

Использование рабочих потоков для задач, требующих больших ресурсов процессора



Рабочие потоки предоставляют из себя механизм для выполнения задач с интенсивным использованием ЦП в приложении Node.js без блокирования основного цикла событий. Они были введены в Node.js v10.5.0 и стали стабильными только в релизе v12.0.0.

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

Вот как создать рабочий поток с помощью модуля worker_threads из стандартной библиотеки:

// main.js
const { Worker } = require("worker_threads");
 
// Create a new worker
const worker = new Worker("./worker.js");
 
// Listen for messages from worker
worker.on("message", (result) => {
  console.log(
    `The prime numbers between 2 and ${result.input} are: ${result.primes}`
  );
});
 
worker.on("error", (error) => {
  console.log(error);
});
 
worker.on("exit", (exitCode) => {
  console.log(exitCode);
});
 
// Send messages to the worker
worker.postMessage({ input: 100 });
worker.postMessage({ input: 50 });

Когда выполняется main.js, он создаёт новый рабочий поток, полученный из файла worker.js. Метод postMessage() отправляет сообщения воркеру, а для получения ответа от воркера используется слушатель. Файл worker.js показан ниже:

const { parent } = require("worker_threads");
 
parent.on("message", (data) => {
  parent.postMessage({
    input: data.input,
    primes: getPrimes(data.input),
  });
});
 
function getPrimes(max) {
  const sieve = [],
    primes = [];
 
  for (let i = 2; i <= max; ++i) {
    if (!sieve[i]) {
      primes.push(i);
 
      for (let j = i << 1; j <= max; j += i) {
        sieve[j] = true;
      }
    }
  }
 
  return primes;
}

В приведённом выше фрагменте функция getPrimes() используется для нахождения всех простых чисел между 2 и указанным аргументом, который получен от родительского потока через слушатель сообщений (message). Результат также отправляется обратно с помощью метода postMessage(), как и раньше:

The prime numbers between 2 and 100 are: 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97
The prime numbers between 2 and 50 are: 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47

Чтобы узнать больше об использовании воркеров Node.js в своих интересах, прочитайте официальную документацию модуля worker_threads.

Дополнительные советы по улучшению производительности Node.js


Вот несколько микрооптимизаций, которые вы можете сделать в своём приложении Node.js, чтобы гарантированно получить лучшие результаты:

  • Всегда используйте последнюю версию Node.js для достижения наилучшей производительности.
  • Обращайте внимание на зависимые компоненты и по возможности выбирайте наиболее производительные библиотеки. Иногда лучше отказаться от добавления зависимостей и вместо этого написать код для выполнения задачи самостоятельно.
  • Убедитесь, что все независимые операции ввода-вывода используют асинхронные примитивы, такие как обратные вызовы, обещания и async/await, чтобы обеспечить неблокирующий поток операций и улучшить задержку на последующих этапах.
  • Не обязательно оптимизировать всё. Как только «горячие точки» вашего приложения будут хорошо оптимизированы, следует остановиться.
  • Ваши «горячие точки» могут меняться со временем, поэтому обязательно используйте какую-либо форму наблюдения или решение для мониторинга, чтобы отслеживать эти изменения.
  • При работе с большими массивами данных используйте потоки Node.js для оптимальной эффективности использования памяти и снижения задержек.
  • Чтобы снизить нагрузку на сборщик мусора (тем самым уменьшая задержки), избегайте распределения памяти в «горячих точках».
  • Оптимизируйте запросы к базе данных и масштабируйте их соответствующим образом, чтобы они не стали узким звеном.
  • Не меняйте производительность на надёжность. Постарайтесь найти баланс между оптимизацией кода для повышения производительности, стоимостью разработки и дальнейшим обслуживанием.

Подведём итоги


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

Если у вас есть дополнительные советы по оптимизации производительности в Node.js, которые не были рассмотрены в этой статье, вы можете поделиться ими с автором статьи здесь.

Спасибо, что прочитали, и счастливого кодинга!


НЛО прилетело и оставило здесь промокод для читателей нашего блога:

15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.
Теги:
Хабы:
+13
Комментарии 3
Комментарии Комментарии 3

Публикации

Информация

Сайт
firstvds.ru
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия
Представитель
FirstJohn