вторник, 20 апреля 2010 г.

Асинхронный веб

Поскольку я имею твердое желание фокрнуть Hunchentoot (при удобном случае), то обдумываю эту возможность в "фоновом режиме". Ниже кое-какие соображения.

Сейчас много пишут про использование асинхронного ввода/вывода в контексте веб приложений и всячески расхваливают event model, которая позволяет добиться небывалых высот производительности, ну и конечно, nginx как главный флагман и пример для подражания, а Apache как главный враг прогрессивного человечества. Но кажется, что "не всё в порядке в Датском королевстве" (с) :) Итак, по порядку, как известно Apache при обслуживании действительно большого количества одновременных соединений сталкивается со следующими основными проблемами:
  • Большое количество процессов/потоков требует существенного объёма памяти для самого факта своего существования
  • Постоянное переключение контекста между большим количеством процессов/потоков приводит к тому, что львиная часть системных ресурсов тратиться именно на переключение контекста
  • Используемый блокирующий ввод/вывод приводит к постоянным и довольно дорогостоящим операциям замораживая/размораживания, что при рассматриваемых масштабах также превращается в существенную проблему
  • Усугубляет ситуацию тот факт, что один процесс/поток полностью обслуживает одно соединение и не завершается до тех пор, пока соединение не будет закрыто. А это значит, что если к ресурсу присоединяется большое количество клиентов с медленными каналами, то они существенно повышают общую нагрузку на систему даже если выполняемые для них запросы были очень простыми и лёгкими
Решения на базе event model используют асинхронный ввод/вывод, что позволяет производить обработку запросов в одном (в случае одного ядра) потоке и максимально задействовать мощности процессора именно для выполнения полезной работы (а не обслуживания системы). Это обеспечивается за счёт поддержки современными системы вызовов наподобие epoll, которые очень хорошо масштабируются даже на очень большого количества соединений. Жизнь прекрасна?

Как бы не так. По-факту, "хвалёный" nginx используется в основном как front-end к "проклятому" Apache. Подобная схема позволяет задействовать многие из преимуществ nginx, но реальная обработка запросов по прежнему ведётся в отдельных процессах/потоках. Думается, что данная ситуация обусловленная следующими причинами:
  • Большинство средств для работы с базами данных работают в синхронном режиме, а значит их нельзя использовать в рамках одно-поточной обработки. Собственно, дело не только в базах данных, но это самый характерный пример.
  • event model - это БОЛЬ, программирование в подобном режиме значительно сложнее традиционного подхода, используемого при разработке веб-приложений. Я, помнится, изрядно помучился с подобных подходом при разработке GUI. Скажем, если нужно было на основе пользовательского ввода нарисовать какую-нибудь замысловатую фигуру, т.е. обработать последовательность связанных пользовательских действий (например - щелчков мышкой), сопровождая их различными свиристелками (подсказками и прочими визуальными эффектами), как одну операцию. При программировании с помощью MFC мне пришлось практический сразу отказаться от стандартной карты событий и реализовать свой диспетчер, работавший в режиме конечного автомата. Но и этого было мало. Самым эффективным и удобным был подсмотренный мной в недрах MFC приём (позже я сталкивался с ним при работе с Autocad) по организации собственного локального цикла выборки сообщений (он начинался в начале операции построении фигуры и заканчивался в её конце) с целью создать хоть какую-то иллюзию синхронного режима работы. В этой связи, мне кажется, что предложение распространить схожие подходы на широкий круг веб-приложений заранее обречено на провал и я этому буду только рад.
Я хочу, что бы веб-сервер Hunchentoot обеспечивал высокую производительность на основе существующих современных подходов, но при этом был бы удобным инструментом для разработки веб-приложений. Т.е. есть желание эффективно совместить асинхронный ввод/вывод с возможностью писать обработчики в традиционном синхронном виде.

Простая модель обработки запроса, которую я буду рассматривать далее:
  • Сервер получает запрос
  • Вызывает обработчик, который сначала производит какую-то предварительную его обработку,
  • затем делает запрос к базе данные,
  • на основе полученных данных формирует ответ,
  • и отправляет его клиенту
Первая идея заключается в том, чтобы освободить обработчик запроса от блокирующего ввода/вывода, связанного с обработкой протокола HTTP: сервер асинхронным образом полностью считывает запрос и только после этого вызывает (в отдельном потоке) обработчик, который формирует ответ в памяти, отдаёт его серверу и на этом завершается, сервер отправляет ответ клиенту асинхронным образом. Подобная схема отвязывает длительность жизни потока-обработчика от ширины канала и возможного наличия "медленных" клиентов. Но что делать с базой?

Для начала идеальный вариант (основанный на том, что postmodern это "pure lisp" реализация сокетного протокола взаимодействия с PostgreSQL, а значит её можно относительно легко приспособить/пропатчить для нужного поведения): при выполнении запроса к базе выполнение код обработчика запроса прерывается, поток обработчика начинает выполнять какую-нибудь другую работу (обрабатывает другие запросы), взаимодействие с базой производиться на основе асинхронного ввода/вывода, а после его завершения в удобный момент работа обработчика запроса возобновляется с прерванного места. Описанное уж очень сильно напоминает "продолжения" и будь для Common Lisp чистая (в смысле, без граблей) и эффективная реализация "продолжений", то вкупе с первым пунктом можно было бы обеспечить высокоэффективную одно-поточную обработку запросов, выжав из железа и инвесторов максимум возможного ;) Но... Продолжений в Common Lisp нет. Я знаю про cl-cont и т.п., но во-первых - это грабли, во-вторых - грабли с набором ограничений, в-третьих - грабли с высокими накладными расходами. Если разбираться внимательней, то напрашивающиеся "продолжения" в описанной схеме выполняют одну единственную функцию - переключение контекста выполнения. Но разве не для этого же нужны потоки? А почему переключение контекста с помощью кода, реализованного средствами языка, должно быть более эффективным, чем переключение контекста средствами ядра операционной системы, которая задействует для этого особенности современных архитектур? Тут можно было бы вспомнить про green threads, которые способны обеспечить дешёвое переключение контекста, но для многоядерных систем нужны и нативные треды, а поддержка и тех и тех есть, кажется, только в Allegro CL, так что об этом варианте можно даже особо не размышлять. Ценители, конечно, укажут не Erlang, но это определённо не тот язык, на котором я бы хотел писать на постоянной основе. Допускаю, что Erlang можно терпеть, когда очень надо, как приходиться порой терпеть C. Но позиционировать данный язык как основной - извините.

Самая страшная страшилка для модели "поток на соединение" описывается как "10 000 одновременных запросов", что будет с системой, имеющий 10 000 потоков? Ответ, в общем-то, не так однозначен, но похоже в этом заключается суть: а зачем нам нужно 10 000 одновременных потоков? Очевидно, что всегда имеется какое-то разумное количество потоков, которые имеет смысл держать в системе и их количество, вероятно, коррелирует с числом ядер + числом запросов, которые может эффективно обрабатывать база данных. Ну да, надо учитывать, что база данных является не единственным подобным ресурсом, есть ещё всякие распределённые кэши или там очереди сообщений, но сути это не меняет. Суть: систему всегда можно "нагрузить" образом, близким к "оптимальному". В общем, в данный момент я вижу следующие аспекты, которые необходимо разрешить для превращений Hunchentoot в удобный и высокопроизводительный веб-сервер:
  • Асинхронная обработка протокола HTTP
  • Каркас для выполнения асинхронных операций в рамках синхронного потока выполнения
  • Балансировщик нагрузки, способный поддерживать оптимальную количество потоков и их статус в зависимости от характеристик железа и выполняемых ими задач
Вроде всё просто, осталось только реализовать ;) Ну и ещё не помешало бы много критики.

29 комментариев:

  1. Чем то напомнило вот это - возможно ошибаюсь
    http://hoytech.com/antiweb/manual.awp/design.html

    ОтветитьУдалить
  2. @DM
    Разве что словами - асинхронный и event model :) Antiweb - лично мне плохо понятная система, кажется автор - админ ;)

    ОтветитьУдалить
  3. Посмотри на SGI's StateThreads

    ОтветитьУдалить
  4. @linoet
    О, спасибо, интересная штука, раньше о ней не слышал

    ОтветитьУдалить
  5. >> Каркас для выполнения асинхронных операций
    >> в рамках синхронного потока выполнения.

    Можно посмотреть на ZeroMQ (и соответствующий биндинг cl-zmq), как и AMPQ он позволяет обеспечивать асинхронный интерфейс к синхронному, например ДБ.

    ОтветитьУдалить
  6. @quasimoto
    Как раз с утра внимательно разглядывал ;) Но это не совсем. Я считаю, что сам факт использования асинхронных операций более эффективен, чем блокирующий ввод/вывод. Поэтому, при необходимости выполнить асинхронный вызов поток должен остановиться, где-то в другом месте производиться обработка вызова в асинхронном виде, а после его завершения результат возвращается в остановленный поток и он возобновляет свою работу. Взаимодействие с MOM - один из вариантов, но далеко не единственный.

    ОтветитьУдалить
  7. отлично! для данной задачи мало реализовать связку event loop и корутин, необходима ещё качественная (быстрая и без копирования) организация памяти и буферов. можно взять ecl за базу, вкрутить в него связку libev + libocoroutine добить какой-нибудь клёвой gc-awared реализацией chain буферов, в этой связке накатать линейные неблокирующие аналоги read/write/connect/accept/etc - и получить ништяковый каркас.

    Потребность в таком каркасе уже пару лет есть у sxemacs для реализации non-preemptive multitasking ..

    ОтветитьУдалить
  8. @lg
    Только я, скажем так, не фанат ecl :) Моя основная реализация - SBCL. Потом, сейчас есть iolib, которая поддерживает event loop через epoll и т.п. И, кстати, там для буферов используются куски foreign-памяти, там что они работают достаточно неплохо. Но если говорить об реализации асинхронного HTTP для Hunchentoot, то я пока держу в уме просто использовать пул foreign-страниц, который не будет учитываться gc, а пользовательский интерфейс надо делать как потоки, на базе gray streams - что бы старый код для hunchentoot ничего не заметил.

    ОтветитьУдалить
  9. Green threads должны быть очень хорошо интегрированы в систему, чтобы ими было удобно пользоваться. В Erlang'е, судя по всему, это сделано очень хорошо.

    ОтветитьУдалить
  10. @13-49
    Ну, собственно, я смотрел на них не долго и решил что оно не надо, даже в том виде, как сделано в Erlang. Моя мысль заключается в интеллектуальном управлении колличеством потоков, в конце концов, в контексте веб-сервера мы не обязаны немедленно порождать новый поток для каждого запроса, а можем приняв запрос от клиента спокойно выждать подходящий момент для его обработки.

    ОтветитьУдалить
  11. Можно и пул воркер-тредов сделать. Только удобной работы с контекстом уже не будет.

    ОтветитьУдалить
  12. А можно ссылочки на те источники, на основании которых утверждается про проблемы Апача и причины использования nginx? Если это ваше собственное профилирование, то пост про это был бы очень интересным.

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

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

    ОтветитьУдалить
  13. @grudnik
    Ссылку конкретную не скажу, ибо просто не фиксирую перерабатываемые источники информации, а просто сохраняю выжимку у себя в голове. Но мне казалось, что это достаточно широко известная информация, которая регулярно обсуждается в разных источниках.

    > что именно не устраивает сейчас

    Используемая в hunchentoot модель поток на соединение.

    > Ну в смысле какой сайт, хостящийся на
    > hoonchentut-е, имеет такую нагрузку,
    > с которой hoonchentut не справляется?

    Вот уж не думаю :) Но, во-первых, если бы Hunchentoot демонстрировал чудеса производительности для экстремальных случаев и при этом предоставлял бы весьма удобную среду для разработки, то это был бы отличный маркетинговый ход для популяризации использования CL в веб. Во-вторых, есть некоторый набор технологий, на которые я ориентируюсь в перспективе на несколько ближайших лет и мне надо знать их предел. И если я вижу принципиальные ограничения, то это вызывает у меня серьёзный "неконфорт", я просто не могу полагаться на решения, которые в какой-то момент могу перестать соответствовать требованиям.

    > при ответе на мой комментарий я как-нибудь
    > смогу об этом узнать

    Например, можно использовать atom-ленту: http://archimag-dev.blogspot.com/feeds/8140333921019433438/comments/default

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

    ОтветитьУдалить
  14. По поводу StateThreads и потоков: никогда не знаешь, когда именно нужно порождать потоки. Никогда не будет такого понятия, как "вот сейчас не могу обработать нового клиента, так что подождём" — это сразу вызовет thrashing из-за забивания очередей в ядре. Единственная схема, которая работает и требует минимальной настройки — это режим «или порождаем тред (green thread, state thread (ST), process (Erlang)) на соединение, ИЛИ выгребаем соединение и тут же его закрываем, с флагом "отошли RST отправителю'». Никаких "подождём до более удобного случая" делать нельзя — его может не наступить.

    Это если есть возможность порождать дешёвые треды, до тысяч и десятков тысяч на экземпляр программы.

    Естественно, если у нас есть ограничение не на сотню (сотни) тредов в системе, а на всего лишь 2-4-8 (по количеству процессоров), или там, например, 32, совет выше может не совсем подходить. В таком случае эффективнее будет выгрести сокет и положить его в отстойник до тех пор, пока освободится кусок машинерии. Если отстойник забит больше чем на N слотов (100? 1000?), то надо принимать и тут же резетить соединения.

    Поверьте написавшему несколько высокоскоростных веб серверов для промышленных применений (Netli/Akamai, Cisco ASA).

    ОтветитьУдалить
  15. @lionet
    Собственно, не увидел противоречия, я кажется это и имею в виду, когда говорю об асинхронной обработке HTTP, что запрос от клиента вычитывается, но если система уже загружена, то реальная его обработка начинается только когда в системе освобождаются необходимые ресурсы. Эту задачу и должен решать "балансировщик нагрузки". Ну а если необработанных запросов накопилось слишком много, то надо резетить новые соединения - это кажется довольно очевидным.

    ОтветитьУдалить
  16. @archimag

    Ключевое отличие («противоречие») в том, что в системах с возможностью плодить один микротред на коннект (например, в C/ST или Erlang), из трёх фаз "принимаем клиента или делаем отлуп | вымачиваем коннект в очереди ожидания | порождаем тред на коннект" вторая фаза лишняя.

    ОтветитьУдалить
  17. @lionet
    О, это я понимаю, но не имею возможности создавать микротреды в Common Lisp и исхожу именно из этого факта. Т.е. поскольку нет возможности полагаться на специальные возможности, положенные в основу языка, то надо делать более "интелектуальное" решение.

    За всё надо платить, за Erlang - тоже, например тем, что это не CL, но такую цену я не согласен ;)

    ОтветитьУдалить
  18. Про статистику и "известную информацию" - я просто занимаюсь этой темой немного (я работаю в конторе, которая разрабатывает некоторый софт для индустрии хостинга), и по моим данным (анализ логов крупных хостеров, моделирование нагрузки, профилирование) недостатки апача очень сильно зависят от характера загрузки, количества сайтов и прочих параметрах.

    То есть если интересно позиционировать hoonchentut как замену apache для mass shared hosting - тогда да, замечания справедливы. Однако же mass shared hosting - это PHP. Хорош ли hoonchentut в связке с PHP? Может ли он быть производительнее, чем nginx + apache + mod_php (в hoonchentut-е ведь будет как максимум fastcgi, верно)? Я думаю нет.

    Если же говорить не о mass shared hosting (а о чём тогда), то у апача проблемы с производительностью не сильно критичны (ну, если мы не говорим о highload), плюс есть такие довольно неплохие решения как lighty.

    Я не то чтобы критикую, дело ваше, но как-то не могу представить себе смысл.

    ОтветитьУдалить
  19. @grundik
    Нет, ни о каком mass shared hosting речь, конечно, не идёт. Никакой PHP здесь тоже не фигурирует. Речь идёт о позиционировании набора инструментов, включающего в себя Hunchentoot, как оптимального для построения веб-приложений (не просто веб-сайтов), которые, в том числе, должны работать и под высокой нагрузкой.

    Если мы не говорим о highload, то сразу обозначаем предел применимости. Кто, например, выберет такой инструмент для начала стартапа, если с самого начала понятно, что он не будет справляться с высокой нагрузкой? Ведь стартап мечтает о высокой нагрузке ))

    > но как-то не могу представить себе смысл.

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

    В общем: создадим вокруг лучшего языка лучшего инфраструктуру ;)

    ОтветитьУдалить
  20. > В общем: создадим вокруг лучшего языка лучшего инфраструктуру ;)

    Если поднапрячься, то из CMUCL в SBCL можно перетащить code/multi-proc.lisp. Разницы между ними не слишком много, нужно лишь разобраться, как в них работа со стеками и lexenv'ом сделана.

    ОтветитьУдалить
  21. > из CMUCL в SBCL можно перетащить
    > code/multi-proc.lisp

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

    Green threads это может и удобно, но это ведь не может быть единственным решением? Если что и патчить на таком уровне, то кажется проще и больше смысла патчить ядро Linux ;)

    ОтветитьУдалить
  22. Сообщество - это как раз мы ;) После однократного портирования код будет работать вечно, т.к. затрагиваемые места меняются не часто (если меняются).

    Зелёные треды в ядро запихать не получится. Они туда идейно не воткнутся.

    ОтветитьУдалить
  23. > Зелёные треды в ядро запихать не получится.

    Не, я про то, что при необходимости лучше планировщик пилить.

    ОтветитьУдалить
  24. Этот комментарий был удален автором.

    ОтветитьУдалить
  25. > Не, я про то, что при необходимости лучше планировщик пилить.


    это не снизит стоимости создания, удаления треда. разве что скорость переключения, но это не самый критичный момент + надо собственно будет делать переключение, тогда как в green threads скедулер сидит внутри процесса.

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

    // всё imho

    ОтветитьУдалить
  26. @Mike
    Конечно не снизит, но может улучшить ситуацию, когда есть большое колличество потоков и лишь несколько из них реально активны.

    Green Threads нет и не будем (в Common Lisp). Так что нечего о них вообще рассуждать. Я исхожу из этого.

    ОтветитьУдалить
  27. Как я понял это всё уместиться во что-то вроде патча :) (?)

    А именно - сдвинуть часть работы из process-connection (который работает в отдельном, вновь созданном, треде) в handle-incoming-connection, который работает в треде ацептора. Так?

    И ещё вопрос про:

    >> сервер асинхронным образом полностью считывает запрос

    и

    >> сервер отправляет ответ клиенту асинхронным образом

    Как асинхронным образом? Имеются в виду aio_read/aio_write?

    ОтветитьУдалить
  28. @quasimoto
    Скорей в виде форка. Технически я ещё не знаю что потребуется. Но, скажем так: нельзя добиться высокой масштабируемости в сотню строк код ;)

    > Имеются в виду aio_read/aio_write?

    Нет. Имеет в виду на основе epoll.

    ОтветитьУдалить
  29. А, так под "будет много потоков, но не все они будут активны" имеется в виду, что тред-рабочий выполнивший epoll_wait отключается до появления готового к диалогу клиента. Т.е. эта штука будет автоматически происходить при использовании event-base. Тогда понятно.

    ОтветитьУдалить