Aplicações distribuídas com OTP
- 5 minutes read - 1012 wordsDistributed applications
O conceito de distributed applications em Erlang/OTP segue a ideia de que determinada aplicação executa em um determinado nó de um cluster. Se a aplicação parar por alguma falha, então a aplicação vai ser iniciada em outro nó.
Isso não quer dizer que a aplicação vai estar rodando em todos os nós ao mesmo tempo. Mas sim em apenas um determinado nó (de maior prioridade configurada).
O que vai estar rodando e ativamente controlando a aplicação é o _distributed application controller. Que é o que realmente faz o controle de onde e quando uma aplicação distribuída vai ser executada.
Principal caso de uso
O melhor caso de uso é quando temos uma aplicação na qual apenas uma instância dela é necessária. E mesmo que haja uma falha, a mesma aplicação vai ser iniciada em outro nó do cluster Erlang.
Distributed applications não é usado ou indicado para cenários onde a mesma aplicação deve rodar em todos os nós.
Conceitos importantes
Os três conceitos mais importantes para usar distributed applications são:
- A aplicação é iniciada chamando application:start/1 em todos os nós. Mas somente em um nó a aplicação vai ser executada
- failover: se a aplicação falhar no nó que ela esta rodando, então, a mesma será executada no próximo nó listado nos parâmetros de configuração
- takeover: se um nó é iniciado e o mesmo tem uma prioridade maior de acordo com a configuração, então a aplicação é reiniciada no novo nó e parada no nó antigo.
Exemplo: toliman
toliman é uma aplicação Erlang/OTP na qual implementa um exemplo usando distributed applications.
A ideia básica é ter em um cluster com N nós duas aplicações:
- proxima: uma instância apenas, usando distributed application
- centauri: iniciada e executando em todos os nós
De acordo com a interação dos nós, proxima pode fazer takeover e failover.
O test case abaixo testa um cenário básico de failover e takeover:
https://github.com/joaohf/toliman/blob/beam-31/apps/centauri/src/centauri_app.erl 1start(_StartType, _StartArgs) ->
2 case application:start(proxima) of
3 ok ->
4 ok;
5 {error, {already_started, Reason}} ->
6 ?LOG_WARNING("proxima already started: ~p", [Reason]),
7 ok
8 end,
9
10 centauri_sup:start_link().
Iniciando a aplicação em todos os nós
O código abaixo mostra a chamada application:start/1 no qual inicia a aplicação proxima no nó. Entretanto, proxima só será realmente iniciada caso o dist_ac deixar (de acordo com as configurações).
https://github.com/joaohf/toliman/blob/beam-31/test/toliman_SUITE.erl 1t_a_b_failover(Config) ->
2 % start normal
3 Nodes = start_slaves(Config, [a, b]),
4
5 ct:sleep(3000),
6
7 NodeA = lists:keyfind(a, #centauri_node.name, Nodes),
8 NodeB = lists:keyfind(b, #centauri_node.name, Nodes),
9
10 start_application(NodeA, centauri),
11 start_application(NodeB, centauri),
12
13 ct:sleep(2000),
14
15 {status, PidA, _, _} = ct_rpc:call(NodeA#centauri_node.node, sys, get_status, [proxima]),
16 PidA = ct_rpc:call(NodeB#centauri_node.node, global, whereis_name, [proxima]),
17
18 % B failing over A
19 stop_slave(NodeA),
20
21 ct:sleep(2000),
22
23 {status, _PidB, _, _} = ct_rpc:call(NodeB#centauri_node.node, sys, get_status, [proxima]),
24
25 % A is taking over B
26 [SecondNodeA] = start_slaves(Config, [a]),
27 start_application(SecondNodeA, centauri),
28
29 ct:sleep(2000),
30
31 {status, SecondPidA, _, _} = ct_rpc:call(SecondNodeA#centauri_node.node, sys, get_status, [
32 proxima
33 ]),
34 SecondPidA = ct_rpc:call(NodeB#centauri_node.node, global, whereis_name, [proxima]),
35
36 ct:sleep(2000),
37
38 stop_slave(NodeB),
39 stop_slave(SecondNodeA),
40
41 ok.
Em todo caso, se houver um failover, a aplicação será efetivamente iniciada.
Como coordenar a transição durante um failover ou takeover
Bom, quando há um failover (o nó que está rodando a aplicação morre ou a aplicação morre), não há o que fazer (caso os dados não estejam persistidos) haverá perca de dados ou estado. Este é o cenário do failover, então uma nova instância irá ser executada no próximo nó disponível.
Já para o caso do takeover, é uma operação mais controlada no qual a instância antiga continua sendo executada no nó antigo enquanto a instância nova é inicia no nó novo. Nestas condições podemos transferir o estado da aplicação antiga para a nova.
Uma das soluções é usar start phases e em cada fase temos a chance de fazer determinada operação para sincronizar o estado remoto. Exemplo:
Os trechos abaixo mostram o que acontece em cada faze. Sendo:
init: qualquer código de inicialização pode ser colocado nesta faze: https://github.com/joaohf/toliman/blob/beam-31/apps/proxima/src/proxima_app.erl
1% init: processes started, basic initialisation 2start_phase(init, _Type, []) -> 3 ?LOG_INFO("Start phase init: ~p", [_Type]), 4 ok;
takeover: caso haja um takeover forçado application:takeover/1 podemos atuar nesta faze. Por exemplo fazer alguma operação para desabilitar algo: https://github.com/joaohf/toliman/blob/beam-31/apps/proxima/src/proxima_app.erl
1% takeover: ignored unless application start type was {takeover, FromNode} 2start_phase(takeover, {takeover, _FromNode}, []) -> 3 ?LOG_INFO("Start phase takeover from ~p", [_FromNode]), 4 ok; 5start_phase(takeover, _Type, []) -> 6 ?LOG_INFO("Start phase takeover: ~p", [_Type]), 7 ok;
go: a aplicação já foi iniciada está pronta. Neste passo podemos sincronizar o estado com a antiga instância: https://github.com/joaohf/toliman/blob/beam-31/apps/proxima/src/proxima_app.erl
A função profima:fetch_state/2 transfere o estado: https://github.com/joaohf/toliman/blob/beam-31/apps/proxima/src/proxima_app.erl1% go: to register global names, do other forms of complex initialisation, etc. 2start_phase(go, Type, []) ->
1 case Type of 2 {failover, _Node} -> 3 ok; 4 {takeover, Node} -> 5 ok = proxima:fetch_state(Node);
global e register
A aplicação proxima é distribuída, rodando em qualquer nó do cluster. Para que outras aplicações no cluster possam encontrar os processos e mandar mensagem, uma das técnicas é registrar os processos locais como globais:
https://github.com/joaohf/toliman/blob/beam-31/apps/proxima/src/proxima_app.erl1start_phase(go, Type, []) ->
2 _ = et:trace_me(80, {proxima_app, erlang:node()}, start, [{go, Type}]),
3
4 ?LOG_INFO("Start phase go: ~p", [Type]),
5 Names = [proxima],
6 Fun = fun(Name) ->
7 Pid = erlang:whereis(Name),
8 yes = global:re_register_name(Name, Pid)
9 end,
O código acima faz o seguinte:
- Encontra qual é o PID que responde pelo nome proxima
- Registra o PID com um nome global onde todos os nós podem encontrar o PID
O módulo (https://erlang.org/doc/man/global.html) cuida de registrar um PID globalmente.
Conclusão
distributed application é algo funcional mas que demanda um certo entendimento e cenários específicos. Além de orientar o design da aplicação pensando em failover e takeover. Muitos podem desaconselhar mas aí cabe o entendimento da feature e avaliar o seu uso.
A realidade é que implementar algo similar é bem mais complicado, e dependendo do cenário, distributed application é algo factível.
Segue algumas referências externas para continuar os estudos: