Introducción y ejemplo de API RPC con Apache Thrift

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente Comentarios

Java

Las aplicaciones están pasando de ser elementos aislados, grandes y monolíticos a ser desarrolladas como varios microservicios que colaboran entre si para en conjunto ofrecer la funcionalidad deseada. Aunque los microservicios presentan sus propias problemáticas resuelven algunas que poseen los sistemas monolíticos, entre algunas de sus características deseables están: mayor cohesión, menor acoplamiento, menor tamaño, mayor independencia de la tecnología usando la más adecuada en cada situación, más fácilmente reemplazables y despliegues más sencillos. También, la funcionalidad ofrecida por una aplicación puede quererse consumirse desde otra aplicación surgiendo de esta forma una API. Para ofrecer una API de una aplicación que pueda consumirse internamente, desde diferentes dispositivos o por terceras partes podemos usar SOAP o REST pero también han surgido algunas alternativas como Apache Thrift supliendo algunas carencias de las anteriores aún basándose en el mismo concepto de llamada a código en una máquina remota (Remote Procedure Call, RPC) ya visto en algunas opciones más antiguas como la misma SOAP, RMI o CORBA. En este artículo explicaré algunas diferencias entre SOAP, REST y Apache Thrift y mostraré un ejemplo sencillo con código de cómo empezar a usar Apache Thrift.

En los modelos RPC las llamadas a métodos se hacen a través de la red de forma transparente aunque tendremos que tener en cuenta que se utilizando un medio no fiable y con un rendimiento menor que llamadas en la misma máquina que notaremos más si se usan muchas llamadas. SOAP es una forma de RPC en la que se utiliza XML, algunas críticas a SOAP son que el XML utilizado para la comunicación es complejo y los servicios SOAP no son fácilmente consumibles desde por ejemplo un navegador. Por otra parte, las API REST tratan de solventar algunas de las deficiencias de SOAP como por ejemplo estar expuestas como recursos fácilmente accesibles utilizando los mismos mecanismos de la web y un formato para el intercambio de datos como JSON más sencillo y fácilmente consumible que XML. Sin embargo, algunas críticas que se le están haciendo REST son:

  • APIs asíncronas: el modelo RESTful de petición y respuesta no se adapta bien a un modelo donde hay necesidad de enviar datos de forma asíncrona evitando sondear continuamente el servidor con peticiones que consumen recursos de red y de servidor. El modelo asíncrono envía nuevos datos únicamente cuando estos se hacen disponibles.
  • Orquestación y experiencia de la API: la granularidad de una API REST no se adapta correctamente a algunas situaciones haciendo necesario realizar varias peticiones HTTP lo que añade carga al cliente, servidor y la red. Orquestando APIs internas en el servidor y publicando una que esté adaptada a lo que necesitan los diferentes clientes supone un mejor rendimiento y simplicidad.
  • SDKs vs APIs: los usuarios de las APIs finalmente las consumen desde un lenguaje de alto nivel como JavaScript, Python, Ruby, Java, PHP, C#, etc. con lo que los proveedores de las APIs necesitan ofrecer librerías cliente para algunos de estos lenguajes.
  • Protocolos binarios: los formatos binarios son más eficientes que el texto plano, lo que es útil en dispositivos limitados como los utilizados en el internet de las cosas (IoT).
  • Alta latencia: la sobrecarga que introduce el protocolo http en cada petición no lo hace adecuado en situaciones en que una baja latencia es necesaria para proporcionar un rendimiento óptimo.

Por otra parte algunos otros puntos a favor de RPC son:

  • Se tiene type safety y puede enviar excepciones que puede ser manejadas con la misma infraestructura ofrecida por el lenguaje de programación usado.
  • Si se hacen grandes volúmenes de llamadas y datos o hay requerimientos de ancho de banda se pueden usar protocolos de transporte más eficientes que HTTP.

Apache Thrift es un framework para desarrollar servicios eficientes e interoperables en diferentes lenguajes. Los lenguajes soportados en cualquier combinación de cliente y servidor son C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml y Delphi y alguno más. Para generar el código del servidor o cliente lo primero que debemos hacer es definir la interfaz del servicio en la que estén incluidas las operaciones, parámetros y retornos junto con sus tipos. A partir de esta interfaz Apache Thrift generará el cliente o servidor en el lenguaje que deseemos. Una vez publicada una versión de la interfaz podremos modificarla sin provocar problemas de compatibilidad en los clientes como ocurría en RMI. Una desventaja de Apache Thrift es que obliga a usar esta tecnología para consumir los servicios, en este sentido una API REST es más agnóstica en la que basta con el protocolo HTTP y JSON. Se puede optar por un modelo en el que de cara al exterior se ofrece una API REST pero internamente se usan APIs RPC. gRPC es una opción muy parecida con una mayor aceptación que Apache Thrift.

Ejemplo con Apache Thrift

Primeramente, para usar Apache Thrift debemos instalar el paquete en la distribución que usemos. En Arch Linux con:

1
2
$ sudo pacman -S thrift

pacman.sh

A continuación deberemos definir la interfaz del servicio, supongamos que queremos hacer un servicio que nos ofrezca un mensaje de ping, la hora del servidor y la suma de dos números. La interfaz de este servicio usando el DSL es:

1
2
3
4
5
6
7
8
namespace java io.github.picodotdev.blogbitix.thrift

service Service {

   string ping()
   i32 add(1:i32 op1, 2:i32 op2)
   string date()
}
Service.thrift

Podemos elegir cualesquiera lenguajes deseemos de la amplia lista soportada anterior, en este caso usaré Java tanto para el servidor como para el cliente. Usando el comando thrift e indicando el lenguaje y la interfaz generamos los artefactos:

1
2
$ thrift -out src/main/java --gen java src/main/thrift/Service.thrift
$ gradlew trift
gradlew.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
description = 'HolaMundoApacheThrift'
version = '0.1'

apply plugin: 'eclipse'
apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
	compile 'org.apache.thrift:libthrift:0.9.2'
	runtime 'org.apache.logging.log4j:log4j-core:2.1'
	runtime 'org.apache.logging.log4j:log4j-slf4j-impl:2.1'
}

task wrapper(type: Wrapper) {
	gradleVersion = '2.3'
}

task thrift() {
	exec {
		commandLine 'thrift', '-out', 'src/main/java', '--gen', 'java', 'src/main/thrift/Service.thrift'
	}
}
build.gradle

Esto nos genera unas clases en Java y una interfaz que implementaremos para proporcionar la funcionalidad del servicio, en el caso del ejemplo la interfaz es Service.Iface. Para que los clientes puedan consumir este servicio debemos iniciar el servidor que no será más que un programa Java que escucha las peticiones de los clientes en un puerto.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package io.github.picodotdev.blogbitix.thrift;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.thrift.TException;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TServer.Args;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TServerTransport;

public class Server {

	public static void main(String[] args) {
		try {
			final Service.Processor<Service.Iface> processor = new Service.Processor<Service.Iface>(new Server.ServiceImpl());

			Runnable simple = new Runnable() {
				public void run() {
					simple(processor);
				}
			};

			new Thread(simple).start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void simple(Service.Processor<Service.Iface> processor) {
		try {
			TServerTransport serverTransport = new TServerSocket(9090);
			TServer server = new TSimpleServer(new Args(serverTransport).processor(processor));

			// Use this for a multithreaded server
			// TServer server = new TThreadPoolServer(new
			// TThreadPoolServer.Args(serverTransport).processor(processor));

			System.out.println("Starting the service server...");
			server.serve();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private static class ServiceImpl implements Service.Iface {

		@Override
		public String ping() throws TException {
			System.out.println("Me han llamado ¡que ilusión! ^^");
			
			return "¡Hola mundo!";
		}

		@Override
		public int add(int op1, int op2) throws TException {
			return op1 + op2;
		}

		@Override
		public String date() throws TException {
			return new SimpleDateFormat("dd-MM-yyyy HH:mm:ss Z").format(new Date());
		}
	}
}
Server.java

Una vez están los servicios disponibles podemos consumirlos con las siguientes siguientes líneas de código de una implementación de cliente, basta hacer uso de las clase Service.Client generada a partir de la interfaz del servicio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.github.picodotdev.blogbitix.thrift;

import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

public class Client {

	public static void main(String[] args) {
	    try {
	      TTransport transport = new TSocket("localhost", 9090);
	      transport.open();

	      TProtocol protocol = new  TBinaryProtocol(transport);
	      Service.Client client = new Service.Client(protocol);

	      System.out.println(String.format("Ping: %s", client.ping()));
	      System.out.println(String.format("Add: %d", client.add(4, 7)));
	      System.out.println(String.format("Date: %s", client.date()));

	      transport.close();
	    } catch (TException e) {
	      e.printStackTrace();
	    } 
	}
}
Client.java

Ejecutando el cliente y llamando a los métodos de la interfaz del servicio veremos en la terminal la siguiente salida:

Salida de consola del cliente Salida de consola del servidor

Si te interesan las arquitecturas de aplicaciones con microservicios ya sea con API RPC o REST un libro muy interesante y recomendable es Building Microservices. Proporciona una visión detallada de los diferentes aspectos que deben tratar este tipo de aplicaciones.

Apache Thrift no es la única herramienta para hacer llamadas RPC, una muy similar es gRPC de Google e igualmente interesante al hacer uso de HTTP/2 y Protocol Buffers.

El código fuente completo del ejemplo lo puedes encontrar en mi repositorio de GitHub.


Comparte el artículo: