Serialización de alto desempeño: Protocol Buffers

El 11 de octubre de 2011 liberé la versión 1.6.0 de jAlarms, y una de las características nuevas de esta versión es la posibilidad de poner un servicio de envío remoto de alarmas.

Una manera de lograr esto es con RMI, de lo cual ya hablaré posteriormente, pero una manera mucho más eficiente es con los Protocol Buffers de Google. Esta es una herramienta bastante buena para comunicación en red, de hecho es lo que usa Google para la comunicación entre sus procesos, porque ofrece las siguientes ventajas:

  • Alto desempeño para serializar y de-serializar objetos.
  • Portabilidad e interoperabilidad: permite comunicación entre aplicaciones hechas en distintos lenguajes y plataformas.
  • Ancho de banda/espacio: los mensajes serializados ocupan muy poco espacio en comparación con otros formatos como XML, JSON, o incluso serialización binaria nativa de varias plataformas.

Algunas de esas características probablemente les suenen conocidas. Son las promesas de los web services, SOAP-RPC, etc. Para comparaciones me remito a un benchmark muy completo que compara Protocol Buffers con Thrift (una alternativa similar de Apache), XML, JSON, serialización nativa de Java... el ganador es Protocol Buffers, por mucho. Diez veces más rápido que la serialización nativa de Java...

Pero bueno, ¿cómo se usa? Pues resulta que necesitamos un compilador... lo pueden construir a partir de los fuentes, bajando la versión más reciente de la página oficial (2.4.1 cuando escribí esto). Un simple   y al cabo de unos minutos tienen   instalado.

Integración a un proyecto

Para no estarme complicando con plugins en un IDE o tener que llamar el compilador de protobuf a mano, me puse a buscar y resulta que hay un plugin para Gradle, lo cual creo que facilita mucho las cosas. Simplemente definimos un nuevo source set en src/main/proto, en donde vamos a poner nuestros mensajes de protobuf.

Archivos .proto

Para usar protobuf, hay que definir los mensajes que se desea enviar. Un mensaje es un conjunto de propiedades, como si se definiera un bean de Java. Hay que indicar el tipo y nombre de cada propiedad, e indicar si es opcional u obligatoria.

Estos mensajes son procesados por un compilador, llamado  , que los convierte a código fuente de otro lenguaje, que puede ser Java, Python, C#, C++, Ruby, Perl, PHP, etc. Una vez que se compila el mensaje al lenguaje destino, ese código se compila o se utiliza directamente ya en la plataforma destino (por ejemplo, código Java se compila a bytecode, pero código Ruby o PHP se puede utilizar ya directamente).

En mi caso, lo que implementé fue un envío de alarma de un lado a otro; solamente necesito tres propiedades, dos opcionales y una obligatoria. La obligatoria es el mensaje de alarma y las opcionales son la fuente de la alarma, y una bandera indicando si se debe enviar forzosamente o sólo si no ha sido enviada muy recientemente. De modo que mi mensaje queda así:

 

Los mensajes se pueden definir dentro de un paquete, para evitar colisiones (similar a Java). Pero son simples nombres; cuando esto se compile a Java, queremos tener un paquete que sea significativo en Java.

En el script de Gradle hay que indicar el plugin, que por ahora no está todavía incluido en los oficiales, de modo que hay que jalarlo de Maven Central. También hay que incluir la biblioteca protobuf, para que el código Java generado por el   pueda ser compilado por  :

 

Al ejecutar Gradle, primero pasa por la fase de protoc, que genera un archivo AlarmProtos.java el cual contiene varias clases; este archivo es agregado al source set principal para que sea compilado ya como cualquier otra clase Java.

La manera de crear un mensaje (tipo Alarm en mi caso), es muy simple: el código contiene un builder el cual configuramos con los datos que queremos y al final le pedimos que nos haga un mensaje. Los mensajes generados son inmutables. Entonces:

 

Las propiedades marcadas como opcionales nos las podemos saltar, pero las obligatorias hay que ponerles valor o al momento de invocar   se arrojará una excepción tipo   indicándonos los campos obligatorios a los que no pusimos valor.

Una vez que tenemos el mensaje, lo único que tenemos que hacer es escribirlo a un stream:

 

La diferencia entre ambos métodos es que el segundo antepone la longitud completa del mensaje, para que quien lo tenga que leer pueda saber cuántos bytes esperar. Para leer mensajes de un stream, simplemente:

 

El primer método es para leer mensajes que fueron escritos con   y el segundo para mensajes escritos con  .

De modo que escribir la comunicación completa utilizando sockets queda bastante sencilla.

Detalles

Me encontré con un par de detalles que me costó un poco de trabajo arreglar, pero a fin de cuentas lo pude resolver.

La naturaleza de la conexión que quiero manejar es algo peculiar: los clientes idealmente casi no se conectan al servidor, más que cuando tienen problemas (y por eso quieren enviar una alarma). Pero cuando hay problemas, es común que no se envíe una alarma, sino cientos o hasta miles; del lado del servidor se filtran todos esos envíos con los mecanismos que ya tiene jAlarms desde versiones anteriores. Para no replicar toda esa funcionalidad en el cliente, lo más sencillo es enviar TODAS las alarmas al servidor. Por eso elegí protobuf, para que no cueste mucho tiempo el envío, y no se acabe el ancho de banda, etc.

Pero en cuanto a recursos, no quiero tampoco mantener constantemente una conexión abierta entre cliente y servidor, si la mayor parte del tiempo no se va a usar. Pero tampoco quiero usar una conexión para enviar un solo mensaje. Lo mejor es abrir una conexión y poder reutilizarla si es que hay varias alarmas que enviar, pero cerrarla después de un periodo (corto) de inactividad. Me decidí por hacer ese periodo 5 segundos. Entonces, cuando el servidor recibe una conexión, la debe mantener abierta en un ciclo donde lee alarmas para procesarlas, pero si pasan 5 segundos y no llegan más, que cierre la conexión.

Resulta que no es suficiente con ponerle   al Socket; no sé cómo lee del socket el método  , pero no detecta el timeout. De modo que tuve que interponer una lectura de un solo byte entre cada lectura de mensaje; esa lectura bloquea el hilo que lee, pero máximo durante 5 segundos y después de ese tiempo se arroja una   la cual puedo cachar para cerrar la conexión.

Del lado del cliente, tengo que tener en cuenta que si han pasado más de 5 segundos desde la última alarma que se envió, lo mejor es cerrar la conexión y abrir una nueva.

De modo que eso es lo que tengo en el cliente, y también en el servidor.

Otros usos

Este fue un uso que le di a Protocol Buffers, y es un uso común (Google creó esta herramienta precisamente para comunicación entre procesos, usando sockets). Pero el intercambio de un alto volumen de mensajes no es el único uso que se le puede dar. Dado que lo que hace en esencia es serializar y deserializar datos a muy alta velocidad y con un formato bastante compacto, se presta también para usarse en conjunto con caches como memcached, Redis, etc.

También se puede usar para guardar datos en sistemas de almacenamiento NoSQL como Cassandra, MongoDB, etc, por las mismas razones: alta velocidad de serialización, formato compacto y portabilidad.

Por ahora, ya tengo una herramienta más a mi disposición para cuando necesite intercambiar datos entre aplicaciones, incluso si están sobre distintas plataformas. Especialmente si están en distintas plataformas. El XML es muy bueno para ser parseado por humanos; para la comunicación entre distintas piezas de software, dejemos que utilicen formatos binarios compactos y de alto desempeño.

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de Nopalin

Hasta la fecha rmi no me ha

Hasta la fecha rmi no me ha fallado en lo absoluto, y eso que tambien envio array de bytes en sus llamadas remotas, dentro de una red local y con vpn, ya lleva años jalando y como si nada, pero como quiera esta opcion de google me llama la atención, habrá que echarle un ojo.

sobres

Imagen de ezamudio

benchmarks

Al principio puse una liga a una comparacion bastante completa entre distintas herramientas: Protobuf, thrift, serializacion nativa de Java (que en eso se basa RMI), json, xml, etc. Protobuf es mas rapido que la serializacion nativa de Java, y mas compacto.

No digo que RMI este mal, especialmente con ayuda de Spring que te facilita muchisimo el publicar un objeto y tambien crear un proxy a un objeto remoto; pero mas alla de la facilidad, el performance de protobuf es suficientemente mayor como para tomarlo en cuenta si tienes un sistema donde hay muchas llamadas RMI. De hecho es mi caso y estoy considerando cambiar la interfaz RMI que comunica dos aplicaciones, por comunicacion con protobuf.

Imagen de bferro

No se debe confundir Protocol Buffer con RMI

Buen post el de Enrique experimentando con Protocol Buffer.
Creo que la comparación debe hacerse entre el formato binario que usa Protocol buffer y el formato binario que usa la serialización nativa de Java cuando se quiere enviar un mensaje (puros datos) y no comparar Protocol Buffer con RMI, porque RMI es mucho más que la representación externa de datos que utiliza en su middleware, que por supuesto que tiene muchas más cosas que ese XDR.
Enrique lo expresa así cuando se refiere al benchmark entre formatos diversos para el envío de mensajes.

Lo que yo decía era eso que

Lo que yo decía era eso que dijo @bferro :)

Websphere MQ

Disclaimer: Antes de leer este post es importatne que sepan que no soy pro IBM, solo quiero complementar con la forma en que trabajan los MQ solo que no he trabajado con otra herramienta que no sea de IBM.

La solucion suena bien, mas sin embargo creo que integrar un servicio de colas a los mensajes le aportaria mas valor que estar monitoreando una conexión y claro, preocuparse por si estamos haciendo buen uso de ella. Me explico:

IBM tiene una herramienta que se llama Message Queue (Websphere MQ) lo que hace en realidad es que recibe un mensaje desde casi cualquier protocolo de comunicación, lo procesa (si tiene que aplicar reglas de conversión o cualquier porceso adicional antes de ser direccionado), despues lo envia a una cola de mensajes donde existe un Listener y al detectar que llega un mensaje nuevo lo procesa, en este caso podria ser jAlarms quien procese el mensaje entrante. @domix anunciaba que usa algo que creo es similar : RabbitMQ. Me parece que "Protocol Buffers" se deberia llevar muy bien con un sericio de colas dado que en mi punto de vista personal: usar timeouts es solo un WorkArround de lo que deberia soportar el envio de varios mensajes.

Solo es ese detalle, lo demas me agrado y suena muy muy bien!

Imagen de ezamudio

EE

Te estás volviendo muy JEE muchacho... no hay que sobrecomplicar las cosas.

Ciertamente es posible lo que mencionas, hacer una interfaz JMS para que se puedan enviar mensajes a jAlarms; crear una nueva implementación de AlarmSender que utilice JMS para enviar un mensaje, y un componente que recibe el mensaje del otro lado y tiene conectado otro AlarmSender (idealmente ya la clase AlarmSenderImpl) para que haga el envío.

También se puede hacer un canal para que cuando se envíe una alarma, salga un mensaje por JMS.

La cosa es que si quieres enviar una alarma para avisar que tu MQ no está funcionando... no va a llegar...