Как Node.js по своей природе быстрее, когда он все еще полагается на потоки внутри?

281

Я только что посмотрел следующее видео: Введение в Node.js и до сих пор не понимаю, как вы получаете преимущества в скорости.

Главным образом, в какой-то момент Райан Даль (создатель Node.js) говорит, что Node.js основан на циклах событий, а не на потоках. Потоки дороги и должны быть оставлены на усмотрение только экспертов по параллельному программированию.

Позже он затем показывает стек архитектуры Node.js, который имеет базовую реализацию C, которая имеет свой собственный внутренний поток. Очевидно, что разработчики Node.js никогда не запускают свои собственные потоки и не используют пул потоков напрямую ... они используют асинхронные обратные вызовы. Это я понимаю.

Что я не понимаю, так это то, что Node.js все еще использует потоки ... он просто скрывает реализацию, так как это быстрее, если 50 человек запрашивают 50 файлов (в данный момент не в памяти), тогда не требуется 50 потоков ?

Единственное отличие состоит в том, что, поскольку он управляется изнутри, разработчику Node.js не нужно кодировать многопоточные детали, но он все еще использует потоки для обработки запросов файлов IO (блокирования).

Так разве вы не просто берете одну проблему (многопоточность) и скрываете ее, пока эта проблема еще существует: главным образом, несколько потоков, переключение контекста, мертвые блокировки ... и т. Д.?

Должна быть какая-то деталь, которую я до сих пор здесь не понимаю.

Ральф Каравео
источник
14
Я склонен согласиться с вами, что претензия несколько упрощена. Я полагаю, что преимущество производительности узла сводится к двум вещам: 1) все действительные потоки содержатся на довольно низком уровне и, таким образом, остаются ограниченными по размеру и количеству, и, следовательно, синхронизация потоков упрощается; 2) «Переключение» на уровне ОС select()происходит быстрее, чем обмен контекста потока.
Заостренный
Пожалуйста, смотрите этот stackoverflow.com/questions/24796334/…
veritas

Ответы:

140

Здесь на самом деле объединяются несколько разных вещей. Но это начинается с мема, что темы просто очень сложны. Поэтому, если они сложны, вы, скорее всего, при использовании потоков 1) нарушаете работу из-за ошибок и 2) не используете их максимально эффективно. (2) это тот, о котором вы спрашиваете.

Подумайте об одном из примеров, которые он приводит, когда приходит запрос, вы запускаете какой-то запрос, а затем что-то делаете с результатами этого. Если вы напишите это стандартным процедурным способом, код может выглядеть так:

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

Если поступивший запрос заставил вас создать новый поток, который выполнял приведенный выше код, у вас будет поток, который будет ничего не делать, пока query() работы. (По словам Райана, Apache использует один поток для удовлетворения исходного запроса, тогда как nginx превосходит его в тех случаях, о которых он говорит, потому что это не так.)

Теперь, если вы действительно умны, вы бы выразили приведенный выше код таким образом, чтобы среда могла работать и делать что-то еще во время выполнения запроса:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

Это в основном то, что делает node.js. Вы в основном декорируете - таким образом, который удобен из-за языка и среды, а следовательно, и из-за замыканий, - вашего кода таким образом, что среда может быть умной в отношении того, что и когда выполняется. Таким образом, файл node.js не нов в том смысле, что он изобрел асинхронный ввод-вывод (не то, чтобы кто-то утверждал что-либо подобное), но он нов в том, что способ его выражения немного отличается.

Примечание: когда я говорю, что среда может быть умной в отношении того, что и когда выполняется, в частности, я имею в виду, что поток, который она использовала для запуска некоторого ввода-вывода, теперь можно использовать для обработки какого-либо другого запроса или выполнения некоторых вычислений. параллельно, или запустите другой параллельный ввод / вывод. (Я не уверен, что узел достаточно сложен, чтобы начать больше работы для того же запроса, но вы поняли.)

jrtipton
источник
6
Хорошо, я определенно вижу, как это может повысить производительность, потому что для меня это звучит так, как будто вы можете максимально использовать ваш ЦП, потому что нет никаких потоков или стеков выполнения, просто ожидающих возвращения ввода-вывода, так что то, что сделал Райан, эффективно найдено способ закрыть все пробелы.
Ральф Каравео
34
Да, единственное, что я хотел бы сказать, это то, что он не нашел способ сократить разрывы: это не новая модель. Отличие состоит в том, что он использует Javascript, чтобы позволить программисту выразить свою программу более удобным способом для такого рода асинхронности. Возможно, придирчивая деталь, но все же ...
jrtipton
16
Также стоит отметить, что для большинства задач ввода-вывода Node использует любой доступный API-интерфейс асинхронного ввода-вывода на уровне ядра (epoll, kqueue, / dev / poll и т. Д.)
Пол
7
Я все еще не уверен, что полностью понимаю это. Если учесть, что внутри веб-запроса операции ввода-вывода - это те операции, которые занимают большую часть времени, необходимого для обработки запроса, и если для каждой операции ввода-вывода создается новый поток, то для 50 запросов, поступающих в очень быстрой последовательности, мы будем вероятно, 50 потоков работают параллельно и выполняют свою часть ввода-вывода. Отличие от стандартных веб-серверов состоит в том, что там весь поток выполняется в потоке, а в node.js - только его часть ввода-вывода, но именно эта часть занимает большую часть времени и заставляет поток ждать.
Флорин Думитреску
13
@ SystemParadox спасибо за указание на это. В последнее время я действительно провел некоторое исследование по этой теме, и, действительно, подвох в том, что асинхронный ввод-вывод, при правильной реализации на уровне ядра, не использует потоки при выполнении операций асинхронного ввода-вывода. Вместо этого вызывающий поток освобождается, как только начинается операция ввода-вывода, и выполняется обратный вызов, когда операция ввода-вывода завершается и для нее доступен поток. Таким образом, node.js может выполнять 50 одновременных запросов с 50 операциями ввода-вывода (почти) параллельно, используя только один поток, если должным образом реализована асинхронная поддержка операций ввода-вывода.
Флорин Думитреску
32

Заметка! Это старый ответ. Хотя это все еще верно в общих чертах, некоторые детали могли измениться из-за быстрого развития Node в последние несколько лет.

Он использует потоки, потому что:

  1. Опция O_NONBLOCK для open () не работает с файлами .
  2. Существуют сторонние библиотеки, которые не предлагают неблокирующий ввод-вывод.

Для подделки неблокирующего ввода-вывода необходимы потоки: блокируйте ввод-вывод в отдельном потоке. Это уродливое решение и вызывает много накладных расходов.

Еще хуже на аппаратном уровне:

  • С DMA CPU асинхронно разгружает IO.
  • Данные передаются непосредственно между устройством ввода-вывода и памятью.
  • Ядро заключает это в синхронный блокирующий системный вызов.
  • Node.js упаковывает блокирующий системный вызов в поток.

Это просто глупо и неэффективно. Но это работает по крайней мере! Мы можем наслаждаться Node.js, потому что он скрывает уродливые и громоздкие детали за асинхронной архитектурой, управляемой событиями.

Может быть, кто-то будет реализовывать O_NONBLOCK для файлов в будущем? ...

Редактировать: я обсуждал это с другом, и он сказал мне, что альтернативой потокам является опрос с помощью select : укажите время ожидания 0 и выполните IO для возвращенных дескрипторов файлов (теперь они гарантированно не блокируются).

nalply
источник
А как насчет Windows?
Pacerier
Извините, не знаю. Я только знаю, что libuv является платформенно-нейтральным уровнем для выполнения асинхронной работы. В начале узла не было либува. Тогда было решено отделить libuv, и это упростило код для конкретной платформы. Другими словами, у Windows есть своя собственная асинхронная история, которая может полностью отличаться от Linux, но для нас это не имеет значения, потому что libuv делает тяжелую работу за нас.
Вскоре
28

Я боюсь, что я "делаю не то" здесь, если так, удалите меня, и я извиняюсь. В частности, я не вижу, как я создаю аккуратные маленькие аннотации, которые создали некоторые люди. Тем не менее, у меня есть много проблем / замечаний, чтобы сделать в этой теме.

1) Закомментированный элемент в псевдокоде в одном из популярных ответов

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

по сути фальшивая. Если поток вычисляет, то он не крутит большие пальцы, он выполняет необходимую работу. Если, с другой стороны, он просто ожидает завершения ввода-вывода, то он не использует процессорное время, весь смысл инфраструктуры управления потоками в ядре состоит в том, что процессор найдет что-то полезное для выполнения. Единственный способ «перевернуть свои пальцы», как предлагается здесь, - создать цикл опроса, и никто, кто закодировал реальный веб-сервер, не настолько неспособен сделать это.

2) «Потоки сложны», имеет смысл только в контексте обмена данными. Если у вас есть по существу независимые потоки, как, например, в случае обработки независимых веб-запросов, то создание потоков является тривиально простым, вы просто кодируете линейный поток обработки одной работы и сидите, зная, что она будет обрабатывать несколько запросов, и каждый будет эффективно независимым. Лично я бы рискнул, что для большинства программистов изучение механизма закрытия / обратного вызова является более сложным, чем простое кодирование версии потока сверху вниз. (Но да, если вам нужно общаться между потоками, жизнь становится действительно тяжелой и очень быстрой, но тогда я не уверен, что механизм закрытия / обратного вызова действительно меняет это, он просто ограничивает ваши варианты, потому что этот подход все еще достижим с потоками Во всяком случае, это

3) До сих пор никто не представил никаких реальных доказательств того, почему один конкретный тип переключения контекста будет более или менее трудоемким, чем любой другой тип. Мой опыт создания многозадачных ядер (в небольшом масштабе для встроенных контроллеров, ничего более изящного, чем «настоящая» ОС) подсказывает, что это не так.

4) Все иллюстрации, которые я до сих пор видел, предназначенные для того, чтобы показать, насколько быстрее Node, чем у других веб-серверов, ужасно ошибочны, однако они ошибочны таким образом, что косвенно иллюстрируют одно преимущество, которое я определенно принял бы для Node (и это отнюдь не незначительно). Узел не выглядит так, как будто он нуждается (и даже не разрешает) в настройке. Если у вас есть многопоточная модель, вам нужно создать достаточное количество потоков для обработки ожидаемой нагрузки. Сделайте это плохо, и вы получите плохую производительность. Если потоков слишком мало, то процессор простаивает, но не может принимать больше запросов, создавать слишком много потоков, и вы будете тратить впустую память ядра, а в случае среды Java вы также будете тратить впустую память основной кучи , Теперь для Java тратить кучу - это первый, лучший способ поднять производительность системы, потому что эффективная сборка мусора (в настоящее время это может измениться с G1, но кажется, что жюри все еще находится на этом этапе по крайней мере в начале 2013 года) зависит от наличия большого количества свободной кучи. Итак, есть проблема: настройте его на слишком малое количество потоков, у вас незанятые процессоры и низкая пропускная способность, настройте его на слишком много, и он застрянет другими способами.

5) Есть другой способ, которым я принимаю логику утверждения, что подход Node «быстрее по замыслу», и это - это. В большинстве моделей потоков используется модель переключения контекста с разделением по времени, расположенная поверх более подходящей модели (предупреждение о значении :) и более эффективной модели (не оценка значения). Это происходит по двум причинам: во-первых, большинство программистов, по-видимому, не понимают приоритетное вытеснение, и, во-вторых, если вы изучаете многопоточность в среде Windows, существует временная привязка, нравится вам это или нет (конечно, это подтверждает первый пункт. Примечательно, что в первых версиях Java использовалось приоритетное вытеснение в реализациях Solaris и временная привязка в Windows. Поскольку большинство программистов не понимали и жаловались, что «многопоточность не работает в Solaris» они поменяли модель на временную шкалу везде). В любом случае, суть в том, что временная привязка создает дополнительные (и потенциально ненужные) переключатели контекста. Каждое переключение контекста отнимает процессорное время, и это время эффективно удаляется из работы, которую можно выполнить в реальной работе под рукой. Однако время, затрачиваемое на переключение контекста из-за временного среза, не должно превышать очень небольшой процент от общего времени, если только не происходит что-то довольно странное, и я не вижу причин, чтобы ожидать, что это произойдет в простой веб-сервер). Итак, да, избыточные переключатели контекста, вовлеченные во временную привязку, неэффективны (и это не происходит в и это время эффективно отводится от работы, которую можно выполнить на реальной работе под рукой. Однако время, затрачиваемое на переключение контекста из-за временного среза, не должно превышать очень небольшой процент от общего времени, если только не происходит что-то довольно странное, и я не вижу причин, чтобы ожидать, что это произойдет в простой веб-сервер). Итак, да, избыточные переключатели контекста, вовлеченные во временную привязку, неэффективны (и это не происходит в и это время эффективно отводится от работы, которую можно выполнить на реальной работе под рукой. Однако время, затрачиваемое на переключение контекста из-за временного среза, не должно превышать очень небольшой процент от общего времени, если только не происходит что-то довольно странное, и я не вижу причин, чтобы ожидать, что это произойдет в простой веб-сервер). Итак, да, избыточные переключатели контекста, вовлеченные во временную привязку, неэффективны (и это не происходит впотоки ядра, как правило, кстати, но разница будет в нескольких процентах пропускной способности, а не в виде целых числовых факторов, которые подразумеваются в заявках на производительность, которые часто подразумеваются для Node.

В любом случае, извинения за то, что все это было длинным и грубым, но я действительно чувствую, что обсуждение пока ничего не доказало, и я был бы рад услышать от кого-то в любой из этих ситуаций:

а) реальное объяснение того, почему Node должен быть лучше (помимо двух сценариев, которые я изложил выше, первый из которых (плохая настройка), я считаю, является реальным объяснением всех тестов, которые я видел до сих пор. ([править ], на самом деле, чем больше я об этом думаю, тем больше мне интересно, может ли быть здесь важна память, используемая огромным количеством стеков. Размеры стеков по умолчанию для современных потоков, как правило, довольно велики, но память, выделяемая система событий на основе замыканий будет только тем, что нужно)

б) настоящий эталонный тест, который фактически дает реальную возможность для выбранного многопоточного сервера. По крайней мере, таким образом, я должен был бы перестать верить, что утверждения по сути ложные;> ([править] это, вероятно, гораздо сильнее, чем я предполагал, но я чувствую, что объяснения, приведенные для повышения производительности, в лучшем случае неполны, и показанные критерии являются необоснованными).

Ура, Тоби

Тоби Эггитт
источник
2
Проблема с потоками: им нужна оперативная память. Очень занятый сервер может работать до нескольких тысяч потоков. Node.js избегает потоков и, следовательно, более эффективен. Эффективность заключается не в быстром запуске кода. Не имеет значения, выполняется ли код в потоках или в цикле событий. Для процессора это то же самое. Но, избавляясь от потоков, мы экономим оперативную память: только один стек вместо нескольких тысяч. И мы также сохраняем переключатели контекста.
13
3
Но узел не покончит с потоками. Он по-прежнему использует их для выполнения задач ввода-вывода, чего требует большинство веб-запросов.
Леви
1
Также узел хранит замыкания обратных вызовов в оперативной памяти, поэтому я не вижу, где он выигрывает.
Александр Папченко
@levi Но nodejs не использует вещи типа «один поток на запрос». Он использует пул потоков ввода-вывода, вероятно, чтобы избежать сложностей с использованием асинхронных API ввода-вывода (и, возможно, POSIX open()нельзя сделать неблокирующим?). Таким образом, амортизируется любое снижение производительности, когда традиционная модель fork()/ pthread_create()-по запросу должна создавать и уничтожать потоки. И, как упоминалось в постскриптуме а), это также амортизирует проблему стекового пространства. Вы можете, вероятно, обслуживать тысячи запросов, скажем, с 16 потоками ввода-вывода.
Бинки
«Размеры стека по умолчанию для современных потоков, как правило, довольно велики, но память, выделенная системой событий на основе замыканий, будет только тем, что нужно». У меня сложилось впечатление, что они должны быть одного порядка. Замыкания недешевы, среда выполнения должна будет хранить в памяти все дерево вызовов однопоточного приложения (так сказать, «эмулирующие стеки») и сможет очищаться, когда лист дерева освобождается как связанное замыкание. получает "решено". Это будет включать в себя множество ссылок на вещи, находящиеся в куче, которые не могут быть собраны сборщиком мусора и которые повлияют на производительность во время очистки.
Дэвид Тонхофер
14

Что я не понимаю, так это то, что Node.js все еще использует потоки.

Райан использует потоки для тех частей, которые блокируют (большинство из node.js использует неблокирующие операции ввода-вывода), потому что некоторые части безумно трудно писать без блокировки. Но я верю, что Райан хочет, чтобы все было неблокирующим. На слайде 63 (внутренний дизайн) вы видите, что Райан использует libev (библиотеку, которая абстрагирует уведомления об асинхронных событиях) для неблокирующей блокировки событий . Из-за цикла обработки событий node.js требует меньших потоков, что уменьшает переключение контекста, потребление памяти и т. Д.

Альфред
источник
11

Потоки используются только для работы с функциями, не имеющими асинхронных возможностей, например stat().

stat()Функция всегда блокирует, поэтому node.js потребности использовать нить для выполнения фактического вызова без блокировки основного потока (цикл событий). Потенциально никакой поток из пула потоков никогда не будет использоваться, если вам не нужно вызывать такие функции.

gawi
источник
7

Я ничего не знаю о внутренней работе node.js, но я вижу, как использование цикла обработки событий может превзойти потоковую обработку ввода-вывода. Представьте запрос диска, дайте мне staticFile.x, сделайте 100 запросов на этот файл. Каждый запрос обычно занимает поток, получающий этот файл, то есть 100 потоков.

Теперь представьте, что первый запрос создает один поток, который становится объектом публикатора. Все 99 других запросов сначала смотрят, существует ли объект публикатора для staticFile.x, если так, слушайте его, пока он работает, в противном случае запускайте новый поток и, таким образом, новый объект издателя.

Как только один поток завершен, он передает staticFile.x всем 100 слушателям и уничтожает себя, поэтому следующий запрос создает новый новый поток и объект издателя.

Таким образом, в приведенном выше примере это 100 потоков против 1 потока, а также 1 поиск диска вместо 100 поиска диска, усиление может быть весьма феноменальным. Райан умный парень!

Другой способ взглянуть на это один из его примеров в начале фильма. Вместо того:

pseudo code:
result = query('select * from ...');

Опять же, 100 отдельных запросов к базе данных против ...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

Если запрос уже выполняется, другие равные запросы просто запрыгнут на подножку, так что вы можете иметь 100 запросов за одну поездку в одну базу данных.

BGerrissen
источник
3
Дело в базе данных - это скорее вопрос не ожидания ответа при задержке других запросов (которые могут или не могут использовать базу данных), а, скорее, запроса чего-то, а затем позволения ему позвонить вам, когда он вернется. Я не думаю, что это связывает их вместе, так как было бы довольно сложно отследить ответ. Также я не думаю, что есть какой-либо интерфейс MySQL, который позволяет вам хранить несколько небуферизованных ответов на одном соединении (??)
Tor Valamo
Это просто абстрактный пример, объясняющий, как циклы событий могут предложить большую эффективность, nodejs ничего не делает с БД без дополнительных модулей;)
BGerrissen
1
Да, мой комментарий был больше к 100 запросам в одной базе данных туда и обратно. : p
Тор Валамо
2
Привет BGerrissen: хороший пост. Итак, когда запрос выполняется, другие подобные запросы будут «слушателем», как в примере staticFile.X выше? например, 100 пользователей, получающих один и тот же запрос, будет выполнен только один запрос, а остальные 99 будут слушать первый? Спасибо !
ЧАПа
1
Это звучит так, будто nodejs автоматически запоминает вызовы функций или что-то в этом роде. Теперь, поскольку вам не нужно беспокоиться о синхронизации совместно используемой памяти в модели циклических событий JavaScript, проще безопасно кэшировать вещи в памяти. Но это не значит, что nodejs волшебным образом сделает это за вас или что это тип повышения производительности, о котором спрашивают.
Бинки