Обработка миллионов строк данных потоками на Node.js
Приходилось ли вам обрабатывать с помощью Node.js одновременно миллионы строк базы данных и выводить всё это на веб-страницу? Это непросто, но у нас есть готовое решение.
Попробуем решить описанную проблему со следующим стеком технологий:
- Node.js
- Sequelize (ORM-библиотека, основанная на промисах)
- MariaDB
Что на клиенте – не имеет значения. Когда размер данных приближается к 4 Гб, Chrome в любом случае упадет.
Потоки
Очевидное решение проблемы большого объема данных – потоковая передача. Если вы попробуете отправить их одним большим куском, Node не справится.
Тут возникает первая большая проблема – Sequelize не поддерживает работу с потоками.
Вот так выглядит классический вызов библиотеки:
Конечно, тут кое-что пропущено – вроде конфигурации базы данных и собственно определение вызова метода get()
(откуда, например, приходит res
?). Но вы, безусловно, разберетесь в этом самостоятельно.
Результат работы этого кода вполне предсказуем – Node падает. Вы можете, конечно, выделить больше памяти – max-old-space-size=8000
, но вряд ли это можно назвать решением проблемы.
Попробуем вручную реализовать потоковую передачу данных:
В этом примере мы знаем, сколько строк вернется из базы данных, отсюда строка if (i === 5
). Это просто тест. Чтобы завершить поток, вы должны отправить null
. Можно заранее получить значение count
(количество строк).
Основная идея состоит в разделении одного большого запроса на несколько маленьких и потоковое получение отдельных чанков. Это работает, Node не падает от перегрузки, но работает очень долго. 3.5 Гб данных вы будете обрабатывать примерно 10 минут!
Нет ли другого – более быстрого – решения?
Коннектор для базы данных
Есть – это неблокирующий клиент MariaDB Node.js connector.
Вот так выглядит обычный запрос:
Он уже достаточно быстрый, однако попробуем потоковый код:
Выглядит загадочно, но тут не происходит ничего сложного. Мы просто создаем пайплайн для данных:
- Поток
queryStream
– результат запроса к базе. - Поток
transformStream
– для отправки преобразованных в строки чанков (здесь можно использовать только строки и буферы). - Класс stream.PassThrough это реализация трансформирующего стрима.
- Функция для обработки ошибок.
ps.pipe(res)
– отправляет результаты обработки клиенту.
Это решение работает гораздо быстрее – те же данные передаются меньше, чем за 4 минуты без перегрузки Node.
Результат
Итак, если вам приходится работать с огромным количеством данных, у вас есть три пути:
- Использовать потоковую передачу.
- Подумать о введении пагинации – получать данные небольшими кусками.
- Убедить клиента, что в условиях современного веба это невозможно.
Как вы решаете подобные задачи?