gRPC como protocolo de comunicación entre servicios. Informe Yandex

gRPC es un marco de código abierto para la llamada a procedimiento remoto. En Yandex.Market, gRPC se utiliza como una alternativa más conveniente a REST. Sergey Fedoseenkov, que dirige el servicio de desarrollo de herramientas para los socios de Market, compartió su experiencia de usar gRPC como protocolo para crear integraciones entre los servicios de Java y C ++. Del informe aprenderá cómo evitar problemas comunes si comienza a usar gRPC después de REST, cómo devolver errores, implementar el seguimiento, depurar consultas y probar llamadas de clientes. Al final hay un registro no oficial del informe.

- Primero, me gustaría presentarle algunos datos sobre Yandex.Market, que serán útiles como parte del informe. Primer hecho: escribimos servicios en diferentes idiomas. Esto impone requisitos del cliente para los servicios.

Y si tenemos un servicio en Java, sería bueno que el cliente para él fuera, por ejemplo, también un plus o uno pequeño.



Todos los servicios que tenemos son independientes, no hay grandes lanzamientos planeados de todo el mercado. Los microservicios se lanzarán de forma independiente, y la compatibilidad con versiones anteriores es importante para nosotros aquí, para que el protocolo lo admita.

El tercer hecho: tenemos integración sincrónica y asincrónica. En el informe, hablaré principalmente sobre síncrono.

¿Qué usamos? Ahora, por supuesto, la base de nuestras integraciones es REST o servicios similares a REST que intercambian XML / JSON sobre HTTP 1.1. También hay XML-RPC: lo usamos principalmente cuando se integra con el código Python, es decir, Python tiene un servidor XML-RPC incorporado. Es lo suficientemente conveniente para implementarlo allí, y lo apoyamos.

Una vez tuvimos CORBA. Afortunadamente, lo abandonamos. Ahora principalmente REST y XML / JSON sobre HTTP.



Las integraciones sincrónicas tienen problemas con los protocolos existentes. Nos encontramos con tales problemas y tratamos de tratarlos con gRPC. ¿Cuáles son estos problemas? Como dije, quiero tener clientes en diferentes idiomas. Es aconsejable que todavía no tengan que ser escritos por nosotros mismos. Y, en general, sería genial si el cliente pudiera ser síncrono y asíncrono, dependiendo de los objetivos del usuario del servicio.

También me gustaría que el protocolo que utilizamos sea compatible con la compatibilidad con versiones anteriores lo suficientemente bien: esto es muy importante con las versiones independientes paralelas. Todos nuestros lanzamientos son compatibles con versiones anteriores, no rompemos los comentarios. Si lo rompió, esto es un error, y solo necesita solucionarlo lo antes posible.

También es necesario un enfoque coherente para el manejo de errores: todos los que hicieron servicios REST saben que no se puede usar el estado HTTP. Por lo general, no permiten una descripción detallada del problema, debe ingresar algunos de sus estados, sus detalles. En los servicios REST, todos introducen su propia implementación de estos errores, cada vez que tiene que trabajar de manera diferente con esto. Esto no siempre es conveniente.

También me gustaría tener una gestión del tiempo de espera en el lado del cliente. Una vez más, aquellos que trabajan con HTTP entienden que si establecemos un tiempo de espera en el lado del cliente y expira, el cliente dejará de esperar a que se complete la solicitud, pero el servidor no sabrá nada al respecto y continuará ejecutándolo. Además, en el medio hay varios servidores proxy que establecen tiempos de espera globales. Y el cliente puede simplemente no saber nada sobre ellos y configurarlos no siempre es trivial.

Y finalmente, el problema de la documentación. No siempre está claro dónde obtener la documentación para los recursos REST o para algunos métodos, qué parámetros aceptan, qué cuerpo se puede transferir y cómo comunicar esta documentación con los consumidores del servicio. Está claro que existe Swagger, pero con él tampoco todo es trivial.

gRPC Teoría


Me gustaría hablar sobre la parte teórica de gRPC: qué es, cuáles son las ideas. Y luego pasaremos a practicar.



En general, gRPC es una especificación abstracta. Describe una RPC abstracta (llamada a procedimiento remoto), es decir, una llamada a procedimiento remoto que tiene ciertas propiedades. Ahora los enumeraremos. La primera propiedad es la compatibilidad con llamadas individuales y transmisión. Es decir, todos los servicios que implementan esta especificación admiten ambas opciones. El siguiente elemento es la disponibilidad de metadatos, es decir, para que junto con la carga útil pueda pasar algún tipo de metadatos, condicionalmente, encabezados. Y: soporte para cancelar una solicitud y tiempos de espera listos para usar.

También supone que la descripción de los mensajes y los servicios en sí se realiza a través de un determinado lenguaje de definición de interfaz o IDL. La especificación también describe el protocolo de conexión a través de HTTP / 2, es decir, gRPC supone que funciona solo a través de HTTP / 2.



Hay una implementación típica de gRPC que se usa en la mayoría de los casos. También lo usamos, y ahora lo veremos. El formato proto se usa como IDL. El complemento gRPC para el compilador de prototipos le permite obtener las fuentes de los servicios generados a partir de la descripción del prototipo. Y hay bibliotecas de tiempo de ejecución en diferentes lenguajes: Java, C ++, Python. En general, casi todos los idiomas populares son compatibles, existen bibliotecas de tiempo de ejecución para ellos. Y a medida que se intercambian mensajes entre servicios, se utiliza un mensaje proto, mensajes estilizados de acuerdo con el esquema protobuf.



Quiero sumergirme un poco en algunas características específicas. Aquí están La escritura fuerte, es decir, un mensaje proto, es un mensaje fuertemente escrito. Aquellos que alguna vez trabajaron con protobuf saben que allí puede describir los campos en su mensaje con tipos. Los tipos existen tanto primitivos como de series de bytes. Pueden ser escalares, pueden ser vectores. Y, de hecho, los mensajes pueden, como campo, contener otros mensajes, lo cual es bastante conveniente, en general, cualquier modelo puede ser representado.



Sobre compatibilidad con versiones anteriores. Me gustaría señalar que proto IDL es un formato en el que la compatibilidad con versiones anteriores se presenta de fábrica, es decir, se concibió con una acumulación de compatibilidad con versiones anteriores, y Google lanzó una versión de proto3 que, en comparación con proto2, mejora aún más la compatibilidad con versiones anteriores. Además, hay todo tipo de especificaciones, cómo y qué se puede cambiar para preservar la compatibilidad con versiones anteriores en algunos casos no triviales.

Existe la posibilidad de valores predeterminados, puede agregar nuevos campos y el consumidor no necesita cambiar nada, de hecho. Todos los campos en proto3 son opcionales y, por ejemplo, se pueden eliminar, y acceder al campo remoto no causa errores en el cliente.



Otra característica de gRPC es que el cliente y el servidor se generan utilizando el compilador de proto y el complemento gRPC según la descripción de proto. Existe la posibilidad en el momento en que se está escribiendo el código para elegir qué cliente se utilizará. Es decir, elija un cliente asíncrono o síncrono, según el tipo de código que escriba. Por ejemplo, un cliente asíncrono es muy adecuado para el código reactivo. Y esta oportunidad es para cualquier idioma. Es decir, una vez que escribe una protodescripción, después de eso puede generar un cliente para cualquier idioma, y ​​no necesita desarrollarlos por separado de alguna manera. Puede distribuir la interfaz para su servicio simplemente como una protodescripción. Cualquier consumidor puede generar un cliente para sí mismo.



Sobre la cancelación de la solicitud y los plazos, me gustaría señalar que la solicitud se puede cancelar en el servidor y en el cliente. Si entendemos que todo, no necesitamos seguir cumpliendo con la solicitud, entonces podemos cancelarla. Es posible establecer un tiempo de espera a pedido. En gRPC, la mayoría de las bibliotecas de tiempo de ejecución usan una fecha límite como concepto de tiempo de espera. Pero de hecho es lo mismo. Es decir, este es el momento en que la solicitud debe completarse.

Y lo más interesante es que el servidor puede averiguar tanto sobre la cancelación de la solicitud como sobre el vencimiento del tiempo de espera y dejar de ejecutar la solicitud por su parte. Esto es genial, me parece que no hay mucho en ningún otro lado.

Sobre la documentación, quería señalar que, dado que el formato proto se usa en el IDL para gRPC, este es un código regular. Allí puede escribir comentarios, incluidos los muy detallados. Y debe comprender que para integrarse con su servicio, sus usuarios deben tener este protoformato en su hogar, y les llegará junto con comentarios, no se encontrarán en otro lugar. Es muy conveniente. Y puede ampliar esta descripción, es decir, es una característica tan conveniente que la documentación va al lado del código, al igual que puede estar al lado de los métodos en forma de javadoc o cualquier otro comentario.

Llamada unaria gRPC. Practica


Sigamos, veamos un poco de práctica. Y el ejemplo más básico del uso de gRPC es la llamada llamada unaria, o llamada única. Este es un esquema clásico: enviamos una solicitud al servidor y obtenemos una respuesta del servidor. Parece que esto funciona en HTTP.



Considere el ejemplo del servicio de eco que hacemos. El servidor se escribirá en pluses, el cliente en Java. El circuito de equilibrio clásico se usó aquí. Es decir, el cliente se dirige al equilibrador, y luego el equilibrador ya selecciona un backend específico para procesar la solicitud.

Quería prestar atención, ya que gRPC funciona sobre HTTP / 2, se utiliza una conexión TCP. Y además, varias corrientes lo atraviesan. Aquí puede ver que la conexión entre el cliente y el equilibrador se establece una vez y permanece persistente, y luego el equilibrador equilibra la carga en diferentes backends para cada llamada. Si miras, sucede así y así si los mensajes se distribuyen.



Aquí hay un código de muestra para nuestro archivo proto. Puede notar que primero describimos el mensaje, es decir, tenemos EchoRequest y EchoResponse. Tiene solo un campo de cadena que almacena el mensaje.

El segundo paso describimos nuestro procedimiento. El procedimiento de entrada acepta EchoRequest, devuelve EchoResponse como resultado, todo es bastante trivial. Esta es la descripción del servicio gRPC y los mensajes que serán perseguidos.




Veamos cómo va esto en el caso de las ventajas, por ejemplo. Se ensambla en tres etapas. En la primera etapa, nuestra tarea es generar fuentes de mensajes. Aquí estamos haciendo esto con este equipo. Llamamos al compilador de proto, pasamos el archivo de proto a la entrada, indicamos dónde colocar los archivos de salida.

El segundo equipo. También generamos servicios de la misma manera. La única diferencia con el comando anterior es que pasamos el complemento y, según la descripción, que está en formato proto, genera servicios.

El tercer paso: recopilamos todo esto en un binario para que nuestro servidor se pueda iniciar.

Se pasa una bandera adicional al enlazador, se llama grpc ++ _ reflexión. Quiero señalar: el servidor gRPC tiene esa característica, reflejo del servidor. Le permite explorar qué tipo de servicios, llamadas RPC y mensajes tiene el servicio. De forma predeterminada, está desactivado y puede acceder al servicio solo si tiene un prototipo a mano. Pero, por ejemplo, para la depuración, es muy conveniente, sin el proto-formato disponible, solo encienda el servidor con la función de reflexión y reciba información de inmediato.




Ahora veamos la implementación. La implementación también es minimalista. Es decir, nuestra tarea principal es implementar el servicio de eco generado. Tiene un método getEcho. Simplemente genera mensajes y los devuelve. Estado correcto: estado de éxito.

A continuación, creamos ServerBuilder, registramos nuestro servicio en él, que creamos un poco más.




Ahora solo comenzamos y esperamos las solicitudes entrantes.





Ahora veamos al cliente en Java. Recolectamos gradle. Nuestra tarea es conectar primero el plugin protobuf.

Hay un conjunto básico de dependencias que necesitamos arrastrar para nuestro servicio, que se necesitan en la etapa de compilación.

También quiero señalar que hay una biblioteca en tiempo de ejecución. Para Java, usa netty como servidor y cliente, es compatible con HTTP / 2, es bastante conveniente y de alto rendimiento.

A continuación configuramos el compilador proto. No es necesario que el compilador en sí mismo se instale localmente para Java; se puede tomar de artefactos.

Lo mismo con los complementos. Localmente para Java, no es necesario. Puedes arrastrar un artefacto. Y es importante configurarlo simplemente para que para todos los shuffle también se llame, de modo que se generen stubs.





Pasemos al código Java. Aquí somos los primeros en crear el trozo de nuestro servicio. Esa es nuestra tarea para Java para proporcionar Canal. Hay un ChannelBuilder en la biblioteca de tiempo de ejecución con el que podemos construir este canal. Aquí activamos manualmente el texto sin formato para simplificar, pero HTTP2 y gRPC cifran todo de forma predeterminada y usan TLS.

Tenemos un trozo de nuestro cliente, aquí se genera un cliente síncrono. Del mismo modo, puede generar un cliente asincrónico, hay otras opciones.

A continuación, creamos nuestra solicitud de protobuff, es decir, construimos un mensaje de protobuff.





Eso es todo, envíelo, en nuestro cliente llamamos a getEcho e imprimimos el resultado. Todo es simple Como puede ver, se necesita bastante código y se construye la integración.

Transmisión de gRPC. Practica


Ahora echemos un vistazo a algo más avanzado, esto es la transmisión. Te diré cómo funciona, y luego te diré cómo usarlo.



La transmisión cliente-servidor se ve arquitectónicamente más o menos igual. Es decir, tenemos una conexión persistente entre el cliente y el equilibrador. Entonces comienzan las diferencias. La esencia de la transmisión es que el cliente está conectado a algún backend final, y la conexión se guarda a través. Es decir, sigue así. Y asi. Aquí me gustaría señalar por separado que el uso de un equilibrador no es típico para la transmisión, es decir, debe comprender que las solicitudes de transmisión pueden durar bastante. Es decir, puede abrirlos e intercambiar mensajes durante mucho tiempo. Y estos mensajes pasarán por el equilibrador, pero, de hecho, siempre irán al mismo backend. Y no está muy claro por qué es necesario.

Una práctica común es cuando un servicio, por ejemplo, es puramente streaming, o principalmente streaming, luego se utiliza el descubrimiento de servicios. GRPC tiene un punto de extensión donde se puede incrustar el descubrimiento de servicios.



¿Qué necesitamos para implementar servicios de transmisión? Tenemos el mismo formato de proto. Estamos agregando otro RPC, y aquí puede notar que hemos agregado dos palabras clave antes de la solicitud y antes de la respuesta. Por lo tanto, declaramos las secuencias EchoRequest y EchoResponse.




Lo más interesante comienza. Nuestra compilación no cambia de ninguna manera para que los servicios de transmisión lo hagan. Nuestra siguiente tarea es anular nuestro nuevo método en nuestro servicio Echo, que funcionará con transmisiones. En el caso del servidor, todo esto es algo más fácil. Es decir, podemos leer constantemente de la transmisión y podemos responder algo. Podemos responder asincrónicamente. Es decir, son independientes, transmiten para escribir y transmiten para leer, y aquí todo es simple para un escenario simple.



Aquí está la lectura ahora, aquí está la grabación.




En clientes Java, las cosas son un poco más complicadas. Allí no puede usar ninguna API síncrona, es decir, simplemente no funciona con transmisiones. Y allí se usa la API asincrónica. Es decir, nuestra tarea es implementar la plantilla Observer. Hay una interfaz StreamObserver allí. Contiene tres métodos: onNext, onCompleted y onError. Aquí, por simplicidad, implementé solo en Next. Se contrae solo cuando la respuesta nos llega del servidor.




Aquí solo puse una cola para mensajes entre hilos.



Cual es la diferencia En lugar de blockStub, simplemente hacemos newStub. Esta es una implementación asincrónica que puede funcionar solo con Observer. De hecho, puede hacer llamadas unitarias en Observer, pero no es tan conveniente. Nosotros, al menos, no lo usamos tan activamente.

A continuación construimos nuestro Observador.

Y hacemos nuestra llamada RPC. Pasamos ResponseObserver a la entrada, y en la salida nos emite RequestObserver. Además, podemos hacer llamadas en RequestObserver, transmitiendo así mensajes al servidor. Y nuestro ResponseObserver se contraerá y procesará los mensajes.

Aquí hay un ejemplo. Solo estamos haciendo una llamada. Llame al siguiente, pase Solicitud allí.

Más allá de la cola, esperamos que el servidor responda e imprima.





Quiero llamar la atención sobre el hecho de que nuestra tarea aquí, como las personas responsables de la implementación de la transmisión, es manejar correctamente el cierre de este RequestObserver. Es decir, en caso de error, debemos llamar al método onError y, en caso de finalización exitosa, cuando creemos que la secuencia puede cerrarse, debemos llamar al método onCompleted.



Seguimos adelante. ¿Cuáles son las aplicaciones de transmisión? Esto es algo más avanzado, no el hecho de que es directamente útil para todos, sino que a veces se usa. Es decir, el primero es descargar y cargar grandes cantidades de datos. El servidor o el cliente pueden producir datos en algunas partes. Estas porciones ya pueden estar agrupadas de alguna manera en el cliente o en el servidor. Es decir, ya puede hacer optimizaciones adicionales aquí.

Además, el esquema de transmisión es muy adecuado para el empuje del servidor. Debe comprender que consideré la opción más extrema cuando tenemos transmisión bidireccional. Y tal vez transmitiendo en una dirección. Por ejemplo, de cliente a servidor o de servidor a cliente. En el caso de un servidor a un cliente, podemos conectarnos a algún servidor, y nos enviará insultos, y para esto no necesitaremos sondear regularmente.

La siguiente ventaja de la transmisión es vinculante para una máquina. Como ya dije, se establecerá una conexión de extremo a extremo para todos los mensajes dentro de la transmisión, y esta conexión estará vinculada a una máquina, y definitivamente no cambiará a ninguna parte. Por lo tanto, es posible, en primer lugar, simplificar algo, algún tipo de sincronización entre servidores y, además, puede hacer cosas transaccionales.

Y la transmisión bidireccional, solo un ejemplo que mostré, es la capacidad de construir algunos de mis propios protocolos. Lo suficientemente interesante. Tenemos colas internas en Yandex que solo usan transmisión bidireccional. Y si de repente alguien tiene tales tareas, entonces es una buena oportunidad para usarlo.

, . . . , - , , . . gRPC .


, gRPC.



, . - . gRPC . , , , , , . , runtime- . , . , OK, runtime- .

, Java . . google.rpc.Status 3 : , . , . , . — , , .

error details, , . : , , , stack traces, . , .

— , HTTP , ? . BadRequest . , , error details, .

. , , BadRequest - ( ), - error detail. , , , - . , .



. . , , , . - - , - - , , . . , , Zipkin. , HTTP , — metadata. .

, . , - , , , .

runtime-, - , . Java ClientInterceptor ServerInterceptor. , , . , , , , , - . , - API - . , , , , - . , gRPC, , - . , , - , , , .



- . -. Java . , , - . - , .



. gRPC — . HTTP/2 . - , ? : , . . , gRPC grpc_cli, curl. , . , -, . , gRPC , .

, evans. , CLI: , , , . , . - , , , , , .

- UI — , Postman, — BloomRPC. Postman . Postman, , , . , BloomRPC , .

- , . , , grpc_cli. . , . , , . , . , - - . — .



, , gRPC. . - , - , . Swagger. , HTTP/1 . OpenAPI , . . , HTTP/2, Swagger — .

WSDL — , . . Swagger, , . . -.

, , , , JAX-RS, Java . .

Twirp. ? Go, . . , , Go , gRPC Twirp. ? , gRPC — , , , IDL . proto- , gRPC-. protoc, , .

Twirp . proto- , HTTP/1.1 , JSON. , Twirp Go. , , Java Jetty. , .



? gRPC — REST . , , , , HTTP/2 balancer. service discovery, . gRPC , . .

gRPC — , . CLI, UI. , .

, gRPC. inter-process-. , sidecar pattern. , . , . , -. - , , -. . , , , , - .

, . gRPC . , . , unary-. , .

:
C gRPC — , . , , , .
Awesome gRPC — GitHub . , , , . . — , .

Puede encontrar muchos otros recursos en Internet, en algunas diapositivas. Pero estos me gustaron más. El código ligeramente modificado de la presentación está aquí . Gracias

Grabación informal de informes

Source: https://habr.com/ru/post/484068/


All Articles