Пользователь
20 сентября 2011 в 11:54

Разработка → Что такое «асинхронная событийная модель», и почему сейчас она «в моде» из песочницы

Сейчас в тематических интернетах модно слово «Node.js». В этой небольшой статье мы попробуем понять («на пальцах»), откуда всё это взялось, и чем такая архитектура отличается от привычной нам архитектуры с «синхронным» и «блокирующим» вводом/выводом в коде приложения (обычный сайт на PHP + MySQL), запущенного на сервере приложений, работающем по схеме «по потоку (или процессу) на запрос» (классический Apache Web Server).

О читабельности статьи


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

О производительности


Современные высоконагруженные сайты типа twitter'а, вконтакта и facebook'а работают на связках вида PHP + Apache + NoSQL или Ruby on Rails + Unicorn + NoSQL, и ничуть не тормозят. Во-первых, они используют NoSQL вместо SQL. Во-вторых, они распределяют запросы («балансируют») по множеству одинаковых рабочих серверов (это называется «горизонтальным масштабированием»). В-третьих, они кешируют всё, что можно: страницы целиком, куски страниц, данные в формате Json для Ajax'овых запросов, и т.п… Кешированные данные являются «статикой», и отдаются сразу серверами наподобие NginX'а, минуя само приложение.

Я лично не знаю, станет ли сайт быстрее, если его переписать с Apache + PHP на Node.js. В тематических интернетах можно встретить как тех, кто считает системные потоки медленнее «асинхронной событийной модели», так и тех, кто отстаивает противоположную точку зрения.

Думая о том, на чём писать очередной проект, следует исходить из его задач, и выбирать ту архитектуру, которая хорошо накладывается на задачи проекта.

Например, если ваша программа поддерживает множество одновременных подключений, и постоянно пишет в них, и считывает из них, то в таком случае вам определённо следует посмотреть в сторону «асинхронной событийной модели» (например, в сторону Node.js'а). Node.js отлично подойдёт, если вы хотите перевести какую-нибудь подсистему на протокол WebSocket.

Примеры систем, которым хорошо подойдёт «асинхронная событийная модель»:
  • система в диспетчерской такси, следящая за перемещением каждого автомобиля, распределяющая поток пассажиров, высчитывающая оптимальные пути и т.п..
  • система поддержания жизнедеятельности, постоянно собирающая данные с множества раскиданных датчиков, и управляющая химическим составом, температурой, влажностью и т.п.
  • организм человека (мозг — логика управления, нервная система — канал передачи данных)
  • чат
  • MMORPG

Что такое «блокирующий» и «неблокирующий» вводы/выводы


Разберёмся с видами ввода/вывода на примере сетевого сокета («socket» – дословно «место соединения»), через который пользователь интернета соединился с нашим сайтом, и загружает на него картинку для аватара. В этой статье мы будем сравнивать «асинхронную событийную модель» с «привычной» архитектурой, где весь ввод/вывод в коде приложения — «синхронный» и «блокирующий». «Привычной» — просто потому что раньше всякими «блокировками» никто не заморачивался, и все так писали, и всем хватало. Что такое «синхронный» и «блокирующий» ввод/вывод? Это самый простой и обычный ввод/вывод, на котором пишется большая часть сайтов:
  • открыть файл
  • начать его считывать
  • ждать, пока не считается
  • файл считался
  • закрыть файл
  • вывести считанное содержимое на экран

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

При этом в коде нашей программы возникает «блокировка», во время которой поток простаивает, хотя мог бы заняться чем-нибудь полезным. Для решения этой задачи был придуман «синхронный» и «неблокирующий» ввод/вывод:
  • начать слушать сокет
  • если на нём нет новых данных, перестать слушать сокет
  • если на него уже поступила какая-нибудь порция данных картинки — считать эти данные
  • перестать слушать сокет

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

Существует ещё «асинхронный» ввод/вывод. В нашей статье мы не будем его рассматривать, но вообще это когда мы вешаем на сокет «функцию обратного вызова» (callback) из нашего кода, которая будет вызываться операционной системой каждый раз, когда на этот сокет будет приходить очередная порция данных картинки. И дальше уже забываем о прослушивании этого сокета вообще, отправляясь делать другие дела. «Асинхронный» ввод/вывод, как и «синхронный», делится на «блокирующий» и «неблокирующий». Но в этой статье под словами «блокирующий» и «неблокирующий» мы будем иметь ввиду именно «синхронный» ввод / вывод.

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

Предпосылки


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

Проверенная годами связка PHP + MySQL + Apache хорошо справлялась с «интернетом 1.0». Сервер запускал новый поток (или процесс, что почти одно и то же с точки зрения операционной системы) на каждый запрос пользователя. Этот поток шёл в PHP, оттуда – в базу данных, чего-нибудь там выбирал, и возвращался с ответом, который отсылал пользователю по HTTP, после чего самоуничтожался.

Однако, для приложений «реального времени» её стало не хватать. Допустим, у нас есть задача «поддерживать одновременно 10 000 соединений с пользователями». Можно было бы для этого создать 10 000 потоков. Как они будут уживаться друг с другом? Их будет уживать друг с другом системный «планировщик», задачей которого является выдавать каждому потоку его долю процессорного времени, и при этом никого не обделять. Действует он так. Когда один поток немного поработал, запускается планировщик, временно останавливает этот поток, и «подготавливает площадку» для запуска следующего потока (который уже ждёт в очереди).

Такая «подготовка площадки» называется «переключением контекста», и в неё входит сохранение «контекста» приостанавливаемого потока, и восстановление контекста потока, который будет запущен следующим. В «контекст» входят регистры процессора и данные о процессе в самой операционной системе (id’шники, права доступа, ресурсы и блокировки, выделенная память и т.д.).

Как часто запускается планировщик – это решает операционная система. Например, в Linux’е по умолчанию планировщик запускается где-то раз в сотую долю секунды. Планировщик также вызывается, когда процесс «блокируется» вручную (например, функцией sleep) или в ожидании «синхронного» и «блокирующего» (то есть, самого простого и обычного) ввода/вывода (например, запрос пользователя в потоке PHP ждёт, пока база данных выдаст ему отчёт по продажам за месяц).

В общем случае полагают, что «переключение контекста» между системными потоками не является таким уж дорогостоящим, и составляет порядка микросекунды.

Если потоки активно читают разные области оперативной памяти (и пишут в разные области оперативной памяти), то, при росте числа таких потоков, им станет не хватать «кеша второго уровня» (L2) процессора, составляющего порядка мегабайта. В этом случае им придётся каждый раз ожидать доставки данных по системной шине из оперативной памяти в процессор, и записи данных по системной шине из процессора в оперативную память. Такой доступ к оперативной памяти на порядки медленнее доступа к кешу процессора: для этого и был придуман этот кеш. В этих случаях, время «переключения контекста» может доходить до 50 микросекунд.

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

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

При создании системного потока, «стек» выделяется операционной системой в оперативной памяти не сразу целиком, а «кусочками», по мере его использования. Это называется «виртуальной памятью». То есть, каждому потоку выделяется сразу большой кусок «виртуальной памяти» под «стек», но на деле вся эта «виртуальная память» дробится на «кусочки», называемые «страницами памяти», и уже эти «страницы памяти» выделяются в «настоящей» оперативной памяти только тогда, когда в них возникает необходимость. Когда поток дотрагивается до «страницы памяти», ещё не выделенной в «настоящей» оперативной памяти (например, пытается отдать процессору команду записать туда что-нибудь), «блок управления памятью» процессора засекает это действие, и вызывает в операционной системе «исключение» «page fault», на которое она отвечает выделением данной «страницы памяти» в «настоящей» оперативной памяти.

В Linux'е размер стека по-умолчанию равен 8-ми мегабайтам, а размер «страницы памяти» — 4-рём килобайтам (под «стек» сразу выделяются одна-две «страницы памяти»). В пересчёте на 10 000 одновременно запущенных потоков мы получим требование около 80 мегабайтов «настоящей» оперативной памяти. Вроде как немного, и вроде как нет повода для беспокойства. Но размер требуемой памяти в этом случае растёт как O(n), что говорит о том, что с дальнейшим ростом нагрузки могут возникнуть сложности с «масштабируемостью»: что, если завтра ваш сайт будет обслуживать уже 100 000 одновременных пользователей, и потребует поддержания 100 000 одновременных соединений? А послезавтра — 1 000 000? А после-послезавтра — ещё неизвестно сколько…

Однонитевые серверы приложений лишены такого недостатка, и не требуют новой памяти с ростом количества одновременных подключений (это называется O(1)). Взгляните на этот график, сравнивающий потребление оперативной памяти Apache Web Server'ом и NginX'ом:

image

Современные web-серверы (включая современный Apache) построены не совсем на архитектуре «по потоку на запрос», а на более оптимизированной: имеется «пул» заранее заготовленных потоков, которые обслуживают все запросы по мере их поступления. Это можно сравнить с аттракционом, в котором имеется 10 лошадей, и 100 ездоков, которые хотят прокатиться: образуется очередь, и пока первые 10 ездоков не прокатятся «туда и обратно», следующие 10 ездоков будут стоять и ждать в очереди. В данном случае аттракцион — это сервер приложений, лошади — потоки из пула, а ездоки — пользователи сайта.

Если мы будем использовать такой «пул» системных потоков, то одновременно мы сможем обслуживать только то количество пользователей, сколько потоков у нас будет «в пуле», то есть никак не 10 000.

Описанные в этом разделе сложности, постоянно рождающие в умах вопрос о пригодности многопоточной архитектуры для обслуживания очень большого количества одновременных подключений, получили собирательное название «The C10K problem».

Асинхронная событийная модель


Нужна была новая архитектура для подобного класса приложений. И в такой ситуации, как нельзя кстати, подошла «асинхронная событийная модель». В основе её лежат «событийный цикл» и шаблон «reactor» (от слова «react» – реагировать).

«Событийный цикл» представляет собой бесконечный цикл, который опрашивает «источники событий» (дескрипторы) на предмет появления в них какого-нибудь «события». Опрос происходит с помощью библиотеки «синхронного» ввода/вывода, который, при этом будет являться «неблокирующим» (в системную функцию ввода/вывода передаётся флаг O_NONBLOCK).

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

«Событием» может быть приход очередной порции данных на сетевой сокет («socket» – дословно «место соединения»), или считывание новой порции данных с жёсткого диска: в общем, любой ввод/вывод. Например, когда вы загружаете картинку на хостинг, данные туда приходят кусками, каждый раз вызывая событие «новая порция данных картинки получена».

«Источником событий» в данном случае будет являться «дескриптор» (указатель на поток данных) того самого TCP-сокета, через который вы соединились с сайтом по сети.

Второй компонент новой архитектуры, как уже было сказано, – это шаблон «reactor». И, для русского человека, это совсем не тот реактор, который стоит на атомной станции. Суть этого шаблона заключается в том, что код сервера пишется не одним большим куском, который исполняется последовательно, а небольшими блоками, каждый из которых вызывается («реагирует») тогда, когда происходит связанное с ним событие. Таким образом, код представляет собой набор множества блоков, задача которых состоит в том, чтобы «реагировать» на какие-то события.

Такая новая архитектура стала «мейнстримом» после появления Node.js’а. Node.js написан на C++, и основывает свой событийный цикл на Сишной библиотеке «libev». Однако Яваскрипт здесь не является каким-то избранным языком: при наличии у языка библиотеки «неблокирующего» ввода/вывода, для него тоже можно написать подобные «фреймворки»: у Питона есть Twisted и Tornado, у Перла – Perl Object Environment, у Руби – EventMachine (которой уже лет пять). На этих «фреймворках» можно писать свои серверы, подобные Node.js’у. Например, для Явы (на основе java.nio) написаны Netty и MINA, а для Руби (на основе EventMachine) – Goliath (который ещё и пользуется преимуществами Fibers).

Преимущества и недостатки


«Асинхронная событийная модель» хорошо подойдёт там, где много-много пользователей одновременно производят какие-нибудь действия, не нагружающие процессор. Например: получают температуру с датчиков в режиме «текущего времени», получают изображения с видеокамер, передают на сервер температуру, снятую с прикреплённых к ним градусников, пишут новые сообщения в чат, получают новые сообщения из чата, и т.п…

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

Поэтому серверы, подобные Node.js’у, подходит только для выполнения задач, не нагружающих процессор, или как «фронтенд» для тяжеловесного «бекенда». А также они подходят как серверы по обслуживанию «медленных» запросов (узкий канал связи, медленная отдача/посылка данных, долгое время отклика где-то внутри, ...). Я бы отвёл серверам, подобным Node.js’у, место «вводяще-выводящего» посредника. Например, место посредника между «клиентом» и «сервером»: всё зрительное представление создаётся и отрисовывается непосредственно в обозревателе пользователя интернета, все нужные данные хранятся на сервере в хранилище, а Node.js выполняет задачу посредника, выдавая «клиенту» требуемые данные по запросу, и записывая в хранилище новые данные, когда они приходят от «клиента».

То обстоятельство, что серверы по «асинхронной событийной модели» запущены в одном системном потоке, на практике порождает ещё два препятствия. Первое — утечки памяти. Если Apache создаёт по системному потоку на каждый новый запрос, то, после отправки ответа пользователю, этот системный поток самоуничтожается, и вся выделенная ему память просто высвобождается. В случае же со, скажем, Node.js'ом, разработчику следует быть осторожным, и не оставлять следов при обработке очередного запроса пользователя (унижчтожать из оперативной памяти все улики того, что такой запрос вообще приходил), иначе процесс будет пожирать больше и больше памяти с каждым новым запросом. Второе — это обработка ошибок программы. Если, опять же, обычный Apache создаст отдельный системный поток для обработки входящего запроса, и обрабатывающий код на PHP выбросит какое-нибудь «исключение», то этот системный поток просто тихо «умрёт», а пользователь получит в ответ страницу типа «500. Internal Server Error». В случае же того же Node.js'а, единственная ошибка, возникшая при обработке единственного запроса, «положит» весь сервер целиком, из-за чего его придётся мониторить и перезапускать вручную.

Ещё один возможный недостаток «асинхронной событийной модели» – иногда (не всегда, но бывает, особенно при использовании «асинхронной событийной модели» для того, для чего она не предназначена) код приложения может стать сложным для понимания из-за переплетения «обратных вызовов». Это называется проблемой «спагетти-кода», и описывается так: «коллбек на коллбеке, коллбеком погоняет». С этим пытаются бороться, и, например, для Node.js’а написана библиотека Seq.

Ещё один путь устранения «обратных вызовов» вообще — так называемые continuations (coroutines). Они введены, например, в Scala, начиная с версии 2.8 (coroutines), и в Руби, начиная с версии 1.9 (Fibers). Вот пример того, как помощью Fibers в Руби можно полностью устранить коллбеки, и писать код так, как будто бы всё происходит синхронно.

Для Node.js'а была написана аналогичная библиотека node-fibers. По производительности (в искусственных тестах, не в реальном приложении) node-fibers пока работают где-то в три-четыре раза медленнее обычного стиля с «обратными вызовами». Автор библиотеки утвреждает, что эта разница в производительности возникает там, где Яваскрипт стыкуется с C++'ным кодом движка V8 (на котором основан сам Node.js), и что замеры производительности нужно трактовать не как «node-fibers в три-четыре раза медленнее коллбеков», а как «по сравнению с остальными низкоуровневыми действиями в вашем коде (работа с байтовыми массивами, подключение к базе данных или к сервису в интернете), отпечаток производительности node-fibers совсем не будет заметен».

В дополнение к привычному стилю программирования, node-fibers возвращает нам ещё и привычный и удобный способ обработки ошибок try/catch'ами. Однако эта библиотека не будет внедрена в ядро Node.js'а, поскольку Райан Даль видит предназначение своего творения в том, чтобы оставаться низкоуровневым и не скрывать ничего от разработчика.

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

Альтернативный путь


В этой статье мы объяснили, почему приложение, использующее «синхронный» и «блокирующий» ввод/вывод, не выдерживает большого количества одновременных подключений. В качестве одного из решений мы предложили перевод этого приложения на «асинхронную событийную модель» (то есть, переписать приложение, скажем, на Node.js'е). Этим способом мы решим задачу фактически (закулисным) переходом с «синхронного» и «блокирующего» ввода/вывода на «синхронный» и «неблокирующий» ввод/вывод. Но это не единственное решение: мы также можем прибегнуть к «асинхронному» вводу/выводу.

А именно, мы можем использовать старый добрый «пул» системных потоков (описанный ранее в этой статье), эволюционировавший на новую ступень развития. Эта ступень развития называется «зелёные процессы» (соответственно, есть ещё и «зелёные потоки»). Это процессы, но не системные, а созданные виртуальной машиной того языка, на котором написан наш код. Виртуальная машина запускает внутри себя обычный «пул» системных потоков (скажем, по количеству ядер в процессоре), и уже на эти системные потоки отображает свои внутренние «зелёные процессы» (полностью скрывая это от разработчика).

«Зелёные процессы» — это именно «процессы», а не «потоки», так как они не имеют никаких общих переменных друг с другом, а общаются только посылкой управляющих «сообщений» друг другу. Такая модель обеспечивает защиту от разных «deadlock»'ов и избегает проблем с совместным доступом к данным, ибо всё, что имеет «зелёный процесс» — это его внутреннее состояние и «сообщение».

Каждый «объект» имеет свою очередь «сообщений» (для этого создаётся «зелёный процесс»). И любой вызов кода «объекта» — это посылка «сообщения» ему. Посылка «сообщений» от одного «объекта» другому «объекту» происходит асинхронно.

В дополнение к этому, виртуальная машина создаёт свою подсистему ввода/вывода, которая отображается на неблокирующий системный ввод/вывод (и снова разработчик ни о чём не подозревает).

И, конечно же, виртуальная машина ещё содержит свой внутренний планировщик.

В итоге, разработчик думает, что он пишет обычный код, с обычным вводом/выводом, а на деле выходит очень высокопроизводительная система. Примеры: Erlang, Actor'ы в Scala.

Как «событийный цикл» опрашивает «источники событий» на предмет появления в них новых данных


Самое простое решение, которое можно придумать – опрашивать все «дескрипторы» (открытые сетевые сокеты, считываемые или записываемые файлы, …) на предмет наличия в них новых данных. Такой алгоритм называется «poll». Выглядит это примерно так:
  • у вас есть два открытых сокета
  • вы создаёте массив из двух структур, которые описывают эти сокеты
  • каждому элементу этого массива вы проставляете, что и о каком сокете в него записать
  • затем вы передаёте этот массив системной функции poll, которая пишет туда описание текущего состояния этих сокетов
  • после этого вы проходитесь по этому массиву ещё раз, выясняя, есть ли там для этих сокетов новые данные
  • если есть – считываете их, и делаете с ними что-нибудь
  • всё это повторяется для нового витка бесконечного «событийного цикла»
Причём упомянутый массив с данными передаётся не по ссылке, а именно копируется, по причине того, что операционная система состоит из «пространства ядра» и «пользовательского пространства», которые не могут иметь общих кусков памяти (для обеспечения безопасности системы).

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

При этом большинство (около 95%) полученного массива (для порядка 10 000 открытых сокетов) являются бесполезными, так как соответствующие сокеты не имеют новых данных.

А раз размер этого массива растёт пропорционально количеству дескрипторов, то получается, что алгоритм этот работает тем медленнее, чем больше сокетов открыто. То есть, чем больше одновременных посетителей на вашем сайте, тем больше «событийный цикл» начинает тормозить. В таком случае говорят: «алгоритм имеет сложность O(n)».

Можно ли написать более оптимальный алгоритм? Можно, и такие были написаны в основных серверных операционных системах: epoll в Linux’е и kqueue во FreeBSD. В Windows'е также имеется IO Completion Ports, которая является своего рода близким родственником epoll'а, и была использована разработчиками Node.js'а при переносе его на Windows, для чего ими была написана библиотека libuv, предоставляющая единый интерфейс как для libev, так и для IO Completion Ports.

Рассмотрим epoll. Он отличается от простого poll’а с двух сторон.
  • Программа не проверяет вообще все дескрипторы (и все виды событий), а подписывается только на те дескрипторы (и виды событий), которые ей нужны.
  • Ядро делает область памяти с данными дескрипторов видимой программе путём создания файла /dev/epoll (на самом деле это «устройство», но с точки зрения философии Linux'а «всё есть файл»). Этот файл программа может читать (и писать в него) с помощью функции mmap без какого-либо копирования вообще. Наличие новых сообщений при этом проверяется системной функцией ioctl

И если обычный перебор дескрипторов занимал O(n) времени, то эти оптимизированные алгоритмы требуют O(1) времени, то есть не становятся медленнее с ростом количества одновременных посетителей на сайте.

Пользователи, принявшие участие в правке статьи


Статья включает смысловые правки, предложенные пользователями: akzhan, erlyvideo, eyeofhell, MagaSoft, Mox, nuit, olegich, reddot, splav_asv, tanenn, Throwable.
А также синтаксические и стилистические правки, замеченные пользователями: Goder, @theelephant.

Ссылки по теме


Вы наверное шутите, мистер Дал, или почему Node.js — это венец эволюции веб-серверов
Введение в EventMachine
Scalable network programming
Kqueue
Stackless Python и Concurrence
Чем отличаются блокирующий и неблокирующий вводы/выводы (а также асинхронный, и другие)
node-sync — псевдо-синхронное программирование на nodejs с использованием fibers
Как работает кеш процессора
What every programmer should know about memory
10 things every Linux programmer should know
Video: Node.js by Ryan Dahl
No Callbacks, No Threads & Ruby 1.9
Asdfs Afasdfas @kuchumovn
карма
0,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (130)

  • 0
    Очень познавательно, спасибо!
    Видно, что какого-то эталонного решения пока не найдено, но поиск идёт, и это хорошо
    • 0
      Поиск уже давно прошёл стороной тех кто пишет подобные статьи :)
      советую лучше прочитать Programming paradigms for dummies и перестать искать какое-то «эталонное решение», тк это ни к чему не приведёт.
    • +23
      В нашем мире, я так полагаю, вообще не существует эталонных решений, и любое решение заточено под свою область применения (поэтому, кстати, изучая любое решение нужно обязательно изучать и контекст его применения, и предпосылки к его возникновению).

      Что лучше: километры или сантиметры? Для географа — километры. Для портного — сантиметры.

      Что лучше: арбуз или кусок мяса? Посреди пустыни — арбуз. В сибири зимой — кусок мяса.

      Слово «лучший» почему-то исторически считают само собой разумеющимся и не требующим пояснения.
      Из-за чего у детей возникает когнитивный диссонанс, типа: — «Мой папа лучше!», — «Нет! Мой папа лучше! А ты получи в морду!».

      Я бы предложил что-то типа: «лучший для X в условиях Y — дающий X наибольшую выгоду в условиях Y». А «выгоду» каждый сам уже может определить для себя.

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

      Смысл всех этих сравнительных таблиц — предоставить человеку удобный инструмент определения наилучшего для него решения в текущий момент времени.
      Чтобы это было доступно каждому, и чтобы это можно было сделать быстро.
      • +2
        Отличный ответ! Надеюсь, это хоть немного заткнет холиварщиков, вечно пытающихся обосновать «лучшесть» какого-либо языка или платформы.
        • +2
          Извините, но хорошие холиваршики обычно пытаются обосновать не «лучшесть», а «практическую приемлемость и выигрышность той или иной платформы для того или иного набора факторов». Если вы перестали холиварить, значит ваше развитие закончилось <=> смерть программиста внутри.
          • 0
            Спасибо, очень показательный комментарий! Плюсанул.
            p.s. «Кто в юности не был революционером — лишен сердца, а кто потом не стал консерватором — лишен мозгов.»
            У. Черчилль
  • +5
    Мало кто об этом сейчас помнит, но у такого непопулярного языка программирования, как Tcl, практически с самого рождения событийная модель не только присутствовала, но и всячески поощрялась. Более того, применительно к вебу существуют такие штуки как древний tclhttpd, а также значительно более современный Wub, в котором всё завязано на асинхронный ввод-вывод (хотя, например, потоки поддерживаются, но практически не нужны). А если вспомнить про наличие в Tcl coroutines, всё становится ещё шоколаднее…
  • –1
    >порядка 10 000 одновременно работающих потоков, которые постоянно что-то вводят/выводят
    Статья доставила :)
    • 0
      ага ) меня тоже порадовало
    • 0
      Причём замечательно выводят :-)
  • +2
    Великолепно. Очень приятно было почитать.
    Если хотите, можете добавить что под Windows аналогом epoll является технология «Completion Ports».
    • –1
      Completion Ports не является аналогом epoll
      • +3
        Completion Ports является аналогом epoll :).
        • –1
          Я писал приложения, использующие iocp и epoll, а не обёртки вроде asio. А вы? :)
          Мне прям интересно, вы не любите признавать ошибки или правда верите в то что утверждаете?
          • +3
            Я писал на Completion Ports сервер, способный держать миллион входящих подключений. А вы сейчас будете угрюмо троллить нюансы реализации и пытаться по-своему трактовать термин аналог. Можете себя не утруждать — API у них, безусловно, разное. Но и то, и другое решает одну и ту же задачу — это асинхронная эволюция select() способная работать с тысячами подключений.
            • 0
              а почему для обслуживания миллиона входящих подключений с помощью iocp вам приходилось под каждый сокет выделять буфера для чтения? А вот мне с аналогичным epoll'ом для этого не потребовалось выделять под каждый полумёртвый сокет этот кусочек памяти. Возможно ваш аналогичный iocp имеет проблемы с масштабируемостью? )
              • 0
                Потому что аналог — это не то же самое что точная копия. Я не знаю, какие причины побудили архитекторов Windows аллоцировать память при создании запроса WSARecv(), но могу предположить что это связано с оптимизацией по скорости работы. Несколько лишних гигабайт памяти сейчас ничего не стоят.

                • 0
                  ладно, можете считать его аналогом :) Но никогда не говорите об этом тем кто писал кроссплатформеные приложения с использованием iocp и epoll, это очень больная тема для них )
                  • +2
                    Что, тяжко с Completion Ports после epoll? Ну вы не один такой, мы с ним тоже <вырезано цензурой> изрядно :). Неудобный интерфейс, куча недокументированного поведения. Но это как бы WinAPI, от него ничего другого и не ожидается. Но по функциональности это аналоги — решают одни и те же задачи похожим образом.
            • НЛО прилетело и опубликовало эту надпись здесь
              • 0
                А вот win2k8- верит :)
                • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Простите что влезаю, но по-моему, они решают одну и ту же задачу — быстрое и масштабируемое (с количеством сокетов в отличие от select/poll) уведомление приложения о сетевых событиях. В чем же не аналогичность?
            • 0
              Наверно потомучто iocp был создан для рисования прогресс баров во время копирования файлов :)
              • 0
                Вы подменяете IO Completion Ports и Overlapped IO. IOCP в Windows много где применяется. И высоконагруженные сетевые сервера — это одно из основнаых применений, см. MSDN.
    • +2
      И еще можно добавить, что для Node.JS разработана новая библиотека libuv, которая является единым интерфейсом для libev и IOCP.
  • +1
    Статья написана доступным языком, достойным Википедии (хотя, там уже есть) и снабжена ссылками. Это очень хорошо.
    По сравнению с аналогичной отличной статьёй о том же самом, но в применении к nodeJS, из ссылок и фактов почёрпнуты дополнительные данные, спасибо.

    … Этот шаблон «reacror» известен давным-давно — микроконтроллеры, не имеющие своей развитой системы событий, вовсю его используют, опрашивая порты и реагируя на них в бесконечном цикле (или как вариант, в одиночном цикле обхода после события таймера). Наконец-то он получил своё имя :).
  • +1
    Любой веб сервер функционирует не совсем так как Вы описали. У него есть уже пул предсозданных тредов (рабочих лошадок). Любой запрос перенаправляется для выполнения свободному треду. По окончании тред возвращается обратно в пул.
    Но даже при 10000 тредах большинство из них находится в состоянии ожидания. Проблема переключения контекстов отчасти решается при помощи Green Threads, когда N виртуальных тредов мэпятся в M нативных (M
    • +2
      … (обрезали)… (M «много меньше» N). Планировщик позволяет эффективно распределить очередь задач для каждого треда.

      Переключение контекстов и большое количество тредов — это далеко не основная причина выбора асинхронной модели. Более того, в асинхронной архитектуре будет больше переключений контекста, нежели в синхронной. Основная задача — это отказ от разделяемых состояний и соответственно исключение транзакций и блокировок общих ресурсов из работы системы. Именно там теряется производительность, особенно если какой-то ресурс не очень быстрый. Вместо одной долгой транзакции, связывающей и блокирующей одновременно множество компонентов на все время выполнения, каждый компонент работает независимо, а связь между ними осуществляется асинхронно. Управляет всем этим асинхронный процесс или конечный автомат (что по-сути одно и то же).

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

      Как плата за масштабируемость — это потеря целостности состояния всей системы в любой момент времени. Поэтому задача должна быть сначала адаптирована, чтобы целостность не была критична. В самом деле, не критично, если на френдленте пост появится на пару секунд позже, нежели в блоге афтора.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Спасибо за замечание.
      Я действительно не задумывался о различиях «асинхронного» и «неблокирующего» ввода/вывода.
      Посмотрел здесь: stackoverflow.com/questions/2625493/asynchronous-vs-non-blocking
      Пишут, вообще, как я понял, что неблокирующий — это сразу ошибку выкидывать, а асинхронный — это «асинхронный».

      Можете мне покидать ссылок на эту тему?
      Если я смогу почитать и понять то, о чём Вы написали — я поправлю статью соответствующим образом.
      • 0
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Ох, наконец, вкурил.
          Сейчас поправлю статью.
          Получается, нужно уточнить, что реактор у нас синхронный (если нет данных — идёт дальше, а на следующем цикле снова спросит).
          • НЛО прилетело и опубликовало эту надпись здесь
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Я тоже доволен, что не зря писал — провёл небольшую генеральную уборку в своих мозгах)
  • +1
    Я когда-то писал на C++ сервак для баннербанка, он, конечно же был асинхронным и работал данные через неблокирующий IO. Это позволило показывать примерно 1000 запросов в секунду на машине класса P3.

    Но я по прежнему не догоняю, почему нужны коллбэки. То есть можно, да, но зачем? У меня была структура данных — и FSM на каждый запрос, и их общий массив, по которому я проходился после poll (тогда еще еpoll не внедрили)

    Я делал так — читал из сокетов асинхронно до конца HTTP запроса, распаршенный опрос кидал в пул потоков-обработчиков, потом обработчик формировал строку ответа и она опять асинхронно отдавалась обратно.

    Но сам обработчик — формировалка строки — был абсолютно синхронный. Я сразу генерил HTML код. Никак не пойму зачем нужно такое усложнение, как формировать ответ по коллбэкам. Наверное, это круто для каких-то comet вещей, но в общем случае, даже для highload… Достаточно что сама работа с сокетами будет асинхронной.
    • 0
      Да, черт, путаница слов. Короче был non-blocking io.
      • 0
        Это нужно например если при обработке так же встречаются какие-то тяжелые операции, например запросы к БД. В остальных же случаях — действительно только лишняя морока при отладке.
        • 0
          То есть, применительно к вебу, это целесообразно, когда время обработки каждого запроса таково, что если просто параллелить генерацию ответов на фиксированное количество тредов — то оно заткнется в этой самой генерации на блокирующий IO с SQL сервером? То есть мы можем еще добавить неблокирующий IO с SQL cервером, и коллбэки вешаются на события внутри генерации ответов?

          Я когда-то ( в 2002 ) понял что если я буду в баннербанке дергать SQL, то все сдохнет намертво, поэтому мы делали так- раз в пол часа на основе БД генирились xml-ные планы показов, которые, после всасывания их в отдельном треде просто становились гигантскими деревьями/хэшами в памяти сервера и вся работа уже шла на основании их. Ну там были нюансы по поводу памяти/mmap, но это уже детали.

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

          А есть примеры масштаба сайтов, когда такая асинхронная генерация ответов была бы разумным применением, а не джастофаном?
          • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            агрегация данных.
    • +1
      >Но я по прежнему не догоняю, почему нужны коллбэки.
      вы не один с этими страданиями :)
      обслуживать сокеты в любом случае удобнее с помощью fsm'ки,
      а обрабатывать запросы можно по-разному, зависит от того какие правила игры мы установим на обработку этих запросов.
      просто оч часто люди пытаются протолкнуть одно решение, которое им показалось чертовски гениальным во все места, даже где оно может быть совсем не пригодным.
    • +2
      я вам расскажу страшную тайну: коллбечная модель — это не просто шаг назад. Это сели на мотоцикл и быстро-быстро поехали обратно в лохматые 70-е.

      Т.е. если кто-то радостно разглагольствует про то, как клево программировать на коллбеках, вообще его не трогайте. Психиаторы рекомендуют именно такую модель поведения.

      Вы, судя по всему, разработали систему файберов, т.е. синхронный код со своим шедулером. Это хорошо и правильно.

      • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Здравствуйте.
        А Вы не Максим Лапшин, случайно?
        Ваши комментарии сейчас добавлю к статье.
        Можете ещё покритиковать конструктивно?
  • 0
    А как Erlang вписывается в эту «новую модель»?
    • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Добавил Эрланг в статью.
  • 0
    >Например, подобный «спагетти-код» является бичом программирования на Node.js’е (коллбек на коллбеке, коллбеком погоняет). Об этом знают, и с этим пытаются бороться.

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

    Кстати в Windows 8 такая же асинхронность и такой же JavaScript. Надеюсь критики ноды не оставят этот момент без внимания.
    • 0
      В принципе, наверное, Ваша правда в том, что кривизна кода зависит в первую очередь от самих разработчиков, так что поправлю статью в этом месте.
      Может быть у Вас и чистый код.
      Но в среднем, будем считать, что склонность к лапше присутствует.
    • +3
      > Еще более простое решение — не использовать анонимные функции.

      О как это интересно. Т.е., если взять сложную лапшу с кучей ассинхронных вызовов, которые не параллеляться, и разрезать её на несколько неанонимных фукнций, как раз по этим вызовам, то это уже не спагетти-код?
      • –2
        Именно.
        • 0
          Забавно :) Что вы понимаете под определением «спагетти-код».
        • 0
          Насколько я понял, люди такое уже пробовали делать, и итог их не очень впечатлил.
          gist.github.com/839545
          • 0
            если на этот код внимательно посмотреть (и даже если не очень внимательно), то видно что f, bcd, cd все выглядят по сути одинаково. а это что означает? это означает, что можно абстрагировать это логику отдельно и переиспользовать:

            function seq(funcs, cb) {
            var i = 0;
            function continue (err) {
            if (err || i == funcs.length) cb(err);
            funcs[i++](continue);
            }
            continue();
            }

            function f(cb) {
            seq([a, b, c, d], cb);
            }


            не сказать, что прекрасно (я сам не поклонник все-через-callback программирования), но вполне нормально, особенно если эта абстракция в рамках проекта используется единообразно.
            • 0
              согласен с Вами в том, что в подобных случаях стоит попробовать найти какой-то общий шаблон построения кода приложения, и выделить его в небольшую библиотеку, берущую часть «некрасивостей» на себя.
      • 0
        Это как Биг-мак: если все месте, то это плохая еда, а если булочку отдельно. салатик отдельно. помидорку отдельно. котлетку отдельно, то это уже здоровый комплексный обед.
    • НЛО прилетело и опубликовало эту надпись здесь
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    весьма хорошее описание. буду сюда ссылать всех кто мня терроризирует подобными вопросами.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      спасибо, вы правы.
      я работаю над этим.
  • 0
    Может я что-то не так понял из текста но проблема в многопоточных системах в основном не в дороговизне контекст свитча.
    • 0
      Может быть это я что-то не понял, а Вы поняли, поэтому напишите, пожалуйста, подробнее.
      • 0
        Здесь стоит уточнить, что такое контекст потока управления.
        В классическом юниксе, например, это контекст процесса со всеми прибамбасами, со своим адресным пространством. Если 1000 форкнутых Апачей реально пугают людей, то 1000 Java threads на аналогичной задаче как-то не особо напрягают. Какой-нибудь Erlang диспатчит на том же железе потоки вообще тучами и не чихает.

        Прелесть nodejs'а заключается в том, что он довольно элегантно прячет от программера всю маету с асинхронностью реального мира, предоставляю одну единственную нить, где все замечательно синхронизовано. Но взамен требует нарезать «юнитс оф ворк» весьма замысловатым способом.
        • 0
          Насколько я читал, потоки в Яве — системные:
          en.wikipedia.org/wiki/Green_threads#Green_threads_in_the_Java_virtual_machine

          Вообще, я не системный программист, поэтому не разбираюсь в тонкостях различий.
          Вроде как я там написал (из википедии), что контекст — это память, связанная с потоком, его блокировки, «всякие прибамбасы».
          Если это неправильно, Вы можете переформулировать моё описание, и я поправлю его в статье.
          • 0
            Что-то я не понял, Вы пишите про «системные потоки» в Java, но даете ссылку на green threads, где упомянута реализация 1.1 от 1997 года. Короче говоря, это было так давно, что даже я уже не помню.

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

            В nodejs тоже переключается контекст в конце колбэка, если можно так выразиться, только там вся магия внутри скрыта. Ну и то, что адресное пространство одно внутри VM тоже многое упрощает.
            • 0
              Ну, я, вообще, с Явой начал знакомиться только в 2006-ом, так что не знаю, что там было до 1.5.

              Тогда, получается, контекст содержит: регистры, стек (тот самый сегмент стека, который ss, если я правильно понял), но не адресное пространство.

              Я вообще в понятиях этих не силён, но, насколько я понимаю, «адресное пространство» — это по какому адресу какая переменная лежит в оперативке.
              Все переменные, к которым есть доступ из потока (в том числе локальные) составляют это самое «адресное пространство».

              Правильно ли я это понял?
  • 0
    Мне вот нужен был обработчик коннектов к сокету. Принимает подключение от флеш-клиентов на одном сокете, принимает от них команды, передаёт на другой и обратно. Одновременное кол-во клиентов — не более десяти. Не тысяч, просто десяти. Сначала возился с похапе (cli), получилось всё реализовать (даже с форками) но коряво (понимаю, php для этого, мягко говоря, не очень). Вчера вечером вспомнил что слышал что-то о ноде, попробовал. К утру уже всё заработало в лучшем виде (приём подключений, авторизация в БД и т.д.) Однако опытные товарищи закритиковали, мол нода ацтой и всё такое прочее. А мне SMP-то и не нужен, дикая хайлод не нужен. Поток команд тоненький… Оправдан ли в моём конкретном случае выбор ноды? или всё ж не стоит?
    • 0
      Вы пишете прокси, получается? Думаю, для таких вещей оправдан. Вот человек писал тоже на событийном цикле подобную вещь: habrahabr.ru/blogs/ruby/126231/
      • 0
        Ну, в принципе, это можно назвать проксёй. Но не в том смысле, в каком обычно понимается. Это одна из частей системы управления вот этими красавцами
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Вообще там от сервера до бота намного меньше чем 50мс, а боты очень шустрые. Но с ретрансляцией команд справлялся даже php. Но php с костылями, а в ноде всё нативно, так что намного проще и стройнее получилось.
            • 0
              А вот на отдаче видео с робота — однозначно Erlyvideo, как раз его ниша
        • 0
          прикольно
    • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Уточнаяющий вопрос:

    "«Асинхронная событийная модель» хорошо подойдёт там, где много-много пользователей одновременно запрашивают какие-нибудь «легковесные» куски данных"

    У нас неблокирующий кусок кода — тот же твитер, достаем из мемкеша например последние твиты Васи. В чем разница по существу между nginx и node.js? Более того, если я верно понимаю, node.js работает только на 1 ядре (если конечно у нас 1 копия запущена только). Те nginx саму обработку запроса может сам раскидывать по ядрам (точнее это делает система тк каждый вокер работает на своем ядре как бы), а node.js работает на одном ядре.

    В каком моменте будет экономия?
    • 0
      Для статики конечно же нужен только nginx.
      А если система динамичная? Скажем, тыща датчиков температуры на космическом корабле, где особо не покешируешь, и система, которая это всё «аггрегирует», то там я бы предложил писать на Node.js.

      Node.js не поддерживает на текущее время многоядрёности. Говорят, это называется «SMP» (Symmetric Multiprocessing): ru.wikipedia.org/wiki/%D0%A1%D0%B8%D0%BC%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%BC%D1%83%D0%BB%D1%8C%D1%82%D0%B8%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5
      Когда начнёт поддерживать SMP — тогда будет использовать мощь всех ядер.

      Сейчас для Node.js есть фреймворк Express, у которого есть плагин для хранения сессий в Redis'е.
      Если сделать так, то, думаю, можно будет запустить Node'ов по количеству процессоров, и всё будет работать как нужно.

      Это моё мнение.
      • 0
        космический корабль на JS это круто :)

        ок, немного поменяем ситуацию.
        Допустим у нас одноядерный проц и мемкеш на удаленносм серваке, те у нас возможны лаги в сети, и надо сделать запрос к мемкеш для формирования одной страницы.

        Как я понимаю конкретно в приеме соединений у node.js нет преимуществ перед тем же nginx, который так же юзает epoll, верно?

        Будет ли событийная модель работать для мемкеш запросов? Те я представляю так: пришли 10 запросов одновременно. nginx распихал их по вокерам, каждый вокер открывает соединение с удаленным серваком и хочет получить инфу из мемкеш. Те 10 запросов одновременно висят и по идее система периодически переключается между каждым из 10 процессов и каждый из них делает проверку типа «а что ж у нас там пришло в сокет от удаленного сервака».

        node.js: не знаю как на самом деле конечно, теоретически он, учитывая что все эти 10 запросов идут фактически внутри одного процесса, может их как бы объеинить в пул такой и по методу epoll ждет когда появится данные от удаленного сервака и в сязи с этим у системы нет необходимости делать context switch между 10 процессами и за этот счет идет определенная экономия.

        так ли оно на самом деле?
        • 0
          Я для себя пока решил, что выигрышь ноды относительно систем с синхронным вводом/выводом состоит именно в том, что она однонитевая, и у неё свой простой «планировщик» — пройтись последовательно по всем дескрипторам и проверить на них наличие новых данных.
          Если nginx использует асинхронный ввод/вывод, то, думаю, он со своими несколькими воркерами отлично справится, мб даже лучше, чем Node.js — ведь на асинхронном вводе/выводе нет постоянных блокировок.
          Но я не знаком с устройством nginx'а, поэтому тут могу только гадать.
    • 0
      Кстати, всякие вконтакты и твиттеры, насколько я понимаю, не парятся, и просто всё кешируют, и у них всё работает нормально.

      Тогда, наверное, можно сбавить тон статьи.
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        да, но вопрос не совсем в этом. То что nginx умеет эффективно отдавать статику я знаю, но тут то не статика.
    • 0
      Внёс в статью оговорки о том, что наша система не кешируется, и добавил в начале абзац «о кешировании».
  • 0
    На кой черт все эти ссылки? И более того, на кой черт эти странные локализации терминов?
    Если читатель этого не знает — ему не нужна ваша статья, потому что ему не интересно. А если знает — то ему не нужны эти ссылки(Правда, скорее всего не нужна статья, потому что это проходят на первом курсе университета, кроме, разве что, деталей epoll.).
    • 0
      Если Вам не интерестно, то что Вы здесь делаете?
      • –1
        Во-первых, я не говорил, что мне не интересно.
        Во-вторых, я не могу узнать, интересно мне или нет до прочтения статьи.
        В-третьих, вам то какое дело?
    • 0
      Ну, я не проходил ничего из этого на первом курсе своего университета, потому что учился на физфаке.
      Кто-то вообще не имел возможности учиться в универе.
      Вы считаете этих людей недостойными приобщиться к касте программистов?
      Кстати, Вы не учились, случайно, году этак в 2005-ом на мехмате МГУ?
      Я просто помню там с местного форума, парень был один, тоже с ником «Stroncium», и почему-то меня троллил по кд.
      • 0
        Ну, подразумевалось проходят в другом смысле. Мне тоже в университете ничего этого не рассказывали.

        Достойность и недостойность — вообще отвлеченная тема. Я, вроде, получше многих, но тем не менее врядли могу уверенно назвать себя достойным. Кроме того, эта статья никого программистом не сделает.

        Нет, не учился в МГУ. Кстати, наверное, вы что-то путаете с его ником, возможно это был «Strontium», потому что исходя из результатов поисковых запросов, кроме одной девушки и меня до последней пары лет stroncium'ов в рунете больше не было, а во всем интернете была только пара малоактивных людей.
  • +2
    А почему вы вообще решили написать статью по довольно обширным темам, если не разбираетесь в:
    >Что такое «асинхронная событийная модель»
    — парадигмах программирования (прочитайте Concepts, Techniques, and Models of Computer Programming)

    >Технические подробности о модели сервера приложений
    — системное программирование (прочитайте The Linux Programming Interface: A Linux and UNIX System Programming Handbook)

    >По мере того, как растёт или память, связанная с потоком, или количество потоков, начинает не хватать «кеша второго уровня» (L2) у процессора, который не может уже вместить в себе все эти «контексты» потоков, и эти «контексты» приходится начинать писать в обычную оперативную память (и потом доставать их оттуда)
    — память (прочитайте What every programmer should know about memory)

    > Новый «тренд» был вызван тем, что серверы с традиционной многопоточной архитектурой перестали справляться с возрастающей нагрузкой в «веб-два-нольном» интернете.
    — архитектура существующих highload приложений (ну тут чего-то отдельного вроде не найти, но всё легко гуглится)

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

    Ну и так для общего развития:
    Проблема с трэдами ОС больше в том что нужно выделять много памяти под стэк (гугловцы реализовали в последнем гцц интересную штуку под названием split stacks, когда он динамически может расширяться, но для этого все либы и програму надо компилировать с этим флагом), а для обслуживания сокетов с блокирующим IO их не используют тупо потомучто это оч неудобно.
    Похожая проблема под винду с iocp — это когда на каждое полуживое соединение надо выделять бессмысленые буфера для чтения, из-за чего система хуже масштабируется. В node.js скорее всего сделали всё как proactor pattern и теперь при использовании линуксов получают такой же недостаток как и под виндой, зато теперь они кроссплатформеные(не смотрел libuv — так что это моя догадка, ибо так почти все делают)

    Разделяйте сущности в системе, то что подходит для работы с сокетам, возможно будет плохо работать с обработкой запросов от сокетов. Например сокеты у нас могут жить вечно и их поведение может быть довольно непредсказуемым, как например события о том что сокет закрылся итп, поэтому очень удобно всё это реализовывать с вашими любимыми колбэками. У запросов обычно ограничения по времени выполнения ~30сек и их поведение довольно предсказуемо, поэтому тут мы можем использовать простенькие аллокаторы памяти со сборщиками мусора, ну а модель, которую выбрать для реализации логики риквестов может быть совсем разной — это уже зависит от задач, но тупое использование колбэков не даст тот выигрыш, который нужен большинству сайтов — понижение лэйтэнси, а не увеличение rps.
    • 0
      Чтобы получить инвайт и годовую подписку на Bookmate.
      Шутка.
      На самом деле я сначала просто решил сам со всем этим разобраться, потом решил, что это будет полезно всем, а потом ещё выяснилось, что без помощи коллективного разума самому разобраться в этой теме будет очень сложно.
      Собираюсь устранить все противоречия в этой статье, и сделать её неким подобием ФАКа для интересующихся.
      В этом мне помогут все, кто явно укажет на противоречивые утверждения в статье, и предложит свою правильную замену этим утверждениям.
      Пойду читать Ваши ссылки.
      • 0
        >Собираюсь устранить все противоречия в этой статье, и сделать её неким подобием ФАКа для интересующихся.
        Тогда нужно начать с разделения взаимодействия с ОС и тем что будет построено в итоге.
        То что блокирующий IO+куча ОС тредов будет неэффективным для обслуживания кучи сокетов при использовании примитивов ОС, ещё не значит что блокирующий IO нельзя сделать эффективным, спрятав всю эту низкоуровневую часть.
        Поэтому я даже не знаю как тут можно привязать тему «асинхронной событийной модели» и почему она сейчас «в моде». Так как поверх низкоуровневых конструкций можно делать что угодно.
        Например представьте такую выдуманую декларативно-императивную штуку вроде мэйкфайлов:
        get_session:
          memcache.get('session')
        
        get_user_info depends on get_session:
          db.get('user_info', session)
        
        get_item_data:
          db.get('item_data')
        
        generate_page depends on get_user_info, get_item_data:
          render_page(user_info, item_data)
        

        и вот теперь чтобы сгенерировать страничку(generate_page), наш планировщик строит деревяшку зависимостей и начинает выполнять их, причём планировщик может одновременно получать данные о пользователе и данные о предмете, тк они не зависят друг от друга. Пример конечно тупой и надуманый, но надеюсь отлично демонстрирует что без всякой «асинхронной событийной модели» мы можем генерировать странички и даже получим выигрыш в понижении лэйтэнси, тк будем одновременно делать несколько запросов к базе.
        • 0
          Мы рассматриваем синхронный ввод/вывод.
          Значит, эта выдуманная штука выполняет каждый таск (из тех, что через запятую) в своём потоке.
          И, получается, каждый такой поток вызовет блокировку и «переключение контекста», и я не вижу здесь выигрыша по сравнению с обычным apache -> php -> db

          Может быть я набил Вам уже оскомину этим словосочетанием, но как я понял, 10 000 потоков одновременно, которые блокируются, и вызывают планировщик, душат производительность именно «переключением контекста», которое является (видимо) слишком дорогостоящим, если вызывается, скажем, 10 000 раз в секунду.
          Если же основной душитель — не «переключение контекста», то можете ли Вы мне объяснить, кто это?
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Я много писал на Яве, и знаю, что там есть слово synchronized. Если его не ставить — то, типа, хоть сколько тредов запусти, каждая из них будет видеть свою картину окружающего мира. И, как я понял, в таком случае не будет никаких блокировок и тразакций на общие ресурсы (никто ничего не обеспечивает, можно даже успеть записать пол-лонга, а другой считает пол-нового лонга и пол-старого).

              Получается, как я понимаю, основной тормоз — это стек. nuit, вроде как, об этом написал (цитирую): «Проблема с трэдами ОС больше в том что нужно выделять много памяти под стэк». Все стеки не влезают в кеш процессора — и нужно каждый раз идти за ними в оперативную память.

              Спросил у гугла, пишут, что по-умолчанию каждому потоку выделяется стек в 8Мб: adywicaksono.wordpress.com/2007/07/10/i-can-not-create-more-than-255-threads-on-linux-what-is-the-solutions/
              Это ж ни в один процессорный кеш не войдёт…
              Получается, при переключении контекста гоняются туда-сюда возможно безполезные 8Мб?
              • 0
                >Получается, как я понимаю, основной тормоз — это стек
                тут не в тормозах дело, а в масштабируемости. Даже если будет под стэк выделяться 64кб, то для обслуживания 10к входящих соединений потребуется как минимум выделить 625мб виртуальной памяти.
                Ну и tanenn правильно сказал, что с такой моделью очень большой удар по производительности будет в блокировках, тк скорее всего придётся часто обращаться к общим ресурсам.

                >Получается, при переключении контекста гоняются туда-сюда возможно безполезные 8Мб?
                кеш работает иначе, никто-ничего не гоняет при переключении контекста :) В «What every programmer should know about memory» всё отлично расписано.
                • 0
                  Да, действительно, со стеком получается, что тормоза от его размеров не зависят.
                  Значит, если у нас 10 000 Апачевских воркеров, то сколько бы каждый из них памяти ни занял, не это вызывает тормозов.

                  Что тогда их может вызывать?

                  Вы говорите, что это «блокировки общих ресурсов».
                  Я немного недопонимаю это выражение.
                  Общие ресурсы — это, видимо, общие переменные, общие дескрипторы, мб что-то ещё.
                  Не знаю, как устроены воркеры в Апаче, но я бы делал их без какой-либо синхронизации переменных.
                  То есть, на чтение и исполнение функций нет ограничений, их могут все одновременно читать и выполнять.
                  Общих переменных для записи мы держать не будем (типа счётчиков запросов), вроде как незачем.
                  Каждый воркер будет независим, иметь только свои дескрипторы, и в чужие не лезть.

                  В таком случае не должно быть никаких «блокировок общих ресурсов»?
                  Или я себе не так представил весь этот процесс?
                  • 0
                    Я если честно даже не понимаю о каких тормозах речь.
                    Есть ли примеры когда какой-нибудь типичный сайт на node.js+postgresql выдавал больше rps'ов чем на примитивной связке nginx+uwsgi+python+postgresql?
                    • 0
                      Я не помню, откуда я это взял.
                      Сейчас пошёл гуглить, и нашёл какой-то замер: www.synchrosinteractive.com/blog/9-nodejs/22-nodejs-has-a-bright-future
                      Там говорят, что если в PHP-коде поставить sleep, то Node.js оказывается гораздо быстрее.

                      Здесь бенчмарки, и, видимо, при росте одновременных подключений, Апаче всё больше отстаёт от Node.js'а:
                      code.google.com/p/node-js-vs-apache-php-benchmark/wiki/Tests
                    • 0
                      Хотя, вот этот бенчмарк, на который я ссылку дал, он никуда в базу не ходит, а просто отдаёт Hello World.
                      Получается, без блокировок в коде.
                      Значит, Node.js выигрывает имеенно на этапе работы с сокетами, а не в области самого кода?
                    • 0
                      Если моя мысль верна, то почему Node.js лучше работает со входящими подключениями, чем Апач?
                    • 0
                      Запощу ещё пару ссылок.
                      В каком-то смысле, они противоречат друг другу.
                      На stackoverflow кто-то спросил, почему однонитевые серверы быстрее Апача, и ему какой-то (с виду разбирающийся) человек тоже начал говорить про «context switch»'и
                      stackoverflow.com/questions/2583350/is-epoll-the-essential-reason-that-tornadowebor-nginx-is-so-fast

                      Здесь же человек проводит свои бенчмарки, и у него Реактор оказывается медленнее Апача, и он вообще критикует тех, кто считает системные потоки более медленными, чем Реактор с его событийным циклом:
                      kaishaku.org/twisted-vs-threads/

                      Какие-то «взаимоисключающие параграфы»…
                      Можно ли вообще в этом деле поставить точку, или это какой-то очередной «холивар»?
                      • 0
                        >Можно ли вообще в этом деле поставить точку, или это какой-то очередной «холивар»?
                        Сервера бывают разные:
                        Типичный веб-сервер (вроде nginx+uwsgi):
                        — обслуживаем клиентские сокеты используя конечный автомат+неблокирующий io+epoll
                        — исполняем запросы от клиентов в пуле полноценных ОС тредов
                        спокойно сможете создать сайт типа Stackoverflow, никаких проблем с нагрузкой не будет, надеюсь это достаточный хайлоад )

                        сервер для ММО игрушки:
                        — обслуживаем клиентские сокеты используя конечный автомат+неблокирующий io+epoll
                        — одним/нескольими тредами в бесконечном цикле каждые 10мс обновляем состояние игрового мира

                        Вариантов может быть очень много. Всё очень сильно зависит от задач.
                        Например попробуйте написать какой-нибудь прокси-сервер, используя треды — это будет чертовски неудобно. Так же и обработка обычных запросов при генерации страничек для интернет-магазина чертовски неудобна, когда всё превращается в кучу асинхронных ф-ций с колбэками. Или например мы живём внутри гугла и работаем с BigTable, тут для удобства мы придумываем что-нибудь вроде A New AppEngine Datastore API.
                        • 0
                          А в случае с нод.жс — автор этого проекта под сильным впечатлением от того что разобрался как работают всякие nginx'ы, решил что это «One True Way» и после нескольких неудачных попыток делать тоже самое на Си, вдруг родил связку v8+libev и выдал людям. В итоге вокруг скопилась куча мартышек, которые теперь сидят и давятся тем что им тут предоставили.
                          Там проскакивают толковые ребята, которые делают node-fibers итд, но как видим — такие вещи не проходят дальше. Да и вообще брать ущербный жаваскрипт и возиться с ним ещё и на серверах — эх, не жалеют себя люди :)
                          p.s. последний год пишу в основном на жаваскрипте :)
                        • 0
                          Поняяятно, значит всё дело в удобстве при решении конкретной задачи, а не в производительности.

                          Тогда что в статье напишем?
                          Что Node.js не быстрее обычного MPM пула в Апаче в плане обслуживания клиентов, и все те блоггеры, которые пишут, что «Node.js is blazing fast», официально пойдут лесом?
                          • 0
                            Ну смотрите, насколько помню первые примеры с нодом были с реализациями чатов и реалтайм аналитики. Если мы попытаемся как-то это реализовать с помощью (nginx+uwsgi+базы данных), то у нас получится решение, которое будет сильно уступать простенькому решению на ноде.
                            А если будем делать сайт типа Stackoverflow с архитектурой как у них (вертикальное масштабирование и вся нагрузка на бд), то тут мы практически не заметим особой разницы между нодом и nginx+uwsgi.
                            • 0
                              ок.
                              поправил статью, и убрал из неё все упоминания о быстроте событийной архитектуры относительно потоковой.
          • 0
            >Значит, эта выдуманная штука выполняет каждый таск (из тех, что через запятую) в своём потоке
            поверх примитивов ОС можно реализовать свой собственный планировщик со своими потоками :)
            shootout.alioth.debian.org/u64q/performance.php?test=threadring
            • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Предположим, ввод/вывод синхронен. И мы создали надстройку в виде «зелёных нитей» (поверх примитивов ОС). Вы утверждаете, можно реализовать свой планировщик, который будет гораздо лучше стандартного линуксового? Почему тогда в Линуксе его до сих пор нет?

              Если же ввод/вывод несинхронен, то, как я понял, мы получаем супер-быстрый Эрланг, но при этом системный ввод/вывод уже на деле асинхронен.

              Или Вы имели ввиду то, что для программиста он как бы синхронен, и поэтому мы будем считать его синхронным, и поэтому моя формулировка о том, что «если синхронен ввод/вывод и куча потоков, то тормозишь» не верна?
              • 0
                >Или Вы имели ввиду то, что для программиста он как бы синхронен, и поэтому мы будем считать его синхронным, и поэтому моя формулировка о том, что «если синхронен ввод/вывод и куча потоков, то тормозишь» не верна?
                да :)
            • 0
              А по ссылке, которую Вы дали, я так понял, замеряют производительности разных «зелёных нитей» (и просто нитей) в разных языках?
              И Haskell оказался чемпионом?
              Я думал раньше, что Эрланг всех делает…
              • 0
                Да Haskell много где чемпион, вот только почему-то он не «в моде» :)
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Знаете ли Вы случайно, не решает ли NPTL этот недостаток?
        • 0
          У меня глупый вопрос:

          Условно у нас база и сам скрипт на одном однопроцессорном одноядерном серваке. Никакие данные не прокешированы и тп.

          В приведенном вами примере будет ли, и если будет то в чем, глобальная разница будет ли сервак выполнять запросы по получению user_info and item_data паралельно или последовательно?

          Более того (теоретически) последовательное выполнение должно бысть быстрее тк системе не надо тратить ресурсы на context switch между этими потоками.

          • +1
            Задачи можно реализовать с помощью Python generators, никаких тяжёлых потоков, переключение между задачами оч быстрое
            Так что возможен и такой исход:
            П — приложение, БД — база данных, * — переключение контекста

            П: выполняет get_user_info
            П: выполняет get_item_data
            *
            БД: возвращает user_info
            БД: возвращает item_data
            *
            П: рендерит страничку
    • 0
      > Новый «тренд» был вызван тем, что серверы с традиционной многопоточной архитектурой перестали справляться с возрастающей нагрузкой в «веб-два-нольном» интернете.
      — архитектура существующих highload приложений (ну тут чего-то отдельного вроде не найти, но всё легко гуглится)

      Это, кстати, уже поправлено.
      Я действительно забыл про существование фейсбуков и твиттеров, пока писал эту статью.
    • 0
      Приведу ссылки (не на амазон) для тех, кто мб тоже захочет почитать.

      «Concepts, Techniques, and Models of Computer Programming» — это тысячестраничная жесть, которую мне не осилить, поэтому она отменяется.
      citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.102.7366&rep=rep1&type=pdf

      «The Linux Programming Interface: A Linux and UNIX System Programming Handbook».
      uploading.com/files/get/8ae5bda1/
      аааа, полторы тыщи страниц.
      отменяется.
      и ещё, мне сносит крышу от линуксовских названий (ls, umount, unmount, cd, chroot, chmod, ...) и названий низкоуровневых функций (mmap, ioctl, memcpy, strcpy, ...).
      для меня это какой-то инопланетный язык.

      «What every programmer should know about memory»
      citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.91.957&rep=rep1&type=pdf
      Вот это, похоже, что-то интересное. Спасибо, почитаю. Сто страниц, вполне нормально.
      • 0
        судя по всему мы ждем от вас еще одну статью на туже тему :)
      • 0
        >«Concepts, Techniques, and Models of Computer Programming» — это тысячестраничная жесть, которую мне не осилить, поэтому она отменяется.
        эта книга очень полезна для всех программистов, просто чтобы понимать причины по которым многие языки такие какие они есть :) что для решения одной задачи удобно применять sql, для решения другой makefile итд. Что нет какой-то «асинхронной событийной модели», которая будет удобна в любых ситуациях. Поэтому у людей, знакомых с кучей различных языков, случается батхёрт, когда они видят весь этот голый колбэк-шит, с которым в большинстве случаев работают все нодовцы.

        >«The Linux Programming Interface: A Linux and UNIX System Programming Handbook».
        тут всё конечно не обязательно читать, но это отличный источник информации по темам, которые вдруг заинтересуют :)
  • 0
    Комменты не читай, сразу отвечай:
    Не совсем верно говорить, что под стек линукс разом выделяет 8мб. 8мб — это предел размера стека и дополнение от гугла всего-лишь позволяет динамически его растягивать до большего размера. По факту в линуксе существует такой механизм как page fault. Т.е. первоначально под стек выделяется 1-2 страницы памяти (4-8кб). При очередном добавлении значения в стек, когда его границы переходят выделенные уже страницы срабатывает этот самый page fault (исключение уровня ос) и ос резервирует под стек еще одну страничку.
    • 0
      Видел как-то раз в каментах также, что данный механизм есть и в Винде тоже, и называется что-то типа «виртуальных страниц памяти».
      То есть, получается, нити не пожирают сразу по 8 мб оперативы…
      Я, когда смотрел лекцию Райана Даля (создателя Node.js'а), видел такую картинку:
      thefoley.net/node/nginx-apache-memory.png
      На цифры смотреть не стал, удовлетворился лишь общей тенденцией.
      В принципе, если у него тут на графике при 4000 соединений 40 Мб оперативы естся, то как раз где-то по 8 Кб получается на нить.
    • 0
      Под стек линукс разом выделяет 8мб виртуальной памяти, дополнение от гугла позволяет делать такие же эффективные нити как в Го, расходуя минимум виртуальной памяти. То что физическая память не сразу выделяется под все странички — это уже другой вопрос, тут решается проблема с виртуальной памятью.
      • 0
        А в чём тогда смысл SplitStacks, если задача выделения малых количеств «настоящей» оперативы решается виртуальной памятью?
        • 0
          ну очевидно же :) они решают проблему с выделением малых количеств виртуальной памяти, она ведь тоже не резиновая и не берётся из воздуха.
          • 0
            Я думал, что её «два в шестьдесят четвёртой» битов, а оказывается, что около 128 Терабайтов
            www.linuxquestions.org/questions/linux-hardware-18/what-is-max-memory-for-x64-pcs-721425/
            Вроде как пока обычному приложению до таких объёмов как до Луны.
            С другой стороны, все знают, как Билл Гейтс говорил в 1981-ом: «640Кб должно быть достаточно для каждого»
            • 0
              она же не магическим образом мапится на физическую, под таблицы нужно выделять физ. память.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.