sábado, 17 de marzo de 2012

Erlang (III) Semana 1

Semana 1

Después de hacer una Semana Cero* de: aprender, aprender, aprender, aprender, aprender.... he empezado el proyectito.

La primera semana simplemente es para jugar y hacer pruebas. Quería empezar creando una primera versión de mi chat. En realidad he rehecho el código 4 veces des de cero, mejorando cada vez la versión anterior. El objetivo primero era conseguir dar de alta usuarios (login), y que pudiesen mandarse mensajes entre ellos. Todo a través de la línea de comandos (intérprete de Erlang). Después también he pensado que estaría bien hacer un poquito de benchmark para probar si la idea básica funciona, o todavía necesito aprender más cosas antes de empezar.

Básicamente estas "iteraciones" de ir rehaciendo código y mejorándolo según los errores y problemas aprendidos ha sido algo así:
  1. Escrito código 100% custom que se encarga de la gestión de mensajes, sin hacer uso de OTP. Básicamente se disponen de dos módulos: gaab_server y gaab_user. El primero sirve para saber qué usuarios que están registrados en el chat, el segundo módulo tiene todas las funcionalidades de un usuario, y dispone de funciones tales como send_message, receive_message, talk_with, ....
  2. Reescrito el código ahora haciendo uso de records para manter el estado e implementando una interfície muy parecida a gen_server, pero todavía hecho todo 100% custom.
  3. Usar gen_server tanto para el gaab_server como para gaab_user, quitar toda la funcionalidad custom que ya me dá gen_server.
  4. Creado un Makefile, organizado el proyecto en las carpetas correspondientes (src, ebin,...) y demás.


[Cómo funciona]

Explico un poquito más cómo funciona el tema... Tenemos un agente de gaab_server que se encarga de almacenar los usuarios logeados. Básicamente el flujo normal será loguearnos en gaab_server y éste creará un agente de tipo gaab_user, y nos devolverá su Pid. El servidor también tiene una función llamada pidof/1 que devuelve el Pid del usuario pasado como parámetro. Simplemente se encarga de saber qué usuarios están logueados en el sistema.

El agente gaab_user tiene funciones para hablar con otros usuarios, y siempre está a la espera de recibir mensajes dirigidos hacia el, que imprime por pantalla. Todo muy simple, por ser una primera versión.

[Benchmarcks]

Vamos a lanzar un par de tests para comprobar que, efectivamente, el servidor soporte algo de carga. Se van a medir básicamente dos cosas: coste de CPU y coste de memória. Los dos parámtros con los que jugamos son básicamente dos: número de usuarios conectados al mismo tiempo y número de mensajes por segundo enviados entre usuarios. La máquina con la que trabajamos es la siguiente:

Memória RAM: 6GiB DDR (1066).

CPU: Intel i5 - M540 2.53GHz (2 cores + 2 de Hyperthreading)


Empezemos.

Prueba 1. Aumentar número de usuarios

1.1 - Número de Usuarios: 10K. Mensajes por segundo: 500.

Carga de CPU ~50% de carga en un core (~13% de el procesador).

Memória total del proceso: 46.336 Kbytes. Memória por usuario: 2,98 Kb.

1.2 - Número de Usuarios: 20K. Mensajes por segundo: 500.

Carga de CPU ~50% de carga en un core (~13% de el procesador).

Memória total del proceso: 78.656 Kbytes. Memória por usuario: 3.11 Kb

1.3 - Número de Usuarios: 30K. Mensajes por segundo: 500.

Carga de CPU ~50% de carga en un core (~13% de el procesador).

Memória total del proceso: 87.692 Kbytes. Memória por usuario: 2.37Kb


Prueba 2. Aumentar el número de mensajes/segundo
2.1 - Número de Usuarios: 10K. Mensajes por segundo: 500. (prueba 1.1 anterior)

Carga de CPU ~50% de carga en un core (~13% del procesador).

Memória total del proceso: 46.336 Kbytes.

2.2 - Número de Usuarios: 10K. Mensajes por segundo: 750.

Carga de CPU ~75% de carga en un core (~20% del procesador).

Memória total del proceso: 37.244 Kbytes.

2.3 - Número de Usuarios: 10K. Mensajes por segundo: 1000.

Carga de CPU ~85% de carga en un core (~23% del procesador).

Memória total del proceso: 30.366 Kbytes.


**Nota: El proceso de Erlang por si solo ocupa 16.508 KBytes.

[Conclusiones]
Pues todo exactamente como esperébamos. Crear muchos usuarios no tiene coste computacional, solo un coste de memória. Ahora mismo los actores son muy chiquititos y pesan unos 3Kbytes. Aún así esta cifra es muy subjetiva ya que dentro de poco la aplicación empezará a crecer, y los usuarios tendrán que guardar un poco más de información, pero no mucha mas. Esto significa que, a priori, con 1GB de RAM podemos tener unos 300K usuarios activos, en este momento ( ya veremos cuando pongamos conexiones TCP por enmedio, jeje... ). Si quisiéramos llegar a 1 millón de usuarios al mismo tiempo, con 4GB para los procesos de usuario, de momento debería ser suficiente (pero lo dicho, ya veremos).

Si los usuarios se mandan muchos mensajes entre ellos aumenta el consumo de CPU. Normal. A más mensajes, más carga. Hombre, 1.000 mensajes por segundo son unos cuantos, no está nada mal, pero prácticamente ocupan un core de CPU, por lo que el consumo no es nada despreciable. Tampoco tengo muy claro en aplicaciones grandes cual es el ratio de número_de_usuarios/número_de_mensajes_por_segundo. Pero bueno, ya lo descubriremos.


Pues nada. Este es el resumen de mi Semana 1. La próxima entrega incluye el servidor web MochiWeb, interfazes web, HTML+CSS+JS, AJAX y la integración de lo que hemos venido haciendo hasta ahora con todo lo anterior.

Un saludo, Jan.

* En realidad Semana Cero es un modo de hablar. Aprendiendo las bases de Erlang me ha llevado entre 2-3 semanas de una o dos horitas al día (cuando todavía trabajaba).

Post siguiente: Erlang (IV) Semana 2

18 comentarios:

  1. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  2. Hola inedit00! un blog genial!, me preguntaba si piensas publicar el código, he trasteado un poco por mi cuenta con el gen_server pero me trabo un poco cuando intento establecer una comunicación entre nodos, asi que me ayudarías muchísimo.

    Un saludo.

    ResponderEliminar
  3. Estoy contento de que el blog te guste, Ricardo. Veo que esto de Erlang tiene tirada ya que estoy recibiendo bastante feedback. Y esto es genial!

    Sobre subir el código, pues claro!! Ningún problema! Resulta que cuando escribí este post todavía no estaba versionando mis pruebas. Digamos rehice el código 4 veces y poner un CSV era mas una incomodidad que una ventaja, pero el código está publicado aquí:

    http://code.krenel.org/gaab
    Que es lo mismo que:
    http://bitbucket.org/inedit00/gaab

    Para cualquier duda, ruego o pregunta, no tengas problema en dejarme un mensaje en el blog.

    Por otra parte, Ricardo, estoy pensando en mover el blog (¡vaya faena!) a un blog de Wordpress bajo el dominio blog.krenel.org, ya que en Blogger es complicado poner 'snipets' de código en los posts, explicando lo que hago. En cambio para Wordpress hay aproximadamente mil millones de pluguins para hacer esto, con resaltado de sintaxis y todo.

    Un saludo

    ResponderEliminar
  4. Si te descargas el código haciendo un:
    hg clone https://bitbucket.org/inedit00/gaab

    y inspeccionas un poco el log de commits ("hg log"), verás que todos los mensajes de commits son claros, pequeños y fáciles de ver si te interesan o no. Lo digo para que puedas ir haciendo "updates" poco a poco sobre la história del proyecto, ya que te será más facil ir asimilando los cambios. Pero bueno, igual eres un hombre de pelo en pecho y ya entiendes mi última version de mi código sin problemas ;)

    Un saludo!

    ResponderEliminar
  5. Muchas gracias! mirare el código desde las versiones iniciales(no soy tan pro xD) y es probable que surjan ,mas dudas xD

    ResponderEliminar
  6. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  7. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  8. Pues sigo sin saber como hacer que 2 nodos interactuen entre sí, en este caso me gustaría saber como desde un nodo usuario solicita hacer login al nodo servidor, he intentado enviando mensajes con el {nombre_nodo,PID} ! mensaje pero no hay suerte :/

    En los ocmentarios pones que haces un spawn de cada modulo, pero con que funcion?, yo por lo general cuando quiero crear un proceso lo hago asi: spawn(nom_modulo,nom_funcion,argumentos)
    para spawnear el gab_server o el gab_user que funcion debería elegir?

    un saludo!

    PD: De momento solo me interesa que funcione el chat.

    ResponderEliminar
  9. Bien. Aquí hay que ver qué entendemos por "nodo". ¿Por "nodo usuario" entendemos la petición HTTP? O sea, cada petición HTTP es un proceso (o Agente, o Actor... llámemosle como queramos) de Erlang, que es efímero. Por efímero me refiero que cuando devolvemos el resultado de la petición HTTP, este proceso muere. El nodo "servidor" es un proceso que siempre está ejecutándose y que mantiene el estado de la aplicación (usuarios logueados, etc...).

    Entonces aquí el problema está en cómo saber el PID del proceso Servidor des de otro nodo. Una posible solución: registrar el proceso servidor con un nombre. Esto se puede hcer mediante el siguiente código[1]

    Server = gaab_server:start_link(),
    register(gserver, Server).

    Y la functión gaab_server:start_link()[2], pone lo siguiente:
    start_link() ->
    {ok, Pid} = gen_server:start_link(?MODULE, #state{users=dict:new()}, []),
    Pid.

    Ahora tenemos registrado el servidor en un átomo llamado "gserver" (obviamente este nombre podría ser cualquier otro). Para saber el PID del servidor des de un proceso hijo, podemos hacer:

    io:format("Pid del proceso Servidor: ~p~n", [whereis(gserver)]).

    Y para mandarle un mensaje sería suficiente con hacer:
    whereis(gserver) ! mensaje


    Espero haberte ayudado. Un saludo!


    [1] Que puedes encontrar en la functión start_chat_server/0 en el fichero src/gaab.erl, revisión #22d9a0609fa9.
    [2] Que se encuentra en src/gaab_server.erl, en la revisión #22d9a0609fa9

    ResponderEliminar
  10. Gracias, eso ya lo tenia masomenos claro, aunque yo el nombre del servidor se lo paso como parametro de la funcion start_link(nombreServidor,modulo,argumentos, opciones) que supongo que es lo mismo que register(gserver, Server). no?, lo que pasa es que no se como probarlo, se que parece tonto, pero sin ejemplos yo soy un poco torpe.

    Para simplificar me gustaría ver en funcionamiento el chat has programado, que tendria que escribir en el terminal para poder loguear 2 usuarios y luego hacer que hablen entre ellos.

    ResponderEliminar
  11. Bien, me gusta que vayas al grano y con dudas concretas :)

    Hoy me pillas liado, ya que tengo que coger un vuelo y demás. Pero seguramente mañana o pasado te contesto con un comentario o una entrada de post respondiendo exactamente esto.

    Un saludo, Jan.

    ResponderEliminar
  12. Espero impaciente tu respuesta! es que no encuentro en ningun lado lo que busco!!

    ResponderEliminar
  13. Finalmente he conseguido lo que queria gracias unos guiris muy majos en el foro http://erlang.2086793.n4.nabble.com/
    Aun asi estoy impaciente por tu respuesta!

    ResponderEliminar
  14. Bien, Ricardo. Ya está explicado. Si en tu copia local del proyecto haces un:
    "hg pull -u", te descargarás la última versión donde he actualizado el fichero README. Allí tenemos los pasos necesarios para poner en marcha el proyecto des de cero, y un ejemplo básico de cómo se comunica un usuario con otro des de la consola.

    Commit: http://code.krenel.org/gaab/changeset/d7ddb3702d7a
    Fichero README completo: http://code.krenel.org/gaab/src/d7ddb3702d7a/README

    Con esto debería ser suficiente como para que puedas ir "tirando del hilo" y ver cómo lo he hecho para que los usuarios se puedan mandar mensajes entre ellos.

    Si tienes alguna duda o ves algo que se pueda mejorar, no dudes en decírmelo.

    Un saludo, Jan.

    ResponderEliminar
  15. hola inedit00, pues la verdad es que yo los ficheros fuente solo los he copiado y pegado, no se manejar muy bien la herramienta, pero creo que de igual manera los puedo ver actualizados

    Te comento que lo que yo queria conseguir era otra cosa y lo he medio conseguido! y creo que es mejor(o peor, tampoco me sorprenderia) la implentacion que he hecho yo.

    Veamos:
    Corrígeme si me equivoco, aqui lo que haces es loggear a muchos usuarios desde un mismo terminal, no?

    BASIC CHAT FUNCTIONALITY TEST
    =============================
    (gaab_dev@tpad)1> Bob = gaab_server:login("bob").
    <0.98.0>
    (gaab_dev@tpad)2> Alice = gaab_server:login("alice").
    <0.100.0>
    (gaab_dev@tpad)3> gaab_user:send_message(Bob, "alice", "Hi Alice!").
    [alice] Message received from bob: "Hi Alice!"
    ok
    (gaab_dev@tpad)4> gaab_user:send_message(Alice, "bob", "Hi, darling.").
    [bob] Message received from alice: "Hi, darling."
    ok
    (gaab_dev@tpad)5> gaab_user:send_message(Alice, "ted", "Ted, are you online?").
    Ummm.... the user "ted" seems to be offline. Message not delivered.
    ok


    Pues lo que yo te comentaba era que desde 2 o mas terminales distintos como pueden ser:

    servidor@ubuntuPC y cliente1@ubuntuPC, cliente2@ubuntuPC... clienteN@ubuntuPC

    Desde el terminal del nodo servidor se inicie el servidor y desde los otros nodos terminal se empiecen a lanzar peticiones, de login, logout y mensajes.

    Pues esto ya lo he conseguido!, aunque no esta finalizado del todo, aun me falta diseñar la interfaz del cliente para que este preparada para recibir de manera mas "agradable" los mensajes.

    Viendo tu codigo me has dado una idea asi que voy a por ello!!

    Luego si me das algun medio para subir o enviarte mi codigo, te lo agradecería, ya que me interesa tu opinion!.

    Como te dije antes, soy un novato en esto de erlang y todavia doy muchos palos de ciego así que espero que me des tu opinion!

    Un saludo!

    ResponderEliminar
  16. Se puede establecer un algun tipo de mailbox a un gen_server?

    Tengo el servidor(implementado con gen_server) y recibe solicitudes de otros nodos(login, logout, enviar_mensaje).

    Es que mi dilema es el siguiente, cuando llega un mensaje y se lo transmito a todos los usuarios lo hago mediante:

    getValor me duevuelve el PID de un user
    Pid = db_login:getValor(_State, User),
    Pid ! Msg

    Entonces se envia el mensaje correctamente a todos los usuarios y asi lo puedo comprobar desde cada terminal con el comando flush()

    Pues mi problema es ese, no se como recibir desde el terminal cliente lo que me llega desde el servidor central, estoy intentandolo hacer con un gen_server pero no se como hacerlo sin registrar el proceso, y no tiene sentido registrar pues se supone que es para muchos clientes....

    ResponderEliminar
  17. Vaaaaaaaaaaaaaaaaale, ahora entiendo que quieres hacer:

    "Pues lo que yo te comentaba era que desde 2 o mas terminales distintos como pueden ser:
    servidor@ubuntuPC y cliente1@ubuntuPC, cliente2@ubuntuPC... clienteN@ubuntuPC"

    Ahora lo pillo. Vale. Lo que tu y yo intentemos conseguir son cosas diferentes. Tu quieres que cada uno de los usuarios de tu chat se instalen un cliente de erlang en su máquina, y que éste se conecte al servidor para intercambiar mensajes y demás.

    Mi objetivo no era este. Mi objetivo es que los usuarios utilizen un navegador web (firefox/chrome/IExplorer), hagan login allí y tengan la interfaz web para poder hablar con unos y con otros, sin tenerse que instalar ningún "cliente" en su máquina, usando la Web como medio para usar el chat.

    O sea, tu estás creando una aplicación de escritorio, y yo estoy creando una aplicación web. Son cosas distintas. Mi código ahora mismo se podría modificar para que cumpla tus propósitos, pero es otro enfoque.

    "Luego si me das algun medio para subir o enviarte mi codigo, te lo agradecería, ya que me interesa tu opinion!." Faltaría, puedes mandarme un e-mail a mi cuenta personal inedit00 [] gmail.com con el código que tengas, y le echo un vistazo. No te creas, yo son nuevo en esto de Erlang y también acabo de empezar hace poco (como explico en los posts) ;)

    Sobre tu pregunta de "Se puede establecer un algun tipo de mailbox a un gen_server?", aquí ya nos metemos en otros temas como la persistencia de datos y demás: dentro de poco publicaré un post de cómo utilizo yo Mnesia para la persistencia de datos. También puedes mirar de utilizar MongoDB o cualquier otro sistema que te sirva para guardar la información en un "mailbox" y que puedas recuperar más tarde. Yo es algo que tengo que hacer en las próximas semanas. En cuando lo tenga hecho, ya publicaré un post de cómo lo he solucionado.

    ResponderEliminar
  18. Jeje pues si son 2 cosas totalmente distintas :), pues en cuando modifique esto un poco te lo envío, ahora me estoy peleando un poco con lo que comenté antes, tengo 2 gen_server, desde el servidor no se como comunicarme con el API del gen_server del usuario, empiezo a creer que sería mas facil hacer un simple módulo con un 4 funciones,
    start-> spawnea un loop definido en el modulo
    login-> envia a un mensaje de login al pid obtenido por el span del start
    logout-> analogo
    send_msg -> analogo
    receive_msg -> aqui es donde me llegaria un mensaje que le llego al servidor desde un usuario X, simulado asi un chat publico
    loop -> pues basicamente el mailbox de la interfaz

    Que te parece? estoy intentando hacerlo con el gen_server pero cada vez mas parece un callejon sin salida...

    ResponderEliminar