Header Ads Widget

Ticker

6/recent/ticker-posts

Uso de gRPC para conectar un ecosistema de microservicios


 gRPC es un sistema poderoso que permite una amplia gama de formas, funciones y capacidades. Si bien anteriormente hemos explorado gRPC desde una vista de arriba hacia abajo , el interés reciente en gRPC ha hecho que la búsqueda de ejemplos contextuales sea bastante importante. Esta pieza discutirá algunos casos de uso de gRPC de la documentación oficial . Veremos algunos casos de uso específicos e implementaciones teóricas, y discutiremos un contexto de microservicios más amplio que muestra el poder real de gRPC.

gRPC: una breve reseña

Si bien asumiremos que cualquier lector de este artículo tiene una familiaridad pasajera con gRPC, vale la pena repetirlo para asegurar su comprensión. gRPC se basa en el concepto de una llamada a procedimiento remoto (RPC), o la acción de llamar a un método en una aplicación de servidor como si fuera un objeto local para el solicitante. En esencia, está solicitando un recurso no local como si fuera un recurso local. Para hacer esto, se necesitan algunas cosas para unificar la llamada y el recurso.

En primer lugar, se necesita algún tipo de lenguaje de definición de interfaz (IDL), así como un formato acordado para el intercambio de mensajes. gRPC utiliza Protocol Buffers para este fin. Como la "g" en gRPC significa "Google", que denota su origen, tiene sentido que utilice un sistema estandarizado de Google en forma de Protocol Buffers. En esencia, estos búferes son métodos para serializar datos estructurados de una manera que puede transformar los datos de un idioma a otro (y de un servicio a otro). Para citar la documentación de Google:

“Los búferes de protocolo son el mecanismo extensible, neutral en cuanto al lenguaje y la plataforma, de Google para serializar datos estructurados; piense en XML, pero más pequeños, más rápidos y más simples. Usted define cómo desea que se estructuran sus datos una vez, luego puede usar un código fuente generado especial para escribir y leer fácilmente sus datos estructurados hacia y desde una variedad de flujos de datos y usando una variedad de lenguajes ".

Aunque gRPC funciona con Protocol Buffers de forma predeterminada, puede hacer que funcione con otros formatos de datos, como JSON. El amplio soporte de gRPC de formatos de datos y lenguajes significa que puede ser interoperable, con Go, Python, Ruby y otras opciones.

Búferes de protocolo ampliados

En este artículo, asumiremos que una instancia de gRPC usa los búferes de protocolo predeterminados. Los búferes de protocolo se basan en la idea de estructura definida. Comprender cómo se interpretarán y compartirán los datos permite a los desarrolladores hacer que estos datos funcionen de manera intercambiable independientemente de la ubicación. Para facilitar esto, Protocol Buffers utiliza un "archivo proto". Estos .protoarchivos crean una pieza de datos estructurada de mensajes , donde los campos establecen un par nombre-valor. Por ejemplo, el siguiente búfer de protocolo crea un Articledato que contiene un conjunto de pares nombre-valor.

message Article {
 string name = 1;
 Int32 id = 2;
}

Luego, los desarrolladores pueden usar estos .protodatos para generar las clases de acceso a datos a través del compilador de búfer de protocolo incluido (conocido como protoc). Esto creará accesos para cada campo con nombre, lo que le permitirá acceder, serializar y analizar el contenido y la estructura de datos. Yendo un paso más allá, los servicios gRPC completos se pueden especificar en un .protoarchivo que define los parámetros del método RPC y los tipos de retorno en el formato de mensajes . Como el código se genera utilizando un compilador incluido, el cliente de gRPC, el código del servidor y la salida del código del búfer del protocolo pueden esencialmente crear un servicio completo de remoto a local siempre que los métodos estén correctamente asignados y nombrados.

Una cartilla en Go

Como gRPC es un proyecto afiliado a Google, parece apropiado profundizar en esto desde la perspectiva de Go . Go, inicialmente conocido como "golang", fue un proyecto apasionante de varios empleados de Google que rápidamente se transformó en una opción extensible y fácil de usar para el desarrollo de API. A lo largo de este artículo, nos referiremos a la documentación oficial de gRPC Go .

Definir el servicio

Antes de que podamos hacer algo, primero debemos definir el servicio. gRPC opera sobre servicios definidos en .protoarchivos, por lo que nuestro primer paso será crear dicho archivo. Crear nuestra primera definición es tan simple como nombrar nuestro servicio:

service RouteGuide {
}

Ahora que hemos nombrado nuestro servicio, podemos comenzar a configurar los métodos RPC específicos que queremos proporcionar. gRPC tiene cuatro tipos de respuesta básicos, cada uno apropiado para casos de uso particulares:

  • RPC simple : como su nombre lo indica, esta es la forma RPC más simple. El RPC simple es una llamada de función básica, donde un cliente realiza una solicitud, espera a que esa solicitud genere una respuesta y luego ingiere la respuesta. Este tipo de tipo de respuesta es mejor para solicitudes singulares que dan como resultado salidas simples. Piense en esto como solicitar un solo libro de la biblioteca y recibir ese libro.
  • RPC de flujo continuo del lado del servidor : este formulario RPC es cuando el cliente envía una solicitud al servidor y recibe un flujo de mensajes secuenciados. Estos mensajes luego se leen hasta el final del flujo de mensajes. Este tipo de RPC se crea cuando se antepone "flujo" al tipo de respuesta. Esta es una capa de interacción más compleja. Para llevar adelante nuestra metáfora del libro de la biblioteca, esto es como si usted solicitara toda la colección de Edgar Allen Poe y el bibliotecario regresara con una caja de todos los libros dentro de ese cuerpo de trabajo.
  • RPC de transmisión por secuencias del lado del cliente : este formulario de RPC es una inversa de la RPC de transmisión por secuencias del lado del servidor. El cliente enviará una serie de mensajes secuenciados, que luego el servidor leerá en secuencia. Luego, la respuesta de salida se devuelve al cliente. Esta es la inversa de nuestra analogía anterior: enumera una serie de libros que le gustan y el bibliotecario responde con la colección a la que pertenecen.
  • RPC de transmisión bidireccional : se trata de una combinación de los dos tipos anteriores de RPC en el sentido de que tanto el cliente como el servidor envían una secuencia de mensajes mediante un sistema de lectura y escritura. Tanto el cliente como el servidor pueden leer en el orden que deseen, ya que ambos flujos de mensajes son independientes entre sí. Este es obviamente el tipo más complejo y, como tal, evitaremos la metáfora del bibliotecario. Basta decir; esto estaría más cerca de una conversación compleja que de una simple solicitud de colección.

Con estos cuatro tipos definidos, creemos el .protoarchivo hasta ahora:

Service RouteGuide {
// Simple RPC – defines the feature at a given point
rpc GetFeature(Point) returns (Feature) {}

// Server-Side Streaming RPC – Shows features within a region defined by a rectangle
rpc ListFeatures(Rectangle) returns (stream Feature) {}

// Client-Side Streaming RPC – Accepts a stream of Points on a route – once the route has finished, a "RouteSummary" response is returned
rpc RecordRoute(stream Point) returns (RouteSummary) {} 

// Directional Streaming RPC – Accepts a stream of content known as RouteNotes while traversing a route
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

}

Adicional a esta funcionalidad es la necesidad de definir un tipo de solicitud y respuesta para los métodos de servicio. Según la documentación, el servicio que se está creando es de naturaleza geográfica. En consecuencia, los Puntos dentro de message Pointse definen como dos números del int32tipo:

message Point {
int32 latitude = 1;
int32 longitude = 2;
}

Con todo esto definido, finalmente podemos emitir un comando para crear el código de cliente y servidor. El protoccompilador mencionado anteriormente utilizará el complemento gRPC Go para generar tanto el código de búfer de protocolo como las interfaces para el cliente (denominado en la documentación como el "stub") y el servidor.

Creando el servidor

Ahora que hemos definido nuestro servicio, necesitamos implementar la interfaz. Si bien la documentación es bastante detallada en cuanto a la naturaleza de cada servicio, aquí solo la resumiremos brevemente. Recomendamos leer más a fondo el tutorial, ya que hay algunos puntos interesantes y advertencias que están fuera del alcance de esta pieza.

Hay dos aspectos centrales de este proceso que la documentación presenta brevemente, pero vale la pena hablar un poco más sobre ambos.

El primero de estos dos aspectos es la interfaz de servicio real. Esta es la parte funcional del servicio del lado del servidor y forma las metodologías de trabajo para cada solicitud. Si bien establecimos y definimos el servicio anteriormente, necesitamos saber realmente cómo se gobiernan estas interacciones y qué equivalen a en los sistemas remotos; esta es la función central de la interfaz, ya que sirve para unir el significado y llevar a cabo la solicitud.

El segundo de estos aspectos es el servidor gRPC real. Este servidor es esencialmente un servicio de escucha que espera las solicitudes del cliente para reenviarlas a los sistemas apropiados. Es mejor considerarlo como el facilitador de la interfaz de servicio "intermedia", ya que es el sistema que realmente envía las solicitudes a lo largo de la ruta.

Dentro del proyecto teórico en la documentación, RouteGuidetiene un servidor (llamado, curiosamente, routeGuideServer) que implementa las interfaces generadas como tales:

type routeGuideServer struct {
...
}
...

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
...
}
...

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
...
}
...

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
...
}
...

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
...
}
...

Aquí, vemos los cuatro tipos de respuesta representados. Analicemos una de las funciones de RPC incluidas y profundicemos en la funcionalidad y el propósito.

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
key := serialize(in.Location)
... // look for notes to be sent to client
for _, note := range s.routeNotes[key] {
if err := stream.Send(note); err != nil {
return err
}
}
}
}

Este es nuestro RPC de transmisión bidireccional y, como puede ver, tiene algunos métodos interesantes. En primer lugar, tenga en cuenta el manejo de errores en el código representado por err:

   if err == io.EOF {

En este caso, io.EOFobserva que se trata de un final de flujo y pasa al siguiente paso, que es el cierre del ciclo de lectura. En otra parte, podemos ver otro paradigma de error para los errores donde una nota no está disponible.

Antes de continuar, deberíamos discutir el Sendmétodo y algunas otras variaciones. Send, en este caso, es una función simple que se explica por sí misma y funciona como un método para enviar contenido simplemente.

Un método relacionado, aunque no está presente en este ejemplo, es CloseSendCloseSendes bastante interesante porque funciona de manera casi idéntica al Sendmétodo, pero también denota el final de la interacción. Una variación adicional es CloseAndRecvque no envía contenido directamente, sino que sigue un fragmento de contenido enviado y luego denota que el autor espera una respuesta.

La elección del método para enviar contenido dependerá en gran medida del tipo de contenido que se envíe y las expectativas de la respuesta. Como el ejemplo en el que nos estamos sumergiendo es un RPC de transmisión bidireccional, podemos ver el uso de en Sendlugar de SendAndClose, ya que no esperamos una cantidad determinada de mensajes o interacciones. Si esperáramos una respuesta establecida en una RPC de transmisión del lado del cliente, podríamos utilizar SendAndClosepara expresar la expectativa de dicha respuesta.

Este código de ejemplo final realmente inicia el servicio. Lo hace indicando el puerto y el protocolo que se está utilizando (en este caso, a través del net.Listenmétodo notando tcp), creando el servidor gRPC usando el NewServermétodo ( grpc.NewServer) y finalmente llamando al grpcServer.Serve(lis)método para notar que el servidor está escuchando y continuará haga una "espera de bloqueo" hasta que lo apaguemos.

Ahora que tenemos un servidor, veamos cómo creamos un cliente.

Integraciones del lado del cliente

En el paradigma de desarrollo de gRPC, un recurso que no es del servidor interactúa con un servidor remoto a través del stub. A veces se hace referencia al stub como un "cliente", pero en todos los casos, son simplemente métodos diseñados para llamar a recursos como si fueran locales. Nuestro primer paso para crear este stub es crear el canal gRPC que utilizará para comunicarse con el servidor remoto. Esto se hace a través de la grpc.Dialfunción de la siguiente manera (según la documentación):

var opts []grpc.DialOption
...
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
...
}
defer conn.Close()

A continuación, debemos crear el código auxiliar real. En este momento, queremos desviarnos de la documentación para analizar cómo se puede aprovechar el cliente en sí dentro de una arquitectura de microservicios. Hasta ahora, hemos discutido algunos métodos bastante estándar como se expresan en la documentación, pero no hemos discutido las maquinaciones internas de los microservicios en lo que respecta a las RPC. Tomemos nuestro servidor como lo hemos diseñado actualmente y extrapolemos algunas interacciones interesantes que podemos admitir.

Una posible implementación puede ser que nuestro sistema gRPC funcione como una especie de capa de compensación entre varias API internas y una API externa impulsada por recursos. En nuestro ejemplo, hemos estado creando una API que ofrece cierto contenido basado en datos de ubicación geográfica derivados de PointsEn realidad, los datos servidos podrían ser de casi cualquier tipo, siempre que el código auxiliar lo defina.

Digamos que queríamos expandir la API que discutimos anteriormente, pero esta vez, queríamos que nuestras notas devolvieran datos de advertencia. En teoría, estos datos podrían servirse fácilmente aprovechando la Printffunción para agregar datos de advertencia adicionales basados ​​en la proximidad geográfica a la Pointsdefinida. Si esta conexión se realiza mediante una conexión RPC de transmisión bidireccional, entonces la mensajería podría estar en curso, advirtiendo a los excursionistas de los próximos obstáculos o características interesantes.

¿Qué pasa con los usuarios que no están viajando activamente por la ruta prevista? Aquí es donde nuestro enfoque de microservicio podría resultar especialmente útil. Si desarrollamos un microservicio secundario que almacena las rutas planificadas localmente, podríamos almacenar estos puntos como discretos plannedRoutey simular el recorrido de la ruta a través del punto inicial en adelante. La documentación proporciona una RPC de transmisión del lado del servidor que se ajusta a esta función. Lo hemos adjuntado a continuación:

rect := &pb.Rectangle{ ... } // initialize a pb.Rectangle
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
...
}
for {
feature, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
}
log.Println(feature)
}

Es una nota interesante aquí que, si bien una RPC de transmisión del lado del cliente puede parecer más sensata, una RPC de transmisión del lado del servidor nos brinda un mejor servicio. Esto se debe a que estamos recopilando un flujo de características geográficas relacionadas con un único punto posible. Si bien podríamos usar el modelo RPC bidireccional para facilitar esto, usar un RPC de transmisión del lado del servidor nos permitirá simular puntos a lo largo de la ruta, así como cambiar esa ruta y ver los posibles cambios obtenidos de un cambio singular en el rumbo.

Gran parte de la experiencia del usuario basada en la salida, en este caso, en realidad sería modelada por la arquitectura de microservicios en lugar de gRPC en sí. Por lo tanto, encontramos el verdadero poder y la belleza de gRPC: puede controlar una cantidad increíble de funcionalidad de backend sin necesariamente requerir que los recursos sean locales. Esto puede reducir el tamaño de la carga útil, aumentar la eficiencia y, por lo tanto, reducir el tamaño de los datos cuando se usa correctamente. En nuestro caso de uso teórico, los usuarios como los excursionistas preferirían una carga útil liviana en un dispositivo impermeable de baja demanda que puede proporcionar interacciones de calidad relativamente alta en una red de baja calidad.

Lea también: Exploración del marco de gRPC para crear microservicios

Conclusión

Si bien cubrimos gran parte de la documentación inicial de gRPC en este artículo, existe un vasto mundo de posibles implementaciones y posibilidades. Sugerimos profundizar primero en tutoriales específicos del idioma y luego en tutoriales agnósticos más amplios, ya que la flexibilidad y el poder de gRPC se comprenden mejor en el contexto de múltiples paradigmas de implementación.

¿Qué opinas de gRPC? ¿Es mejor insistir en recursos locales o remotos? Háganos saber a continuación.

Publicar un comentario

0 Comentarios