Кейс: воркер-треды
Node.js работает в одном потоке. Чаще всего это не создаёт проблем, потому что почти все операции в наших приложениях неблокирующие. Недавно я делал обработку натурального языка на JS и упёрся в однопоточность. Решение — воркер-треды.
Я не буду рассказывать о воркер-тредах, об асинхронной модели в Node.js. Об этом уже есть хорошая статья — https://habr.com/ru/company/ruvds/blog/437984/
Кейс
В моем приложении происходит обработка натурального языка. Это блокирующая операция и она занимала для типичного текста около 6 секунд. Длительность совершенно не беспокоила — пользователь готов подождать, а после первого процессинга результат кешируется и отдается моментально. Проблема в другом — на эти 6 секунд приложение не может обслуживать других клиентов, то есть ждать будут даже те пользователи которые хотят увидеть уже закешированный результат.
Плюс, если два пользователя одновременно загрузят не закешированный текст — приложение задумается уже на 12 секунд. Меня совершенно не устраивала ситуация, когда приложение можно так легко заставить перестать обслуживать клиентов.
Самое простое решение — разбить текст на части и обрабатывать их по одной, ставя задачу на обработку следующей части через setTimeout(..., 0)
. В таком случае между чанками приложение сможет ответить некоторому количеству пользователей. Но у этого решения слишком серьезные минусы — время обработки увеличивалось в 3-4 раза, алгоритм пришлось сильно переписать, он стал нечитаемым. Вторая проблема (с одновременными запросами на обработку нескольких текстов) совсем не решилась.
Другой вариант — поднять несколько экземпляров приложения через менеджер процессов pm2
. Но это только временное решение проблемы — на каждого пользователя отдельное приложение не поднимешь. К тому же, этот способ противоречит самой сути Node.js.
Тогда я обратился к экспериментальной возможности Node.js — worker_threads. Работать с встроенным API не очень удобно, но есть замечательная библиотека — threds.js.
В Node.js 11.7.0 эта функциональность перестала быть экспериментальной, но, на мой взгляд, надежнее пользоваться LTS версией платформы 10.13.0, пусть даже с флагом для worker_threads.
Подготовить функцию для выполнения внутри воркер-треда легко. Эта функция будет выполняться в изолированном окружении, поэтому она должна вычислять результат своей работы только на основе входящих параметров, то есть быть “чистой”.
const { expose } = require('threads')const { doJob } = require('../doJob')expose(doJob)
Теперь изначально блокирующая функция doJob
может быть исполнена без блокировки основного потока. Для этого нам нужно создать новый воркер, передав ему путь до файла с expose
и после выполнения работы уничтожить его.
const { spawn, Thread, Worker } = require('threads')const asyncDoJob = async (data) => {
const doJob = await spawn(new Worker('./workers/doJob'))
const result = await doJob(data)
await Thread.terminate(doJob)
return result
}
Для нашего небольшого прототипа такого решения было вполне достаточно. Но в “настоящем” приложении нужно учесть еще одну штуку. Создание нового воркера — задача, которая требует значительных ресурсов, хотелось бы использовать один воркер несколько раз, а не пересоздавать его после каждой выполненной задачи. Библиотека поддерживает такую возможность — thread pool. В таком режиме работы единожды созданный воркер выполняет задачи по мере их поступления и уведомляет о готовности.
Каждый воркер требует своего контекста исполнения, что потребляет память компьютера. Не создавайте слишком много воркер-тредов.
Резюме
Последнее время для Node.js-приложений открываются все новые сферы. Есть версия TensorFlow для JS, библиотека для обработки натурального языка compromise, библиотека для работы с большими числами. Недавно, на HolyJS Николай Матвиенко рассказывал, как он организовал обработку озера данных на Node.js (презентация).
Node.js стала серьезной платформой, которая не накладывает больших ограничений на запускаемые приложения. Это производительная и простая в освоении технология, которая позволяет строить надежные и дешевые приложения. Кроме этого, фронтенд-разработчики получают возможность писать для себя простые бэкенды, ориентироваться в коде всего приложения. Упрощаются коммуникации в команде — все программисты говорят на одном языке. Для подавляющего большинства сайтов, Node.js — отличный выбор.