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