Здравствуйте!

Я занимаюсь разработкой приложений NodeJs с 2010 года. Мне всегда было сложно оптимизировать приложения NodeJs под свои требования. Я прочитал множество документов (клише) по оптимизации приложений NodeJs. Наконец, я здесь, чтобы поделиться своими мыслями и дополнениями к этим штампам. Читай ниже!

Никогда не блокируйте цикл событий.

Вы, наверное, уже слышали, что «NodeJs однопоточный». Но на самом деле это не так (мы поговорим об этом позже). Хотя цикл событий однопоточный. Вот почему мы не должны его блокировать. Что вы имеете в виду под блокировкой?

Цикл событий состоит из шести фаз.

  • Таймеры: этап, на котором выполняются обратные вызовы, запланированные setTimeout() и setInterval().
  • Обратные вызовы ввода-вывода: выполняет обратные вызовы ввода-вывода, отложенные до следующего цикла.
  • Ожидание, подготовка: этот этап используется для внутренних операций.
  • Опрос: получение новых событий ввода / вывода; выполнять обратные вызовы, связанные с вводом-выводом (почти все, кроме закрытых обратных вызовов, запланированных таймерами и setImmediate()). Узел будет здесь блокироваться, когда это необходимо.
  • Проверить: здесь вызываются setImmediate() обратные вызовы.
  • Закрыть обратные вызовы: некоторые закрытые обратные вызовы, например socket.on('close', ...).

За этот цикл событий отвечает только один поток. Вот почему вам следует позаботиться. Секрет масштабируемости NodeJs: он использует очень небольшое количество потоков, так как снижает затраты на переключение контекста и сборку мусора. Но если вы заблокируете это, вы потерпите неудачу.

Как можно это заблокировать?

Это обратный вызов, который обрабатывает запрос клиента с постоянным временем.

app.get('/', (req, res,next) => {
  res.sendStatus(200);
});

Это еще один обратный вызов с o(n^2)

app.get('/', (req, res,next) => {
  let n = req.query.n;
for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      console.log(`Iter ${i}.${j}`);
    }
  }
res.sendStatus(200);
});

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

  1. Разбиение с помощью setImmediate
  2. Разгрузка с помощью аддонов C ++, Дочерних процессов, Кластеризации, Рабочих потоков. Я часто выбираю рабочие потоки и надстройки C ++.

Заблокированный поток - это Event Loop, а не NodeJs. Как мы уже говорили, NodeJs не является однопоточным. В NodeJs есть рабочий пул, реализованный libuv для интенсивных задач, и по умолчанию в этом пуле 4 потока, но мы также можем его изменить. Этот рабочий пул используется как для операций ввода-вывода, так и для задач с интенсивным использованием ЦП. Эти:

  • DNS: поиск
    Мы рассмотрим это в будущем, это важная операция ввода-вывода.
  • FS: Файловые операции
    Никогда не выполняйте операции с файловой системой. Да, клише, но знаете, почему это так медленно? Эти API-интерфейсы предоставляются привязками C ++, связанными с рабочим пулом. И цикл обработки событий платит некоторую стоимость для создания привязки и передачи задачи работнику. Блокирующая часть не читает байты из вашего файла, не создает привязку и не передает ей задачу. Это также важно для других методов разгрузки, таких как рабочие потоки или процессы. Если вы собираетесь выполнить очень простое вычисление, затраты на создание привязки и передачу ей задачи могут быть больше, чем ваши вычисления.
  • Злиб: Мы расскажем об этом в будущем.

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

Поиск DNS

Поисковые запросы DNS не кэшируются NodeJ. Это не может быть большой проблемой, если вы запускаете приложение NodeJs на своем компьютере, потому что оно может использовать DNS вашей ОС. Но если вы докерили свое приложение с помощью alpine Node, вы должны сами управлять своим DNS-кешированием. Если вы не настроили keep-alive или запрашиваемый сервер не поддерживает его, при каждом вызове будет выполняться поиск DNS. Ранее мы уже говорили о том, что DNS предоставляется в C ++ и представляет собой обширную операцию ввода-вывода. Скорее всего, это добавит 100 мс для каждого нового соединения. Чтобы решить эту проблему, вы можете либо установить DNS в свою докерную ОС, либо установить пакет, который обертывает модуль DNS для кеширования. Кроме того, ваши кеши не должны быть длиннее 2 часов, чтобы предотвратить другие проблемы, которые могут привести к сбою приложения.

Сжатие

Чтобы сжать или распаковать ваши ответы, NodeJs использует Zlib. Все API Zlib, кроме явно синхронных, используют пул потоков libuv. Поскольку все вызовы Zlib обрабатываются пулом потоков libuv, это означает много переключения контекста и фрагментацию памяти. Также по умолчанию у нас всего 4 воркера, поэтому задача может быть поставлена ​​в очередь.

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

Увеличение размера пула потоков

В зависимости от ваших требований вы все равно можете выполнять поиск DNS, операции с файлами или вызовы Zlib. Тогда вы можете найти увеличение размера пула потоков эффективным. Для этого просто используйте UV_THREADPOOL_SIZE=64 в качестве переменной среды.

Самостоятельное преобразование JSON в строку

Производительность JSON.stringify резко ниже по сравнению с инструментами для создания строк JSON для пользовательской сборки. Вы можете оптимизировать текущее поведение операции строкового преобразования с помощью toJSON или просто использовать для этого пакет.

Используйте NODE_ENV

Использование переменной среды NODE_ENV со значением production - важное и простое улучшение. Эта переменная среды - не что иное, как принятый сообществом способ реализации журналов отладки или функций разработки. Чтобы закрыть их все, используйте производственную стоимость. Пример: экспресс

Использование Keep-Alive

Скорее всего, вам придется вызывать внешние API для создания контента. Для этого вы используете http или другие внешние пакеты. Всякий раз, когда вы пытаетесь получить контент с другого компьютера, вы создаете соединение от вашего компьютера к целевому компьютеру. После того, как соединение установлено, теперь запускается уровень 7.

Вы создаете HTTP-запрос и передаете его через ваше соединение на целевой компьютер. И целевой компьютер готовит HTTP-ответ и передает его на ваш компьютер по тому же соединению.

Стоимость создания HTTP-запроса намного ниже, чем создание и установка соединения с целевым компьютером. Установление TCP-соединения означает трехстороннее рукопожатие и многое другое. Когда вы используете keep-alive, вы значительно сокращаете время приема-передачи от установления соединения, потому что вы начинаете использовать существующие соединения для каждого предстоящего запроса.

Чтобы реализовать это, вы можете просто передать агент в HTTP.

const http = require("http");
const agent = new http.Agent({
    keepAlive: true,
    maxSockets: 45
});
http.request({
    agent: agent,
    method: "GET",
    hostname: "localhost",
    port: 3000
});
...

Теперь предстоящие запросы будут использовать тот же пул сокетов агента для этого хоста.
Пулы сокетов создаются для каждого имени хоста реализацией агента. Это важно. Используйте розетки с умом. NodeJs по умолчанию использует maxSockets как Infinity. NodeJs запрашивает сокет у операционной системы, и этот процесс стоит затрат. Так что уменьшение maxSockets до 45 может оказаться полезным. Но всегда сравнивайте свои изменения.