Каждая неблокирующая операция в Node.js имеет дополнительную «цену» и она не всегда выгодна

Прочитав статью Episode 8: Interview with Ryan Dahl, Creator of Node.js и комментарии к переводу, я решил протестировать эффективность блокирующей и неблокирующей операции чтения файла в Node.js, под катом таблицы и графики.

Блокирующая операция (fs.readFileSync одна из таких) предполагает что исполнение всего приложения будет приостановлено пока не связанные с JS на прямую операции не будут выполнены.

Неблокирующие опрации позволяют выполнять не связанные с JS операции асинхронно в параллельных потоках (например, fs.readFile).

Больше об blocking vs non-blocking здесь.

Хоть Node.js исполняется в одном потоке, с помощью child_process или cluster можно распределить исполнение кода на несколько потоков.

Проведя тесты с параллельным и последовательным чтением разных по величине файлов, я получил следующие результаты.

При чтении 3.3 kB файла 10 000 раз








SymbolNameops/secPercents
ALoop readFileSync7.4100%
BPromise chain readFileSync4.4760%
CPromise chain readFile1.0915%
DPromise.all readFileSync4.5862%
EPromise.all readFile1.6923%
FMultithread loop readFileSync1.3518%

При чтении 3.3 kB файла 100 раз








SymbolNameops/secPercents
ALoop readFileSync747100%
BPromise chain readFileSync64186%
CPromise chain readFile12016%
DPromise.all readFileSync66489%
EPromise.all readFile23832%
FMultithread loop readFileSync1.550.2%

При чтении 6.5 MB файла 100 раз








SymbolNameops/secPercents
ALoop readFileSync0.6383%
BPromise chain readFileSync0.6687%
CPromise chain readFile0.6180%
DPromise.all readFileSync0.6687%
EPromise.all readFile0.76100%
FMultithread loop readFileSync0.5674%

Загрузка процессора при чтении 3.3 kB файла 10 000 раз (без Multithread loop readFileSync)

file.txt, reading 10000 times

Загрузка процессора при чтении 6.5 MB файла 100 раз

bigFile.txt, reading 100 times

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

Следовательно, чтение небольших файлов лучше проводить с помощью fs.readFileSync, а больших файлов с помощью fs.readFile (насколько файл должен быть большой зависит от компьютера и софта).

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

Пример ситуации чтения файла.

Допустим у нас крутится веб сервер на ноде в одном потоке T1. На сервер одновременно приходит два запроса (P1 и P2) чтения и обработки небольших файлов (по одному на запрос). При использовании fs.readFileSync последовательность исполнения кода в потоке Т1 будет выглядеть так:

P1 -> P2

При использовании fs.readFile последовательность исполнения кода в потоке Т1 будет выглядеть так:

P1-1 -> P2-1 -> P1-2 -> P2-2

Где P1-1, P2-1 — делегирование чтения в другой поток, P1-2, P2-2 — получение результатов чтения и обработка данных.

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

Источник