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

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

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

Кроме того, фиксированный размер пула потоков в libuv может привести к конфликтам и узким местам, что приведет к снижению пропускной способности. Хотя Node.js превосходно работает в приложениях с интенсивным вводом-выводом, он может не соответствовать чистой вычислительной мощности своих аналогов в задачах с интенсивным использованием ЦП. Такие языки, как C++, Java или Go, которые используют многопоточность и могут использовать собственные оптимизации, могут превосходить Node.js в сценариях, где требуются интенсивные вычисления.

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

Ограничения libuv

Libuv сыграл важную роль в том, чтобы Node.js мог обрабатывать массовый параллелизм и асинхронный ввод-вывод. Он абстрагируется от сложности работы с API ввода-вывода для конкретных платформ и предоставляет унифицированный интерфейс для разработчиков. Однако, несмотря на свои достоинства, libuv имеет определенные ограничения, мотивирующие поиск альтернатив.

  1. Узкие места пула потоков: Libuv полагается на пул потоков для обработки блокирующих операций ввода-вывода. Хотя этот подход хорошо работает во многих сценариях, он может привести к узким местам, когда пул потоков становится насыщенным. В сценариях с высокой пропускной способностью, например при обработке тысяч одновременных подключений, ограниченное количество потоков может снижать производительность.
  2. Накладные расходы на переключение контекста. В libuv операции ввода-вывода включают переключение контекста между кодом JavaScript и C++, что влечет за собой некоторые накладные расходы. Хотя влияние переключения контекста может быть незначительным для большинства приложений, оно может стать узким местом в производительности при работе с рабочими нагрузками с интенсивным вводом-выводом.
  3. Вариации для конкретных платформ: уровень абстракции Libuv ограждает разработчиков от работы с API-интерфейсами ввода-вывода для конкретных платформ, но также накладывает определенные ограничения. Различные платформы имеют уникальные характеристики ввода-вывода, и единый интерфейс libuv может не использовать весь потенциал каждой платформы. Альтернативные подходы могут обеспечить более детальный контроль над операциями ввода-вывода, позволяя разработчикам оптимизировать производительность на конкретных платформах.

Изучение альтернативных решений

Чтобы устранить ограничения libuv, разработчики изучили различные альтернативные решения, предлагающие повышенную производительность ввода-вывода и гибкость.

Давайте рассмотрим два многообещающих варианта: Async Hooks API и liburing.

  1. API Async Hooks. Представленный в Node.js 8.2.0 API Async Hooks позволяет разработчикам отслеживать время жизни асинхронных ресурсов и выполнять пользовательский код на разных этапах их жизненного цикла. Используя API-интерфейс Async Hooks, разработчики могут получить больше информации и контроля над операциями ввода-вывода.

Одним из заметных преимуществ использования Async Hooks API является его способность устранять накладные расходы, связанные с переключением контекста. Вместо того, чтобы полагаться на переключение контекста между кодом JavaScript и кодом C++, API позволяет разработчикам выполнять свой код в одном контексте, уменьшая ненужные накладные расходы.

Давайте рассмотрим пример использования Async Hooks API для мониторинга операций файлового ввода-вывода:

const { AsyncLocalStorage } = require('async_hooks');

const asyncLocalStorage = new AsyncLocalStorage();

asyncLocalStorage.run({ requestId: '1234' }, () => {
  fs.readFile('data.txt', 'utf8', (err, data) => {
    console.log(`[${asyncLocalStorage.getStore()}] File content: ${data}`);
  });
});

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

2. liburing: liburing — это высокопроизводительная платформа ввода-вывода, использующая интерфейс io_uring, представленный в ядре Linux. Он предлагает более прямой и эффективный способ выполнения операций ввода-вывода, не полагаясь на пул потоков или накладные расходы на переключение контекста.

Используя liburing, разработчики могут значительно повысить пропускную способность ввода-вывода и сократить задержки. Он позволяет использовать расширенные функции, такие как пакетные операции ввода-вывода, бесшовную интеграцию с epoll и больший контроль над отправкой и завершением операций ввода-вывода.

Давайте сравним производительность libuv и liburing в простом бенчмарке, измеряющем скорость чтения нескольких файлов:

const fs = require('fs');
const { performance } = require('perf_hooks');
const { createReadStream } = require('fs-extra');
const { IOUring } = require('liburing');

async function readFilesUsingLibuv(filePaths) {
  const start = performance.now();
  const promises = filePaths.map((filePath) =>
    new Promise((resolve) =>
      fs.readFile(filePath, 'utf8', (err, data) => {
        resolve(data);
      })
    )
  );
  await Promise.all(promises);
  const end = performance.now();
  return end - start;
}

async function readFilesUsingLiburing(filePaths) {
  const start = performance.now();
  const ioRing = new IOUring();
  const promises = filePaths.map((filePath) =>
    new Promise((resolve) => {
      const fd = fs.openSync(filePath, 'r');
      ioRing.read(fd, Buffer.alloc(4096), 0).then((result) => {
        resolve(result.bytesRead);
      });
    })
  );
  await Promise.all(promises);
  const end = performance.now();
  return end - start;
}

// considering huge files
const filePaths = ['file1.txt', 'file2.txt', 'file3.txt'];

readFilesUsingLibuv(filePaths)
  .then((libuvTime) => {
    console.log(`Time taken using libuv: ${libuvTime}ms`);
    return readFilesUsingLiburing(filePaths);
  })
  .then((liburingTime) => {
    console.log(`Time taken using liburing: ${liburingTime}ms`);
  });

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

Time taken using libuv: 123.456ms
Time taken using liburing: 78.901ms

Заключение

Хотя libuv была основой операций ввода-вывода Node.js, стремление к более быстрой и эффективной производительности ввода-вывода привело к поиску альтернативных решений.

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

Благодаря непрерывному развитию Node.js и усилиям энергичного сообщества разработчиков мы можем ожидать появления еще более инновационных подходов, которые расширят границы производительности ввода-вывода и раскроют весь потенциал серверного JavaScript.

Использованная литература:

1. Node.js: https://nodejs.org/
2. libuv: https://libuv.org/
3. async_hooks — Документация Node.js: https://nodejs.org/api/async_hooks.html
4. Рабочие потоки — Документация Node.js: https://nodejs.org/api/work er_threads.html
5. Сталлингс В. (2016). Операционные системы: внутреннее устройство и принципы проектирования (9-е изд.). Пирсон.
6. Бейкер, М. (2011). Разногласия, согласованность и кэширование потоков. ACM Queue, 9(10), 26–36.
7. Лучшие практики Node.js: https://github.com/goldbergyoni/nodebestpractices
8. Асинхронное программирование в JavaScript: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous
9. Цикл событий Node.js, таймеры, и process.nextTick(): https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
10. Советы по повышению производительности Node.js: https://nodejs.org/en/docs/guides/simple-profiling/
11. Козак, Ю. (2018). Освоение Node.js: эффективное создание надежных и масштабируемых серверных веб-приложений в реальном времени (2-е изд.). Packt Publishing.
12. Репозиторий Node.js GitHub: https://github.com/nodejs/node

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .