GRPC: créer un client à haut débit en Java / Scala

9

J'ai un service qui transfère des messages à un taux assez élevé.

Actuellement, il est desservi par akka-tcp et génère 3,5 millions de messages par minute. J'ai décidé d'essayer grpc. Malheureusement, cela a entraîné un débit beaucoup plus faible: ~ 500 000 messages par minute, encore moins.

Pourriez-vous s'il vous plaît recommander comment l'optimiser?

Ma configuration

Matériel : 32 cœurs, tas de 24 Go.

version grpc: 1.25.0

Format de message et point de terminaison

Le message est essentiellement un blob binaire. Le client diffuse de 100 000 à 1 million de messages et plus dans la même demande (de manière asynchrone), le serveur ne répond avec rien, le client utilise un observateur sans opération

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

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

message DummyResponse {
}

Problèmes: le taux de messages est faible par rapport à la mise en œuvre d'Akka. J'observe une faible utilisation du processeur, donc je soupçonne que l'appel grpc se bloque réellement en interne malgré le contraire. L'appel en onNext()effet ne revient pas immédiatement mais il y a aussi du GC sur la table.

J'ai essayé de générer plus d'expéditeurs pour atténuer ce problème, mais je n'ai pas obtenu beaucoup d'amélioration.

Mes résultats Grpc alloue en fait un tampon de 8 Ko sur chaque message lors de sa sérialisation. Voir le stacktrace:

java.lang.Thread.State: BLOCKED (on object monitor) sur com.google.common.io.ByteStreams.createBuffer (ByteStreams.java:58) sur com.google.common.io.ByteStreams.copy (ByteStreams.java: 105) sur io.grpc.internal.MessageFramer.writeToOutputStream (MessageFramer.java:274) sur io.grpc.internal.MessageFramer.writeKnownLengthUncompressed (MessageFramer.java:230) sur io.grpc.internal.MessageFramer.compresseur : 168) sur io.grpc.internal.MessageFramer.writePayload (MessageFramer.java:141) sur io.grpc.internal.AbstractStream.writeMessage (AbstractStream.java:53) sur io.grpc.internal.ForwardingClientStream.writeMessage (Forwarding). java: 37) sur io.grpc.internal.DelayedStream.writeMessage (DelayedStream.java:252) sur io.grpc.internal.ClientCallImpl.sendMessageInternal (ClientCallImpl.java:473) à io.grpc.internal.ClientCallImpl.sendMessage (ClientCallImpl.java:457) à io.grpc.ForwardingClientCall.sendMessage (ForwardingClientCall.java:Client.For.Cost.For.Client.Force.For.ClientCompendio.ForientiFormage.Forward.ClientCompendors.Forward.ClientCall.js (ForwardingClientCall.java:37) sur io.grpc.stub.ClientCalls $ CallToStreamObserverAdapter.onNext (ClientCalls.java:346)

Toute aide avec les meilleures pratiques sur la construction de clients grpc à haut débit appréciée.

simpadjo
la source
Utilisez-vous Protobuf? Ce chemin de code ne doit être utilisé que si InputStream renvoyé par MethodDescriptor.Marshaller.stream () n'implémente pas Drainable. Le Protobuf Marshaller prend en charge Drainable. Si vous utilisez Protobuf, est-il possible qu'un ClientInterceptor change le MethodDescriptor?
Eric Anderson
@EricAnderson merci pour votre réponse. J'ai essayé le protobuf standard avec gradle (com.google.protobuf: protocole: 3.10.1, io.grpc: protocol-gen-grpc-java: 1.25.0) et aussi scalapb. Cette trace de pile provenait probablement du code généré par scalapb. J'ai supprimé tout ce qui concerne le scalapb, mais cela n'a pas beaucoup amélioré les performances.
simpadjo
@EricAnderson J'ai résolu mon problème. Vous cingler en tant que développeur de grpc. Ma réponse a-t-elle un sens?
simpadjo

Réponses:

4

J'ai résolu le problème en créant plusieurs ManagedChannelinstances par destination. Malgré les articles, un ManagedChannelpeut générer lui-même suffisamment de connexions, donc une instance suffit, ce n'était pas vrai dans mon cas.

Les performances sont en parité avec l'implémentation akka-tcp.

simpadjo
la source
1
ManagedChannel (avec des stratégies LB intégrées) n'utilise pas plus d'une connexion par backend. Donc, si vous êtes à haut débit avec peu de backends, il est possible de saturer les connexions à tous les backends. L'utilisation de plusieurs canaux peut augmenter les performances dans ces cas.
Eric Anderson
@EricAnderson merci. Dans mon cas, la génération de plusieurs canaux, même vers un seul nœud principal, a aidé
simpadjo
Moins il y a de backends et plus la bande passante est élevée, plus vous avez besoin de plusieurs canaux. Ainsi, le "backend unique" rendrait plus probable la multiplication des canaux.
Eric Anderson
0

Question interessante. Les packages de réseaux informatiques sont codés à l'aide d'une pile de protocoles , et ces protocoles sont construits en plus des spécifications du précédent. Par conséquent, les performances (débit) d'un protocole sont limitées par les performances de celui utilisé pour le construire, car vous ajoutez des étapes d'encodage / décodage supplémentaires par-dessus celle sous-jacente.

Par exemple, gRPCest construit au-dessus de HTTP 1.1/2, qui est un protocole sur la couche Application , ou L7, en tant que tel, ses performances sont liées par les performances de HTTP. Maintenant, HTTPlui-même est construit au-dessus de TCP, qui se trouve dans la couche Transport , ou L4, nous pouvons donc en déduire que le gRPCdébit ne peut pas être supérieur à un code équivalent servi dans la TCPcouche.

En d'autres termes: si votre serveur est capable de gérer des TCPpackages bruts , comment l'ajout de nouvelles couches de complexité ( gRPC) améliorerait-il les performances?

Batato
la source
C'est exactement pour cette raison que j'utilise l'approche en streaming: je paie une fois pour établir une connexion http et envoyer ~ 300 millions de messages en l'utilisant. Il utilise des websockets sous le capot, ce qui, je pense, a une surcharge relativement faible.
simpadjo
Car gRPCvous payez également une fois pour établir une connexion, mais vous avez ajouté le fardeau supplémentaire de l'analyse de protobuf. Quoi qu'il en soit, il est difficile de faire des suppositions sans trop d'informations, mais je parierais qu'en général, puisque vous ajoutez des étapes d'encodage / décodage supplémentaires dans votre pipeline, l' gRPCimplémentation serait plus lente que la socket Web équivalente.
Batato
Akka ajoute également des frais généraux. Quoi qu'il en soit, le ralentissement x5 semble trop.
simpadjo
Je pense que vous pouvez trouver cela intéressant: github.com/REASY/akka-http-vs-akka-grpc , dans son cas (et je pense que cela s'étend au vôtre), le goulot d'étranglement peut être dû à une utilisation élevée de la mémoire dans protobuf (de ) la sérialisation, qui à son tour déclenche plus d'appels au garbage collector.
Batato
merci, intéressant malgré que j'ai déjà résolu mon problème
simpadjo
0

Je suis assez impressionné par la qualité des performances d'Akka TCP ici: D

Notre expérience a été légèrement différente. Nous travaillions sur des instances beaucoup plus petites utilisant Akka Cluster. Pour Akka distant, nous sommes passés d'Akka TCP à UDP en utilisant Artery et avons obtenu un taux beaucoup plus élevé + un temps de réponse plus faible et plus stable. Il y a même une configuration dans Artery qui aide à équilibrer la consommation du processeur et le temps de réponse après un démarrage à froid.

Ma suggestion est d'utiliser un cadre basé sur UDP qui prend également en charge la fiabilité de la transmission pour vous (par exemple, Artery UDP), et de sérialiser simplement en utilisant Protobuf, au lieu d'utiliser gRPC à chair pleine. Le canal de transmission HTTP / 2 n'est pas vraiment à des fins de temps de réponse bas débit élevé.

Wang Xian
la source