← Blog of oldlisper

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

· 20.04.2010 13:19
· original author: archimag
Поскольку я имею твердое желание фокрнуть 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
  • Каркас для выполнения асинхронных операций в рамках синхронного потока выполнения
  • Балансировщик нагрузки, способный поддерживать оптимальную количество потоков и их статус в зависимости от характеристик железа и выполняемых ими задач
Вроде всё просто, осталось только реализовать ;) Ну и ещё не помешало бы много критики.