Полная разработка

Ускорение работы Node.js с помощью цикла событий: повышение производительности на примере

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

Введение

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

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

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

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

Почему однопоточный?

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

Асинхронное программирование и неблокирующий ввод-вывод

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

Цикл событий в Node.js

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

Стек вызовов и очередь событий

Стек вызовов — это структура данных, которая записывает, где в программе мы находимся, и если мы входим в функцию, мы помещаем что-то в стек, а когда мы возвращаемся из функции, мы выталкиваем вершину стека.

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

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

Механизм цикла событий

Цикл событий имеет одну простую задачу — отслеживать стек вызовов и очередь событий, поэтому, если стек вызовов пуст, он берет первое событие из очереди и помещает его в стек вызовов, который эффективно его запускает.

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

Таймеры и Process.nextTick()

В цикле событий приоритет операций определяется их типом: таймеры (setTimeout и setInterval) и process.nextTick() имеют особые роли.

process.nextTick() операции запускаются до того, как будет запущено любое другое событие ввода-вывода, а таймеры будут запускаться, как только они могут быть запланированы.

Эту функцию можно использовать для изменения поведения вашей программы Node.js, но она также может вызвать проблему, известную как «голодание»; например, если процесс постоянно добавляет обратные вызовы nextTick, операции ввода-вывода могут никогда не выполняться, поскольку они постоянно теряют приоритет.

Оптимизация производительности

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

  • Избегайте синхронного кода. Поскольку Node.js использует один поток, синхронный код может заблокировать все приложение, а затем использовать асинхронный код, чтобы поддерживать работу цикла событий.
  • Не ограничивайте операции ввода-вывода. Как уже упоминалось, слишком много process.nextTick() обратных вызовов или таймеров могут привести к голоданию операций ввода-вывода, поэтому лучше сбалансировать операции, чтобы этого не произошло.
  • Разгрузка тяжелых вычислений. Если у вас есть задачи, интенсивно использующие ЦП, подумайте о том, чтобы разгрузить их в отдельную службу или рабочие потоки, чтобы не блокировать основной поток.
  • Используйте инструменты профилирования. Такие инструменты, как встроенный профилировщик Node.js или Chrome DevTools, могут помочь вам диагностировать проблемы с производительностью, связанные с циклом событий.

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

Узнать больше:





Практический пример: оптимизация веб-сервера Node.js

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

Исходный код

const express = require('express');
const sharp = require('sharp');
const app = express();

app.post('/upload', (req, res) => {
    let image = req.body.image;
    sharp(image)
        .resize(300, 300)
        .toBuffer()
        .then(data => {
            // save the image data into a database
            db.save(data);
            res.status(200).send("Image processed and saved!");
        })
        .catch(err => {
            console.error(err);
            res.status(500).send("An error occurred");
        });
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

В приведенном выше коде у нас есть очень простой сервер, использующий модуль sharp для обработки изображений, который прослушивает загрузку изображений в конечной точке «/upload». Хотя на первый взгляд этот код выглядит нормально, когда сервер начинает получать много запросов, вы можете начать замечать некоторые проблемы с производительностью, поскольку обработка изображений является задачей с интенсивным использованием ЦП и может заблокировать цикл обработки событий, если не обрабатывается должным образом.

Оптимизация производительности

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

const express = require('express');
const { Worker } = require('worker_threads');
const app = express();

app.post('/upload', (req, res) => {
    let image = req.body.image;

    const worker = new Worker('./imageProcessor.js', {
        workerData: image
    });

    worker.on('message', data => {
        // save the image data into a database
        db.save(data);
        res.status(200).send("Image processed and saved!");
    });

    worker.on('error', err => {
        console.error(err);
        res.status(500).send("An error occurred");
    });
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

В imageProcessor.js мы займемся обработкой изображения:

const sharp = require('sharp');
const { workerData, parentPort } = require('worker_threads');

sharp(workerData)
    .resize(300, 300)
    .toBuffer()
    .then(data => {
        parentPort.postMessage(data);
    })
    .catch(err => {
        parentPort.postMessage(err);
    });

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

Расширение тематического исследования: обработка потоковой передачи мультимедиа и преобразования форматов

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

Исходный код

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

const express = require('express');
const sharp = require('sharp');
const ffmpeg = require('fluent-ffmpeg');
const fs = require('fs');
const app = express();

app.post('/upload', (req, res) => {
    let image = req.body.image;
    sharp(image)
        .resize(300, 300)
        .toBuffer()
        .then(data => {
            // save the image data into a database
            db.save(data);
            res.status(200).send("Image processed and saved!");
        })
        .catch(err => {
            console.error(err);
            res.status(500).send("An error occurred");
        });
});

app.get('/stream/:filename', (req, res) => {
    let file = fs.createReadStream(`./media/${req.params.filename}`);
    file.pipe(res);
});

app.get('/convert/:filename', (req, res) => {
    ffmpeg(`./media/${req.params.filename}`)
        .outputFormat('mp3')
        .on('end', () => res.status(200).send("Conversion completed"))
        .on('error', err => {
            console.error(err);
            res.status(500).send("An error occurred during conversion");
        })
        .pipe(fs.createWriteStream(`./media/${req.params.filename}.mp3`));
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

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

Оптимизация производительности

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

const express = require('express');
const { Worker } = require('worker_threads');
const app = express();

app.post('/upload', (req, res) => {
    // Same as before
});

app.get('/stream/:filename', (req, res) => {
    const worker = new Worker('./mediaStreamer.js', {
        workerData: req.params.filename
    });

    worker.on('message', data => {
        if (data.error) {
            console.error(data.error);
            return res.status(500).send("An error occurred during streaming");
        }
        res.write(data.chunk);
    });

    worker.on('exit', () => res.end());
});

app.get('/convert/:filename', (req, res) => {
    const worker = new Worker('./mediaConverter.js', {
        workerData: req.params.filename
    });

    worker.on('message', data => {
        if (data.error) {
            console.error(data.error);
            return res.status(500).send("An error occurred during conversion");
        }
        res.status(200).send("Conversion completed");
    });
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

В mediaStreamer.js мы будем обрабатывать потоковую передачу мультимедиа:

const fs = require('fs');
const { workerData, parentPort } = require('worker_threads');

let file = fs.createReadStream(`./media/${workerData}`);

file.on('data', chunk => parentPort.postMessage({ chunk }));
file.on('error', err => parentPort.postMessage({ error: err }));

В mediaConverter.js мы будем обрабатывать преобразования формата:

const ffmpeg = require('fluent-ffmpeg');
const { workerData, parentPort } = require('worker_threads');

ffmpeg(`./media/${workerData}`)
    .outputFormat('mp3')
    .on('end', () => parentPort.postMessage({}))
    .on('error', err => parentPort.postMessage({ error: err }))
    .pipe(fs.createWriteStream(`./media/${workerData}.mp3`));

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

Тестирование сервера: практический подход

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

Нагрузочное тестирование

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

Давайте определим простой скрипт Artillery:

config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 20
scenarios:
  - flow:
    - post:
        url: "/upload"
        beforeRequest:
          - "generateRandomImage"
    - get:
        url: "/stream/sample-file"
    - get:
        url: "/convert/sample-file"

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

Функция generateRandomImage (здесь не показана) может генерировать или получать случайный файл изображения для загрузки: чтобы попробовать, реализуйте эту функцию в соответствии с вашими потребностями и, наконец, чтобы запустить нагрузочный тест, используйте команду: artillery run load-test-script.yml

Модульное тестирование

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

const sharp = require('sharp');
const { resizeImage } = require('../imageProcessor');

test('resizeImage should resize image to 300x300', async () => {
  const inputImage = await sharp('path-to-sample-image').toBuffer();
  const outputImage = await resizeImage(inputImage);
  const metadata = await sharp(outputImage).metadata();
  
  expect(metadata.width).toBe(300);
  expect(metadata.height).toBe(300);
});

Интеграционное тестирование

Интеграционные тесты обеспечивают правильную совместную работу различных компонентов нашего сервера, и для этой цели мы можем использовать такую ​​библиотеку, как Supertest, которая позволяет нам тестировать конечные точки HTTP:

const request = require('supertest');
const app = require('../app');

describe('POST /upload', () => {
  it('should process and save an image', async () => {
    const response = await request(app)
      .post('/upload')
      .attach('image', 'path-to-sample-image');
    
    expect(response.status).toBe(200);
    expect(response.text).toBe('Image processed and saved!');
  });
});

describe('GET /stream/:filename', () => {
  // Similar testing for streaming endpoint
});

describe('GET /convert/:filename', () => {
  // Similar testing for conversion endpoint
});

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

Конец цикла

Теперь, когда вы освоили цикл событий Node.js, почему бы не сделать еще один шаг вперед? В другой моей статье, озаглавленной Учебное пособие по окончательному шлюзу API Node.js: простое соединение микросервисов», я подробно расскажу о создании мощного шлюза API с помощью Node.js, и это очень полезный ресурс. научиться без труда подключать различные типы микросервисов, включая RESTful, GraphQL и gRPC.



Его нет в библиотеке Full-Stack Development, а в библиотеке «Шаблоны архитектуры программного обеспечения», где вы найдете множество знаний о создании эффективной и масштабируемой архитектуры программного обеспечения. Увидимся там!

📙 Нежное напоминание: покупка этой книги означает поддержку моих усилий без каких-либо дополнительных затрат для вас. Ваша поддержка очень ценна! 🎗

Создавайте приложения с повторно используемыми компонентами, как Lego

Инструмент с открытым исходным кодом Bit помогает более чем 250 000 разработчиков создавать приложения с компонентами.

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

Подробнее

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

Микро-интерфейсы

Система дизайна

Совместное использование кода и повторное использование

Монорепо

Узнать больше: