Сравнение взятия из кэша одного и того же файла с помощью fs.readFileSync и fs.readFile (и чтения множества файлов)

в 12:53, , рубрики: javascript, libuv, node.js, performance

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

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

UPD2: Статья отредактирована, бенчмарк исправлен, результаты добавлены.

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

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

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

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

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

Все тесты проходили на одном компьютере, с одним HDD, а иммено:

OS: Ubuntu 16.04
Node.js version: 8.4.0
Processor: AMD Phenom(tm) 9750 Quad-Core Processor
Physical cores: 4
HDD: 2TB 7200rpm 64MB
File system type: ext4
file.txt size: 3.3 kB
bigFile.txt size: 6.5 MB

Результаты для закэшированого файла.

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

Symbol Name ops/sec Percents
A Loop readFileSync 7.4 100%
B Promise chain readFileSync 4.47 60%
C Promise chain readFile 1.09 15%
D Promise.all readFileSync 4.58 62%
E Promise.all readFile 1.69 23%
F Multithread loop readFileSync 20.05 271%
G Multithread promise.all readFile 4.98 67%

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

Symbol Name ops/sec Percents
A Loop readFileSync 747 100%
B Promise chain readFileSync 641 86%
C Promise chain readFile 120 16%
D Promise.all readFileSync 664 89%
E Promise.all readFile 238 32%
F Multithread loop readFileSync 1050 140%
G Multithread promise.all readFile 372 50%

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

Symbol Name ops/sec Percents
A Loop readFileSync 0.63 83%
B Promise chain readFileSync 0.66 87%
C Promise chain readFile 0.61 80%
D Promise.all readFileSync 0.66 87%
E Promise.all readFile 0.76 100%
F Multithread loop readFileSync 0.83 109%
G Multithread promise.all readFile 0.81 107%

Загрузка процессора при чтении 3.3 kB файла 10 000 раз
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. Грубо говоря, запустить саму ноду, а не операции в несколько потоков.

UPD2
Ниже данные полученные для при чтении множества незакешированых файлов одинакового размера (3,3kB).

При чтении по 1000 файлов

Symbol Name ops/sec Percents
A Loop readFileSync 8.47 74%
B Promise chain readFileSync 6.28 55%
C Promise chain readFile 5.49 48%
D Promise.all readFileSync 8.06 70%
E Promise.all readFile 11.05 100%
F Multithread loop readFileSync 3.71 32%
G Multithread promise.all readFile 5.11 44%

При чтении по 100 файлов

Symbol Name ops/sec Percents
A Loop readFileSync 79.19 85%
B Promise chain readFileSync 50.17 54%
C Promise chain readFile 48.46 52%
D Promise.all readFileSync 54.7 58%
E Promise.all readFile 92.87 100%
F Multithread loop readFileSync 80.46 86%
G Multithread promise.all readFile 92.19 99%

Загрузка процессора при чтении незакешированых файлов небольшая, порядка 20%. Результаты варьируются ± 30%.
По результатам видно что использовать неблокирующий fs.readFile выгоднее.

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

Допустим у нас крутится веб сервер на ноде в одном потоке 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 — получение результатов чтения и обработка данных.

Автор: Shvab

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js