Показаны сообщения с ярлыком hunchentoot. Показать все сообщения
Показаны сообщения с ярлыком hunchentoot. Показать все сообщения

пятница, 29 октября 2010 г.

RESTAS и специальные страницы

Если в обработчике запроса для сервера Hunchentoot выставляется код возврата в какой-либо из кодов ошибок, то Hunchentoot игнорирует возвращаемое диспетчером значение и самостоятельно формирует соответствующую стандартную специальную страницу. Такое поведение не всегда является приемлемым. Изменить его можно с помощью переменной hunchentoot:*handle-http-errors-p* - если она выставлена в NIL, то вся ответственность за формирование специальных страниц ложится на пользовательский код. Однако, изменять её глобально (например, при загрузке системы) тоже не очень хорошо (потому что глобально). На самом деле я бы хотел, что бы пользовательский код смог указать будет ли он сам формировать специальную страницу или же пусть Hunchentoot генерирует стандартную. Самое простое решение - изменять значение переменной hunchentoot:*handle-http-errors-p* в пользовательском коде, но так, что бы эти изменения происходили не глобально, а только в контексте обрабатываемого запроса. Превратить глобальную переменную hunchentoot:*handle-http-errors-p* в "более локальную" очень просто, для RESTAS я сделал это так:
(defmethod hunchentoot:process-request ((request restas-request))
(let ((hunchentoot:*handle-http-errors-p* hunchentoot:*handle-http-errors-p*))
(call-next-method)))
Теперь можно смело менять значение переменной hunchentoot:*handle-http-errors-p* без какого-либо страха что это отразится на функционировании всей системы.

Если обработчик маршрута в RESTAS возвращает целое число, то оно интерпретируется как код возврата, за что отвечает соответствующая специализация метода restas:render-object. Что бы упростить определение для модулей своих способов формирования специальных страниц я переписал эту специализацию следующим образом:
(defmethod render-object :before (designer (code integer))
(setf hunchentoot:*handle-http-errors-p* nil
(hunchentoot:return-code*) code))

(defmethod render-object (designer (code integer))
"Default handler for HTTP status code"
(declare (ignore designer))
(setf hunchentoot:*handle-http-errors-p* t))
Это реализация по-умолчанию, которая в :before методе выставляет код возврата и присваивает переменной hunchentoot:*handle-http-errors-p* значение NIL, а в основном методе присваивает переменной hunchentoot:*handle-http-errors-p* значение T. Таким образом, по-умолчанию при возврате из маршрута целого числа за формирование специальной страницы будет отвечать Hunchentoot. Но, определив свой тип designer (и присвоив объект этого типа переменной модуля *default-render-method*) можно изменить это поведение и формировать специальные страницы в пользовательском коде. Очень удобно, что при этом также можно использовать и eql-специализаторы, например:
(defmethod restas:render-object ((designer mydrawer) (code (eql hunchentoot:+http-internal-server-error+)))
(setf (hunchentoot:content-type*) "text/plain")
"Шеф, всё очень плохо!")
При этом, может случить так, что если маршрут возвращает hunchentoot:+http-not-found+, то будет формироваться "красивое" сообщение о том, что на данном ресурсе такой страницы нет, но если RESTAS просто не найдёт подходящего маршрута, то будет отдаваться стандартная страница Hunchentoot. Для решение данной проблемы проще всего определить "универсальный" маршрут, который будет проверяться в последнюю очередь:
(restas:define-route not-found ("*any")
hunchentoot:+http-not-found+)
Описанный подход (который можно использовать с git-версией RESTAS) хорошо подходит для обработки специальных страниц общим для всего модуля образом. Но сейчас у меня есть один модуль, который предоставляет некоторое API для клиентского кода. Если маршрут в этом модуле отрабатывает успешно, то клиенту возвращаются данные в формате JSON. А в случае ошибки я бы хотел возвращать код возврата hunchentoot:+http-internal-server-error+ и прикреплять к нему сообщение, описывающие тип ошибки. Для упрощения реализации данного функционала я ввёл следующую функцию:
(defun abort-route-handler (obj &key return-code content-type) ..)
Эта функция немедленно прекращает обработку маршрута, а obj передаётся в restas:render-object для формирования ответа. Тонкость в том, что если в данную функцию передан return-code, то значение переменной hunchentoot:*handle-http-errors-p* выставляется в NIL. Теперь, вызов
(restas:abort-route-handler "Очень плохие данные!"
:return-code hunchentoot:+http-internal-server-error+
:content-type "text/plain")
отправит клиенту ответ с кодом 500 и сообщением "Очень плохие данные!".

понедельник, 12 июля 2010 г.

RESTAS: упрощение отладки удалённых серверов

Я писал ранее, что в репозиторий Hunchentoot был принят мой патч, который должен был упростить отладку удалённых серверов. Однако, в последующем он был отменён с сообщением: "Fix breakage of LW version". LispWorks у меня нет, в чём проблема там я не знаю, но Edi мог хотя бы сообщить об этом в рассылку, а то ведь этого можно было и не заметить. Ну да ладно, немного поразмыслив решил просто перенести данный функционал в код RESTAS. Теперь отладочные режимы RESTAS и Hunchentoot независимы друг от друга. Теперь в RESTAS:
  • Постоянно поддерживается список отлаживаемых в данный момент потоков
  • Добавлен параметр *max-debugging-threads*: максимально возможное количество одновременно отлаживаемых потоков, значение по умолчанию - 5.
  • Функция debug-mode-on - активизирует отладочный режим.
  • Функция debug-mode-off - отменяет отладочный режим, имеет необязательный параметр kill-debugging-threads (по-умолчанию T) , который определяет надо ли уничтожать отлаживаемые в данный момент потоки.
  • В случая наличия в системе swank-сервера, в переменную swank::*connection-closed-hook* добавляется вызов debug-mode-off , который обеспечивает отмену отладочного режима и уничтожение отлаживаемых потоков после разрыва соединения.
  • Описанная схема применяется только к потокам, которые созданы Hunchentoot для обработки запросов.

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

воскресенье, 7 марта 2010 г.

Hunchentoot: упрощение отладки удалённых серверов

В hunchentoot версии 1.1 для управления отладочным режимом используется глобальная переменная *catch-errors-p*: если она установлена в значение, отличное от NIL, то hunchentoot перехватывает все conditions, в обратном случае следует вызов invoke-debugger и разработчик получает возможность разобраться в причинах ошибки с помощью стандартного отладчика. При разработке на машине разработчика никаких проблем не возникает, наоборот, это очень удобно. Однако, при отладке на боевых удалённых серверах (порой это необходимо) эта простая схема начинает представлять угрозу. Во-первых, на серверах с реальной нагрузкой если ошибка возникает на регулярной основе может случиться так, что отладчик будет активизироваться чаще, чем вы способны на это реагировать. Во-вторых, можно забыть сбросить значение *catch-errors-p* при разрыве соединения с удалённым swank-сервером, что приведёт к тому, что потоки начнут подвисать на вызове invoke-debugger и в конце концов система просто откажется работать. Таким образом, для удобной и безопасной отладки удалённых серверов необходимо ограничивать число возможных активных вызовов invoke-debugger, а также автоматически отменять отладочный режим при отключении от удалённого процесса. Сегодня в репозиторий hunchentoot был принят мой патч, решающий описанные проблемы следующим образом:
  • Постоянно поддерживается список отлаживаемых в данный момент потоков
  • Добавлен параметр *max-debugging-threads*: максимально возможное количество одновременно отлаживаемых потоков, значение по умолчанию - 5.
  • Функция debug-mode-on - активизирует отладочный режим.
  • Функция debug-mode-off - отменяет отладочный режим, имеет необязательный параметр kill-debugging-threads (по-умолчанию T) , который определяет надо ли уничтожать отлаживаемые в данный момент потоки.
  • В случая наличия в системе swank-сервера, в переменную swank::*connection-closed-hook* добавляется вызов debug-mode-off , который обеспечивает отмену отладочного режима и уничтожение отлаживаемых потоков после разрыва соединения.
  • Описанная схема применяется только к потокам, которые созданы hunchentoot для обработки запросов.
В поддерживаемой мной git версии репозитория hunchentoot данные изменения будут через пару дней (когда попаду на работу).

суббота, 9 января 2010 г.

Ошибка в Hunchentoot

Сразу после запуска lisper.ru наблюдались достаточно регулярные зависания (раз в несколько дней): система работала, но http-запросы отрабатывать переставала. Удивительно, но подобное я наблюдал на одном единственном сервере, на других машинах (которые работают в заметно более интенсивном режиме) ничего подобного я не видел. Отладить lisp-процесс на удалённом сервере (в данном случае в Германии) с помощью SLIME почти так же просто как и локальный и при очередном зависании я обнаружил следующую картину:
 23: (SB-IMPL::%WITH-STANDARD-IO-SYNTAX #)
24: (INVOKE-DEBUGGER #)
25: (ERROR SB-BSD-SOCKETS:NOT-CONNECTED-ERROR)[:EXTERNAL]
26: (SB-BSD-SOCKETS:SOCKET-ERROR "getpeername")
27: ((SB-PCL::FAST-METHOD SB-BSD-SOCKETS:SOCKET-PEERNAME (SB-BSD-SOCKETS:SOCKET)) # # #)
28: ((SB-PCL::FAST-METHOD USOCKET:GET-PEER-ADDRESS (USOCKET:STREAM-USOCKET)) # # #)
29: (HUNCHENTOOT::CLIENT-AS-STRING #)
30: ((SB-PCL::FAST-METHOD HUNCHENTOOT:HANDLE-INCOMING-CONNECTION (HUNCHENTOOT:ONE-THREAD-PER-CONNECTION-TASKMASTER T)) ..)
31: ((SB-PCL::FAST-METHOD HUNCHENTOOT:ACCEPT-CONNECTIONS (HUNCHENTOOT:ACCEPTOR)) # # #)
32: ((LAMBDA ()))
Судя по этому стэку вызовов проблема связана с ошибкой при вызове функции getpeername и происходит в функции HUNCHENTOOT::CLIENT-AS-STRING, которая вызывается в HUNCHENTOOT:ACCEPT-CONNECTIONS. Вот код HUNCHENTOOT:ACCEPT-CONNECTIONS:
(defmethod accept-connections ((acceptor acceptor))
(usocket:with-server-socket (listener (acceptor-listen-socket acceptor))
(loop
(when (acceptor-shutdown-p acceptor)
(return))
(when (usocket:wait-for-input listener :ready-only t :timeout +new-connection-wait-time+)
(when-let (client-connection
(handler-case* (usocket:socket-accept listener)
;; ignore condition
(usocket:connection-aborted-error ())))
(set-timeouts client-connection
(acceptor-read-timeout acceptor)
(acceptor-write-timeout acceptor))
(handle-incoming-connection (acceptor-taskmaster acceptor)
client-connection))))))
Вероятно, подобные ошибки являются большой редкостью, но к (не)счастью на одной машине я наблюдаю их постоянно. Для решения проблемы с зависаниями я поступил довольно грубо, изменив данную функцию в RESTAS путём добавление в основной цикл макроса ignore-errors:
(defmethod hunchentoot:accept-connections ((acceptor restas-acceptor))
(usocket:with-server-socket (listener (hunchentoot::acceptor-listen-socket acceptor))
(loop
(when (hunchentoot::acceptor-shutdown-p acceptor)
(return))
(ignore-errors
(when (usocket:wait-for-input listener :timeout hunchentoot::+new-connection-wait-time+)
(handler-case
(hunchentoot::when-let (client-connection (usocket:socket-accept listener))
(hunchentoot::set-timeouts client-connection
(hunchentoot:acceptor-read-timeout acceptor)
(hunchentoot:acceptor-write-timeout acceptor))
(hunchentoot:handle-incoming-connection (hunchentoot::acceptor-taskmaster acceptor)
client-connection))
;; ignore condition
(usocket:connection-aborted-error ())))))))
С тех пор прошло около месяца и никаких зависаний больше не было, так что можно считать, что данный код хотя и грубоват, но таки работает :)

P.S. Описанная проблема слишком сложна для моего английского, поэтому в рассылку hunchentoot я об этом не писал. Было бы здорово, если бы кто-нибудь сделал это вместо меня :)

среда, 16 декабря 2009 г.

git-репозиторий с hunchentoot

С исходным кодом Hunchentoot невозможно работать, ибо он лежит под Subversion. Это просто ненормально, 2010-й год на носу. Ладно, теперь есть и под git: http://github.com/archimag/hunchentoot. Обязуюсь регулярно синхронизировать с основным репозиторием и не вносить каких-либо своих изменений. Т.е. это будет чистая копия. Может кто-нибудь форкнет, в конце-то концов...

среда, 28 октября 2009 г.

Hunchentoot, Apache и имя хоста

При использовании hunchentoot в связке с Apache (или там nginx), есть небольшая проблема: метод hunchentoot:host возвращает имя хоста вместе с реальным портом, на котором работает hunchentoot. Проблема возникает из-за того, что это имя может использоваться при генерации url или для операций hunchentoot:redirect. На самом деле, этот порт, на котором работает hunchentoot, совершенно не интересен, мне бы хотелось, что бы метод hunchentoot:host возращал имя хоста на котором работает Apache (nginx), т.е. то имя, которое видят пользователи. 30 секундное изучение исходников hunchentoot подсказывает простое решение:
(defmethod hunchentoot:header-in ((name (eql :host)) request)
(or (hunchentoot:header-in :x-forwarded-host request)
(call-next-method)))
Всё, теперь hunchentoot:host всегда будет возвращать "правильное" значение, независимо от того, работает ли hunchentoot в связке с Apache (nginx) или же самостоятельно.

четверг, 18 июня 2009 г.

Hunchentoot на 80-ом порту

Навеяно сообщением в списке рассылки tbnl-devel, я вдруг обнаружил, что не помню как это сделать, что называется, сделал и забыл. Порывшись в истории (я показывал эту команду catap-у) нашёл нужную команду:
sudo setcap CAP_NET_BIND_SERVICE=ep /usr/bin/sbcl
Пишу здесь, что б в следующий раз больше не искать, точнее рыться не в истории jabber-а, а просто в блоге (я пишу здесь заметно меньше) :)

пятница, 10 апреля 2009 г.

Отладка в hunchentoot-1.0.0

Недавно я писал о проблемах с отладкой приложений для новой версии hunchentoot. Проблема обсуждалась в рассылке и наконец Andreas Fuchs предложил решение этой проблемы, как оказалось, достаточно тривиальное. Оригинальное сообщение можно посмотреть здесь: http://common-lisp.net/pipermail/tbnl-devel/2009-April/004688.html. Я несколько модифицировал этот код (добавил управление режимом через пресловутую переменную *catch-errors-p*):
(defvar *catch-errors-p* t)

(defclass debuggable-acceptor (hunchentoot:acceptor) ())

(defmethod hunchentoot:process-connection ((acceptor debuggable-acceptor) (socket t))
(declare (ignore socket))
(if *catch-errors-p*
(call-next-method)
(handler-bind ((error #'invoke-debugger))
(call-next-method))))

(defmethod hunchentoot:acceptor-request-dispatcher ((acceptor debuggable-acceptor))
(if *catch-errors-p*
(call-next-method)
(let ((dispatcher (handler-bind ((error #'invoke-debugger))
(call-next-method))))
(lambda (request)
(handler-bind ((error #'invoke-debugger))
(funcall dispatcher request))))))
Теперь, для получения удовольствия от использования отладчика достаточно просто использовать debuggable-acceptor вместо hunchentoot:acceptor при запуске сервера (hunchentoot:start).

пятница, 6 марта 2009 г.

Утерянная отладка в hunchentoot

Недавно вышел принципиальной новый релиз hunchentoot - 1.0.0. Я попробовал обновиться и через два часа мытарств был вынужден откатиться к предыдущей версии. Да, там большие изменения, но для меня камнем преткновения оказалось только одно: утеряна возможность отладки с помощью стандартного отладчика. Раньше, если переменная *catch-errors-p* имела значение nil, то hunchentoot, в случае возниковения ошибок просто вызывал invoke-debuger и давал возможность насладиться всеми прелестями разработки на Common Lisp. Этой переменной больше нет... И вызова invoke-debuger в коде тоже больше нигде нет. Новый hunchentoot перехватывает всё и предлагает несколько вариантов обработки ошибок, типа логирования. Но мне нужен отладчик, иначе это будет не разработка, а сплошное мучение.

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

P.S. Ну и задно подписался на рассылку, что бы ничего не упустить.