Protocolos de comunicación entre microservicios
gRPC y ProtoBuf
Todo lo que necesitas saber sobre gRPC y ProtoBuf
Artículo publicado originalmente en Secture&Code
Qué es gRPC?
gRPC es un sistema de comunicación RPC (Remote Procedure Call) de alto rendimiento y multiplataforma desarrollado por Google.
gRPC nace en Google como una evolución de un sistema de comunicación interno llamado “Stubby”. Stubby se utilizaba para la comunicación entre servicios distribuidos dentro de la infraestructura de Google. Sin embargo, a medida que Google crecía y diversificaba sus servicios, surgieron desafíos en cuanto a la eficiencia y la facilidad de desarrollo y mantenimiento de los sistemas de comunicación.
En este contexto, Google desarrolló gRPC como una solución para abordar estos desafíos. El objetivo principal era crear un sistema de comunicación RPC (Remote Procedure Call) que fuera eficiente, fácil de usar y compatible con múltiples lenguajes y plataformas. gRPC se diseñó para ser un sistema de comunicación de alto rendimiento, utilizando el protocolo HTTP/2 para mejorar la eficiencia en la transferencia de datos y ofreciendo soporte para múltiples lenguajes de programación mediante la generación automática de código a partir de un archivo de definición de servicio protobuf.
El propósito de gRPC era proporcionar una forma estandarizada y eficiente de comunicación entre servicios distribuidos, lo que facilitaría el desarrollo y la integración de sistemas en entornos complejos y escalables. Además, al ser de código abierto y compatible con múltiples lenguajes, gRPC se convirtió en una opción atractiva para la comunidad de desarrollo de software fuera de Google, lo que contribuyó a su rápida adopción y popularidad en la industria.
Actualmente, gRPC soporta los siguientes lenguajes:
Qué es protobuf?
Protocol Buffers (protobuf) es un formato de serialización de datos desarrollado por Google para estructurar y serializar datos de manera eficiente. Se utiliza principalmente para la comunicación entre sistemas distribuidos, como en el caso de gRPC, pero también puede ser utilizado para almacenar datos o cualquier otra tarea que implique la transferencia o almacenamiento eficiente de datos estructurados.
En protobuf, los datos se definen utilizando un lenguaje de esquema simple y legible llamado “Protocol Buffers Language” o simplemente “proto”. Este esquema define la estructura de los datos que se van a serializar, incluyendo los tipos de datos y sus nombres. A partir de este esquema, se genera automáticamente código fuente en diferentes lenguajes de programación que permite serializar y deserializar los datos de manera eficiente.
Por ejemplo, supongamos que queremos definir un mensaje de usuario con un nombre y una dirección de correo electrónico utilizando protobuf.
El archivo de definición de esquema proto sería algo así:
syntax = "proto3";
message User {
string name = 1;
string email = 2;
}
Este esquema lo compilaremos usando el compilador de ProtoBuf: protoc
. Para que nos devuelva un archivo para usar en Python ejecutamos lo siguiente:
protoc --python_out=. user.proto
Esto nos generaría un archivo, con un nombre similar a user_pb2.py
(lamentablemente no podemos elegir el nombre con que se genera), que podremos importar y usar en nuestro código:
import user_pb2
# Create user
user = user_pb2.User()
user.name = "John Doe"
user.email = "john.doe@example.com"
# Serialize user
serialized_user = user.SerializeToString()
# Deserialize user
deserialized_user = user_pb2.User()
deserialized_user.ParseFromString(serialized_user)
# Print name and email
print("Name:", deserialized_user.name)
print("Email:", deserialized_user.email)
Con protobuf no solo definimos los modelos de datos, sino que también definimos las interfaces de los servicios y los mensajes que se utilizan para comunicarse entre ellos, por ejemplo, vamos a implementar un sencillo servicio que saluda a nuestros usuarios.
Para ello vamos a definir:
- Un mensaje
HelloRequest
, que recibe como parámetro un objeto de tipoUser
- Un mensaje
HelloResponse
- Un mensaje
User
- Un servicio
Greeter
con un metodo RPCSayHello
, que acepta como entrada un mensaje de tipoHelloRequest
y devuelve otro de tipoHelloResponse
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
message HelloRequest {
User user = 1;
}
message HelloResponse {
string message = 1;
}
message User {
string name = 1;
string email = 2;
}
Implementación de un servicio
Ahora que ya hemos visto qué es gRPC y qué es ProtoBuf y cómo podemos definir servicios y mensajes, vamos a hacer una implementación de un servicio de cambio de divisas.
El servicio constara de un método Exchange
que aceptará un objeto de tipo Money
(compuesto una cantidad y una divisa) y una divisa destino. Por su parte, nos devolverá una respuesta que incluirá un objeto de tipo Money
.
Para ilustrar la flexibilidad de comunicación entre lenguajes, vamos a implementar el servidor en Python, y dos clientes, uno en Python y otro en NodeJS.
Definición en ProtoBuf
Veamos la especificación en ProtoBuf de este servicio:
syntax = "proto3";
service ExchangeService {
rpc Exchange(ExchangeRequest) returns (ExchangeResponse);
}
message ExchangeRequest {
Money money = 1;
string to_currency = 2;
}
message ExchangeResponse {
Money money = 1;
}
message Money {
double amount = 1;
string currency = 2;
}
Implementación del servidor en Python
Antes de nada, necesitaremos tener instalados los paquetes grpcio
y grpcio-tools
:
python3 -m pip install grpcio grpcio-tools
Partiendo de la definición en ProtoBuf de antes, vamos a compilar el archivo para generar el código necesario para implementar el servidor en Python:
python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. exchange.proto
Con el código generado, podemos implementar nuestro server de la siguiente forma:
import grpc
import exchange_pb2
import exchange_pb2_grpc
from concurrent.futures import ThreadPoolExecutor
class ExchangeService(exchange_pb2_grpc.ExchangeServiceServicer):
def Exchange(self, request, context):
# Simplemente devolveremos la misma cantidad cambiando la divisa por simplicidad
new_money = exchange_pb2.Money(amount=request.money.amount, currency=request.to_currency)
return exchange_pb2.ExchangeResponse(
money=new_money,
)
def serve():
server = grpc.server(ThreadPoolExecutor(max_workers=10))
exchange_pb2_grpc.add_ExchangeServiceServicer_to_server(ExchangeService(), server)
server.add_insecure_port('[::]:50051')
server.start()
print("Server listening on port 50051...")
server.wait_for_termination()
if __name__ == '__main__':
serve()
Implementación del cliente en Python
Al igual que antes, antes de nada necesitaremos tener instalados los paquetes grpcio
y grpcio-tools
:
python3 -m pip install grpcio grpcio-tools
Partiendo de la definición en ProtoBuf de antes, vamos a compilar el archivo para generar el código necesario para implementar el servidor en Python:
python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. exchange.proto
Con el código generado, podemos implementar nuestro cliente de la siguiente forma:
import grpc
import exchange_pb2
import exchange_pb2_grpc
from termcolor import colored
def run_client():
# Conectar al servidor gRPC
channel = grpc.insecure_channel('grpc_server_python:50051')
# Crear un cliente para el servicio Exchange
stub = exchange_pb2_grpc.ExchangeServiceStub(channel)
amount = float(input(colored('[?] Amount: ', 'blue')))
currency_from = input(colored('[?] Currency from: ', 'blue'))
currency_to = input(colored('[?] Currency to: ', 'blue'))
request = exchange_pb2.ExchangeRequest(
money=exchange_pb2.Money(amount=amount, currency=currency_from.upper()),
to_currency=currency_to.upper()
)
response = stub.Exchange(request)
print(colored(f"\n[+] Amount: {response.money.amount} {response.money.currency}", 'yellow'))
print(colored(f"[+] Exchange rate: {response.rate}\n", 'yellow'))
if __name__ == '__main__':
run_client()
Implementación del cliente en NodeJS
Con Node tenemos la posibilidad de hacer la implementación de dos formas diferentes:
- Con generación de código dinámica: El código se genera en runtime.
- Con generación de código estática: El código se genera en un paso previo.
Como ya hemos visto el ejemplo con generación de código estática, vamos a seguir el enfoque dinámico. Este enfoque solo lo he visto disponible en Node, y tiene la característica de que, usándolo, podemos prescindir de los objetos que representan los mensajes y usar objetos planos de NodeJS. Esto puedes verlo como una ventaja o un inconveniente, a mi personalmente me gusta más usar los objetos definidos, ya que nos permite ser más coherentes con el resto de implementaciones y añade una capa de validación a los mensajes que mandamos. De hecho, de hacer la implementación con TypeScript se usarían los mensajes definidos.
Previo a la implementación, necesitaremos instalar los paquetes @grpc/grpc-js
y @grpc/proto-loader
Veamos un ejemplo de implementación:
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const input = require('readline-sync');
const packageDefinition = protoLoader.loadSync('protos/exchange.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const exchange_proto = grpc.loadPackageDefinition(packageDefinition);
const client = new exchange_proto.ExchangeService('grpc-server-python:50051', grpc.credentials.createInsecure());
const amountFrom = parseFloat(input.question('[?] Amount: '));
const currencyFrom = input.question('[?] Currency from: ').toUpperCase();
const currencyTo = input.question('[?] Currency to: ').toUpperCase();
const request = {
money: {
amount: amountFrom,
currency: currencyFrom
},
to_currency: currencyTo
};
client.Exchange(request, (err, response) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('Amount:', response.money.amount.toFixed(2), response.money.currency);
console.log('Exchange rate:', response.rate.toFixed(2));
});
Demo funcional
Puedes encontrar una demo funcional de todo lo visto aquí en el siguiente repositorio de Github.
Podrás ver las implementaciones y ejecutar el servidor y los clientes a través de un sencillo Makefile
y Docker.
Conclusiones
gRPC parece que se va abriendo un pequeño hueco en el mundo de las comunicaciones entre microservicios. Me parece una tecnología útil y potente, además de que las definiciones de los servicios y la mensajería me parece una gran idea para mantener a los equipos alineados y al mismo tiempo documentar nuestros servicios.
Si bien es cierto que he publicado solo las pruebas con Python y Node, he hecho muchas otras con lenguajes como PHP o Golang, y tengo que decir que no me ha sido nada fácil.
A pesar de tener una amplia experiencia en PHP, la instalación de las librerías, la compilación de los modelos… etc, es bastante complejo, me he encontrado con decenas de errores que me han tenido buceando en StackOverflow, ChatGPT y Github buscando soluciones. Además que la instalación de la librería para PHP es lentísima y desesperante.
Con Golang también he tenido problemas, pero asumo mi parte de la culpa, ya que mi experiencia con Go es bastante limitada en comparación.