Песочница →
Что такое «асинхронная событийная модель», и почему сейчас она «в моде»
Сейчас в интернетах очень модно слово «Node.js». В этой небольшой статье мы попробуем понять, откуда всё это взялось и чем такая архитектура отличается от привычной «по процессу на запрос».
Новый «тренд» был вызван тем, что серверы с традиционной многопоточной архитектурой перестали справляться с возрастающей нагрузкой в «веб-два-нольном» интернете. Интернет превратился в подобие огромного пчелиного улья, в котором всё жужжит и движется. Проверенный годами Apache хорошо справлялся с «интернетом 1.0». Сервер запускал новый процесс (или поток, что почти одно и то же с точки зрения операционной системы) на каждый запрос пользователя. Этот поток шёл в PHP, оттуда – в базу данных, чего-нибудь там выбирал, и возвращался с ответом, который отсылал пользователю по HTTP, после чего самоуничтожался. Однако когда количество одновременно работающих запросов начало близиться к символическому порядку 10 000, начались тормоза. Допустим, у нас есть 10 000 потоков. Как они будут уживаться друг с другом? Их будет уживать друг с другом системный «планировщик», задачей которого является выдавать каждому потоку его долю процессорного времени, и при этом никого не обделять. Действует он так. Когда один поток немного поработал, запускается планировщик, временно останавливает этот поток, и «подготавливает площадку» для запуска следующего потока (который уже ждёт в очереди). Такая подготовка площадки называется «переключением контекста», и в неё входит сохранение «контекста» приостанавливаемого потока, и восстановление контекста потока, который будет запущен следующим. В «контекст» входят регистры процессора и данные о процессе в самой операционной системе (id’шники, права доступа, ресурсы и блокировки, выделенная память и т.д.). Как часто запускается планировщик – это решает операционная система. Например, в Linux’е «захардкожен» период запуска планировщика в сотую долю секунды. Планировщик также вызывается, когда процесс «блокируется» вручную или в ожидании ввода/вывода (например, запрос пользователя в потоке PHP ждёт, пока база данных выдаст ему отчёт по продажам за месяц). «Переключение контекста» не является таким уж дорогостоящим, если у вас дома на компьютере запущены несколько десятков процессов (системные процессы, музыка, браузер, торренты, скайп, фотошоп, все дела). Переключение контекста между потоками, когда в системе их несколько десятков, составляет порядка микросекунды. По мере того, как растёт или память, связанная с потоком, или количество потоков, начинает не хватать «кеша второго уровня» (L2) у процессора, который не может уже вместить в себе все эти «контексты» потоков, и эти «контексты» приходится начинать писать в обычную оперативную память (и потом доставать их оттуда), скорость доступа к которой из процессора на порядки ниже скорости доступа к кешу процессора (поэтому и был придуман этот кеш). Из-за этого время «переключения контекста» может доходить до десятой доли миллисекунды. При этом, напомню, у нас в «высоконагруженной» системе предполагается порядка 10 000 одновременно работающих потоков, которые постоянно что-то вводят/выводят, а, значит, «блокируются», и этим запускают планировщик ещё и ещё раз. И система начинает захлёбываться от такой нагрузки, тратя большинство процессорного времени на решение своих системных задач, а не на выполнение полезной работы. Эта загвоздка получила название «The C10K problem».
Серверы интернета нуждались в новой архитектуре, и она была найдена – это «асинхронная событийная модель». В основе её лежат «событийный цикл» и шаблон «reactor» (от слова «react» – реагировать). «Событийный цикл» представляет собой бесконечный цикл, который опрашивает «источники событий» на предмет появления в них какого-нибудь «события». «Событием» может быть приход очередной порции данных на сетевой сокет («socket» – дословно «место соединения»), или считывание новой порции данных с жёсткого диска: в общем, любой ввод/вывод. Например, когда вы загружаете картинку на хостинг, данные туда приходят кусками, каждый раз вызывая событие «новая порция данных картинки получена». «Источником событий» в данном случае будет являться «дескриптор» (указатель на поток данных) того самого TCP-сокета, через который вы соединились с сайтом по сети. Второй компонент новой архитектуры, как уже было сказано, – это шаблон «reactor». И, для русского человека, это совсем не тот реактор, который стоит на атомной станции. Суть этого шаблона заключается в том, что код сервера пишется не одним большим куском, который исполняется последовательно, а небольшими блоками, каждый из которых вызывается («реагирует») тогда, когда происходит связанное с ним событие. Таким образом, код представляет собой набор множества блоков, задача которых состоит в том, чтобы «реагировать» на какие-то события.
Такая новая архитектура стала «мейнстримом» после появления Node.js’а. Node.js написан на C++, и основывает свой событийный цикл на Сишной библиотеке «libev». Однако Яваскрипт здесь не является каким-то избранным языком: для множества других языков тоже написаны похожие серверы и «фреймворки». Всё, что требуется в этом случае от языка – это наличие библиотеки «асинхронного» («неблокирующего») ввода/вывода. При обычном, «синхронном», вводе/выводе вы пишете код типа «открыть файл; считать его в строку; вывести на экран», а при «асинхронном» вводе/выводе вы пишете «открыть файл, и когда он будет открыт, считать его в строку и вывести на экран; а пока, делать то-то и то-то». «Асинхронные» библиотеки ввода/вывода гораздо сложнее в написании (чем синхронные), поэтому не для каждого языка они написаны. На основе таких библиотек пишутся «фреймворки» для запуска приложений по «асинхронной событийной модели». Например, у Питона есть Twisted, у Перла – Perl Object Environment, у Руби – EventMachine (которой уже лет пять). На этих «фреймворках» можно писать свои серверы, подобные Node.js’у. Например, для Явы (на основе java.nio) написаны Netty и MINA, а для Руби (на основе EventMachine) – Goliath (который ещё и пользуется преимуществами Fibers).
«Асинхронная событийная модель» хорошо подойдёт там, где много-много пользователей одновременно запрашивают какие-нибудь «легковесные» куски данных, или производят какие-нибудь «легковесные» действия. Например: проверить, есть ли новые «твиты» у Васи, загрузил ли Петя новые фотки, есть ли новые комментарии к моей статье или сообщения в моём личном ящике, плюсануть кого-нибудь, проставить рейтинг кому-нибудь, написать камент, отослать сообщение в чат, и т.п… Требование «легковесности» действий проясняется, когда мы вспоминаем о том, что весь этот бесконечный цикл запущен в одном единственном потоке, и если вы вклините в этот цикл какое-нибудь тяжеловесное вычисление, то все остальные пользователи будут ждать в очереди, пока одно это вычисление не закончится. Поэтому серверы, подобные Node.js’у, подходит только для выполнения «легковесных» задач, или как «фронтенд» для тяжеловесного «бекенда».
Ещё один недостаток «асинхронной событийной модели» – код становится менее читаемым из-за переплетения множества «обратных вызовов». Например, подобный «спагетти-код» является бичом программирования на Node.js’е (коллбек на коллбеке, коллбеком погоняет). Об этом знают, и с этим пытаются бороться. Например, для Node.js’а написана библиотека Seq. А в Руби, начиная с версии 1.9, введены Fibers, с помощью которых можно полностью устранить коллбеки, и писать код так, как будто бы всё происходит синхронно.
На этом основная часть этой статьи закончена, и напоследок мы рассмотрим то, как «событийный цикл» опрашивает «источники событий» на предмет появления в них новых данных. Самое простое решение, которое можно придумать – опрашивать все «дескрипторы» (открытые сетевые сокеты, считываемые или записываемые файлы, …) на предмет наличия в них новых данных. Такой алгоритм называется «poll». Выглядит это примерно так:
Ссылки по теме:
Введение в EventMachine
Scalable network programming
Kqueue
Новый «тренд» был вызван тем, что серверы с традиционной многопоточной архитектурой перестали справляться с возрастающей нагрузкой в «веб-два-нольном» интернете. Интернет превратился в подобие огромного пчелиного улья, в котором всё жужжит и движется. Проверенный годами Apache хорошо справлялся с «интернетом 1.0». Сервер запускал новый процесс (или поток, что почти одно и то же с точки зрения операционной системы) на каждый запрос пользователя. Этот поток шёл в PHP, оттуда – в базу данных, чего-нибудь там выбирал, и возвращался с ответом, который отсылал пользователю по HTTP, после чего самоуничтожался. Однако когда количество одновременно работающих запросов начало близиться к символическому порядку 10 000, начались тормоза. Допустим, у нас есть 10 000 потоков. Как они будут уживаться друг с другом? Их будет уживать друг с другом системный «планировщик», задачей которого является выдавать каждому потоку его долю процессорного времени, и при этом никого не обделять. Действует он так. Когда один поток немного поработал, запускается планировщик, временно останавливает этот поток, и «подготавливает площадку» для запуска следующего потока (который уже ждёт в очереди). Такая подготовка площадки называется «переключением контекста», и в неё входит сохранение «контекста» приостанавливаемого потока, и восстановление контекста потока, который будет запущен следующим. В «контекст» входят регистры процессора и данные о процессе в самой операционной системе (id’шники, права доступа, ресурсы и блокировки, выделенная память и т.д.). Как часто запускается планировщик – это решает операционная система. Например, в Linux’е «захардкожен» период запуска планировщика в сотую долю секунды. Планировщик также вызывается, когда процесс «блокируется» вручную или в ожидании ввода/вывода (например, запрос пользователя в потоке PHP ждёт, пока база данных выдаст ему отчёт по продажам за месяц). «Переключение контекста» не является таким уж дорогостоящим, если у вас дома на компьютере запущены несколько десятков процессов (системные процессы, музыка, браузер, торренты, скайп, фотошоп, все дела). Переключение контекста между потоками, когда в системе их несколько десятков, составляет порядка микросекунды. По мере того, как растёт или память, связанная с потоком, или количество потоков, начинает не хватать «кеша второго уровня» (L2) у процессора, который не может уже вместить в себе все эти «контексты» потоков, и эти «контексты» приходится начинать писать в обычную оперативную память (и потом доставать их оттуда), скорость доступа к которой из процессора на порядки ниже скорости доступа к кешу процессора (поэтому и был придуман этот кеш). Из-за этого время «переключения контекста» может доходить до десятой доли миллисекунды. При этом, напомню, у нас в «высоконагруженной» системе предполагается порядка 10 000 одновременно работающих потоков, которые постоянно что-то вводят/выводят, а, значит, «блокируются», и этим запускают планировщик ещё и ещё раз. И система начинает захлёбываться от такой нагрузки, тратя большинство процессорного времени на решение своих системных задач, а не на выполнение полезной работы. Эта загвоздка получила название «The C10K problem».
Серверы интернета нуждались в новой архитектуре, и она была найдена – это «асинхронная событийная модель». В основе её лежат «событийный цикл» и шаблон «reactor» (от слова «react» – реагировать). «Событийный цикл» представляет собой бесконечный цикл, который опрашивает «источники событий» на предмет появления в них какого-нибудь «события». «Событием» может быть приход очередной порции данных на сетевой сокет («socket» – дословно «место соединения»), или считывание новой порции данных с жёсткого диска: в общем, любой ввод/вывод. Например, когда вы загружаете картинку на хостинг, данные туда приходят кусками, каждый раз вызывая событие «новая порция данных картинки получена». «Источником событий» в данном случае будет являться «дескриптор» (указатель на поток данных) того самого TCP-сокета, через который вы соединились с сайтом по сети. Второй компонент новой архитектуры, как уже было сказано, – это шаблон «reactor». И, для русского человека, это совсем не тот реактор, который стоит на атомной станции. Суть этого шаблона заключается в том, что код сервера пишется не одним большим куском, который исполняется последовательно, а небольшими блоками, каждый из которых вызывается («реагирует») тогда, когда происходит связанное с ним событие. Таким образом, код представляет собой набор множества блоков, задача которых состоит в том, чтобы «реагировать» на какие-то события.
Такая новая архитектура стала «мейнстримом» после появления Node.js’а. Node.js написан на C++, и основывает свой событийный цикл на Сишной библиотеке «libev». Однако Яваскрипт здесь не является каким-то избранным языком: для множества других языков тоже написаны похожие серверы и «фреймворки». Всё, что требуется в этом случае от языка – это наличие библиотеки «асинхронного» («неблокирующего») ввода/вывода. При обычном, «синхронном», вводе/выводе вы пишете код типа «открыть файл; считать его в строку; вывести на экран», а при «асинхронном» вводе/выводе вы пишете «открыть файл, и когда он будет открыт, считать его в строку и вывести на экран; а пока, делать то-то и то-то». «Асинхронные» библиотеки ввода/вывода гораздо сложнее в написании (чем синхронные), поэтому не для каждого языка они написаны. На основе таких библиотек пишутся «фреймворки» для запуска приложений по «асинхронной событийной модели». Например, у Питона есть Twisted, у Перла – Perl Object Environment, у Руби – EventMachine (которой уже лет пять). На этих «фреймворках» можно писать свои серверы, подобные Node.js’у. Например, для Явы (на основе java.nio) написаны Netty и MINA, а для Руби (на основе EventMachine) – Goliath (который ещё и пользуется преимуществами Fibers).
«Асинхронная событийная модель» хорошо подойдёт там, где много-много пользователей одновременно запрашивают какие-нибудь «легковесные» куски данных, или производят какие-нибудь «легковесные» действия. Например: проверить, есть ли новые «твиты» у Васи, загрузил ли Петя новые фотки, есть ли новые комментарии к моей статье или сообщения в моём личном ящике, плюсануть кого-нибудь, проставить рейтинг кому-нибудь, написать камент, отослать сообщение в чат, и т.п… Требование «легковесности» действий проясняется, когда мы вспоминаем о том, что весь этот бесконечный цикл запущен в одном единственном потоке, и если вы вклините в этот цикл какое-нибудь тяжеловесное вычисление, то все остальные пользователи будут ждать в очереди, пока одно это вычисление не закончится. Поэтому серверы, подобные Node.js’у, подходит только для выполнения «легковесных» задач, или как «фронтенд» для тяжеловесного «бекенда».
Ещё один недостаток «асинхронной событийной модели» – код становится менее читаемым из-за переплетения множества «обратных вызовов». Например, подобный «спагетти-код» является бичом программирования на Node.js’е (коллбек на коллбеке, коллбеком погоняет). Об этом знают, и с этим пытаются бороться. Например, для Node.js’а написана библиотека Seq. А в Руби, начиная с версии 1.9, введены Fibers, с помощью которых можно полностью устранить коллбеки, и писать код так, как будто бы всё происходит синхронно.
На этом основная часть этой статьи закончена, и напоследок мы рассмотрим то, как «событийный цикл» опрашивает «источники событий» на предмет появления в них новых данных. Самое простое решение, которое можно придумать – опрашивать все «дескрипторы» (открытые сетевые сокеты, считываемые или записываемые файлы, …) на предмет наличия в них новых данных. Такой алгоритм называется «poll». Выглядит это примерно так:
- у вас есть два открытых сокета
- вы создаёте массив из двух структур, которые описывают эти сокеты
- каждому элементу этого массива вы проставляете, что и о каком сокете в него записать
- затем вы передаёте этот массив системной функции poll, которая пишет туда описание текущего состояния этих сокетов
- после этого вы проходитесь по этому массиву ещё раз, выясняя, есть ли там для этих сокетов новые данные
- если есть – считываете их, и делаете с ними что-нибудь
- всё это повторяется для нового витка бесконечного «событийного цикла»
Ссылки по теме:
Введение в EventMachine
Scalable network programming
Kqueue
20.09.2011 12:54+0400