Altenwald Blog
Blog sobre programación, software libre, redes, servidores, ...
Menú
Acerca de... ¿Quiénes somos? RSS
Categorías
sistemas (70) desarrollo (128) historias (25) productividad (49) seguridad (10) libros (25) noticias (45) opinión (35) humor (3)
Etiquetas
programación (111) desarrollo de software (79) erlang (75) opinión (37) noticia (36) libros (28) servidores (26) desarrollo web (24) base de datos (24) administración de sistemas (23) php (22) desarrollo ágil (22) empresa (21) otp (20) ruby (19) ingeniería de negocio (18) elixir (18) desarrollo profesional (16) redes (16) seguridad (14)
2012-05-07
6 min desarrollo
Estructura Líder/Trabajador en Erlang
[ programación ]  [ erlang ]  [ gen_leader ]  [ lider/trabajador ]  [ maestro/esclavo ]  [ otp ] 

Erlang es muy bueno para programación distribuida, y paralela, y concurrente, así mismo se hace muy simple la creación de servidores, tal y como he mostrado en entradas anteriores (servidor UDP y servidor TCP), pero nos queda una tipo de comportamiento que es algo complejo llevar a la práctica. Me refiero al paradigma de Maestro-Esclavo.

La teoría

El paradigma Maestro-Esclavo se basa en que, para acceder a un recurso que está disponible solo desde un punto o solo para un servicio, ya sea por que su acceso es crítico en concurrencia o porque solo se pueda acceder desde un único punto cada vez, se hace necesaria una estructura en la que un único punto sea el responsable de ese acceso, quedando todos los demás supeditados a este.

Esto genera igualmente un problema de cuello de botella, pero la misión no es distribuir, en este caso, para ganar más potencia de procesamiento, sino conseguir: una mayor alta disponibilidad para un recurso accesible solo desde un punto cada vez.

En la terminología de Erlang, la nomenclatura empleada es líder (leader) para referirse al maestro elegido cada vez, y trabajador (worker) para referirse a los esclavos. Esto es así por dos sutiles diferencias que existen entre el modelo clásico de maestro-esclavo frente a este de lider-trabajador:

Aunque es un comportamiento bastante básico, aún no se ha introducido en la base de Erlang, ya que sus algoritmos de elección han sufrido cambios, incluso hasta hace pocos meses. Supongo que incluso aún habrá algún que otro cambio más, por lo que conviene mantenerse al día. El repositorio oficial (de momento) es este de github, gen_leader_revival de abecciu.

Los autores de la base son Andrew Thompson, Dave Fayram, Hans Svensson y Ulf Wiger.

La implementación

El código podemos descargarlo del repositorio indicado anteriormente, solo necesitaremos el código de gen_leader.erl y el código de skeleton.erl.

La plantilla nos da lo básico para que el gen_leader funcione. Si compilamos ambas y lanzamos desde una consola el skeleton, podemos ver que funciona correctamente. Es más, vamos a modificarlo muy poquito agregando solo impresiones por pantalla para saber en qué momento se ejecuta cada uno:

elected(State, Election, undefined) ->
    Synch = [],
    io:format("no one elected? ~p~n", [Election]),
    {ok, Synch, State};

elected(State, Election, Node) ->
    io:format("elected node [~p]: ~p~n", [Node, Election]),
    {reply, [], State}.

surrendered(State, Synch, Election) ->
    io:format("surrendered (~p): ~p~n", [Synch, Election]),
    {ok, State}.

handle_leader_call(Request, _From, State, Election) ->
    io:format("leader_call (~p): ~p~n", [gen_leader:leader_node(Election), Request]),
    {reply, ok, State}.

from_leader(Synch, State, Election) ->
    io:format("from_leader (~p): ~p~n", [gen_leader:leader_node(Election), Synch]),
    {ok, State}.

handle_DOWN(Node, State, Election) ->
    io:format("DOWN (~p): ~p~n", [Node, gen_leader:leader_node(Election)]),
    {ok, State}.

El código modificado es llamado como callback cuando se suceden las siguientes situaciones:

Teniendo estas características en cuenta, podemos ejecutar el código y ver cómo se comporta:

En lider1@bosqueviejo:

(lider1@bosqueviejo)1> {ok, Pid} = datos:start_link(['lider1@bosqueviejo', 'lider2@bosqueviejo']).
no one elected? {election,<0.55.0>,none,datos,lider1@bosqueviejo,
                          [lider1@bosqueviejo,lider2@bosqueviejo],
                          [],
                          [lider2@bosqueviejo],
                          [],[],none,norm,
                          {1,6,0},
                          [],[],5000,
                          {interval,#Ref<0.0.0.49>},
                          lider2@bosqueviejo,6,1,sender}
{ok,<0.55.0>}

Ahora levantamos en lider2@bosqueviejo:

(lider2@bosqueviejo)1> {ok, Pid} = datos:start_link(['lider1@bosqueviejo', 'lider2@bosqueviejo']).
{ok,<0.55.0>}
surrendered ([]): {election,<6327.55.0>,none,datos,lider1@bosqueviejo,
                            [lider1@bosqueviejo,lider2@bosqueviejo],
                            [],[],
                            [{lider1@bosqueviejo,lider1@bosqueviejo},
                             {#Ref<0.0.0.59>,lider1@bosqueviejo},
                             {#Ref<0.0.0.57>,lider1@bosqueviejo}],
                            [],none,norm,
                            {1,6,0},
                            [],[],5000,undefined,undefined,5,1,sender}

En lider1@bosqueviejo se sucede el siguiente mensaje al levantar al lider2@bosqueviejo:

elected node [lider2@bosqueviejo]: {election,<0.55.0>,none,datos,
                                       lider1@bosqueviejo,
                                       [lider1@bosqueviejo,lider2@bosqueviejo],
                                       [],[],
                                       [{#Ref<0.0.0.74>,lider2@bosqueviejo}],
                                       [],none,norm,
                                       {1,6,0},
                                       [],[],5000,
                                       {interval,#Ref<0.0.0.49>},
                                       lider2@bosqueviejo,6,1,sender}

Ahora en lider1@bosqueviejo enviamos un mensaje:

(lider1@bosqueviejo)2> gen_leader:leader_call(Pid, "hola").
leader_call (lider1@bosqueviejo): "hola"
ok

Y lo que pasaba al mismo tiempo en lider2@bosqueviejo:

from_leader (lider1@bosqueviejo): [118,117,101,115,116,114,111,32,108,105,100,
                                   101,114,32,100,105,99,101,58,32,"hola",
                                   "\n"]

En caso de que salgamos de la consola del lider1@bosqueviejo vemos lo siguiente en lider2@bosqueviejo:

no one elected? {election,<0.55.0>,<6327.55.0>,datos,lider2@bosqueviejo,
                          [lider1@bosqueviejo,lider2@bosqueviejo],
                          [],
                          [lider1@bosqueviejo],
                          [{#Ref<0.0.0.59>,lider1@bosqueviejo},
                           {#Ref<0.0.0.57>,lider1@bosqueviejo}],
                          [],none,norm,
                          {2,5,1},
                          [],[],5000,
                          {interval,#Ref<0.0.0.75>},
                          lider2@bosqueviejo,5,2,sender}
DOWN (lider1@bosqueviejo): lider2@bosqueviejo

Es decir, se elige a un nuevo líder y se envía el mensaje de caída del líder.

Conclusiones

Este esquema es bastante útil ya que simplifica el problema de tener varios servidores, por ejemplo, y recursos que solo pueden estar disponibles en un solo servidor cada vez. Por ejemplo, el acceso a un recurso propietario que solo nos da una licencia de uso, el acceso con control de concurrencia a un recurso.

Su interfaz de broadcast, permite que el sistema pueda recibir peticiones de lectura y escritura, haciendo que su broadcast deje la información en cada worker para, en caso de que cayese el líder, puedan seguir funcionando sin problema alguno.

Realmente, otro elemento más para facilitar la creación de sistemas servidores de alta disponibilidad en Erlang.

Autor
Manuel Rubio
Programación Concurrente & Erlanger