Как стать автором
Обновить

Комментарии 20

Если нужны аналогичные статьи по другим частям Mojolicious — скажите по каким. У меня пока в планах только нычка-копилка ->stash.
доброго времени суток, интересна статья о разработке приложений с использованием технологии websocket и особенности реализации этой технологии в mojolicious

Зы: спасибо за статью
Какие преимущества / недостатки у асинхронного подхода в сравнении с форками?
Очевидное преимущество — потребление памяти — все в одном процессе.
Может ли асинхронное приложение справляться с нагрузкой?
Apache и Nginx за много лет вроде бы вполне чётко показали, что в плане производительности и памяти форки это слишком медленно и дорого, а асинхронное приложение способно справляться с громадной нагрузкой без проблем (а при необходимости держать совсем дикую нагрузку самый эффективный вариант — нафоркать по одному асинхронному приложению на каждое ядро). Но дело вовсе не в нагрузке и не в производительности — для абсолютного большинства веб-приложений в этом плане и форки и асинхронность будут работать достаточно хорошо.

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

Но если в процессе обработки этого запроса необходимо делать запросы в разные базы данных/redis/memcached на разные сервера, или выполнить несколько HTTP-запросов (напр. скачать какие-то данные или отправить что-то на другие сервера), или выполнить несколько RPC-запросов в разные сервисы — вот тут оказывается, что без асинхронности жить очень тяжело.

Если делать все эти операции последовательно в одном процессе, то они займут в несколько раз больше времени, чем если бы часть их них выполнялась одновременно. Конечно, всё зависит от ситуации, но разница легко может быть раз в 10. Что любопытно, практически 99% времени процесс который этим занят — ничего не делает, только ждёт сетевого I/O… и в случае форков он просто занимает место — и в памяти, и в списке worker-ов (что легко может привести к тому, что большинство worker-ов будет «занято» и новые запросы будет некому обслуживать) — а в случае асинхронной модели этот процесс мог бы параллельно продолжать обслуживать сотни других запросов.

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

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

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

Что касается преимуществ, то если приложение работает асинхронно в одном процессе возникает масса новых возможностей:
  • т.к. процесс один нет необходимости в синхронизации, блокировках, и сложном обмене информацией между процессами (через IPC или базу данных)
  • можно держать в памяти этого процесса любые кеши, и это будет проще и быстрее чем использовать memcached — ведь все входящие запросы приходят только в этот процесс, поэтому нет необходимости во внешнем «общем» для всех процессов хранилище вроде memcached
  • можно упростить некоторые задачи благодаря тому, что легко пользоваться таймерами — например, асинхронному приложению очень легко чистить все эти кеши или обновлять какую-то информацию с определённой периодичностью (нет нужды во внешней «дёргалке», когда в системный cron добавляется задача выкачивания какой-то url нашего приложения только ради того, чтобы оно периодически выполняло какую-то операцию)
  • удобно и просто в одном приложении совмещать несколько сервисов — например, помимо обработки входящих HTTP-запросов этот же процесс может работать RPC-сервисом на другом TCP-порту

Я предпочитаю использовать асинхронное программирование «по умолчанию», даже если в данный момент острой необходимости в этом нет. Во-первых, если позже возникнет необходимость добавить ту же работу с сервисами или сложные парсеры, а изначально приложение было не асинхронным, то его придётся серьёзно модифицировать, фактически переписать (да, такая «причина» звучит сомнительно, но как я уже говорил у нас часто используется SOA, поэтому в моей личной практике такое действительно случается достаточно часто). Во-вторых, я предпочитаю хорошо освоить один подход (который к тому же всё-равно приходится использовать при программировании фронтэнда на Javascript, GUI, и в кучке других мест — за отсутствием других вариантов) чем иметь одну кучу проблем вызванных форками и наличием группы worker-ов в одних проектах и вторую кучу проблем вызванных асинхронностью в других проектах.
О, ещё я забыл упомянуть о задачах вроде вебсокетов — когда сервис должен поддерживать кучу соединений с клиентами, и время от времени что-то им отправлять или получать от них запросы — здесь тоже без асинхронного программирования никуда.
Ещё стоит упомянуть один важный недостаток асинхронного подхода: callback-обработчик какого-то события не должен работать слишком долго, т.к. это приостановит все остальные задачи, которые выполнял этот процесс. Это касается только сложных вычислительных задач, и их приходится выносить в отдельные процессы/сервисы.
Да, это я и подозревал. Промахнулся комментарием, см. ниже.
можно держать в памяти этого процесса любые кеши, и это будет проще и быстрее чем использовать memcached — ведь все входящие запросы приходят только в этот процесс, поэтому нет необходимости во внешнем «общем» для всех процессов хранилище вроде memcached


Как это возможно, ведь все равно основной процесс приходится форкать при старте сервиса, значит и память и кеши у каждого потомка свои?
Это получится только, если наш сервис настолько прост, что не нуждается в форках.
Если же в рамках обработки одного запроса делается много запросов к БД, API и другим внешним сервисам, или проводится большая вычиислительная работа, то нужно строить сервис на форках, при асинхронном подходе будет блокироваться обработка других параллельно поступивших запросов.

Правильно?
Не, не правильно. Всё работает вообще без форков. В случае Mojo запускается hypnotoad, который вообще-то форкающий сервер, но я его ограничиваю одним worker-ом, так что формально процессов запущено два, но фактически все запросы обрабатывает только один.
Вот, кстати, отличная статья MarcusAurelius, описывающая в том числе и преимущества асинхронного подхода: Назад, к технологиям верхнего палеолита, от любимых всеми REST, STATEless, CRUD, CGI, FastСGI и MVC.
Интересный подход — привязывать клиента к процессу, можно на уровне nginx роутить.
Я в своих проектах использую ExtDirect RPC для доступа API, какой смысл ограничивать себя RESTом.
Да и чистым REST API не обойдешься, все равно нужно передавать данные, которые в URL не запихнешь. REST — дань моде).
При чём тут мода? REST хорош тогда, когда некоторые данные по своей сути это типичные для REST-а «ресурсы», причём есть необходимость обращаться к ним обычным HTTP-клиентам и активно их кешировать в браузерах и проксях. А в остальных случаях лучше более универсальный RPC.
Я имел ввиду, что его действительно часто хотят использовать там, где он архитектурно не подходит, в этом и выражается дань моде.
Спасибо за ответ.
С nginx все понятно, там асинхронность уместна, т.к. работа в основном с чтение / записью в сокет.

Если я правильно понял, вы все-таки используете форки, а каждый отдельный процесс внутри себя может использовать асинхронность для каких-то задач?

То, что форкать подпроцесс для асинхронного вызова накладно — это понятно, есть разные подходы, как распараллелить вызовы, в основном базирующиеся на неблокирующих сокетах.

Вопрос был «имеет ли смысл использовать чисто асинхронный подход, без форков основного процесса вообще».

Скорее всего это возможно только для очень простых сервисов.
Не совсем так. Форки я не использую. Чисто асинхронный подход отлично работает в куче проектов много лет (я его активно использую примерно с 2005-го, как в линухе появился epoll — в то время пришлось самому писать XS-модуль чтобы получить доступ к epoll из перла).

Что касается медленных чисто вычислительных задач, которые нельзя делать в callback-ах асинхронного приложения — мне пока что такие вообще не встречались. Просто держу в уме, что если такое понадобится — это нужно будет делать в отдельном процессе (а скорее всего просто отдельным сервисом).

На самом деле много лет единственной такой «медленной» задачей была работа с MySQL т.к. не было нормальной поддержки неблокирующей работы с базой. Но сейчас уже есть поддержка в драйвере, и есть AnyEvent::DBI::MySQL, да и для других баз уже появились модули поддерживающие асинхронность, так что об этой проблеме можно забыть.
Очень интересно.
Т.е. типичные задачи веб приложения нормально выполняются в одном асинхронном процессе и вы не сталкивались с недостатком производительности из-за блокировок процесса.
Но ведь есть же различные операции, которые всегда будут блокирующими, например, расчет хеша bcrypt. Это будет серьезно тормозить общую производительность.
Конечно, возможность держать все кеши в памяти очень подкупает, но, наверное, лучшей будет модель nginx — несколько процессов / потоков, которые могут обрабатывать запросы асинхронно.

А как происходит работа с БД, каждая асинхронная ветка кода создает собственное соединение?
Что касается bcrypt, то в любом случае надо ограничивать количество запросов при обработке которых используется bcrypt — иначе тривиально заDoSить любое приложение, сколько worker-ов не нафоркай.

Да, каждый входящий запрос использует свой собственный $dbh (используется пул уже соединённых с базой $dbh чтобы сэкономить время на connect-е). Вот пример для Mojo и AnyEvent::DBI::MySQL.
Использование какого асинхронного фреймворка, на ваш взгяд, наиболее оптимально?
А по статье не заметно? ☺

Если серьёзно, то много лет назад я тщательно протестировал все event loop-движки для перла. Поскольку до этого я успел написать свой собственный (на epoll), то я уже знал где искать потенциальные проблемы. Так что тестирование было достаточно сложным и серьёзным. На тот момент у всех протестированных движков кроме EV были серьёзные проблемы: при некоторых условиях терялись события, утекала память, и даже бывали segfault-ы. Возможно, за прошедшие годы ситуация улучшилась, но лично я сомневаюсь: разработчики допустившие такие серьёзные ошибки в настолько критичном коде (причём не в версии 0.01 — все движки были вполне mature на первый взгляд) вряд ли внезапно перестанут делать ошибки. А event loop должен работать как часы, иначе будут возникать крайне сложно отлаживаемые баги в приложении пользователя. Поэтому с тех пор я использую исключительно EV, и всем рекомендую пользоваться только им — всё это время он отлично работал. И AnyEvent и Mojo умеют работать через EV.

Что касается более высокоуровневых модулей — я много лет использовал свои (свой веб-фреймворк я на CPAN так целиком и не выложил, но базовые модули вроде IO::Stream выложены) пока не узнал про Mojolicous. У меня, безусловно, есть сильная тяга писать свои велосипеды, но это не из принципа и не от хорошей жизни — просто я люблю качественные и надёжные решения с идеологически правильной архитектурой, и не всегда такое удаётся найти готовое. Поэтому когда удаётся найти чужое правильное решение — я с радостью на него переключаюсь. Это же произошло и с Mojolicious — я его достаточно тщательно изучил, и пришёл к выводу что он делает примерно то же самое, что и мой фреймворк, примерно тем же способом, и вообще придраться почти не к чему. Так что я с радостью на него перешёл и пока абсолютно об этом не жалею. Возможно технически Dancer тоже неплох, но он мне идеологически не понравился — впрочем это может быть дело вкуса.

В общем, я рекомендую, в зависимости от задач, EV, IO::Stream, AnyEvent и Mojo (в случае последних двух — проследить чтобы они использовали EV).
Большое спасибо за статью и за пояснения.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории