Artículo
Jose Tomas Salvador · Jun 14 Lectura de 17 min

gRPC - Qué es y un Hola Mundo!

Introducción

Este artículo pretende dar una introducción a qué es gRPC y mostrar un ejemplo de cómo jugar con el Hola Mundo oficial utilizando IRIS Embedded Python.

En este repositorio puedes encontrar todo el código expuesto aquí.

gRPC

El gRPC (gRPC remote procedure call) es un estilo de API arquitectural basada en el protocolo RPC. El proyecto fue creado por Google en 2015 y está licenciado bajo Apache 2.0. Actualmente el proyecto es soportado por la Cloud Native Computing Foundation (CNCF).

Los casos de uso exitosos están relacionados con la conexión de servicios en backends, tales como los servicios en arquitecturas de microservicios.

Protocol buffer

La mayoría de los protocoles basados en RPC utilizan un Lenguaje de Descripción de Interfaz o IDL (del inglés, Interface Description Language) para definir un contrato de comunicación entre un servidor y un cliente.

El gRPC utiliza un formato de serialización llamado Protocol Buffer.

El propósito de este formato es similar al WSDL, donde puedes definir métodos y estructuras de datos. Sin embargo, al contrario que el WSDL, que se define en XML, el Protocol Buffer utilizar una lenguaje (Protocol Buffer Language) similar a una mezcla de los lenguajes más comunes.

Por ejemplo, para definir un mensaje para intercambiar información, puedes utilizar la siguiente definición de Protocol Buffer:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
}

También puedes definir contratos de métodos de servicio para mensajes. Por ejemplo:

// La definición del servicio Greeter.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// El mensaje de petición conteniendo el nombre de usuario.
message HelloRequest {
  string name = 1;
}

// El mensaje de respuesta conteniendo los saludos
message HelloReply {
  string message = 1;
}

La utilización de Protocol Buffers en gRPC se corresponde más con un principio de diseño funcional que con un diseño basado en el recurso, utilizado por REST.

Herramientas gRPC

Lo que defines en el lenguaje de Protocol Buffers no puede utilizarse directamente. Debes "transpilar" el lenguaje de Protocol Buffers a otro lenguaje soportado por gRPC.

Tal "transpilación" es hecha por un paquete llamado gRPC tools. Actualmente, la plataforma gRPC soporta lenguajes como Java, C++, Dart, Python, Objective-C, C#, Ruby, JavaScript y Go.

En este artículo, vamos a emplear el soporte a Python para utilizar gRPC con la funcionalidad de Python Embebido en IRIS.

Por ejemplo, con este comando de las herramientas gRPC, puedes transpilar la definición en Protocol Buffers del servicio Greeter, y los mensajes HelloRequest y HelloReply a Python:

python3 -m grpc_tools.protoc -I ../../protos --python_out=. --grpc_python_out=. ../../protos/helloworld.proto

Este comando produce los siguientes ficheros Python:

-rwxrwxrwx  1 irisowner irisowner 2161 Mar 13 20:01 helloworld_pb2.py*
-rwxrwxrwx  1 irisowner irisowner 3802 Mar 13 20:01 helloworld_pb2_grpc.py*

Estos ficheros son el código fuente Python de los mensajes y métodos de servicio (respectivamente) generado desde el fichero proto. El servidor implementa los métodos de servicio, y el cliente (también llamado stub) los llama.

De este modo, puedes utilizar Python Embebido para enviar/recibir mensajes a través del servicio Greeter.

Otra herramienta útil para gRPC es la equivalente a curl, la utilidad grpcurl. Después de hacer nuestro ejemplo "Hola Mundo", mostraremos como utilizar esta otra herramienta..

Tipos de métodos de servicio

Los clientes pueden variar en como envían y reciben mensajes hacía y desde los métodos de servicio. Se puede enviar un mensaje por llamada o un stream de mensajes. Para cada combinación, gRPC tiene un tipo de método:

  • RCP Simple o Unaria: los clientes envían un mensaje simple y reciben una respuesta simple desde el servidor, es decir, como una llamada a función estándar.
  • Respuesta-Streaming or streaming desde servidor: los clientes envían un mensaje simple y reciben, desde el servidor, un stream de mensajes.
  • Petición-Streaming o streaming desde cliente: los clientes envían un stream de mensajes y reciben una respuesta sencilla desde el servidor.
  • Streaming-Bidireccional: los clientes envían un stream de mensajes y reciben otro stream de mensajes desde el servidor.

La comunicación a través de esos métodos puede ser asíncrona (por defecto) o síncrona, según las necesidades del cliente.

La pagína de conceptos core de gRPC define esos tipos desde las características del ciclo de vida de gRPC. También remarca otras características que salen fuera del alcance de esta introducción, pero que puedes comprobar en los siguientes enlaces si quieres más información:

Pros y contras

Aquí tenemos algunos pros y contras que he encontrado en algunos artículos:

Pros:

  • Mensajes más ligeros. El Protocol Buffer es un formato binario, lo que evita la sobrecarga de los mensajes JSON.
  • Serialización/Deserialización rápida. De nuevo, gracias a su formato binario, Protocol Buffers puede ser serializado/deserializado en pequeños conectores cliente (client stubs) utilizando lenguajes específicos sin intérpretes.
  • Clientes pre-construidos (stubs). Protocol buffers tiene generadores pre-construidos para la mayoría de los lenguajes más utilizados, al contrario que JSON que depende de herramientes de terceros como OpenAPI y sus generadores de clientes.
  • Peticiones Paralelas. HTTP/1 permite haste 6 conexiones simultáneas, bloqueando cualquier otra petición hasta que las 6 han finalizado - un problema conocido como HOL (del inglés Head of Line Blocking, bloqueo de línea de cabecera); HTTP/2 elimina esas limitaciones.
  • Diseño de API Contrato-Primero. Aunque las APIs REST pueden exponer un contrato a través de herramientas de terceros como OpenAPI, en gRPC dicho contrato es explícitamente declarado por el Protocol Buffers.
  • Streaming Nativo. Gracias a las capacidades de streaming de HTTP/2, gRPC permite un modelo de streaming bidireccional nativo, al contrario que REST, que debe simular ese comportamiento sobre HTTP/1.

Contras:

  • Escasa flexibilidad en Protocol Buffers.
  • Los mensajes vía Protocol Buffers no son legibles.
  • Muchas más personas especializadas, recursos y herramientas/proyectos para REST/JSON que para gRPC/Protocol Buffers.

No obstante, no hay consenso sobre estos pros y contras. Dependen mucho de las necesidades de tu aplicación.

Por ejemplo, una supuesta desventaja de REST/JSON es la necesidad de herramientas de terceros como OpenAPI. Sin embargo, podría no ser ningún problema ya que eas herramientas cuentan con gran respaldo, son mantenidas y utilizadas por varias compañías/comunidades de desarrollo en todo el mundo.

Por otro lado, si tu proyecto necesita tratar con las complejidades que gRPC puede abordar mejor que REST, deberías optar por gRPC, incluso si esa decisión te puede ocasionar complicaciones, como crear un equipo de desarrollo cualificado.

¿Dónde/Cuándo utilizar gRPC?

Aquí tenemos algunos casos de uso en que puedes necesitar gRPC:

  • Comunicación entre Microservicios
  • Aplicaciones Cliente/Servidor, donde los clientes se ejecutan sobre un hardware y/o red limitadas, asumiento que HTTP/2 está disponible
  • Interoperabilidad facilitada por un diseño de API muy bien cerrado y definido

Actores gRPC

Algunas big techs están utilizando gRPC para lidiar con retos específicos:

  • Salesforce: gRPC rige la plataforma de la compañía con un incremento en la robustez de su interoperabilidad gracias al diseño muy bien delimitado por Protocol Buffers.
  • Netflix: utiliza gRPC para mejorar su entorno de microservicios.
  • Spotify: similar a Netflix, utiliza gRPC para manejar los retos de los microservicios y tratar con gran cantidad de APIs.

Hola Mundo utilizando Python Embebido

De acuerdo, trás una breve introducción a lo que es gRPC y qué hace, ahora podemos avanzar, así que vamos a jugar un poquito con ello. Como bien dice el título de este apartado, esto es sólo una adaptación al ejemplo Hola Mundo original utlizando el Python Embebido de IRIS.

Realmente, este ejemplo es una variación de otras 2 instancias que se pueden encontar en el repositorio ejemplo de gRPC - helloworld y hellostreamingworld. Con su ayuda, me gustaría mostrarte como enviar y recibir un mensaje sencillo en modo simple y modo stream. Aunque se trate de una ejemplo básico sin características realmente útiles, te ayudará a comprender los conceptos principales relacionados con el desarrollo de una aplicación gRPC.

Instalación de gRPC para Python

Lo primero, vamos a instalar el paquete requerido para utilizar gRPC en Python. Si estás corriendo el ejemplo desde el contenedor definido en mi proyecto github, probablemente tengas estos paquetes ya instalados, si es así, puedes saltar este paso.

python3 -m pip install --upgrade pip
python3 -m pip install --upgrade --target /usr/irissys/mgr/python grpcio grpcio-tools

Definición del contrato de servicio

Ahora, veamos el contrato de servicio, es decir, el fichero Protocol Buffer (abreviado protobuf), con el esquema de mensajes y los métodos disponibles:

syntax = "proto3";

package helloworld;

// Definición del servicio greeting 
service MultiGreeter {
  // Envía un saludo
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // Envía múltiples saludos
  rpc SayHelloStream (HelloRequest) returns (stream HelloReply) {}
}

// Mensaje de petición conteniendo el nombre de usuario y cuantos saludos quieren
message HelloRequest {
  string name = 1;
  Int32 num_greetings = 2;
}

// Un mensaje de respuesta conteniendo un saludo
message HelloReply {
  string message = 1;
}

Este fichero protobuf define un servicio llamado MultiGreeter con 2 métodos RPC: SayHello() y SayHelloStream().

El método SayHello() recibe un mensaje HelloRequest y envía un mensaje HelloReply. Igualmente, el método SayHelloStream() recibe y devuelve los mismos mensajes, pero devuelve un stream de mensajes HelloReply en lugar de uno único.

Tras la definición del servicio, tenemos la definición de los mensajes: HelloRequest y HelloReply. El mensaje HelloRequest simplemente encapsula 2 campos: uno literal llamado name y un entero llamado num_greetings. El mensaje HelloReply contiene sólo un campo, un literal llamado message.

Los números tras los campos son llamados números de campo. Estos números no se deben cambiar una vez el mensaje está siendo utilizado ya que actúan como identificadores.

Generación de código Python a partir del contrato de servicio

Como probablemente habrás notado, no necesitamos escribir ningún código en la definición protobuf, sólo interfaces. La tarea de implementar el código para los diferentes lenguajes de programación la hace el compilador de protobuf protoc. Hay un compilador protocpor cada uno de los lenguajes soportados por gRPC.

Para Python, el compilador se despliega como un módulo llamado grpc_tools.protoc.

Para compilar la definición protobuf a código Python, ejecuta el siguiente comando (asumientdo que estás utilizando mi proyecto):

cd /irisrun/repo/jrpereira/python/grpc-test
/usr/irissys/bin/irispython -m grpc_tools.protoc -I ./ --python_out ./ --grpc_python_out ./ helloworld.proto

Este comando invoca al módulo grpc_tools.protoc - el compilador protoc de Python, con los siguientes parámetros:

  • helloword.proto: el fichero principal .proto para el contrato de servicio
  • -I: la ubicación de los ficheros .proto donde el compilador buscará posibles dependencias
  • --python_out: la ubicación del código Python generado para los mentajes
  • --grpc_python_out: la ubicación del código Python generado para un servidor y un cliente (stub) basados en los métodos RPC de la definición del servicio

En este caso, todos los parámetros de ubicación están apuntando al directorio actual.

El código generado por el compilador protocno es el mejor ejemplo de legibilidad, aunque no es difícil de entender. Puedes comprobarlo en el directorio pasado al compilador protoc.

En todo caso, estos ficheros están pensados para que los importes en tu propio código, así que vamos a usarlos, implementando un servidor y un cliente.

Implementación de un servidor para el servicio

Para implementar un servidor para el servicio definido arriba, vamos a usar Python embebido.

Primero, vamos a definir un servidor utilizando un fichero Python donde esté implementada la lógica del servidor. He decidido hacerlo de esta manera por la necesidad de utilizar una librería de concurrencia de Python.

"""
La implementación Python del servidor gRPC helloworld.Greeter .
Adaptado de:
    - https://github.com/grpc/grpc/blob/master/examples/python/helloworld/async_greeter_server.py
    - https://github.com/grpc/grpc/blob/master/examples/python/hellostreamingworld/async_greeter_server.py
    - https://groups.google.com/g/grpc-io/c/6Yi_oIQsh3w
"""

from concurrent import futures
import logging
import signal
from typing import Any
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter

import grpc
from helloworld_pb2 import HelloRequest, HelloReply
from helloworld_pb2_grpc import MultiGreeterServicer, add_MultiGreeterServicer_to_server

import iris

NUMBER_OF_REPLY = 10

parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument("-p", "--port", default="50051", help="Server port")
args = vars(parser.parse_args())

class Greeter(MultiGreeterServicer):

    def SayHello(self, request: HelloRequest, context) -> HelloReply:
        logging.info("Serving SayHello request %s", request)
        obj = iris.cls("dc.jrpereira.gRPC.HelloWorldServer")._New()
        # hook to your ObjectScript code
        return obj.SayHelloObjectScript(request)

    def SayHelloStream(self, request: HelloRequest, context: grpc.aio.ServicerContext) -> HelloReply:
        logging.info("Serving SayHelloStream request %s", request)
        obj = iris.cls("dc.jrpereira.gRPC.HelloWorldServer")._New()
        n = request.num_greetings
        if n == 0:
            n = NUMBER_OF_REPLY
        for i in range(n):
            # hook to your ObjectScript code
            yield obj.SayHelloObjectScript(request)

def get_server():
    port = args["port"]
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    add_MultiGreeterServicer_to_server(Greeter(), server)
    listen_addr = f"[::]:{port}"
    server.add_insecure_port(f"[::]:{port}")
    logging.info("Starting server on %s", listen_addr)
    return server

def handle_sigterm(*_: Any) -> None :
    """Shutdown gracefully."""
    done_event = server.stop(None)
    done_event.wait(None)
    print('Stop complete.')

logging.basicConfig(level=logging.INFO)

server = get_server()
server.start()

# https://groups.google.com/g/grpc-io/c/6Yi_oIQsh3w
signal.signal(signal.SIGTERM, handle_sigterm)

server.wait_for_termination()

Como puedes ver, aquí están implementados los métodos definidos en la especificación protobuf - SayHello() y SayHelloStream().

El método SayHello() envía un único valor, mientas que el método SayHelloStream() devuelve un número de mensajes al cliente igual a NUMBER_OF_REPLY, a través del operador de Python yield.

Además, date cuenta que he creado un hook para inyectar lógica definida en ObjectScript. Así, definí un método llamado SayHelloObjectScript en la clase dc.jrpereira.gRPC.HelloWorldServer:

Method SayHelloObjectScript(request)
{
    Set sys = $system.Python.Import("sys")
    Do sys.path.append("/usr/irissys/mgr/python/grpc-test/")

    Set helloworldpb2 = $system.Python.Import("helloworld_pb2")

    Set reply = helloworldpb2.HelloReply()
    Set reply.message = "Hi "_request.name_"! :)"

    Return reply
}

De este modo, puedes recibir peticiones desde clientes gRPC desde Python, y procesarlos utilizando una mezcla de lógica codificada en Pyhon y en ObjectScript.

Implementando un cliente para el servicio

Como el código de 1 cliente no necesita concurrencia, lo he implementado utilizando código Python directamente en una clase ObjectScript:

ClassMethod ExecutePython() [ Language = python ]
{
    import sys
    sys.path.append('/usr/irissys/mgr/python/grpc-test/')

    import grpc
    from helloworld_pb2 import HelloRequest
    from helloworld_pb2_grpc import MultiGreeterStub

    channel = grpc.insecure_channel('localhost:50051')
    stub = MultiGreeterStub(channel)

    response = stub.SayHello(HelloRequest(name='you'))
    print("Greeter client received: " + response.message)

    for response in stub.SayHelloStream(HelloRequest(name="you")):
        print("Greeter client received from stream: " + response.message)
}

Primero, añadimos el directorio grpc-test a las rutas de Python para poder importar el código protobuf generado.

Después, creamos una conexión a localhost en el puerto 50051 y un stub (cliente).

Con ese cliente, podemos solicitar información al servidor que está escuchando en localhost:50051, a través de los métodos SayHello() y SayHelloStream().

El método SayHello() devuelve un valor simple, así que simplemente tenemos que hacer una petición y utilizar su respuesta. Por otro lado, el método SayHelloStream() devuelve un stream de datos en una colección, así que tenemos que iterar sobre él para obtener todos sus datos.

Probando el código

Ok, ahora vamos a probar nuestro código.

Puedes probar todo este código en mi proyecto Hola Mundo. Sigue estos pasos para hacerlo funcionar:

git clone https://github.com/jrpereirajr/iris-grpc-example
cd iris-grpc-example
docker-compose up -d

Una vez hecho, abre un terminal de IRIS a través del terminal de sistema o con el Visual Studio Code:

docker exec -it iris-grpc-example_iris_1 bash
iris session iris

Arranca nuestro servidor gRPC:

Set server = ##class(dc.jrpereira.gRPC.HelloWorldServer).%New()
Do server.Start()

Ahora, vamos a crear un cliente gRPC para interactuar con este servidor:

Set client = ##class(dc.jrpereira.gRPC.HelloWorldClient).%New()
Do client.ExecutePython()

Si todo está OK, deberías ver un puñado de mensajes de saludo en el terminal.

Para terminar, paremos el servidor:

Do server.Stop()

Utilizando la utilidad grpcurl dentro de nuestro Hola Mundo

Como decía antes, la utilidad grpcurl es un equivalente a curl, pero aquí, en lugar de actuar como un cliente HTTP (como curl), utilizamos grpcurlcomo un cliente gRPC para probar servicios de un servidor gRPC. Asi que, vamos a usarla para jugar un poco más con nuestro Hola Mundo.

Lo primero, descarguemos e instalemos la utilidad grpcurl:

cd /tmp
wget https://github.com/fullstorydev/grpcurl/releases/download/v1.8.6/grpcurl_1.8.6_linux_x86_64.tar.gz
tar -zxvf grpcurl_1.8.6_linux_x86_64.tar.gz

Chequea si la instalación es correcta, ejecutando:

./grpcurl --help

Si todo está bien, deberías recibir una salida con todas las opciones de grpcurl.

Ahora, preguntemos qué servicios están disponibles en el servidor:

./grpcurl \
    -plaintext \
    -import-path /irisrun/repo/jrpereira/python/grpc-test \
    -proto helloworld.proto \
    localhost:50051 \
    list

Deberías recibir esta respuesta:

helloworld.MultiGreeter

Como puedes ver, la utilidad retornó nuestro servicio definido en el fichero proto (helloworld.MultiGreeter) como una respuesta para listar todos los servicios disponibles.

En el comando de arriba, puse cada parámetro en una línea separada. Vamos a explicar cada uno:

-plaintext: permite utilizar gRPC sin TLS (modo inseguro); estamos utilizándolo aquí porque no hemos implementado una conexión segura para nuestro servidor. Por supuesto, sólo deberíamos hacer esto en un entorno no-productivo
-import-path y -proto: nombre y ruta al fichero .proto (definición del servicio); necesaria si el servidor no implemeta gRPC Server Reflection

Tras estos parámetros, damos el nombre del servidor y el puerto, y un comando de grpcurl, en este caso - list .

Ahora, pidamos todos los métodos en el servicio helloworld.MultiGreeter:

./grpcurl \
    -plaintext \
    -import-path /irisrun/repo/jrpereira/python/grpc-test \
    -proto helloworld.proto \
    localhost:50051 \
    list helloworld.MultiGreeter

Deberías recibir esta salida:

helloworld.MultiGreeter.SayHello
helloworld.MultiGreeter.SayHelloStream

Como puedes ver, estos son métodos definidos en el fichero proto utilizado para generar código para nuestro servidor.

De acuerdo, ahora probemos el método SayHello() :

./grpcurl \
    -plaintext  \
    -d '{"name":"you"}' \
    -import-path /irisrun/repo/jrpereira/python/grpc-test \
    -proto helloworld.proto \
    localhost:50051 \
    helloworld.MultiGreeter.SayHello

Esta es la respuesta esperada (como la del cliente que implementamos antes):

{
  "message": "Hi you! :)"
}

Vamos a probar también el otro método, SayHelloStream():

./grpcurl \
    -plaintext -d '{"name":"you"}' \
    -import-path /irisrun/repo/jrpereira/python/grpc-test \
    -proto helloworld.proto localhost:50051 \
    helloworld.MultiGreeter.SayHelloStream

Y ahora deberíamos haber obtenido un stream con 10 mensajes de saludo:

{
  "message": "Hi you! :)"
}
{
  "message": "Hi you! :)"
}
...
{
  "message": "Hi you! :)"
}

Finalmente, vamos a hacer un ligero cambio en este comando para utilizar otra propiedad en el mensaje protobuf, la num_greetings. Este propiedad la utiliza el servidor para controlar cuántos mensajes serán enviados en el stream.

Este comando le pide al servidor que devuelva sólo 2 mensajes en el stream, en lugar de los 10 por defecto:

./grpcurl \
    -plaintext -d '{"name":"you", "num_greetings":2}' \
    -import-path /irisrun/repo/jrpereira/python/grpc-test \
    -proto helloworld.proto localhost:50051 \
    helloworld.MultiGreeter.SayHelloStream

Y esto debería ser lo que veas en el terminal:

{
  "message": "Hi you! :)"
}
{
  "message": "Hi you! :)"
}

Conclusión

En este artículo hemos visto una visión general de gRPC, con sus pros y sus contras - principalmente sobre REST. También probamos algunos ejemplos de su uso con IRIS, adaptando algunos ejemplos para Python, presentados en el repositorio oficial gRPC.

Como dije, gRPC tiene algunos casos de uso donde está siendo empleado, y los relacionados con la interoperabilidad son algunos de los posibles, así que crear una adaptador de interoperabilidad en IRIS es algo natural cuando pensamos en un uso práctico de gRPC dentro de IRIS.

No obstante, eso va a necesitar más esfuerzo, así que será el tema de otro artículo. =)

Espero que te haya sido útil la información que te he presentado. Nos vemos!!

Referencias

https://grpc.io/docs/what-is-grpc/introduction/
https://developers.google.com/protocol-buffers
https://en.wikipedia.org/wiki/GRPC
https://www.imaginarycloud.com/blog/grpc-vs-rest/
https://www.vineethweb.com/post/grpc/
https://www.capitalone.com/tech/software-engineering/grpc-framework-for-...
https://www.altexsoft.com/blog/what-is-grpc/

1
1 55
Debate (0)1
Inicie sesión o regístrese para continuar