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 не кажутся на первый взгляд чем-то сверхпродвинутым, именно они помогают писать сложные отказоустойчивые серверные приложения, уделяя при этом минимум времени на написание кода, обрабатывающего ошибки.

12 comments:

turist said...

Как человек, развращённый Python-ом и Си, заявляю - ну и синтаксис! :)))

Виталий Бондарь said...

Хорошее дело затеял автор. Большое спасибо.
Сам сейчас потихоньку ерланг изучаю и с любопытством прочитал статьи. Жду продолжения :)

axet said...

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

lrrr said...

axet> По сравнению с чем?

rushman said...

2axet

она и не должна быть предсказуемой и линейной.

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

Krasin said...

Я правильно понимаю, что программа через некоторое время упадет со stack overflow? ping_loop вызывает сам себя.

lrrr said...

Krasin> Нет, как и любой другой функциональный язык, Erlang поддерживает оптимизацию хвостовых вызовов -- т.е. этот рекурсивный вызов ping_loop будет преобразован компилятором в цикл.

Krasin said...

О, спасибо. Я забыл про хвостовую рекурсию :)

gds said...

очень понравилось (вместе с первой частью).

Сергей Кищенко said...

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

Если же добавить, что принципами дизайна заложено автоматическое восстановление упавших процессов(самовосстанавливающееся дерево), то совместно с возможностью замены кода на лету это может убедить каждого, на какие чудеса выносливости способен Эрланг :)

Anonymous said...

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

Anonymous said...

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

И тут мы вплотную подходим к вопросу использования БД... В Erlang есть встроенная БД mnesia для хранения небольших объемов данных, но можно использовать и более мощные РСУБД, если есть такая необходимость.

Erlang - функциональный язык, а то, что вы хотите, называется хранимым состоянием (что в ФП считается дурным тоном). Такими вещами программа заниматься не должна (в большинстве случаев), а должна вместо этого использовать спецсредства. СУБД - одно из таких средств. Прямая передача контекста через параметры функции - второй способ передачи хранимого состояния, но он не всегда применим.