Courier: migraci贸n de Dropbox a gRPC



Nota del traductor


La mayor铆a de los productos de software modernos no son monol铆ticos, sino que consisten en muchas partes que interact煤an entre s铆. En esta situaci贸n, es necesario que la comunicaci贸n de las partes interactivas del sistema tenga lugar en un idioma (a pesar de que estas partes pueden escribirse en diferentes lenguajes de programaci贸n y ejecutarse en diferentes m谩quinas). Para simplificar la soluci贸n a este problema, ayuda a gRPC - open-source-framework de Google, lanzado en 2015. Inmediatamente resuelve una serie de problemas, permitiendo:

  • use el lenguaje de Protocol Buffers para describir la interacci贸n de los servicios;
  • generar c贸digo de programa basado en el protocolo descrito para 11 idiomas diferentes para la parte del cliente y la parte del servidor;
  • implementar la autorizaci贸n entre componentes que interact煤an;
  • use tanto la interacci贸n sincr贸nica como la asincr贸nica.

gRPC me pareci贸 un marco bastante interesante, y estaba interesado en conocer la experiencia real de Dropbox en la construcci贸n de un sistema basado en 茅l. El art铆culo tiene muchos detalles relacionados con el uso del cifrado, la construcci贸n de un sistema confiable, observable y productivo, el proceso de migraci贸n de la antigua soluci贸n RPC a la nueva.

Descargo de responsabilidad
El art铆culo original no contiene una descripci贸n de gRPC, y algunos puntos pueden no parecerle claros. Si no est谩 familiarizado con gRPC u otros marcos similares (por ejemplo, Apache Thrift), le recomiendo que primero se familiarice con las ideas principales (ser谩 suficiente leer dos peque帽os art铆culos del sitio web oficial: 鈥溌縌u茅 es gRPC?鈥 Y 鈥淐onceptos de gRPC鈥 ).

Gracias a Aleksey Ivanov, alias SaveTheRbtz, por escribir el art铆culo original y ayudar a traducir lugares dif铆ciles.

Dropbox gestiona muchos servicios escritos en diferentes idiomas y atiende millones de solicitudes por segundo. En el centro de nuestra arquitectura orientada a servicios se encuentra Courier, un marco RPC basado en gPC. En el proceso de su desarrollo, aprendimos mucho sobre la extensibilidad de gRPC, la optimizaci贸n del rendimiento y la transici贸n desde el sistema RPC anterior.

Nota: la publicaci贸n contiene fragmentos de c贸digo para Python y Go. Tambi茅n usamos Rust y Java.

Camino a gRPC


Courier no es el primer marco de Dropbox RPC. Incluso antes de comenzar a dividir el sistema monol铆tico de Python en servicios separados, necesit谩bamos una base confiable para el intercambio de datos entre servicios, especialmente porque elegir un marco tendr铆a consecuencias a largo plazo.

Antes de eso, Dropbox experiment贸 con diferentes marcos RPC. Primero, ten铆amos un protocolo individual para la serializaci贸n y deserializaci贸n manual. Algunos servicios, como el registro basado en Scribe , utilizan Apache Thrift . Al mismo tiempo, nuestro marco principal de RPC era un protocolo HTTP / 1.1 con mensajes serializados usando Protobuf.

Al crear un marco, elegimos entre varias opciones. Podr铆amos introducir Swagger (ahora conocido como OpenAPI ) en el antiguo marco RPC, introducir un nuevo est谩ndar o crear un marco basado en Thrift o gRPC. El argumento principal a favor de gRPC fue la posibilidad de utilizar prototipos existentes. Adem谩s, la transferencia de datos multiplex HTTP / 2 y bidireccional fueron 煤tiles para nuestras tareas.

Nota: si fbthrift existiera en ese momento, probablemente examinar铆amos m谩s de cerca las soluciones de Thrift.

Lo que Courier trae a gRPC


Courier no es un protocolo RPC; Es un medio para integrar gRPC en una infraestructura existente. Se supon铆a que el marco era compatible con nuestras herramientas de autenticaci贸n, autorizaci贸n y descubrimiento de servicios, as铆 como con la recopilaci贸n, registro y seguimiento de estad铆sticas. Entonces creamos Courier.

Aunque en algunos casos usamos Bandaid como un proxy gRPC, la mayor铆a de nuestros servicios se comunican directamente entre s铆 para minimizar el impacto de RPC en la latencia.

Fue importante para nosotros reducir la cantidad de c贸digo de rutina que debe escribirse. Dado que Courier sirve como un marco general para el desarrollo de servicios, contiene caracter铆sticas que todos necesitan. La mayor铆a de ellos est谩n habilitados de manera predeterminada y pueden controlarse mediante argumentos de l铆nea de comando, y algunos est谩n marcados con una casilla de verificaci贸n.

Seguridad: identidad de servicio y autenticaci贸n mutua TLS


Courier implementa nuestro mecanismo de identificaci贸n de servicio est谩ndar. A cada servidor y cliente se le asigna un certificado TLS individual emitido por nuestra propia autoridad de certificaci贸n. El identificador personal codificado en el certificado, que se utiliza para la autenticaci贸n mutua: el servidor verifica al cliente, el cliente verifica al servidor.

En TLS, donde controlamos ambos lados de la conexi贸n, hemos introducido restricciones estrictas. Todos los RPC internos requieren cifrado PFS . La versi贸n requerida de TLS es 1.2 y superior. Tambi茅n limitamos el n煤mero de algoritmos sim茅tricos y asim茅tricos, prefiriendo ECDHE-ECDSA-AES128-GCM-SHA256 .

Despu茅s de pasar por la identificaci贸n y descifrado de la solicitud, el servidor verifica si el cliente tiene los permisos necesarios. Las listas de control de acceso (ACL) y los l铆mites de velocidad se pueden configurar tanto para servicios en general como para m茅todos individuales. Sus par谩metros tambi茅n se pueden cambiar a trav茅s de nuestro sistema de archivos distribuido (AFS). Gracias a esto, los propietarios de servicios pueden soltar la carga en segundos, sin siquiera reiniciar los procesos. Courier se encargar谩 de suscribirse a las notificaciones y actualizar la configuraci贸n.

El servicio de identidad es un identificador global para ACL, l铆mites de velocidad, estad铆sticas, etc. Adem谩s, es criptogr谩ficamente seguro.

Aqu铆 hay un ejemplo de configuraci贸n de ACL y l铆mite de velocidad utilizado en nuestro servicio de reconocimiento de patrones 贸pticos :

limits:  dropbox_engine_ocr:    # All RPC methods.    default:      max_concurrency: 32      queue_timeout_ms: 1000      rate_acls:        # OCR clients are unlimited.        ocr: -1        # Nobody else gets to talk to us.        authenticated: 0        unauthenticated: 0 



Estamos considerando la posibilidad de cambiar al formato SVID (documento SPIFFE verificado criptogr谩ficamente), lo que ayudar谩 a combinar nuestro marco con muchos proyectos de c贸digo abierto.

Observabilidad: estad铆sticas y seguimiento


Con solo un identificador, puede encontrar f谩cilmente registros, estad铆sticas, archivos de rastreo y otros datos sobre Courier.



Durante la generaci贸n de c贸digo, se agrega la recopilaci贸n de estad铆sticas para cada servicio y cada m茅todo tanto en el lado del cliente como en el lado del servidor. Las estad铆sticas del lado del servidor se dividen por ID de cliente. En la configuraci贸n est谩ndar, recibir谩 datos detallados sobre la carga, los errores y el tiempo de retraso para cada servicio que utiliza Courier.



Las estad铆sticas de mensajer铆a incluyen datos sobre disponibilidad y latencia en el lado del cliente, as铆 como en el n煤mero de solicitudes y el tama帽o de la cola en el lado del servidor. Hay otros gr谩ficos 煤tiles, en particular histogramas de tiempo de respuesta para cada m茅todo y el tiempo de apretones de manos TLS para cada cliente.

Una de las ventajas de nuestra generaci贸n de c贸digo es la posibilidad de inicializaci贸n est谩tica de estructuras de datos, como histogramas y gr谩ficos de trazas. Esto minimiza el impacto en el rendimiento.



El antiguo sistema RPC solo distribu铆a request_id a trav茅s de la API. Esto permiti贸 combinar datos de los registros de diferentes servicios. En Courier, presentamos una API basada en un subconjunto de las especificaciones OpenTracing . Escribimos nuestras propias bibliotecas en el lado del cliente, y en el lado del servidor implementamos una soluci贸n basada en Cassandra y Jaeger .



El rastreo nos permite generar diagramas de dependencia de un servicio en tiempo de ejecuci贸n. Esto ayuda a los ingenieros a ver todas las dependencias transitivas de un servicio en particular. Adem谩s, la funci贸n es 煤til para rastrear dependencias no deseadas despu茅s de la implementaci贸n.

Fiabilidad: plazos y desconexi贸n


Courier proporciona un lugar central para implementar funciones comunes del cliente (por ejemplo, tiempos de espera) en diferentes idiomas. Gradualmente agregamos varias caracter铆sticas, a menudo basadas en los resultados de un an谩lisis "p贸stumo" de problemas emergentes.

Plazos


Cada solicitud de gRPC tiene una fecha l铆mite que indica el tiempo de espera del cliente. Dado que los ap茅ndices de Courier distribuyen autom谩ticamente los metadatos conocidos, la fecha l铆mite de solicitud incluso se transfiere fuera de la API. Dentro del proceso, los plazos reciben una visualizaci贸n nativa. Por ejemplo, en Go, est谩n representados por el resultado de context.Context del m茅todo WithDeadline .

De hecho, pudimos solucionar clases enteras de problemas de confiabilidad al obligar a los ingenieros a establecer plazos para definir los servicios apropiados.

Este enfoque va m谩s all谩 de RPC. Por ejemplo, nuestro ORM MySQL serializa un contexto RPC junto con una fecha l铆mite en un comentario de consulta SQL. Nuestro proxy SQL puede analizar comentarios y "matar" consultas cuando se produce la fecha l铆mite. Y como beneficio adicional al depurar llamadas a la base de datos, tenemos un enlace de consulta SQL a una consulta RPC espec铆fica.

Desconectar


Otro problema com煤n que enfrentaron los clientes del sistema RPC anterior fue la implementaci贸n del algoritmo de retardo exponencial individual y fluctuaciones a pedido repetido.

Intentamos encontrar una soluci贸n inteligente al problema de desconexi贸n en Courier, comenzando con la implementaci贸n del b煤fer LIFO (煤ltimo en entrar, primero en salir) entre el servicio y el grupo de tareas.



En caso de sobrecarga, LIFO se desconectar谩 autom谩ticamente. La cola, que es importante, est谩 limitada no solo por el tama帽o, sino tambi茅n por el tiempo (la solicitud puede pasar en la cola solo un cierto tiempo).

Menos LIFO: cambio del orden de procesamiento de solicitudes. Si desea conservar el pedido original, use CoDel . Tambi茅n existe la posibilidad de desconectarse, y el orden de procesamiento de las solicitudes seguir谩 siendo el mismo.



Introspecci贸n: puntos finales de depuraci贸n


Aunque los puntos finales de depuraci贸n no son directamente parte de Courier, se usan ampliamente en Dropbox y son demasiado 煤tiles para no mencionarlos.

Por razones de seguridad, puede abrirlos en un puerto separado o en un socket Unix (para controlar el acceso mediante permisos de archivo). Tambi茅n debe considerar la autenticaci贸n mutua de TLS, con la cual los desarrolladores tendr谩n que proporcionar sus certificados para acceder a los puntos finales (principalmente no solo de lectura).

Ejecuci贸n


La capacidad de analizar el estado de un servicio durante su funcionamiento es muy 煤til para la depuraci贸n. Por ejemplo, se puede acceder a los perfiles de memoria din谩mica y CPU a trav茅s de puntos finales HTTP o gRPC .

Planeamos aprovechar esta oportunidad en el procedimiento de verificaci贸n canaria, para automatizar la b煤squeda de la diferencia entre las versiones antigua y nueva del c贸digo.

Los puntos finales permiten modificar el estado de un servicio en tiempo de ejecuci贸n. En particular, los servicios basados 鈥嬧媏n Golang pueden configurar din谩micamente GCPercent .

La biblioteca


La exportaci贸n autom谩tica de datos espec铆ficos de la biblioteca como punto final RPC puede ser 煤til para los desarrolladores de la biblioteca. Por ejemplo, la biblioteca malloc puede volcar estad铆sticas internas en un volcado . Otro ejemplo: un punto final de depuraci贸n puede cambiar el nivel de registro de servicio sobre la marcha.

Rpc


Por supuesto, la soluci贸n de problemas en protocolos cifrados y codificados no es f谩cil. Por lo tanto, introducir tantas herramientas como sea posible a nivel RPC es una buena idea. Un ejemplo de una API tan introspectiva es la soluci贸n Channelz .

Nivel de aplicaci贸n


Ser capaz de aprender las opciones de nivel de aplicaci贸n tambi茅n puede ser 煤til. Un buen ejemplo es un punto final con informaci贸n general sobre la aplicaci贸n (con un hash de archivos fuente o de ensamblaje, una l铆nea de comando, etc.). Puede ser utilizado por un sistema de orquestaci贸n para verificar la integridad al implementar un servicio.

Optimizaci贸n del rendimiento


Al expandir nuestro marco gRPC a la escala requerida, encontramos varios cuellos de botella espec铆ficos para Dropbox.

Consumo de recursos de TLS Handshakes


En los servicios que sirven a muchas relaciones, como resultado de los apretones de manos TLS, la carga combinada de la CPU puede ser bastante grave (especialmente al reiniciar un servicio popular).

Para mejorar el rendimiento al firmar, reemplazamos los pares de claves RSA-2048 con el ECDSA P-256. Aqu铆 hay ejemplos de su rendimiento (nota: con RSA, la verificaci贸n de firma es m谩s r谩pida).

RSA:

 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'RSA 2048' Did ... RSA 2048 signing operations in ..............  (1527.9 ops/sec) Did ... RSA 2048 verify (same key) operations in .... (37066.4 ops/sec) Did ... RSA 2048 verify (fresh key) operations in ... (25887.6 ops/sec) 

ECDSA:

 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'ECDSA P-256' Did ... ECDSA P-256 signing operations in ... (40410.9 ops/sec) Did ... ECDSA P-256 verify operations in .... (17037.5 ops/sec) 

Dado que la verificaci贸n con RSA-2048 es aproximadamente tres veces m谩s r谩pida que con ECDSA P-256, puede elegir RSA para certificados ra铆z y finales para aumentar la velocidad de operaci贸n. Pero desde el punto de vista de la seguridad, no todo es tan simple: construir谩 cadenas de varias primitivas criptogr谩ficas y, por lo tanto, el nivel de los par谩metros de seguridad resultantes ser谩 el m谩s bajo. Y si desea mejorar el rendimiento, no recomendamos utilizar certificados de la versi贸n RSA-4096 (y superior) como certificados ra铆z y finales.

Tambi茅n descubrimos que elegir una biblioteca TLS (y marcas de compilaci贸n) tiene un impacto significativo tanto en el rendimiento como en la seguridad. Compare, por ejemplo, la compilaci贸n LibreSSL en macOS X Mojave con el OpenSSL autoescrito en el mismo hardware.

LibreSSL 2.6.4:

 ~ openssl speed rsa2048 LibreSSL 2.6.4 ...                 sign verify sign/s verify/s rsa 2048 bits 0.032491s 0.001505s     30.8 664.3 

OpenSSL 1.1.1a:

  ~ openssl speed rsa2048 OpenSSL 1.1.1a  20 Nov 2018 ...                 sign verify sign/s verify/s rsa 2048 bits 0.000992s 0.000029s   1208.0 34454.8 

Sin embargo, la forma m谩s r谩pida de crear un apret贸n de manos TLS es no crearlo en absoluto. Hemos incluido soporte para la reanudaci贸n de la sesi贸n en gRPC-core y gRPC-python, reduciendo as铆 la carga en la CPU durante la implementaci贸n.

El cifrado es econ贸mico


Muchos creen err贸neamente que el cifrado es costoso. De hecho, incluso las computadoras modernas m谩s simples realizan un cifrado sim茅trico casi al instante. Un procesador est谩ndar puede cifrar y autenticar datos a una velocidad de 40 Gb / s por n煤cleo:

 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'AES' Did ... AES-128-GCM (8192 bytes) seal operations in ... 4534.4 MB/s 

Sin embargo, a煤n ten铆amos que configurar gRPC para nuestros bloques de memoria, operando a una velocidad de 50 Gb / s. Descubrimos que si la velocidad de cifrado es aproximadamente igual a la velocidad de copia, entonces es importante minimizar el n煤mero de operaciones de memoria. Adem谩s, realizamos algunos cambios en el propio gRPC.

Los protocolos autenticados y encriptados evitaron muchos problemas desagradables (por ejemplo, corrupci贸n de datos por el procesador, DMA o en la red). Incluso si no usa gRPC, le recomendamos usar TLS para contactos internos.

Canales de datos de alta latencia (BDP)


Nota del traductor: el subt铆tulo original usaba el t茅rmino producto de retraso de ancho de banda , que no tiene una traducci贸n establecida al ruso.

La red troncal de Dropbox incluye muchos centros de datos . A veces, los nodos ubicados en diferentes regiones tienen que comunicarse a trav茅s de RPC, por ejemplo, para la replicaci贸n. Cuando se usa TCP, el n煤cleo del sistema es responsable de limitar la cantidad de datos transmitidos en una conexi贸n particular (dentro de / proc / sys / net / ipv4 / tcp_ {r, w} mem ), aunque gRPC basado en HTTP / 2 tiene su propia herramienta control de flujo El l铆mite superior de BDP en grpc-go est谩 estrictamente limitado a 16 MB , lo que puede provocar un cuello de botella.

net.Server Golang o grpc.Server


Inicialmente, en nuestro c贸digo Go, admit铆amos HTTP / 1.1 y gRPC con un 煤nico servidor de red . La soluci贸n ten铆a sentido en t茅rminos de mantenimiento del c贸digo del programa, pero no funcion贸 a la perfecci贸n. La distribuci贸n de HTTP / 1.1 y gRPC a trav茅s de servidores y la migraci贸n de gRPC a grpc. El servidor mejor贸 significativamente el ancho de banda de Courier y el uso de memoria.

golang / protobuf o gogo / protobuf


Cambiar a gRPC puede aumentar el costo de la clasificaci贸n y la desorganizaci贸n. Para el c贸digo Go, pudimos reducir significativamente la carga de la CPU en los servidores Courier al cambiar a gogo / protobuf .

Como siempre, la transici贸n a gogo / protobuf estuvo acompa帽ada de algunas preocupaciones , pero si limita razonablemente la funcionalidad, no deber铆a haber problemas.

Detalles de implementaci贸n


En esta secci贸n, penetraremos m谩s profundamente en el dispositivo Courier, consideraremos los esquemas de protobuf y ejemplos de trozos de varios idiomas. Todos los ejemplos se toman del servicio de prueba, que utilizamos durante las pruebas de integraci贸n de Courier.

Descripci贸n del servicio


Eche un vistazo a un extracto de la definici贸n del servicio de prueba:

 service Test {   option (rpc_core.service_default_deadline_ms) = 1000;   rpc UnaryUnary(TestRequest) returns (TestResponse) {       option (rpc_core.method_default_deadline_ms) = 5000;   }   rpc UnaryStream(TestRequest) returns (stream TestResponse) {       option (rpc_core.method_no_deadline) = true;   }   ... } 

Como se indic贸 anteriormente, se requiere una fecha l铆mite para todos los m茅todos de mensajer铆a. Con la siguiente opci贸n, puede establecer la fecha l铆mite para todo el servicio:

 option (rpc_core.service_default_deadline_ms) = 1000; 

Al mismo tiempo, cada m茅todo se puede establecer en su propia fecha l铆mite, cancelando la fecha l铆mite de todo el servicio (si corresponde):

 option (rpc_core.method_default_deadline_ms) = 5000; 

En casos raros cuando la fecha l铆mite no tiene sentido (por ejemplo, al rastrear un recurso), el desarrollador puede deshabilitarlo:

 option (rpc_core.method_no_deadline) = true; 

Adem谩s de esto, la descripci贸n del servicio debe contener documentaci贸n API detallada, posiblemente con ejemplos de uso.

Generaci贸n de trozos


Para proporcionar una mayor flexibilidad, Courier genera sus propios ap茅ndices sin depender de la funcionalidad del interceptor proporcionada por gRPC (con la excepci贸n de Java, en el que la API del interceptor tiene suficiente potencia). Comparemos nuestros talones con los talones est谩ndar de Golang.

As铆 es como se ven los trozos de servidor gRPC predeterminados:

 func _Test_UnaryUnary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {       in := new(TestRequest)       if err := dec(in); err != nil {               return nil, err       }       if interceptor == nil {               return srv.(TestServer).UnaryUnary(ctx, in)       }       info := &grpc.UnaryServerInfo{               Server: srv,               FullMethod: "/test.Test/UnaryUnary",       }       handler := func(ctx context.Context, req interface{}) (interface{}, error) {               return srv.(TestServer).UnaryUnary(ctx, req.(*TestRequest))       }       return interceptor(ctx, in, info, handler) } 

Todo el procesamiento se lleva a cabo en el interior: decodificaci贸n de protobuf, lanzamiento de interceptores (ver la variable interceptor en el c贸digo), lanzamiento del controlador UnaryUnary.

Ahora eche un vistazo a los trozos de Courier:

 func _Test_UnaryUnary_dbxHandler(       srv interface{},       ctx context.Context,       dec func(interface{}) error,       interceptor grpc.UnaryServerInterceptor) (       interface{},       error) {       defer processor.PanicHandler()       impl := srv.(*dbxTestServerImpl)       metadata := impl.testUnaryUnaryMetadata       ctx = metadata.SetupContext(ctx)       clientId = client_info.ClientId(ctx)       stats := metadata.StatsMap.GetOrCreatePerClientStats(clientId)       stats.TotalCount.Inc()       req := &processor.UnaryUnaryRequest{               Srv: srv,               Ctx: ctx,               Dec: dec,               Interceptor: interceptor,               RpcStats: stats,               Metadata: metadata,               FullMethodPath: "/test.Test/UnaryUnary",               Req: &test.TestRequest{},               Handler: impl._UnaryUnary_internalHandler,               ClientId: clientId,               EnqueueTime: time.Now(),       }       metadata.WorkPool.Process(req).Wait()       return req.Resp, req.Err } 

Aqu铆 hay bastante c贸digo, as铆 que analic茅moslo.

Primero, aplazamos la llamada al controlador de p谩nico, que es responsable de recopilar los errores autom谩ticamente. Esto nos permitir谩 recopilar todas las excepciones no detectadas en el repositorio central para su posterior agregaci贸n e informes:

 defer processor.PanicHandler() 

Otra raz贸n por la que ejecutamos nuestro propio controlador de p谩nico es para asegurarnos de que la aplicaci贸n se bloquea si se produce un error. El controlador HTTP est谩ndar de golang / net en este caso ignorar谩 el problema y continuar谩 atendiendo nuevas solicitudes (incluso da帽adas e inconsistentes).

Luego pasamos el contexto, redefiniendo los valores basados 鈥嬧媏n los metadatos de la solicitud entrante:

 ctx = metadata.SetupContext(ctx) clientId = client_info.ClientId(ctx) 

Tambi茅n creamos (y cach茅 para mayor eficiencia) estad铆sticas del cliente del lado del servidor para una agregaci贸n m谩s detallada:

 stats := metadata.StatsMap.GetOrCreatePerClientStats(clientId) 

Esta l铆nea crea estad铆sticas para cada cliente (es decir, un identificador TLS) durante la ejecuci贸n. Tambi茅n tenemos estad铆sticas sobre todos los m茅todos para cada servicio. Dado que el generador de c贸digo auxiliar tiene acceso a todos los m茅todos durante la generaci贸n de c贸digo, podemos crearlos de forma est谩tica de antemano, evitando as铆 retrasar el programa.

Despu茅s de eso, creamos una estructura de solicitud, la transferimos al grupo de tareas y esperamos la ejecuci贸n:

 req := &processor.UnaryUnaryRequest{       Srv:        srv,       Ctx:        ctx,       Dec:        dec,       Interceptor:    interceptor,       RpcStats:       stats,       Metadata:       metadata,       ... } metadata.WorkPool.Process(req).Wait() 

Tenga en cuenta que en este punto no decodificamos protobuf, ni lanzamos el interceptor. Antes de esto, el grupo de acceso, la priorizaci贸n y la limitaci贸n del n煤mero de solicitudes ejecutadas deben pasar por el grupo de tareas.

Tenga en cuenta que la biblioteca gRPC admite la interfaz TAP, que le permite interceptar solicitudes a una velocidad tremenda. La interfaz proporciona la infraestructura para construir limitadores de velocidad efectivos con un consumo m铆nimo de recursos.

C贸digos de error espec铆ficos para diferentes aplicaciones.


Nuestro generador de c贸digo auxiliar tambi茅n permite a los desarrolladores asignar c贸digos de error espec铆ficos de la aplicaci贸n utilizando opciones especiales:

 enum ErrorCode { option (rpc_core.rpc_error) = true; UNKNOWN = 0; NOT_FOUND = 1 [(rpc_core.grpc_code)="NOT_FOUND"]; ALREADY_EXISTS = 2 [(rpc_core.grpc_code)="ALREADY_EXISTS"]; ... STALE_READ = 7 [(rpc_core.grpc_code)="UNAVAILABLE"]; SHUTTING_DOWN = 8 [(rpc_core.grpc_code)="CANCELLED"]; } 

Tanto los errores de gRPC como de aplicaci贸n se propagan dentro del servicio, y en el borde de la API, todos los errores son reemplazados por DESCONOCIDO. Gracias a esto, podemos evitar transferir el problema a otros servicios, lo que puede provocar un cambio en su sem谩ntica.

Cambios de Python


Los stubs de Python agregan un par谩metro de contexto expl铆cito a todos los manejadores de Courier:

 from dropbox.context import Context from dropbox.proto.test.service_pb2 import (       TestRequest,       TestResponse, ) from typing_extensions import Protocol class TestCourierClient(Protocol):   def UnaryUnary(           self,           ctx, # type: Context           request, # type: TestRequest           ):       # type: (...) -> TestResponse       ... 

Al principio parec铆a extra帽o, pero con el tiempo, los desarrolladores se acostumbraron a ctx expl铆cito como sol铆an hacerlo.

Tenga en cuenta que nuestros talones est谩n completamente escritos para mypy , que se compensa durante la refactorizaci贸n principal. Adem谩s, la integraci贸n con algunos IDEs (por ejemplo, PyCharm) se simplifica.

Continuando con la tendencia de la escritura est谩tica, agregamos anotaciones mypy a los protocolos mismos:

 class TestMessage(Message):   field: int   def __init__(self,       field : Optional[int] = ...,       ) -> None: ...   @staticmethod   def FromString(s: bytes) -> TestMessage: ... 

Estas anotaciones evitar谩n muchos errores comunes, como asignar un valor de Ninguno a un campo de tipo cadena , por ejemplo .

Este c贸digo est谩 disponible aqu铆 .

Proceso de migracion


Crear una nueva pila RPC no es una tarea f谩cil, pero ni siquiera se encuentra al lado del proceso de una transici贸n completa, si se mira desde el punto de vista de la complejidad operativa. Por lo tanto, tratamos de hacer que sea lo m谩s f谩cil posible para los desarrolladores cambiar del antiguo RPC a Courier. Dado que la migraci贸n suele ir acompa帽ada de errores, decidimos implementarla por etapas.

Paso 0: congela el antiguo RPC


En primer lugar, congelamos el antiguo RPC para no disparar a un objetivo en movimiento. Tambi茅n incit贸 a las personas a cambiar a Courier, porque todas las nuevas funciones, como el rastreo, solo estaban disponibles en los servicios de Courier.

Paso 1: interfaz com煤n para viejos RPC y Courier


Comenzamos definiendo una interfaz com煤n para el antiguo RPC y Courier. Se supon铆a que nuestra generaci贸n de c贸digo asegurar铆a que ambas versiones de los ap茅ndices correspondieran a esta interfaz:

 type TestServer interface {  UnaryUnary(     ctx context.Context,     req *test.TestRequest) (     *test.TestResponse,     error)  ... } 

Paso 2: migra a la nueva interfaz


Despu茅s de eso, comenzamos a cambiar cada servicio a una nueva interfaz, mientras continuamos usando el antiguo RPC. A menudo, los cambios en el c贸digo fueron una gran diferencia, afectando a todos los m茅todos del servicio y sus clientes. Como esta etapa es la m谩s problem谩tica, quer铆amos eliminar completamente el riesgo cambiando solo una cosa a la vez.

Los servicios simples con una peque帽a cantidad de m茅todos y el derecho a cometer errores se pueden migrar simult谩neamente, sin prestar atenci贸n a nuestras advertencias.

Paso 3: migrar clientes al RPC Courier


Durante el proceso de migraci贸n, comenzamos a lanzar simult谩neamente servidores antiguos y nuevos en diferentes puertos de la misma m谩quina. El cambio de la implementaci贸n RPC del lado del cliente se realiz贸 cambiando una l铆nea:

 class MyClient(object): def __init__(self): -   self.client = LegacyRPCClient('myservice') +   self.client = CourierRPCClient('myservice') 

Tenga en cuenta que con este modelo, puede transferir un cliente a la vez, comenzando con aquellos con un nivel m谩s bajo de SLA.

Paso 4: limpieza


, , RPC ( ). 鈥 .

Conclusiones


, Courier 鈥 RPC-, , Dropbox.

, Courier:

  1. 鈥 . .
  2. 鈥 , .
  3. , . Codegen.
  4. . , , . , : .
  5. RPC- 鈥 , . . .


Courier, gRPC , , , .

gRPC Python , C++ Python Rust . ALTS TLS- (, ).

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


All Articles