Graduado en Ingenier´ıa Informatica´ - Archivo Digital...
Transcript of Graduado en Ingenier´ıa Informatica´ - Archivo Digital...
“Ingeniamos el futuro”
CAMPUS DE EXCELENCIAINTERNACIONAL
Graduado en Ingenierıa Informatica
Universidad Politecnica de Madrid
Escuela Tecnica Superior de
Ingenieros Informaticos
TRABAJO FIN DE GRADO
Comunicacion de microservicios usando colas de mensajes
Autor: Cesar Guzman Alpızar
Director: Angel Herranz Nieva
MADRID, JUNIO DE 2019
Indice1. Resumen 2
1.1. Version en espanol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2. English version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2. Introduccion 32.1. Arquitectura monolıtica . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.2. Arquitectura orientada a microservicios . . . . . . . . . . . . . . . . . . 3
3. Colas de mensajes 43.1. Protocolo AMQP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.2. RabbitMQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.2.1. Elementos de RabbitMQ . . . . . . . . . . . . . . . . . . . . . . 7
3.2.2. Ejemplo practico usando RabbitMQ . . . . . . . . . . . . . . . . 9
4. Diseno 164.1. Caso de uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.2. Eventos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.3. Listeners y Processors . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.3.1. Listeners . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.3.2. Processors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.4. RabbitMQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
5. Tecnologıas usadas 21
6. Detalles de implementacion 216.1. Metaprogramacion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
6.2. Behaviours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
7. Conclusiones 23
8. Referencias 24
Trabajo Fin de Grado Cesar Guzman Alpızar
1. Resumen
1.1. Version en espanolEn esta trabajo, se ha transformado una parte de un sistema con una arquitectura monolıti-
ca de una empresa real para conseguir una arquitectura orientada a microservicios.
Una vez separados varios modulos en microservicios, se han comunicado entre si median-
te colas de mensajes, en concreto la implementacion que ofrece RabbitMQ.
El resultado del trabajo es una clara mejorıa en cuanto a escalabilidad, mantenibilidad
y rendimiento del sistema de la empresa gracias a la separacion de funcionalidades en
microservicios.
1.2. English versionIn this thesis, a monolithic system from a real company has been transformed into a mi-
croservice system.
Once separated several modules in microservices, they have been connected between
them with message queues so they can communicate between them, this communication
system has been implemented with RabbitMQ.
The results of this thesis shows a clear improvement of scalability, maintainability and
performance of the company system thanks to splitting some functionalities into micro-
services.
2
Trabajo Fin de Grado Cesar Guzman Alpızar
2. IntroduccionLa arquitectura del software son las estructuras de alto nivel que componen un sistema
software, de las multiples que existen, en este trabajo se hablara de dos ampliamente
conocidas: arquitectura monolıtica [1, Chapter 1] y arquitectura orientada a microservi-cios [2] [3].
2.1. Arquitectura monolıticaEn la arquitectura monolıtica se opta por tener un solo sistema que ofrezca todas las fun-
cionalidades que se desean, gracias a la modularizacion se puede separar correctamente
cada parte de este unico sistema. Un ejemplo de este tipo de arquitectura es el de una apli-
cacion web Java que se compone de un archivo WAR que se despliega en un contenedor
web como Apache Tomcat, los principales beneficios de este sistema son:
Facil de desarrollar con una programacion modular
Facil de desplegar, hay que desplegar unicamente una aplicacion, o como se ha
mencionado anteriormente, un solo archivo WAR
Facil de escalar, se puede escalar ejecutando multiples copias tras un balanceador
de carga
Aun ası actualmente se evita la arquitectura monolıtica por las siguientes razones:
Una vez comenzado el proyecto, la gran cantidad de codigo en una sola aplicacion
puede intimidar a nuevos desarrolladores si no se ha modularizado correctamente
Al ser un solo sistema tardara mucho en arrancar y detenerse, lo cual puede ser un
problema para sistemas que pretender dar una alta cobertura de servicio
Escalar el desarrollo puede ser complicado, ya que los equipos de desarrollo se
dividiran distintas areas de la aplicacion y esto fuerza la coordinacion entre distintos
equipos de desarrollo y evita que puedan trabajar independientemente
Requiere un compromiso a largo plazo con la tecnologıa que se use desde el prin-
cipio ya que puede ser difıcil mas adelante cambiar de tecnologıa
2.2. Arquitectura orientada a microserviciosLa arquitectura orientada a microservicios se compone de varios servicios que realizan
cada uno una sola tarea, una aplicacion en este caso se compondrıa de varios de estos
servicios. Esta arquitectura permite combinar componentes independientes para realizar
las tareas que se deseen. En este caso las principales ventajas son:
Software mas mantenible, al ser servicios relativamente pequenos son faciles de
entender y, si fuera necesario, de cambiar
3
Trabajo Fin de Grado Cesar Guzman Alpızar
Facil de probar, al ser funcionalidades concretas encapsuladas en un servicio pe-
queno es mas sencillo crear tests para comprobar su buen funcionamiento
Facil de desplegar, al contrario que en la arquitectura monolıtica no hace falta re-
desplegar toda la aplicacion, basta con parar y arrancar un solo servicio
Permite la organizacion del desarrollo en equipos independientes
Permite cambio de tecnologıas si es necesario evitando ası comprometerse con una
sola tecnologıa
Permite un escalado dependiendo de la demanda, si un servicio requiere mas uso
de CPU se puede redesplegar en una maquina con mas capacidad de computo
Al igual que la arquitectura monolıtica los microservicios tienen varias complicaciones:
Los desarrolladores deben lidiar con la comunicacion de los microservicios
Crear tests para probar la interaccion entre los servicios es complicado
Los despliegues pueden llegar a ser muy complejos si hay excesivas dependencias
entre los servicios, ya que tendrıan que arrancarse en un orden concreto lo cual
anula la ventaja de un despliegue sencillo
La comunicacion entre servicios puede hacer que el sistema sea impredecible y por
lo tanto mas complejo de entender plenamente
Uno de los retos que tiene la arquitectura de microservicios es saber cuando se deberıa
usar. En un desarrollo pequeno no suele tener mucho sentido invertir esfuerzo en sepa-
rar distintas funcionalidades en microservicios, ya que esto ralentiza el desarrollo. Esto
perjudica a startups que necesitan un desarrollo rapido para tener un sistema funcional lo
antes posible.
Otro gran reto es saber definir los distintos microservicios. Hay muchas maneras de sepa-
rar las distintas funcionalidades de un sistema, pero idealmente cada parte deberıa tener
pocas responsabilidades de las que hacerse cargo.
En este trabajo se mostrara un caso real de un cambio de arquitectura monolıtica a arqui-
tectura orientada a microservicios, comunicando los distintos servicios gracias a colas de
mensajes.
3. Colas de mensajesComo se ha visto en la seccion 2.2, los microservicios requieren un sistema de comu-
nicacion [4] entre ellos para poder llevar a cabo tareas complejas, este puede ser de dos
tipos:
Sıncrono: suponiendo que tenemos dos servicios, un cliente que envıa un mensaje
y un servidor que lo recibe. Para que la comunicacion sea sıncrona el cliente debe
esperar a que el servidor procese el mensaje y envıe una respuesta
4
Trabajo Fin de Grado Cesar Guzman Alpızar
Asıncrono: una vez mas con los mismos dos servicios mencionados en el punto
anterior, para que la comunicacion sea asıncrona el cliente envıa el mensaje sin
esperar respuesta, de esta manera el servidor recibira el mensaje y el cliente puede
seguir realizando cualquier otra tarea pendiente
Un ejemplo de comunicacion sıncrona es uno peticion a traves del protocolo HTTP a
una API como puede ser la de Spotify. Al realizar una peticion a dicha API para pedir
informacion sobre una cancion el servicio que realiza dicha peticion tiene que esperar
a que el servidor de Spotify conteste con una respuesta. A continuacion se muestra el
resultado de hacer una peticion con restclient-mode un cliente HTTP que ofrece Emacs:
{"artists": [
{"id": "12Chz98pHFMPJEknJQMWvI","name": "Muse","type": "artist","uri": "spotify:artist:12Chz98pHFMPJEknJQMWvI"
}],"href":"https://api.spotify.com/v1/tracks/6KR9NaynH8bF9w727wKQBL",
"id": "6KR9NaynH8bF9w727wKQBL","name": "New Born","popularity": 47,"type": "track","uri": "spotify:track:6KR9NaynH8bF9w727wKQBL"
}// GET https://api.spotify.com/v1/tracks/6KR9NaynH8bF9w727wKQBL// HTTP/1.1 200 OK// Content-Type: application/json; charset=utf-8// Cache-Control: public, max-age=7200// Access-Control-Allow-Origin: *// Access-Control-Allow-Headers: Accept,// Authorization, Origin, Content-Type, Retry-After// Access-Control-Allow-Methods: GET, POST,// OPTIONS, PUT, DELETE, PATCH// Access-Control-Allow-Credentials: true// Access-Control-Max-Age: 604800// content-length: 2274// Date: Thu, 30 May 2019 08:03:38 GMT// Via: 1.1 google// Alt-Svc: clear// Request duration: 0.133811s
Como se indica en los metadatos de la peticion se han tardado 0.133811 segundos en
5
Trabajo Fin de Grado Cesar Guzman Alpızar
completar la comunicacion.
Por otro lado, un ejemplo de comunicacion asıncrona es un envıo de un mensaje de texto
a traves Telegram, una aplicacion de mensajerıa instantanea. A continuacion se muestra
un ejemplo de un bot enviando un mensaje a un usuario:
iex(1)> ExGram.send_message(14977303, "Hello"){:ok,%ExGram.Model.Message{
audio: nil,text: "Hello",date: 1559203979,chat: %{
first_name: "Cuwano",id: 14977303,type: "private",username: "Cuwano"
},venue: nil,reply_to_message: nil,pinned_message: nil,...,from: %{
first_name: "Iron Test Bot",id: 376323488,is_bot: true,username: "irontest_bot"
}}}
En el codigo anterior da la sensacion de que el servidor ha contestado, pero la tupla
resultante es simplemente la informacion del mensaje que se ha enviado.
Aunque tambien puede utilizarse para la comunicacion sıncrona el protocolo AMQP [5]
es usado habitualmente para comunicacion asıncrona, en la siguiente seccion se explicara
en que consiste este protocolo y una de sus implementaciones. AMQP define una manera
de usar las colas de mensajes, que son buffers a los que se les envıan mensajes para ser
guardados a la espera de ser consumidos por servicios.
3.1. Protocolo AMQPEl protocolo AMQP permite comunicar varios servicios al mismo tiempo para que inter-
cambien mensajes de negocio ofreciendo un servidor central, el cual puede replicarse y
distribuirse, por el que pasan todos los mensajes los cuales se distribuyen en colas segun
la configuracion del servidor. Esto permite a cualquier sistema comunicarse con cualquier
otro independientemente de la tecnologıa, es decir, un servicio hecho Golang puede enviar
6
Trabajo Fin de Grado Cesar Guzman Alpızar
mensajes al servidor para que otro servicio hecho en Scala procese informacion.
Actualmente existen multiples implementaciones de AMQP [6], cada una de ellas imple-
mentadas en distintos lenguajes de programacion, algunos de ellos son:
RabbitMQ
SwiftMQ
Kaazing
Apache Qpid
En este trabajo se explicara detenidamente RabbitMQ, dado que es una implementacion
Open Source que permite investigarlo a fondo.
3.2. RabbitMQRabbitMQ [7] [8] es uno de los sistemas de colas de mensajes Open Source mas desple-
gados gracias a su simplicidad a la hora de desplegar y su ligereza. El servidor esta pro-
gramado en Erlang, un lenguaje disenado para hacer aplicaciones altamente escalables y
mantenibles, esto permite a RabbitMQ heredar esas propiedades para tener la posibilidad
de ser un sistema distribuido.
3.2.1. Elementos de RabbitMQ
A continuacion se explicaran los conceptos mas importantes necesarios para usar Rab-bitMQ desde un cliente.
Connection
Es una conexion a un servidor de RabbitMQ, a partir de este elemento se podra operar
con el servidor.
La conexion requiere un host, un puerto, un usuario y una contrasena. El host y puerto
son necesarios ya que RabbitMQ puede usarse de manera distribuida y no necesita estar
en la misma maquina que el resto de componentes de la comunicacion. Por otro lado, el
usuario y contrasena determinaran los permisos que un cliente tiene en el servidor.
Channel
Permite multiplexar la conexion TCP/IP con el servidor, cada canal es independiente y en
clientes que soportan multi-threading se recomienda usar un canal por thread.
Queue
Es una cola FIFO (First In First Out) que almacena los mensajes hasta que son consumi-
dos por un cliente. Para que el comportamiento sea estrictamente FIFO solo debe haber
7
Trabajo Fin de Grado Cesar Guzman Alpızar
un cliente consumiendo mensajes de cada cola, aunque RabbitMQ permite multiples con-
sumidores en una misma cola.
Exchange
Es uno de los elementos clave de RabbitMQ es el componente que recibe los mensajes y
los encamina o descarta hacia distantas colas de mensajes dependiendo de las propiedades
del exchange o del propio mensaje.
Antes de explicar los distintos tipos de exchanges es conveniente conocer los bindings,
son enlaces entre un exchange y una queue. Dependiendo del tipo de exchange los bin-dings se realizaran con distintos parametros.
Los exchanges tienen distintos tipos:
Direct: este tipo de exchange enruta los mensajes segun una cadena de texto llamada
routing key, cuando una cola se enlaza a un exchange de este tipo se especifica
su routing key. Cuando un mensaje es enviado a este tipo de exchanges se debe
especificar una routing key, de esta manera el exchange compara la routing key del
mensaje y envıa dicho mensaje a todas las colas que han usado la misma routingkey para enlazarse.
Fanout: este tipo de exchange no requiere ningun parametro para enlazarse a el
porque envıa todos los mensajes a todas las colas enlazadas a el.
Topic: este tipo de exchange actua de manera parecida al direct, pero cambia el fun-
cionamiento de la routing key. Ahora la routing key no es simplemente una cadena
de texto, es una cadena de texto que se compone de cero a mas palabras separadas
por puntos, por ejemplo: log.error. Tambien se ofrece una sintaxis para que los
mensajes se envıen a varias colas que satisfagan un patron, los caracteres especia-
les son el asterısco * que corresponde con una sola palabra sin importar cual y la
almohadilla # que corresponde con cero o mas palabras. Gracias a esta sintaxis se
pueden enviar mensajes con routing keys como: log.*, log.#, square.red.*o *.blue.#.
Headers: este tipo de exchange enruta los mensajes gracias a unos parametros que
se envıan junto a cada mensaje llamados headers. Los headers son una lista de
tuplas que se comparan al igual que se hace en el caso de un topic exchange con
la routing key. Un ejemplo de headers serıa: [{“format”, “pdf”}, {“type”, “log”},
{“x-match”, “any”}]. Como se ve en el ejemplo es necesario anadir un parametro
llamado x-match el cual solo puede tener dos valores: all o any. Si el valor de
x-match es any los mensajes se enrutaran si tienen alguno de los headers que se
hayan definido en el binding. Pero si el valor es all, para que se enrute un mensaje
sus headers deben coincidir completamente con los especificados a excepcion de
x-match que es ignorado.
8
Trabajo Fin de Grado Cesar Guzman Alpızar
3.2.2. Ejemplo practico usando RabbitMQ
Para ilustrar las posibilidades de RabbitMQ se mostrara un ejemplo practico en el que se
usara un exchange de tipo topic para enrutar los mensajes a cinco colas distintas.
El ejemplo consiste en un sistema de logs distribuidos. Cualquier sistema enviara un men-
saje JSON serializado con el siguiente formato:
{"log_level": "level","log_message": "Example message"
}
Cada mensaje sera enviado a un exchange llamado logs que tendra cinco colas enlaza-
das, en la siguiente tabla se muestran las colas con sus respectivas routing keys:
Queue Routing keydebug queue logs.debug
info queue logs.info
warn queue logs.warn
error queue logs.error
all queue logs.*
Gracias a estos bindings cada cola recibira un nivel distinto de log, a excepcion de all queueque recibira todos los mensajes de log.
A este sistema podran enviar mensajes cualquier cantidad de servicios que llamaremos
Producers, estos seran los encargados de introducir mensajes en RabbitMQ. Por otro lado,
a cada una de las colas estara un Consumer conectado que ira procesando los mensajes.
Por simplicidad en este ejemplo solo habra un Producer y los Consumers simplemente
mostraran en pantalla los mensajes que reciban. A continuacion se muestra un diagrama
que describe la estructura de este sistema:
9
Trabajo Fin de Grado Cesar Guzman Alpızar
Log Generator
Producer
logs
Exchangewarn queue
info queue
debug queue
error queue
all queue
Debug Client
Consumer
Info Client
Consumer
Warn Client
Consumer
Error Client
Consumer
All Client
Consumer
Figura 2: Ejemplo practico
Para este ejemplo se ha usado Elixir un lenguaje de programacion funcional influenciado
por Erlang.
Se han disenado dos modulos LogSystem.LogGenerator que sera el Producer de
nuestro sistema y LogSystem.LogReceiver que seran los Consumers. En un modu-
lo llamado LogSystem se ha creado una funcion que declara en el servidor de RabbitMQlos elementos de la figura 2. A continuacion se muestra el codigo de los distintos modulos:
LogSystem
En este modulo se define la funcion declare system que crea una conexion, un canal,
un exchange y las distintas colas.
1 defmodule LogSystem do2 use AMQP3
4 def declare_system() do5 {:ok, connection} = Connection.open()6 {:ok, channel} = Channel.open(connection)7
8 exchange_name = "logs"9 Exchange.declare(channel, exchange_name, :topic)
10
11 # Create a queue for each log level12 ["debug", "info", "warn", "error"]13 |> Enum.each(fn queue ->14 queue_name = "#{queue}_queue"15 routing_key = "logs.#{queue}"16 Queue.declare(channel, queue_name)17
10
Trabajo Fin de Grado Cesar Guzman Alpızar
18 Queue.bind(19 channel,20 queue_name,21 exchange_name,22 routing_key: routing_key23 )24 end)25
26 # Create a general queue27 Queue.declare(channel, "all_queue")28
29 Queue.bind(30 channel,31 "all_queue",32 exchange_name,33 routing_key: "logs.*"34 )35
36 Channel.close(channel)37 end38 end
Como se puede ver en el codigo anterior se usa el modulo AMQP de la librerıa amqp de Eli-xir para comunicarse con el servidor de RabbitMQ. Al llamar a la funcion Connection.open()sin parametros AMQP intentara conectarse a un servidor en localhost en el puerto
5672 con usuario guest y contrasena guest.
Producer
1 defmodule LogSystem.LogGenerator do2 use GenServer3 use AMQP4
5 require Logger6
7 def random_log_level(),8 do: Enum.random(["debug", "info", "warn", "error"])9
10 def random_phrase() do11 number_of_words = Enum.random(5..10)12
13 Task.async_stream(14 1..number_of_words,15 fn _ ->16 Dictionary.random_word()17 end,18 ordered: false19 )20 |> Stream.map(&elem(&1, 1))21 |> Enum.join(" ")22 end23
11
Trabajo Fin de Grado Cesar Guzman Alpızar
24 # Client API25 def start_link() do26 {:ok, connection} = Connection.open()27 GenServer.start_link(__MODULE__, connection)28 end29
30 # Server callbacks31 def init(state) do32 # Start to send phrases in 2 seconds33 {:ok, state, 2_000}34 end35
36 def handle_info(:timeout, connection) do37 {:ok, channel} = Channel.open(connection)38 log_level = random_log_level()39 routing_key = "logs.#{log_level}"40 phrase = random_phrase()41
42 Logger.info("""43 Sending:44 #{IO.ANSI.bright()}[#{log_level}] #{IO.ANSI.blue()}#{phrase}45 """)46
47 message =48 Jason.encode!(%{49 log_level: log_level,50 log_message: phrase51 })52
53 Basic.publish(channel, "logs", routing_key, message)54 Channel.close(channel)55
56 # Send a phrase every second57 {:noreply, connection, 1_000}58 end59 end
En este caso el modulo LogSystem.LogGenerator es un GenServer [9]. Un Gen-Server es un proceso Erlang que permite ejecutar codigo de manera asıncrona y mantener
un estado propio, en este caso se usa para generar texto aleatorio y enviarlo al servidor de
RabbitMQ indicando un nivel log.
Para enviar los mensajes de manera automatica se usa una propiedad de los GenServersque es el timeout, como se puede ver en la lınea 33 cuando el GenServer comienza su
ejecucion se le asigna un timeout de 2,000 milisegundos, esto hara que al iniciar esperara
2 segundos y se enviara a si mismo el mensaje :timeout que se trata en la funcion
handle info que aparece en la lınea 36.
La funcion handle info generara un texto automatico al recibir el mensaje de :timeout,
creara el JSON con el formato que se especifıca al principio de esta seccion y lo enviara
con la funcion Basic.publish que esta en la lınea 53. Una vez hecho esto el GenSer-
12
Trabajo Fin de Grado Cesar Guzman Alpızar
ver vuelve a esparar, pero esta vez 1,000 milisegundos.
Para generar el texto automatico se usa una librerıa llamada Dictionary, es una librerıa
de codigo libre publicada en GitHub, un host de repositorios Git.
LogReceiver
1 defmodule LogSystem.LogReceiver do2 use GenServer3 use AMQP4
5 require Logger6
7 # Client API8 def start_link(log_level) do9 GenServer.start_link(__MODULE__, log_level)
10 end11
12 defp log_level_map(level) do13 case level do14 "debug" -> &Logger.debug/115 "info" -> &Logger.info/116 "warn" -> &Logger.warn/117 "error" -> &Logger.error/118 _ -> &IO.puts/119 end20 end21
22 def log(level, message) do23 f = log_level_map(level)24 (IO.ANSI.bright() <> message) |> f.()25 end26
27 # Server callbacks28 def init(log_level) do29 Logger.info("""30 Log receiver started, reading #{log_level} level logs31 """)32
33 {:ok, connection} = Connection.open()34 {:ok, channel} = Channel.open(connection)35
36 Queue.declare(channel, "#{log_level}_queue")37 Basic.consume(channel, "#{log_level}_queue")38
39 {:ok, %{log_level: log_level, channel: channel}}40 end41
42 def handle_info(43 {44 :basic_deliver,45 payload,
13
Trabajo Fin de Grado Cesar Guzman Alpızar
46 %{delivery_tag: tag}47 },48 state49 ) do50 spawn(fn ->51 Basic.ack(state[:channel], tag)52 end)53
54 %{55 "log_level" => log_level,56 "log_message" => log_message57 } = Jason.decode!(payload)58
59 log(log_level, log_message)60
61 {:noreply, state}62 end63 end
Por ultimo, el modulo LogSystem.LogGenerator que tambien es un GenServer.
Se lanzara un LogGenerator por cada cola que tengamos en el sistema, en este caso
cinco.
Como podemos ver en la lınea 8, al ser llamado, este GenServer recibe un nivel de log
que sera debug, info, warn, error o all.
Al iniciar su ejecucion cada LogGenerator se suscribira a la cola que le corresponda, si
el log level con el que ha sido llamado es debug se suscribira a la cola debug queuede la que escuchara los mensajes.
Como se puede ver en la lınea 42 se reciben los mensajes de la cola, se responde con un
ACK al servidor para indicar que se va a tratar ese mensaje y el servidor puede eliminarlo
de la cola en la que estaba. Despues se deserializa el mensaje y se muestra por pantalla el
mensaje recibido.
Ejecucion del ejemplo
En este apartado se explicara como se ejecuta este pequeno ejemplo para poder ver el
resultado.
En primer lugar ejecutaremos un servidor de RabbitMQ, como la instalacion y ejecucion
es dependiente del sistema no se incluira.
Se usara iex, la shell interactiva de Elixir, cargando este proyecto. Se ejecutara el co-
mando iex -S mix desde la raız del proyecto.
Una vez abierto iex se debe ejecutar la funcion LogSystem.declare system para
declarar el exchange y las colas.
Dado que este sistema se puede distribuir y ejecutar en distintas maquinas se abriran cinco
iex mas para poder visualizar el resultado de este ejemplo correctamente.
14
Trabajo Fin de Grado Cesar Guzman Alpızar
Se ejecutara en las cinco nuevas terminales LogSystem.LogReceiver.start link("level")siendo level cada uno de los siguientes: debug, info, warn, error y all.
Por ultimo ejecutaremos el Producer con LogSystem.LogGenerator.start link,
el resultado se puede ver en las siguientes figuras:
Figura 3: Producer y Consumer de todos los logs
En la figura 3 se ve en la mitad superior el Producer mostrando los mensajes que envıa y
en la mitad inferior el Consumer de la cola all queue.
15
Trabajo Fin de Grado Cesar Guzman Alpızar
Figura 4: Consumers de los distintos niveles de log
En la figura 4, diferenciados cada uno con un color, se pueden ver el resto de Consumersrecibiendo unicamente el nivel de log al que esta suscrito.
4. DisenoEn esta seccion se explicara como se ha disenado una comunicacion de microservicios en
una empresa real llamada Coowry.
4.1. Caso de usoCoowry permite realizar micropagos de telefono movil a telefono movil gracias a un
sistema programado en Erlang y Elixir.
En este trabajo se ha transformado una parte de un servicio llamado coowry-core que,
ademas de ser la API de Coowry, provee las funcionalidades principales de todo el siste-
ma. Este servicio esta separado en modulos pero tiene una arquitectura monolıtica de la
que se han extraido algunas funcionalidades a microservicios externos.
En el funcionamiento original de coowry-core cuando un usuario realizaba una accion en
el sistema, por ejemplo enviar dinero a otro usuario, se ejecutaban las acciones pertinentes
para que se lleve a cabo dicha accion y se notificaba a los usuarios afectados por esta
accion mediante llamadas a funciones en modulos dedicados a la comunicacion con los
usuarios. Estos modulos envıan mensajes a traves de SMS, email o Telegram (aplicacion
de mensajerıa instantanea).
16
Trabajo Fin de Grado Cesar Guzman Alpızar
Lo que se ha conseguido en este trabajo es que las acciones que se realizan sean notifica-
das a traves RabbitMQ a un servicio encargado de las comunicaciones llamado cwcomm,
este servicio escucha eventos que envıa coowry-core, y dependiendo del evento proce-
sado comunica a los usuarios de Coowry las acciones que se han realizado por las vıas
mencionadas anteriormente.
4.2. EventosPara determinar los eventos se han estudiado los distintos casos de uso del sistema. A
continuacion se muestran algunos de los eventos con una pequena descripcion:
Receipt requested: se crea cuando un usuario pide una factura de una transaccion
que ha realizado
Pin set: se crea cuando un usuario establece un pin de seguridad a traves de la
aplicacion movil o la web
Pin removed: se crea cuando un usuario deshabilita el pin de seguridad que ha
establecido anteriormente
Trade created: se produce cuando una transaccion se crea, esto puede ser una peti-
cion de saldo o un envıo de saldo
Email changed: se crea cuando un usuario cambia el email que ha dado a Coowry
a traves de la aplicacion movil o la web
Estos son algunos de los eventos que se envıan a traves de RabbitMQ serializados. Para
deserializar correctamente estos mensajes cwcomm dispone de un modulo donde guarda
los tipos de los distintos eventos en forma de struct, de esta manera cuando recibe un
mensaje lo deserializara al struct correspondiente, validando ası que el mensaje ha sido
enviado en el formato correcto.
A continuacion se muestra un fragmento del modulo Cwcomm.Event:
1 defmodule Cwcomm.Event do2 # Events from API (coowry-core)3 defmodule ReceiptRequested do4 defstruct [:trid, :to, :from]5
6 @type t :: %ReceiptRequested{7 trid: :cw_model.trid(),8 to: String.t(),9 from: String.t()
10 }11 end12
13 defmodule PinSet do14 defstruct [:user]15 @type t :: %PinSet{user: String.t()}16 end17
17
Trabajo Fin de Grado Cesar Guzman Alpızar
18 defmodule PinRemoved do19 defstruct [:user]20 @type t :: %PinRemoved{user: String.t()}21 end22
23 defmodule TradeCreated do24 defstruct [:trid]25 @type t :: %TradeCreated{trid: :cw_model.trid()}26 end27
28 defmodule EmailChanged do29 defstruct [:to, :code, :action]30 @type t :: %EmailChanged{31 to: String.t(),32 code: String.t(),33 action: String.t()34 }35 end36 end
Como se puede ver en el codigo para cada evento se define un struct y los tipos de los
elementos de ese struct.
4.3. Listeners y ProcessorsPara escuchar y procesar los mensajes no se ha optado por un enfoce distribuido, aunque
gracias a la implementacion realizada es perfectamente viable distribuir los Consumersen distintas maquinas.
Se han creado dos elementos llamados Listeners y Processors que se describiran a conti-
nuacion.
4.3.1. Listeners
Los Listeners son GenServers a los que se les da la informacion necesaria para saber
a que colas tienen que escuchar y que eventos deben escuchar. Aunque se detallara su
implementacion en la seccion 6 a continuacion se muestra como se crea un Listener en
cwcomm:
1 defmodule Cwcomm.Listener.PinSet do2 alias Cwcomm.Event.PinSet3
4 use Cwcomm.Listener,5 queues: ["pin_set"],6 event_type: %PinSet{},7 primary_channels: %{8 "mail" => Cwcomm.Processor.Mail.PinSet,9 "push" => Cwcomm.Processor.Push.PinSet,
10 "telegram" => Cwcomm.Processor.Telegram.PinSet11 },
18
Trabajo Fin de Grado Cesar Guzman Alpızar
12 secondary_channels: %{}13 end
Como se puede ver es algo muy sencillo, simplemente hay que usar la macro use Cw-comm.Listener, a la que pasamos los siguientes parametros:
queues: es una lista de strings que determina de que colas tiene que consumir este
Listener
event type: el tipo de evento que debe escuchar de las colas a las que este suscrito
primary channels: es un map (estructura clave/valor) en el que se ensena para cada
canal de comunicacion que modulo de cwcomm se encarga de procesarlo, se hablara
detenidamente de esto en la seccion 4.3.2
secondary channels: tiene la misma estructura que primary channels pero estos ca-
nales se usan en el caso de que ninguno los primary channels no esten disponibles
4.3.2. Processors
Un Processor es un modulo que se encarga de enviar un mensaje por un canal en concreto,
normalmente un Listener llama a la funcion process de varios Processors para notificar
a un usuario, a continuacion se muestra un Processor del evento PinSet para el canal
de Telegram:
1 defmodule Cwcomm.Processor.Telegram.PinSet do2 alias Cwcomm.Event.PinSet3 alias CwEcto.People.Telegram4
5 @behaviour Cwcomm.Processor6
7 require Logger8
9 @spec process(PinSet.t()) :: Cwcomm.Channel.response()10 def process(%{user: id}) do11 case Telegram.get_tguser_account(id) do12 [tg_user] ->13 params = %{14 to: tg_user.tg_id,15 locale: tg_user.language,16 action: :on17 }18
19 Cwcomm.Channel.Telegram.send("/api/pins", params)20
21 error ->22 Logger.debug("""23 Could not get telegram user,24 got instead:\n#{inspect(error)}25 """)26 end
19
Trabajo Fin de Grado Cesar Guzman Alpızar
27 end28 end
Todos los modulos que sean Processors de un canal tienen que tener definida la funcion
process obligatoriamente, en la seccion 6 se vera como se consigue esto.
En el codigo anterior podemos ver que la funcion process obtiene los datos nece-
sarios del usuario que se va a notificar a traves de Telegram y se llama a la funcion
Cwcomm.Channel.Telegram.send para que el modulo del canal de Telegram se
ocupe de enviar la notificacion llamando a traves de una peticion HTTP a otro microser-
vicio que existe en Coowry llamado cwbottg el cual expone una API para poder operar
con el.
4.4. RabbitMQEn este proyecto se ha utilizado una estructura parecida a la creada en la seccion 3.2.2.
A continuacion se muestra un diagrama con los eventos mencionados en la seccion 4.2:
coowry-core cwexchange
receipt requested
pin set
pin removed
trade created
email changed
Pin Removed Listener
Pin Set Listener
Receipt Requested Listener
Trade Created Listener
Email Changed Listener
Figura 5: Estructura de RabbitMQ en Coowry
En este servidor de RabbitMQ la librerıa cwcore que usa coowry-core inicializa todos
los elementos necesarios, un exchange de tipo topic y una cola por cada evento con los
siguientes bindings:
20
Trabajo Fin de Grado Cesar Guzman Alpızar
Queue Routing keyreceipt requested receipt.requested
pin set pin.set
pin removed pin.removed
trade created trade.created
email changed email.changed
Gracias al exchange de tipo topic podemos crear bindings como pin.set y pin.removed,
estos bindings ofrecen la posibilidad de tener en el futuro una cola con el binding pin.*que recibira todos los mensajes relacionados con el pin de seguridad del usuario. Esta
flexibilidad ha sido determinante a la hora de elegir el tipo de exchange.
5. Tecnologıas usadasEl sistema de Coowry estaba mayormente programado en Erlang, este lenguaje es usado
ampliamente en telecomunicaciones, empresas de banca y comercio electronico. Erlangbasa su diseno en gran parte en el modelo de actores.
Pero con el tiempo y despues de varias refactorizaciones de codigo, cada vez hay mas
servicios en Elixir, un lenguaje creado por Jose Valim que quiso aunar la comunidad y las
herramientas que ofrece un lenguaje como Ruby con la potencia y capacidades de Erlang,
creando ası un lenguaje funcional que permite programar sistemas altamente escalables y
potentes con herramientas que ayudan al desarrollador.
Para mantener todo el codigo se ha usado el sistema de control de versiones Git alojado
en Bitbucket.
6. Detalles de implementacionElixir ha ofrecido grandes funcionalidades para facilitar la implementacion del sistema, a
continuacion se veran las que mas se han aprovechado.
6.1. MetaprogramacionElixir da la posibilidad de extender el propio lenguaje mediante macros. Las macros son
funciones especiales que generan codigo que se inserta en la aplicacion en el proceso de
compilacion.
En este trabajo se ha usado la macro using para poder crear Listeners de manera sen-
cilla como se menciono en la seccion 4.3.1. Para crear un Listener se llama a la macro usecon unos parametros, la macro se define en otro modulo llamado Cwcomm.Listenery es la siguiente:
1 defmacro __using__(args) when is_list(args) do2 quote do
21
Trabajo Fin de Grado Cesar Guzman Alpızar
3 def start_link(connection) do4 # Add channel, and process function to the state5 args =6 [7 connection: connection,8 event_module: __MODULE__9 ] ++ unquote(args)
10 Cwcomm.Listener.start_link(args)11 end12 def child_spec(args \\ []) do13 %{14 id: __MODULE__,15 start: {__MODULE__, :start_link, args},16 type: :worker17 }18 end19 end20 end
Definiendo esta macro, Elixir nos permite inyectar codigo en la fase de compilacion como
el que vemos en el ejemplo anterior, en el se crean dos funciones: start link en la que
se construye una keyword list con los argumentos que se han especificado al usar la
macro use, y child spec que es una funcion que define como tiene que ser ejecutado
ese GenServer.
6.2. BehavioursPara asegurar que los Processors tienen el mismo comportamiento siempre que se pro-
grame uno se han utilizado behaviours, una funcionalidad que ofrece Elixir que permite
definir una serie de funciones que tienen que ser implementadas por los modulos que usen
un behaviour.
A continuacion se muestra el behaviour que define un Processor:
1 defmodule Cwcomm.Processor do2 @callback process(struct()) ::3 Cwcomm.Channel.response()4 | Cwcomm.Channel.callback_response()5 end
Para crear un processor solo tendremos que crear la funcion process como se especifica
en el codigo anterior. Este es un ejemplo de un Processor:
1 defmodule Cwcomm.Processor.Telegram.PinRemoved do2 alias Cwcomm.Event.PinRemoved3 alias Cwcomm.Channel.Telegram4 import CwEcto.People.Telegram5
6 @behaviour Cwcomm.Processor7
22
Trabajo Fin de Grado Cesar Guzman Alpızar
8 require Logger9
10 @doc """11 Gets the Telegram user id from de DB and sends a PinRemoved message12 """13 @spec process(PinRemoved.t()) :: Cwcomm.Channel.response()14 def process(%{user: id}) do15 case get_tguser_account(id) do16 [tg_user] ->17 params = %{18 to: tg_user.tg_id,19 locale: tg_user.language,20 action: :off21 }22
23 Telegram.send("/api/pins", params)24
25 error ->26 Logger.debug("""27 Could not get telegram user,28 got instead:\n#{inspect(error)}29 """)30 end31 end32 end
7. ConclusionesEn este trabajo se ha puesto en practica la transformacion de una arquitectura monolıtica
a una orientada a microservicios y se han podido aprovechar las ventajas que esta ofrece.
Aunque se ha requerido un esfuerzo inicial por parte de todo el equipo de desarrollo de
software para aprender a usar RabbitMQ y aplicar tecnicas de metaprogramacion, final-
mente ha compensado el esfuerzo porque ahora se aprecian las ventajas del mismo.
El nuevo codigo es mas mantenible que su version monolıtica ya que se compone de
modulos mas pequenos y la desventaja de disenar un sistema de comunicacion no ha sido
tan grave dada la naturaleza de Erlang que usa procesos ligeros que hay que comunicar al
igual que los microservicios, aunque no se comunican de la misma manera el cambio no
ha sido tan drastico.
Otra ventaja que se ha apreciado ha sido la capacidad de hacer desarrollo independiente,
un desarrollador puede trabajar en cwcomm mientras otro trabaja en coowry-core o cwcorey no hay problemas de coordinacion con el control de versiones.
23
Trabajo Fin de Grado Cesar Guzman Alpızar
8. Referencias[1] C. Richardson, Microservices Patterns. Manning Publications Co., 2019, ISBN: 9781617294549.
[2] T. Ziade, Python Microservices Development. Packt Publishing Ltd., 2017, ISBN:
978-1-78588-111-4.
[3] Chris Richardson, ed. (). What are microservices?, direccion: https://microservices.io/.
[4] .NET Foundation, ed. (). Communication in a microservice architecture, direccion:
https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/architect-microservice-container-applications/communication-in-microservice-architecture#communication-types.
[5] OASIS®, ed. (). AMQP is the Internet Protocol for Business Messaging, direccion:
https://www.amqp.org/about/what.
[6] Pivotal Software, ed. (). Products and Success Stories, direccion: https://www.amqp.org/about/examples.
[7] Pivotal Software, ed. (). RabbitMQ is the most widely deployed open source messa-
ge broker., direccion: https://www.rabbitmq.com/.
[8] Cisco Systems et al, ed. (). AMQP Advanced Message Queuing Protocol, direccion:
https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf.
[9] Plataformatec, ed. (). GenServer, direccion: https://elixir-lang.org/getting-started/mix-otp/genserver.html.
24
Este documento esta firmado porFirmante CN=tfgm.fi.upm.es, OU=CCFI, O=Facultad de Informatica - UPM,
C=ES
Fecha/Hora Sun Jun 02 20:32:35 CEST 2019
Emisor delCertificado
[email protected], CN=CA Facultad deInformatica, O=Facultad de Informatica - UPM, C=ES
Numero de Serie 630
Metodo urn:adobe.com:Adobe.PPKLite:adbe.pkcs7.sha1 (AdobeSignature)