GRPC: hacer cliente de alto rendimiento en Java / Scala

9

Tengo un servicio que transfiere mensajes a una velocidad bastante alta.

Actualmente es atendido por akka-tcp y genera 3,5 millones de mensajes por minuto. Decidí probar grpc. Desafortunadamente, resultó en un rendimiento mucho menor: ~ 500k mensajes por minuto e incluso menos.

¿Podría recomendarme cómo optimizarlo?

Mi configuración

Hardware : 32 núcleos, montón de 24 Gb.

versión de grpc: 1.25.0

Formato del mensaje y punto final

El mensaje es básicamente un blob binario. El cliente transmite 100K - 1M y más mensajes en la misma solicitud (asíncronamente), el servidor no responde con nada, el cliente usa un observador no operativo

service MyService {
    rpc send (stream MyMessage) returns (stream DummyResponse);
}

message MyMessage {
    int64 someField = 1;
    bytes payload = 2;  //not huge
}

message DummyResponse {
}

Problemas: la tasa de mensajes es baja en comparación con la implementación de akka. Observo un bajo uso de la CPU, por lo que sospecho que la llamada grpc en realidad está bloqueando internamente a pesar de que dice lo contrario. Las llamadas onNext()no vuelven de inmediato, pero también hay GC sobre la mesa.

Traté de generar más remitentes para mitigar este problema, pero no obtuve muchas mejoras.

Mis hallazgos Grpc en realidad asigna un búfer de bytes de 8 KB en cada mensaje cuando lo serializa. Ver el stacktrace:

java.lang.Thread.State: BLOCKED (en el monitor de objetos) en com.google.common.io.ByteStreams.createBuffer (ByteStreams.java:58) en com.google.common.io.ByteStreams.copy (ByteStreams.java: 105) en io.grpc.internal.MessageFramer.writeToOutputStream (MessageFramer.java:274) en io.grpc.internal.MessageFramer.writeKnownLengthUncompressed (MessageFramer.java:230) en io.grpc.internal.MessageFramer.wpressed.MessageFramer.vader. : 168) en io.grpc.internal.MessageFramer.writePayload (MessageFramer.java:141) en io.grpc.internal.AbstractStream.writeMessage (AbstractStream.java:53) en io.grpc.internal.ForwardingClientStream.writeMessageS (ForwardingClientStream.writeMessageS. java: 37) en io.grpc.internal.DelayedStream.writeMessage (DelayedStream.java:252) en io.grpc.internal.ClientCallImpl.sendMessageInternal (ClientCallImpl.java:473) en io.grpc.internal.ClientCallImpl.sendMessage (ClientCallImpl.java:457) en io.grpc.ForwardingClientCall.sendMessage (ForwardingClientCall.javagr37) en io.gr. (ForwardingClientCall.java:37) en io.grpc.stub.ClientCalls $ CallToStreamObserverAdapter.onNext (ClientCalls.java:346)

Se agradece cualquier ayuda con las mejores prácticas en la construcción de clientes grpc de alto rendimiento.

simpadjo
fuente
¿Estás usando Protobuf? Esta ruta de código solo debe tomarse si InputStream devuelto por MethodDescriptor.Marshaller.stream () no implementa Drainable. El Protobuf Marshaller admite Drainable. Si está utilizando Protobuf, ¿es posible que un ClientInterceptor esté cambiando el MethodDescriptor?
Eric Anderson
@EricAnderson gracias por tu respuesta. Probé el protobuf estándar con gradle (com.google.protobuf: protocol: 3.10.1, io.grpc: protocol-gen-grpc-java: 1.25.0) y también scalapb. Probablemente este stacktrace fue de hecho al código generado por scalapb. Eliminé todo lo relacionado con scalapb pero no ayudó mucho el rendimiento de wrt.
simpadjo
@EricAnderson Resolví mi problema. Pinging como desarrollador de grpc. ¿Tiene sentido mi respuesta?
simpadjo

Respuestas:

4

Resolví el problema creando varias ManagedChannelinstancias por destino. A pesar de que los artículos dicen que un ManagedChannelpuede generar suficientes conexiones en sí mismo, por lo que una instancia es suficiente, no fue cierto en mi caso.

El rendimiento está a la par con la implementación de akka-tcp.

simpadjo
fuente
1
ManagedChannel (con políticas integradas de LB) no usa más de una conexión por back-end. Entonces, si tiene un alto rendimiento con pocos backends, es posible saturar las conexiones a todos los backends. El uso de múltiples canales puede aumentar el rendimiento en esos casos.
Eric Anderson
@EricAnderson gracias. En mi caso, la creación de varios canales, incluso a un solo nodo de fondo, ha ayudado
simpadjo
Cuantos menos backends y mayor ancho de banda, más probable es que necesite múltiples canales. Por lo tanto, el "backend único" haría que sea más probable que más canales sean útiles.
Eric Anderson
0

Interesante pregunta. Los paquetes de red de computadoras se codifican usando una pila de protocolos , y dichos protocolos se construyen sobre las especificaciones del anterior. Por lo tanto, el rendimiento (rendimiento) de un protocolo está limitado por el rendimiento del utilizado para construirlo, ya que está agregando pasos adicionales de codificación / decodificación sobre el subyacente.

Por ejemplo, gRPCestá construido encima de HTTP 1.1/2, que es un protocolo en la capa de aplicación , o L7, y como tal, su rendimiento está limitado por el rendimiento de HTTP. Ahora HTTPse construye encima de TCP, que está en la capa de transporte , o L4, por lo que podemos deducir que el gRPCrendimiento no puede ser mayor que un código equivalente servido en la TCPcapa.

En otras palabras: si su servidor es capaz de manejar TCPpaquetes en bruto , ¿cómo agregar nuevas capas de complejidad ( gRPC) mejoraría el rendimiento?

Batato
fuente
Exactamente por esa razón, uso el enfoque de transmisión: pago una vez para establecer una conexión http y envío ~ 300 millones de mensajes con ella. Utiliza websockets debajo del capó que espero que tenga una sobrecarga relativamente baja.
simpadjo
Para gRPCque también paga una vez para establecer una conexión, pero que ha añadido la carga adicional de analizar protobuf. De todos modos, es difícil hacer conjeturas sin demasiada información, pero apuesto a que, en general, dado que está agregando pasos adicionales de codificación / decodificación en su canalización, la gRPCimplementación sería más lenta que la del socket web equivalente.
Batato
Akka suma algo de gastos generales también. De todos modos, la ralentización de x5 parece demasiado.
simpadjo
Creo que puede encontrar esto interesante: github.com/REASY/akka-http-vs-akka-grpc , en su caso (y creo que esto se extiende al suyo), el cuello de botella puede deberse al uso de memoria en protobuf (de ) serialización, que a su vez activa más llamadas al recolector de basura.
Batato
gracias, interesante a pesar de que ya resolví mi problema
simpadjo
0

Estoy bastante impresionado con lo bien que Akka TCP se ha desempeñado aquí: D

Nuestra experiencia fue un poco diferente. Estábamos trabajando en instancias mucho más pequeñas usando Akka Cluster. Para el control remoto de Akka, cambiamos de Akka TCP a UDP usando Artery y logramos una tasa mucho más alta + un tiempo de respuesta más bajo y más estable. Incluso hay una configuración en Artery que ayuda a equilibrar el consumo de CPU y el tiempo de respuesta desde un arranque en frío.

Mi sugerencia es usar un marco basado en UDP que también se encargue de la confiabilidad de la transmisión para usted (por ejemplo, ese Artery UDP), y simplemente serializar usando Protobuf, en lugar de usar gRPC completo. El canal de transmisión HTTP / 2 no es realmente para fines de alto rendimiento y bajo tiempo de respuesta.

Wang Xian
fuente