Прочитав статью 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 раз
Загрузка процессора при чтении 6.5 MB файла 100 раз
Как видим 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