Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Фреймворк Yesod

Глава 22 из книги "Архитектура приложений с открытым исходным кодом", том 2.

Оригинал: Yesod
Автор: Michael Snoyman
Перевод: Н.Ромоданов

22.2. Интерфейс веб-приложений

Веб-приложениям необходим некоторый способ общения с сервером. Одним из возможных способов является встраивание сервера непосредственно во фреймворк, но такой подход обязательно будет ограничивать ваши возможности по развертыванию веб-приложения и ведет к созданию плохих интерфейсов. Для решения этой проблемы во многих языках создавались стандартные интерфейсы: в языке Python есть WSGI, а в Ruby есть Rack. В Haskell, у нас есть WAI: Web Application Interface - веб-интерфейс приложений.

Интерфейс WAI не предназначен для использования в качестве интерфейса высокого уровня. У него есть две конкретные цели: обеспечить единообразность и производительность. Оставаясь единообразным, интерфейс WAI поддерживает работу с различными фоновыми процессами начиная от автономно работающих серверов и до технологии CGI в старом стиле, и даже до непосредственного использования Webkit при создании приложений, работающих на рабочем столе. Что касается производительности, то мы представим вам ряд интересных функций языка Haskell.

Рис.22.1: Общая структура приложения Yesod

Типы данных

Одно из самых больших преимуществ языка Haskell и то, что мы пытаемся максимально использовать в Yesod, это строгая статическая типизация. Перед тем, как начать писать код для того, чтобы что-нибудь решить, нам нужно будет подумать о том, как будут выглядеть данные. Интерфейс WAI является прекрасным примером этой парадигмы. Основная концепция, которую мы хотим выразить, является приложение. Самым основным базовым выражением является функция, к которой делается запрос и от которой возвращается ответ. На языке Haskell:

type Application = Request -> Response

Только возникает вопрос: как должен выглядеть запрос Request и ответ Response? В запросе есть ряд информационных фрагментов, но самым основным являются запрашиваемый путь, строка запроса, заголовки запроса и тело запроса. А в ответе должны быть только три компонента: код состояния, заголовки ответа и тело ответа.

Как мы представляем себе что-то, похожее на строку запроса? В языке Haskell соблюдается строгое разделение между двоичными и текстовыми данными. Первые можно представить с помощью ByteString, а вторые — с помощью Text. Оба типа являются хорошо оптимизированными типами данных, для которых предоставляется высокоуровневый безопасный интерфейс API. В случае строки запроса мы сохраняем необработанные байт данных, передаваемые по сети в виде типа ByteString, а проанализированные декодированные значения — в виде типа Text.

Потоки

Тип ByteString представляет собой отдельно взятый буфер памяти. Если бы мы по наивности использовали простой тип ByteString для передачи тела запроса и получения тела ответа, то наши приложения никогда нельзя было бы масштабировать для работы с большими запросами и ответами. Вместо этого мы используем технологию, называемую enumerator (перечисление), очень похожую по концепции на generators (генераторы) в языке Python. Наше приложение Application становится потребителем потока объектов типа ByteStrings, представляющих собой тело входящего запроса, и создает отдельный поток для ответа.

Теперь нам нужно немного пересмотреть наше определение приложения Application. Приложение Application выдает значение типа Request, в котором содержатся заголовки, строка запроса и т.д., и будет получать поток объектов типа ByteString, из которых состоит ответ Response. Таким образом, пересмотренное определение приложения будет Application следующим:

type Application = Request -> Iteratee ByteString IO Response

Обозначение IO просто указывает какие типы побочных эффектов приложение может выполнять. В случае IO, оно может выполнять любые виды взаимодействий с внешним миром, что является очевидной необходимостью для подавляющего большинства веб-приложений.

Сборщик

Весь фокус нашего арсенала состоит в том, как мы создаем буферы с нашими ответами. У нас есть здесь два конкурирующих пожелания: минимизация количества системных вызовов и минимизация количества копирований буфера. С одной стороны, мы хотим свести к минимуму количество системных вызовов при передаче данных через сокет. Для этого нам нужно хранить исходящие данные в буфере. Тем не менее, если мы сделаем этот буфер слишком большим, то мы исчерпаем нашу память и замедлим время отклика приложения. С другой стороны, мы хотим свести к минимуму количество копирований, когда данные копируются между буферами, предпочтительно копировать только один раз из буфера источника данных в буфер назначения.

В языке Haskell есть решение, которое называется сборщиком (builder). Сборщик представляет собой инструкцию о том, как заполнять буфер памяти, например: разместить пять байтов «hello» в следующей свободной позиции. Вместо передачи потока буферов памяти на сервер, приложение WAI обрабатывает поток этих инструкций. Сервер берет поток и использует его так, чтобы оптимальным образом использовать буфера памяти, имеющие определенный размер. Как только буфер будет заполнен, сервер осуществляет системный вызов с тем, чтобы послать данные по сети, а затем начинает заполнение буфера следующего.

Оптимальный размер буфера будет зависеть от многих факторов, например, от размера кэш-памяти. Библиотека blaze-builder, на основе которой все это выполняется, прошла через существенное тестирование производительности прежде, чем был найден наилучший компромисс.

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

Обработчики

Теперь, когда у нас есть приложение, мы должны каким-то образом его запустить. С точки зрения интерфейса WAI, это делает обработчик (handler). В WAI есть некоторые базовые стандартные обработчики, например, автономные серверы Warp (о нем будет рассказано ниже), FastCGI, SCGI и CGI. Такой спектр серверов позволяет запускать приложения WAI приложений, которые будут работать начиная от специально выделенных серверов и до виртуального хостинга. Но, в дополнение к этому, в WAI есть еще несколько интересных возможностей:

  • Webkit: Этот движок встроен в сервер Warp и вызывается при обращении к QtWebkit. Когда запускается сервер, то затем открывается новое отдельное окно браузера и у нас есть приложение, работающее на рабочем столе.
  • Launch: Это несколько упрощенный вариант Webkit. Необходимость развертывать библиотеки Qt и Webkit может быть несколько обременительной, так что мы вместо этого просто запускаем браузер, используемый пользователем по умолчанию.
  • Test: Даже тестирование рассматривается как обработчик. В конце концов, тестирование просто акт запуска приложения и проверки его ответов.

Большинство разработчиков, скорее всего, будут использовать Warp. Это достаточно легковесный сервер, которого будет достаточно для тестирования. Он не требует конфигурационных файлов, нет иерархии папок и долгоиграющего процесса, принадлежащего администратору. Это простая библиотека, которая компилируется в вашем приложении или запускается с помощью интерпретатора Haskell. Warp является невероятно быстрым сервером с защитой от всех видов атак, например, Slowloris или бесконечного списка заголовков. Warp может быть единственным веб-сервером, который вам нужен, хотя все будет также в порядке, если его поместить за прокси-сервером HTTP.

С помощью бенчмарка PONG было измерено количество запросов, состоящих из четырехбайтового тела «PONG» и обрабатываемого за секунду на различных серверах. На графике, показанном на рисунке 22.2, Yesod измеряется как фреймворк, работающий поверх Warp. Как видно, сервера Haskell (Warp, Happstack и Snap) лидируют.

Рис.22.2: Бенчмарк PONG для сервера Warp

Большинство причин такой скорости сервера Warp уже были изложены в общей характеристике интерфейса WAI: счетчики, сборщики и упакованные типы данных. Последняя часть головоломки заключается в многопоточности времени выполнения компилятора Glasgow Haskell Compiler (GHC's). GHC, флагманский компилятор языка Haskell, использует легковесные потоки с незначительным потреблением ресурсов. В отличие от системных потоков, можно запустить тысячи таких потоков без серьезной загрузки по производительности. Таким образом, в каждом соединении Warp обрабатывается свой собственный поток со слабым потреблением ресурсов.

Следующая хитрость состоит в асинхронном вводе / выводе. В любом веб-сервере, в котором нужно масштабирование до десятков тысяч запросов в секунду, требуется некоторый тип асинхронной связи. В большинстве языков, это связано с участием сложного в программировании механизма обратного вызова. GHC позволяет нам обмануть систему: мы программируем как будто мы используем синхронный интерфейс API, а GHC автоматически переключается между различными потоками, ожидающими своей очереди.

Глубже GHC пользуется всем, что предоставляет хостовая операционная система, например, kqueue, epoll и select. В результате мы получаем производительность системы ввода / вывода, использующей события, и не беспокоимся о кросс-платформенных вопросах и не пишем в стиле, ориентированном на обратные вызовы.

Промежуточный слой middleware

Между обработчиками и приложениями у нас есть промежуточный слой middleware. Технически он представляет собой преобразователь приложений application transformer: он берет одно приложение и возвращает новое. Это определяется следующим образом:

type Middleware = Application -> Application

Лучший способ понять назначение промежуточного слоя - это рассмотреть некоторые распространенные примеры:

  • gzip автоматически сжимает ответ, получаемый от приложения.
  • jsonp автоматически преобразует ответы JSON в ответы JSON-P, когда клиент представляет параметр функции обратного вызова.
  • autohead будет генерировать соответствующие ответы HEAD на основе ответа GET, поступившего от приложения.
  • debug будет на каждый запрос выдавать отладочную информацию в консоль или в журнал.

Идея заключается в том, чтобы вынести из приложений за скобки общий код и позволить его использовать совместно нескольким приложениям. Обратите внимание, что, исходя из определения такого промежуточного слоя, мы можем легко расширять такие возможности. Общая схема работы промежуточного слоя состоит в следующем:

  1. Берем значение запроса и применяем некоторые модификации.
  2. Передаем модифицированный запрос в приложение и принимаем ответ.
  3. Модифицируем ответ и возвращаем его обработчику.

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

Тесты wai-test

Никакая статическая типизация не позволит устранить необходимость тестирования. Мы все знаем, что автоматизированное тестирование является необходимостью для любого серьезного приложения. wai-test является рекомендуемым подходом к тестированию приложений с интерфейсом WAI. Поскольку запросы и ответы являются простыми типами данных, легко сформировать искусственный запрос, передать его в приложение и проверить, какой будет ответ. В wai-test просто предлагаются некоторые удобные функции для тестирования общих свойств, таких как наличие заголовка или кода состояния.


Далее: Шаблоны