GRPC: crea client ad alto rendimento in Java / Scala


9

Ho un servizio che trasferisce i messaggi a un ritmo piuttosto elevato.

Attualmente è servito da Akka-TCPC e invia 3,5 milioni di messaggi al minuto. Ho deciso di provare grpc. Sfortunatamente ha prodotto un throughput molto più piccolo: ~ 500k messaggi al minuto ancora meno.

Potresti consigliarmi come ottimizzarlo?

La mia configurazione

Hardware : 32 core, heap da 24 GB.

versione di grpc: 1.25.0

Formato e endpoint del messaggio

Il messaggio è fondamentalmente un BLOB binario. Il client trasmette 100 K - 1 M e più messaggi nella stessa richiesta (in modo asincrono), il server non risponde con nulla, il client utilizza un osservatore no-op

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

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

message DummyResponse {
}

Problemi: la velocità dei messaggi è bassa rispetto all'implementazione akka. Osservo un basso utilizzo della CPU, quindi sospetto che la chiamata grpc stia effettivamente bloccando internamente nonostante dice diversamente. La chiamata onNext()infatti non ritorna immediatamente ma c'è anche GC sul tavolo.

Ho provato a generare più mittenti per mitigare questo problema ma non ho ottenuto molti miglioramenti.

Le mie scoperte Grpc alloca effettivamente un buffer di byte da 8 KB su ciascun messaggio quando lo serializza. Vedi lo stacktrace:

java.lang.Thread.State: BLOCKED (sul monitor oggetti) su com.google.common.io.ByteStreams.createBuffer (ByteStreams.java:58) su com.google.common.io.ByteStreams.copy (ByteStreams.java: 105) at io.grpc.internal.MessageFramer.writeToOutputStream (MessageFramer.java:274) at io.grpc.internal.MessageFramer.writeKnownLengthUncompressed (MessageFramer.java:230) at io.grpc.internal.MessageFramerwram : 168) at io.grpc.internal.MessageFramer.writePayload (MessageFramer.java:141) at io.grpc.internal.AbstractStream.writeMessage (AbstractStream.java:53) at io.grpc.internal.ForwardingClientStream.writeMessage (Forwarding. java: 37) su io.grpc.internal.DelayedStream.writeMessage (DelayedStream.java:252) su io.grpc.internal.ClientCallImpl.sendMessageInternal (ClientCallImpl.java:473) at io.grpc.internal.ClientCallImpl.sendMessage (ClientCallImpl.java:457) at io.grpc.ForwardingClientCall.sendMessage (ForwardingClientCall.jgr.C37 (ForwardingClientCall.java:37) at io.grpc.stub.ClientCalls $ CallToStreamObserverAdapter.onNext (ClientCalls.java:346)

Apprezzato qualsiasi aiuto con le migliori pratiche per la creazione di client grpc ad alto rendimento.


Stai usando Protobuf? Questo percorso di codice dovrebbe essere preso solo se InputStream restituito da MethodDescriptor.Marshaller.stream () non implementa Drainable. Protobuf Marshaller supporta Drainable. Se stai usando Protobuf, è possibile che un ClientInterceptor stia cambiando MethodDescriptor?
Eric Anderson,

@EricAnderson grazie per la tua risposta. Ho provato il protobuf standard con gradle (com.google.protobuf: protoc: 3.10.1, io.grpc: protoc-gen-grpc-java: 1.25.0) e anche scalapb. Probabilmente questo stacktrace proveniva davvero dal codice generato da scalapb. Ho rimosso tutto ciò che riguarda scalapb ma non ha aiutato molto le prestazioni di wrt.
simpadjo,

@EricAnderson Ho risolto il mio problema. Esegui il ping come sviluppatore di grpc. La mia risposta ha senso?
simpadjo,

Risposte:


4

Ho risolto il problema creando diverse ManagedChannelistanze per destinazione. Nonostante gli articoli affermino che una ManagedChannellattina può generare abbastanza connessioni, quindi un'istanza è sufficiente, nel mio caso non era vero.

Le prestazioni sono in linea con l'implementazione akka-tcp.


1
ManagedChannel (con criteri LB integrati) non utilizza più di una connessione per back-end. Pertanto, se si dispone di un throughput elevato con pochi backend, è possibile saturare le connessioni a tutti i backend. L'uso di più canali può aumentare le prestazioni in questi casi.
Eric Anderson,

@EricAnderson grazie. Nel mio caso, la creazione di più canali anche su un singolo nodo back-end ha aiutato
simpadjo

Minore è il backend e maggiore è la larghezza di banda, più è probabile che siano necessari più canali. Quindi "single backend" renderebbe più probabile che più canali siano utili.
Eric Anderson,

0

Domanda interessante. I pacchetti di reti di computer sono codificati usando una pila di protocolli e tali protocolli sono costruiti in cima alle specifiche del precedente. Quindi le prestazioni (throughput) di un protocollo sono limitate dalle prestazioni di quello utilizzato per costruirlo, poiché si stanno aggiungendo ulteriori passaggi di codifica / decodifica sopra a quello sottostante.

Ad esempio, gRPCè basato su HTTP 1.1/2, che è un protocollo a livello di applicazione , o L7, e come tale, le sue prestazioni sono vincolate dalle prestazioni di HTTP. Ora HTTPstesso si basa su TCP, che si trova al livello Trasporto , o L4, quindi possiamo dedurre che la gRPCvelocità effettiva non può essere maggiore di un codice equivalente offerto nel TCPlivello.

In altre parole: se il tuo server è in grado di gestire TCPpacchetti non elaborati, in che modo l'aggiunta di nuovi livelli di complessità ( gRPC) migliorerebbe le prestazioni?


Proprio per questo motivo utilizzo l'approccio di streaming: pago una volta per stabilire una connessione http e invio ~ 300 milioni di messaggi che la utilizzano. Utilizza prese Web sotto il cofano che mi aspetto di avere un sovraccarico relativamente basso.
Simpadjo,

Perché gRPCpaghi anche una volta per stabilire una connessione, ma hai aggiunto l'onere aggiuntivo dell'analisi di protobuf. Ad ogni modo è difficile fare ipotesi senza troppe informazioni, ma scommetto che, in generale, dal momento che stai aggiungendo ulteriori passaggi di codifica / decodifica nella tua pipeline, l' gRPCimplementazione sarebbe più lenta di quella del socket web equivalente.
Batato,

Akka aggiunge anche un po 'di spese generali. Comunque il rallentamento x5 sembra troppo.
Simpadjo,

Penso che potresti trovare questo interessante: github.com/REASY/akka-http-vs-akka-grpc , nel suo caso (e penso che questo si estenda al tuo), il collo di bottiglia potrebbe essere dovuto all'elevato utilizzo della memoria in protobuf (de ) serializzazione, che a sua volta attiva più chiamate al garbage collector.
Batato

grazie, interessante nonostante abbia già risolto il mio problema
simpadjo il

0

Sono abbastanza colpito da quanto bene Akka TCP abbia funzionato qui: D

La nostra esperienza è stata leggermente diversa. Stavamo lavorando su istanze molto più piccole usando Akka Cluster. Per i remoti Akka, siamo passati da Akka TCP a UDP usando Artery e abbiamo ottenuto un tasso molto più alto + tempi di risposta più bassi e più stabili. C'è anche una configurazione in Artery che aiuta a bilanciare il consumo di CPU e i tempi di risposta da un avvio a freddo.

Il mio suggerimento è di utilizzare un framework basato su UDP che si occupi anche dell'affidabilità della trasmissione per te (ad es. Artery UDP) e di serializzare semplicemente usando Protobuf, invece di utilizzare gRPC full flesh. Il canale di trasmissione HTTP / 2 non è realmente per scopi con tempi di risposta bassi e throughput elevati.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.