Saturday, February 09, 2008

Yaws — веб-сервер на Erlang

В самом начале обещал рассказать про веб-приложения на эрланге. Наверное, пора сворачивать ближе к теме.

В качестве веб-сервера для Erlang-приложений лучшим вариантом является написанный на эрланге же веб-сервер Yaws. Чем он хорош — он довольно сильно заточен под высо­копро­изво­ди­тельные приложения, и динамические страницы там удобно генерировать на эрланге.

Сначала насчет производительности:

Картинка известная, подробнее узнать, как она получена можно тут.  По горизонтали это количество параллельных запросов  к серверу (очень медленных клиентов), по вертикали — сколько кб/с при этом параллельно может отдавать сервер одному быстрому клиенту. Красненькое это yaws, синенькое и зелененькое это Apache, который после нескольких тысяч тихо умирает.

Конечно, Apache сравнивать с yaws не очень честно — все-таки апач продукт сложный и намного более многофункциональный, с кучей дополнительных модулей и настроек, и, в общем-то использовать его для отдачи статических страниц не очень принято. Однако ж картинка все-таки очень показательна, возможность держать много параллельных соединений важна в любом случае, да и с динамикой дела у yaws должны быть тоже хорошо (см. например Erlang vs PHP на Computer Language Shootout).

Пару слов об установке Yaws — с линуксом проблем, конечно, нет, на винду поставить тоже можно из-под Суgwin, но чтобы избежать лишней головной боли, обратите внимание, чтобы в пути к эрлангу и директории home не было пробелов (а последняя по умолчанию в "Documents And Settings" находится). Еще пара полезных замечаний про процесс установки есть тут.

 

Настраивается yaws с помощью конфиг-файла, там все довольно тривиально — дефолтный файл можно пока особенно не менять, разве что установить root директорию (параметр docroot).

Простейшее приложение с использованием yaws:

<head><title>Hello world using Yaws</title></head>
<body>
<erl>
out(A) ->
   case queryvar(A, "name") of
      {ok, Name} ->
        {html, "<div style=\"font-size: 12pt;\"> Hello, " ++ Name ++"</div>"};

      undefined ->
        {html, "<b> Name parameter required </b>"}
    end.
</erl>
</body>

 

Сохраняем его как hello.yaws в root директории.

hello-yaws

.yaws файл, как уже видно, это просто HTML шаблон. Каждый блок между тегами <erl> и </erl> компилируется в отдельный Erlang модуль. Каждый такой блок должен содержать функцию out/1, которой в качестве параметра передается структура, содержащая параметры запроса. Есть много вариантов как можно возвращать ответ, наиболее интересны два:

использованный выше

{html, HtmlString}

Где HtmlString — просто кусок HTML в виде строки, как в примере выше.

И чуть более удобный

{ehtml, Term}

Где Term — тот же HTML в виде терма Erlang, где тэги представляются кортежами или списками кортежей. Например, вот такой терм

{p, [],
    [
        {b, [], "Hello World"},
        {a, [{href, "http://www.example.com"}], 
           "link"}
    ]
}

преобразуется сервером в HTML-строку

<p>
  <b>Hello World</b>
  <a href="http://example.com">link</a>
</p>

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

Еще Yaws, кроме того что является просто веб-сервером, предоставляет немало дополнительных инструментов, таких как автоматическая поддержка сессий, cookie, механизмов удаленного вызова процедур на основе SOAP и JSON-RPC.

Для полноценного фреймворка — аналога Ruby on Rails и прочих, API Yaws, конечно, не дотягивает (для этого есть библиотека ErlyWeb), но для небольших приложений этого должно быть вполне достаточно.


Wednesday, February 06, 2008

Erlang: инструменты

Наверное, в самом начале неплохо было б написать про средства отладки, установке Erlang и так далее — так вот, исправляюсь.

Вообще, некоторая проблема с Эрлангом состоит в том, что его разработчики очень не любят писать статьи для начинающих, рисовать туториалы с красивыми скриншотами. Документации по некоторым модулям вообще минимум, по другим — в основном user reference, иногда маленький юзер гайд. Поэтому сначала кажется что разработчик вынужден жить исключительно  наедине с консолью интерпретатора — однако это не так!

IDE

В качестве IDE можно использовать ErlyBird — IDE для Эрланга на платформе NetBeans, соответственно получаем от нее все соответствующие плюшки, выглядит очень красиво:

Screenshot-ErlyBird 070930

Есть еще проект ErlIDE на базе Eclipse, но про него ничего сказать не могу — не пробовал.

Ортодоксальные товарищи также, конечно, могут заюзать vim, emacs или Far под виндой (простенькими hrc файлами для colorer могу поделиться).

Интерпретатор

erl это собственно интерпретатор Эрланга. Я, кстати, тут везде подразумеваю, что у вас установлен последний релиз R12 — там действительно есть порядочное количество  новых полезных плюшек.

Полезные функции интерпретатора:

- скомпилировать и загрузить модуль

> c(boo.erl).
  {ok}

 

- установить текущую директорию

> file:set_cwd(Dir).

 

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

Можно это сделать с помощью того же интерпретатора:

> erl -noshell -run <module> <function> [Params]

(Модуль предварительно надо скомпилировать в байт-код, из интерпретатора или с помощью компилятора erlc).

После окончания работы функции интерпретатор останется висеть, чтобы он закрывался автоматически, можно добавить флаг "- s init stop" (тут за подсказку спасибо анонимному комментатору  предыдущего поста).

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

-module(test).
-export([main/1]).

main(Args) ->
    io:format("Hello world~n").
> escript test.erl
test.erl:4: Warning: variable 'Args' is unused
Hello world

 

Отладчик

Кроме этого есть вполне функциональный отладчик с GUI. Если мы хотим поотлаживать наш модуль foo:
- компилируем его и загружаем его с отладочной информацией

1> c(foo, [debug_info]). 
{ok, foo}

 

- Вызываем монитор эрланговских процессов

2> im(). 
<0.181.0>

Screenshot-Monitor

- загружаем модуль в отладчик

3> ii(foo). 
{module, foo}

 

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

4> iaa([init]). 
true

Screenshot-Attach Process _0.31.0_

Отладчик, естественно, умеет работать по сети, отслеживать сообщения, приходящие процессам, и много всего другого.

Остальное

Если приложение использует эрланговскую распределенную БД mnesia, то нередко нужен удобный способ заглянуть что лежит в табличках. Для этого есть table viewer, который загрузить можно функцией

> tv:start().

Screenshot-tv2

Dialyzer — утилита для статического анализа модулей. Иногда позволяет найти интересные ошибки без запуска программы. Запускается так:

> dialyzer:gui(). 
Хотя с некоторых пор dialyzer идет как отдельное приложение в пакете с Erlang.

Screenshot-Dialyzer v1.7.0 @ lrrr-desktop

Интерфейс всех инструментов, конечно, весьма неказист, но интуитивно понятен, и их вполне хватает для полноценного процесса разработки.


Saturday, February 02, 2008

Erlang: распределенные приложения

Основная фишка erlang — это все-таки его жуткая распределенность.

Модель программирования основана исключительно на посылке сообщений. То есть никакой расшаренной памяти, никаких,
мьютексов, евентов и прочей ерунды, обычно ассоциирующейся с параллельными (concurrent) приложениями.

Примитивных операций есть три:
- можно создать процесс с помощью функции spawn:

spawn(Function)

- можно ему послать сообщение с помощью выражения !

Pid ! Message

- и получить сообщение

receive
    Pattern1 -> ...
    Pattern2 -> ...
    Pattern3 -> ...
after N ->
    ...
end

 

    Конструкция receive очень похожа на выражение case, только для обработки таймаутов можно добавить кейс after N,
где N — время в мс, по истечении которого перестаем ждать ответа.

[Избитый] простейший пример, с процессами, пингующими друг друга:

-module(ping_test).
-compile(export_all).

ping() ->
% создаем процесс pong
    Pid = spawn(fun pong/0),
% посылаем ему сообщение -- атом ping и свой Pid
    Pid ! {ping, self()},
% ждем от него сообщений
    ping_loop(Pid).

% обработка сообщений от процесса pong
ping_loop(Pid) ->
    receive
        {pong, Pid} ->
% получили сообщение pong и Pid отправителя
            io:format("received pong~n"),
% ждем случайный интервал времени от 0 до 1200 мс.
            timer:sleep(random:uniform(1200)),
% шлем ему опять сообщение ping
            Pid ! {ping, self()},
            ping_loop(Pid) % и опять ждем...
        after 1000 ->
            io:format("pong timed out~n")
    end.

% входная точка процесса pong
pong() ->
    receive
        {ping, Pid} ->
% получили сообщение ping и Pid отправителя
            io:format("received ping~n"),
% подождем..
            timer:sleep(random:uniform(1200)),
% шлем обратно pong
            Pid ! {pong, self()},
            pong() % цикл
    after 1000 ->
        io:format("ping timed out~n")
    end.


 

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

1> c(ping_test).
{ok,ping_test}
2> ping_test:ping().
received ping
received pong
received ping
received pong
received ping
received pong
received ping
connection to pong timed out
ok
3> connection to ping timed out

 

Рантайм обеспечивает, что сообщения доставляются в том же порядке, как и были отправлены, и никогда не выпадают из последовательности (то есть, если процесс N+1 сообщение не будет доставлено, если пропало N-ное).

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

Чтобы превратить пример с ping в распределенное приложение, нужно просто заменить вызов

spawn(fun ...)

на

spawn(node, fun ...)

Где node это атом-имя узла на котором требуется запустить процесс. виртуальной машины. (А узел (node) — это просто запущенный экземпляр виртуальной машины Erlang).

Итак, немножко изменим функцию ping (модифицированный исходник тут):

ping(Node) ->
    Pid = spawn(Node, fun pong/0),
    Pid ! {ping, self()},
 ping_loop(Pid).

Теперь запустим два интерпретатора в качестве узлов с именами boo и foo (оба должны иметь доступ к скомпилированному модулю, так что проще запустить их из одной директории):

$ erl -name foo@127.0.0.1
$ erl -name boo@127.0.0.1

(boo@127.0.0.1)2> c(ping_distrib). {ok,ping_distrib} (boo@127.0.0.1)1> ping_distrib:ping('foo@127.0.0.1'). received ping received pong received ping received pong received ping received pong received ping connection to pong timed out ok (boo@127.0.0.1)2> connection to ping timed out

Создание процесса в Эрланге — очень дешевая операция (цифры — порядка сотен тысяч созданных и убитых процессов в секунду, см. бенчмарк здесь ).

Запущенные процессы также жрут очень мало памяти, а в режиме ожидания сообщений — еще и не жрут CPU (например, 500000 таких спящих процессов у меня ест 0% CPU и 600 Мб памяти — около 1.4 кб на процесс).


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

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

erlang-process-crash

Связываются процессы с помощью функции link/2. Для удобства есть еще spawn_link/2, и если ее использовать в нашем примере вместо spawn, а также выходить из процессов не нормально, а с помощью erlang:error/1 (см. модифицированный исходник тут), вывод будет немного другим:

4> ping_test:ping().
received ping
received pong
connection to ping timed out

=ERROR REPORT==== 2-Feb-2008::14:30:23 ===
Error in process <0.43.0> with exit 
    value: {ping_timeout,[{ping_test,pong,0}]}

** exception exit: pong
     in function  ping_test:pong/0

 

Не рекомендуется использовать другие механизмы, такие как коды ошибок — идеология "let it crash". Скажем, если функция принимает на вход строку — это проблема вызывающего не передавать туда атомы, числа и прочую хрень.

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