Як Node.js притаманний швидше, коли він все ще покладається на потоки внутрішньо?


281

Я щойно переглянув таке відео: Вступ до Node.js і досі не розумію, як ви отримуєте переваги швидкості.

В основному, в один момент Райан Дал (творець Node.js) каже, що Node.js - це цикл подій замість потоку. Нитки коштують дорого, і їх слід залишати лише фахівцям одночасного програмування, які потрібно використовувати.

Пізніше він показує стек архітектури Node.js, який має основну C-реалізацію, яка має власний пул потоків внутрішньо. Тож очевидно, що розробники Node.js ніколи не запускатимуть власні нитки або використовуватимуть пул потоків безпосередньо ... вони використовують зворотні дзвінки async. Стільки я розумію.

Те, що я не розумію, полягає в тому, що Node.js все ще використовує нитки ... це просто приховує реалізацію, і як це швидше, якщо 50 людей вимагають 50 файлів (наразі не в пам'яті), то не потрібні 50 потоків ?

Єдина відмінність полягає в тому, що оскільки він управляється внутрішньо, розробник Node.js не повинен кодувати деталі потоку, але під ним все ще використовується потоки для обробки запитів на файли вводу-виводу (блокування).

Тож чи не справді ви просто приймаєте одну проблему (нарізування різьби) та приховуєте її, поки ця проблема все ще існує: головним чином, декілька потоків, комутація контексту, мертві блокування ... тощо?

Має бути якась деталь, яку я досі не розумію.


14
Я схильний погодитися з вами, що претензія дещо надто спрощена. Я вважаю, що перевага в продуктивності вузла зводиться до двох речей: 1) фактичні потоки містяться на досить низькому рівні і, таким чином, залишаються обмеженими за розміром і кількістю, і синхронізація потоків таким чином спрощується; 2) "перемикання" на ОС на рівні OS select()відбувається швидше, ніж контекстові потоки.
Pointy

Перегляньте цей 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 не є новим у тому сенсі, що він винайшов асинхронний введення-вивід (не те, що хтось щось подібне стверджував), але новий у тому, що спосіб його вираження дещо інший.

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


6
Гаразд, я точно можу побачити, як це може підвищити продуктивність, тому що мені це здається, що ти можеш збільшити максимум свого процесора, оскільки немає жодних потоків чи стеків виконання, просто чекаючи, поки IO повернеться, тому те, що зробив Райан, ефективно знайдеться спосіб закрити всі прогалини.
Ральф Каравео

34
Так, одне, що я б сказав, - це не так, як він знайшов спосіб закрити прогалини: це не новий шаблон. Різне в тому, що він використовує Javascript, щоб дозволити програмісту висловити свою програму способом, що набагато зручніше для такого роду асинхронії. Можливо, прискіплива деталь, але все ж ...
jrtipton

16
Варто також зазначити, що для багатьох завдань вводу / виводу Node використовує будь-які доступні для ядра асинхронізації на рівні ядра (epoll, kqueue, / dev / опитування, що завгодно)
Пол,

7
Я досі не впевнений, що цілком це розумію. Якщо ми врахуємо, що всередині веб-запиту операції вводу-виводу - це ті, які займають більшу частину часу, необхідного для обробки запиту, і якщо для кожної операції вводу-виводу створюється нова нитка, то для 50 запитів, які надходять дуже швидко, ми будемо ймовірно, 50 потоків працюють паралельно і виконують їх частину IO. Відмінність від стандартних веб-серверів полягає в тому, що там весь запит виконується на потоці, тоді як у node.js - лише його частина IO, але це та частина, яка займає більшу частину часу і змушує потік чекати.
Флорін Думітреску

13
@SystemParadox дякую за те, що вказав на це. Насправді я нещодавно зробив деякі дослідження з цієї теми, і справді заперечення полягає в тому, що асинхронний введення-вивід, при правильній реалізації на рівні ядра, не використовує потоки під час виконання операцій з асинхронним введенням-виведенням. Натомість потік виклику звільняється, як тільки починається операція вводу / виводу, і виконується зворотний виклик, коли операція вводу / виводу закінчується і потік для неї доступний. Таким чином, node.js може виконувати 50 одночасних запитів з 50 операціями вводу-виводу в (майже) паралельно, використовуючи лише один потік, якщо підтримка асинхронізації для операцій вводу-виводу належним чином реалізована.
Флорін Думітреску

32

Примітка! Це стара відповідь. Незважаючи на те, що це все ще відповідає дійсності, деякі деталі, можливо, змінилися через швидкий розвиток Node в останні кілька років.

Він використовує теми, тому що:

  1. Параметр O_NONBLOCK open () не працює для файлів .
  2. Існують сторонні бібліотеки, які не пропонують неблокуючий IO.

Щоб підробити незаблокувальний IO, потрібні нитки: виконайте блокування IO в окремій потоці. Це некрасиве рішення і спричиняє багато накладних витрат.

Ще гірше на апаратному рівні:

  • З DMA процесор асинхронно вивантажує IO.
  • Дані передаються безпосередньо між пристроєм вводу-виводу та пам'яттю.
  • Ядро завершує це в синхронний, блокуючий системний виклик.
  • Node.js обертає виклик блокуючої системи в потоці.

Це просто нерозумно і неефективно. Але це працює принаймні! Ми можемо насолоджуватися Node.js, оскільки він приховує потворні та громіздкі деталі за асинхронною архітектурою, керованою подіями.

Можливо, хтось у майбутньому реалізує O_NONBLOCK для файлів? ...

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


Що з Windows?
Pacerier

Вибачте, ідеї немає. Я знаю лише, що libuv - це нейтральний шар платформи для асинхронної роботи. На початку Вузла не було лібув. Тоді було вирішено розділити libuv, і це полегшило специфічний для платформи код. Іншими словами, у Windows є своя асинхронна історія, яка може бути зовсім відмінна від Linux, але для нас це не має значення, тому що libuv робить для нас важку роботу.
nalply

28

Я боюся, що я тут роблю неправильну справу, якщо так видаліть мене і вибачаюся. Зокрема, я не бачу, як я створюю акуратні невеликі анотації, які створили деякі люди. Однак у мене є багато питань / зауважень щодо цієї теми.

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

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

є по суті неправдивим. Якщо нитка обчислюється, то це не великі пальці, вона робить необхідну роботу. Якщо, з іншого боку, він просто чекає завершення IO, то він не використовує час процесора, вся суть інфраструктури управління потоком в ядрі полягає в тому, що процесор знайде щось корисне. Єдиний спосіб "закрутити пальці", як тут пропонується, - створити цикл опитування, і ніхто, хто закодував справжнього веб-сервера, недостатньо невмілий для цього.

2) "Нитки важкі", має сенс лише в контексті обміну даними. Якщо у вас є, по суті, незалежні потоки, як це має місце при обробці незалежних веб-запитів, то введення ниток тривіально просте, ви просто кодуєте лінійний потік, як впоратися з однією роботою, і сидите, знаючи, що вона буде обробляти кілька запитів, і кожен буде ефективно незалежним. Особисто я б задумався, що для більшості програмістів вивчення механізму закриття / зворотного виклику є складнішим, ніж просто кодування версії з низу вниз. (Але так, якщо вам доведеться спілкуватися між потоками, життя стає дуже важким дуже швидко, але тоді я не переконаний, що механізм закриття / зворотного виклику дійсно змінює це, він просто обмежує ваші варіанти, тому що такий підхід все ще досягається за допомогою потоків У будь-якому випадку, це "

3) Поки ніхто не представив жодних реальних доказів того, чому один конкретний тип контекстного перемикання забирає більше чи менше часу, ніж будь-який інший тип. Мій досвід створення багатозадачних ядер (в невеликих масштабах для вбудованих контролерів, нічого такого фантазійного, як "справжня" ОС) говорить про те, що це було б не так.

4) Усі ілюстрації, які я бачив на сьогоднішній день, що мають на меті показати, наскільки швидше Вузол, ніж інші веб-сервери, страшенно хибні, однак вони хибні таким чином, що це опосередковано ілюструє одну перевагу, яку я б точно прийняв за Node (і це аж ніяк несуттєво). Вузол не виглядає так, як він потребує (навіть навіть не дозволяє) налаштування. Якщо у вас є різьбова модель, вам потрібно створити достатню кількість ниток для обробки очікуваного навантаження. Зробіть це погано, і у вас виявиться погана робота. Якщо ниток занадто мало, то процесор простоює, але не в змозі приймати більше запитів, створити занадто багато потоків, і ви будете витрачати пам’ять ядра, а у випадку середовища Java ви також будете витрачати основну пам'ять купи . Тепер для Java витрачаючи купу - це перший, найкращий спосіб налагодити продуктивність системи, тому що ефективне збирання сміття (зараз це може змінитися з G1, але, здається, присяжні все ще залишаються з цього приводу, як мінімум, на початку 2013 року) залежить від того, щоб мати багато запасної маси. Отже, є проблема, налаштуйте її на занадто мало потоків, у вас непрацюючі процесори та погана пропускна спроможність, налаштуйте їх на занадто багато, і це зменшиться іншими способами.

5) Є ще один спосіб, яким я приймаю логіку твердження, що підхід Вузла «швидший за дизайном», і це ось що. Більшість моделей потоків використовують модель контекстного перемикача з часовим нарізанням, шарувату поверх більш підходящої (попередження про оцінку вартості :) та більш ефективної (не судження про значення) попереджувальної моделі. Це трапляється з двох причин, по-перше, більшість програмістів, здається, не розуміють пріоритетного пріоритету, а по-друге, якщо ви навчитеся нарізування в середовищі Windows, часові накладки є, вам це подобається чи ні (звичайно, це підсилює перший пункт Перш за все, перші версії Java використовували попередження щодо пріоритету при впровадженні Solaris та синхронізації в Windows. Оскільки більшість програмістів не розуміли і скаржилися, що "в Соляріс не працює нитка". вони скрізь міняли модель на часовий шрифт). У будь-якому разі, підсумок полягає в тому, що часове оформлення створює додаткові (і потенційно непотрібні) контекстні комутатори. Кожен контекстний перемикач займає час процесора, і цей час фактично вилучається з роботи, яку можна виконати на реальній роботі. Однак кількість часу, вкладеного в перемикання контексту через часове укладання часу, не повинна становити більше, ніж дуже малий відсоток від загального часу, якщо не трапиться щось досить чуже, і я не маю жодної причини, щоб я очікував, що це буде так простий веб-сервер). Отже, так, надлишкові контекстні комутатори, що беруть участь у часовій формі, неефективні (а це не відбувається в і цей час фактично усувається від роботи, яку можна виконати на справжній роботі. Однак кількість часу, вкладеного в перемикання контексту через часове укладання часу, не повинна становити більше, ніж дуже малий відсоток від загального часу, якщо не трапиться щось досить чуже, і я не маю жодної причини, щоб я очікував, що це буде так простий веб-сервер). Отже, так, надлишкові контекстні комутатори, що беруть участь у часовій формі, неефективні (а це не відбувається в і цей час фактично усувається від роботи, яку можна виконати на справжній роботі. Однак кількість часу, вкладеного в перемикання контексту через часове укладання часу, не повинна становити більше, ніж дуже малий відсоток від загального часу, якщо не трапиться щось досить чуже, і я не маю жодної причини, щоб я очікував, що це буде так простий веб-сервер). Отже, так, надлишкові контекстні комутатори, що беруть участь у часовій формі, неефективні (а це не відбувається внитки ядра, як правило, btw), але різниця становитиме кілька відсотків пропускної здатності, а не виду цілих чисельних факторів, що маються на увазі у формулі заявок на продуктивність, які часто маються на увазі для Node.

У будь-якому випадку, вибачте за те, що все це довге і розгублене, але я дійсно відчуваю, що поки що обговорення нічого не довело, і мені було б приємно почути від когось у будь-якій з цих ситуацій:

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

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

Ура, Тобі


2
Проблема з потоками: їм потрібна оперативна пам’ять. Дуже зайнятий сервер може працювати до кількох тисяч потоків. Node.js уникає потоків і, таким чином, є більш ефективним. Ефективність полягає не в тому, щоб швидше запустити код. Не має значення, чи запускається код у потоках або в циклі подій. Для процесора це те саме. Але виконуючи потоки, ми економимо оперативну пам’ять: лише один стек замість кількох тисяч стеків. І ми також зберігаємо контекстні комутатори.
nalply

3
Але вузол не усуває нитки. Він все ще використовує їх внутрішньо для завдань IO, а саме цього вимагає більшість веб-запитів.
levi

1
Також вузол зберігає закриття зворотних викликів в оперативній пам'яті, тому я не можу побачити, де він виграє.
Олександр Папченко

@levi Але nodejs не використовує щось з "одного потоку на запит". Він використовує пульт потоків IO, ймовірно, щоб уникнути ускладнень з використанням асинхронних API вводу-виводу (а може бути, POSIX open()не можна зробити неблокуючим?). Таким чином, він амортизує будь-яку хіт продуктивності, коли традиційна модель fork()/ pthread_create()на запит повинна створювати та знищувати потоки. І, як згадується в постскрипті а), це також амортизує випуск простору стеку. Можливо, ви можете обслуговувати тисячі запитів, скажімо, 16 потоків вводу-виводу просто чудово.
бінкі

"Розмір стека за замовчуванням для сучасних потоків, як правило, досить величезний, але пам'ять, виділена системою подій на основі закриття, буде лише тим, що потрібно". Я складаю враження, що вони повинні бути того ж порядку. Закриття недешеве, час виконання повинен буде зберегти в пам'яті все дерево викликів однопотокової програми ("так би мовити, емуляція стеків") і зможе очистити, коли лист дерева буде випущений як пов'язане з ним закриття стає "вирішеним". Сюди буде включено багато посилань на цілі кучі речей, які неможливо зібрати сміття, і вони будуть вражати продуктивність під час очищення.
Девід Тонхофер

14

Я не розумію, що Node.js все ще використовує теми.

Райан використовує нитки для тих блокуючих частин (Більшість node.js використовує неблокуючий IO), оскільки деякі частини божевільно важко писати не блокуючими. Але я вважаю, що бажання Райана - це все, що не блокує. На слайді 63 (внутрішній дизайн) ви бачите, що Ryan використовує libev (бібліотеку, яка абстрагує асинхронне сповіщення про події) для неблокуючого eventloop . Через loop події node.js потребує менших потоків, що зменшує комутацію контексту, споживання пам'яті тощо.


11

Нитки використовуються лише для роботи з функціями, що не мають асинхронного об'єкта, наприклад stat().

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


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 запитів в одній зворотному базі даних.


3
Справа в базі даних - це більше питання не чекати відповіді, тримаючи інші запити (які можуть або не можуть використовувати базу даних), а краще запитати щось, а потім дозволити йому зателефонувати вам, коли він повернеться. Я не думаю, що це пов'язує їх між собою, оскільки це було б досить важко відстежувати. Крім того, я не думаю, що немає інтерфейсу MySQL, який дозволяє вам проводити кілька неблокованих відповідей на одне з'єднання (??)
Tor Valamo

Це лише абстрактний приклад, який пояснює, як петлі подій можуть запропонувати більшу ефективність, nodejs нічого не робить з БД без додаткових модулів;)
BGerrissen

1
Так, мій коментар був більше спрямований на 100 запитів в одній обороті бази даних. : p
Tor Valamo

2
Привіт BGerrissen: приємний пост. Отже, коли запит виконується, інші подібні запити будуть "слухати", як приклад staticFile.X вище? наприклад, 100 користувачів, які отримують той самий запит, буде виконаний лише один запит, а інші 99 прослуховуватимуть перший? Дякую !
CHAPa

1
Ви звучаєте так, що nodejs автоматично запам'ятовує виклики функцій або щось подібне. Тепер, оскільки вам не потрібно турбуватися про спільну синхронізацію пам'яті в моделі циклу подій JavaScript, простіше кешувати речі в пам'яті. Але це не означає, що nodejs магічно робить це для вас, або що це тип підвищення продуктивності, про який запитують.
бінкі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.