GRPC: Erstellen Sie einen Client mit hohem Durchsatz in Java / Scala

9

Ich habe einen Dienst, der Nachrichten mit einer ziemlich hohen Rate überträgt.

Derzeit wird es von akka-tcp bereitgestellt und sendet 3,5 Millionen Nachrichten pro Minute. Ich beschloss, grpc auszuprobieren. Leider führte dies zu einem viel geringeren Durchsatz: ~ 500.000 Nachrichten pro Minute und noch weniger.

Könnten Sie bitte empfehlen, wie Sie es optimieren können?

Mein Setup

Hardware : 32 Kerne, 24 GB Heap.

grpc version : 1.25.0

Nachrichtenformat und Endpunkt

Nachricht ist im Grunde ein binärer Blob. Der Client überträgt 100K - 1M und mehr Nachrichten in dieselbe Anforderung (asynchron), der Server antwortet mit nichts, der Client verwendet einen No-Op-Beobachter

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

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

message DummyResponse {
}

Probleme: Die Nachrichtenrate ist im Vergleich zur akka-Implementierung niedrig. Ich beobachte eine geringe CPU-Auslastung, daher vermute ich, dass der grpc-Aufruf tatsächlich intern blockiert, obwohl etwas anderes angegeben ist. BerufungonNext() kehrt zwar nicht sofort zurück, aber es liegt auch GC auf dem Tisch.

Ich habe versucht, mehr Absender zu erzeugen, um dieses Problem zu beheben, aber keine großen Verbesserungen erzielt.

Meine Ergebnisse Grpc weist jeder Nachricht bei der Serialisierung tatsächlich einen 8-KB-Byte-Puffer zu. Siehe die Stapelverfolgung:

java.lang.Thread.State: BLOCKED (auf dem Objektmonitor) unter com.google.common.io.ByteStreams.createBuffer (ByteStreams.java:58) unter com.google.common.io.ByteStreams.copy (ByteStreams.java: 105) unter io.grpc.internal.MessageFramer.writeToOutputStream (MessageFramer.java:274) unter io.grpc.internal.MessageFramer.writeKnownLengthUncompressed (MessageFramer.java:230) unter io.grpc : 168) unter io.grpc.internal.MessageFramer.writePayload (MessageFramer.java:141) unter io.grpc.internal.AbstractStream.writeMessage (AbstractStream.java:53) unter io.grpc.internal.ForwardingClientStream.writeM. java: 37) bei io.grpc.internal.DelayedStream.writeMessage (DelayedStream.java:252) bei io.grpc.internal.ClientCallImpl.sendMessageInternal (ClientCallImpl.java:473) unter io.grpc.internal.ClientCallImpl.sendMessage (ClientCallImpl.java:457) unter io.grpc.ForwardingClientCall.sendMessage (ForwardingClientCall.allCall.sendMallage) (ForwardingClientCall.java:37) unter io.grpc.stub.ClientCalls $ CallToStreamObserverAdapter.onNext (ClientCalls.java:346)

Jede Hilfe mit Best Practices beim Aufbau von Grpc-Clients mit hohem Durchsatz wird geschätzt.

Simpadjo
quelle
Verwenden Sie Protobuf? Dieser Codepfad sollte nur verwendet werden, wenn der von MethodDescriptor.Marshaller.stream () zurückgegebene InputStream Drainable nicht implementiert. Der Protobuf Marshaller unterstützt Drainable. Wenn Sie Protobuf verwenden, ist es möglich, dass ein ClientInterceptor den MethodDescriptor ändert?
Eric Anderson
@ EricAnderson, danke für deine Antwort. Ich habe den Standard-Protobuf mit gradle (com.google.protobuf: protoc: 3.10.1, io.grpc: protoc-gen-grpc-java: 1.25.0) und auch ausprobiert scalapb. Wahrscheinlich war dieser Stacktrace tatsächlich von Scalapb-generiertem Code. Ich habe alles entfernt, was mit Scalapb zu tun hat, aber es hat nicht viel zur Leistung beigetragen.
Simpadjo
@ EricAnderson Ich habe mein Problem gelöst. Pingen Sie als Entwickler von grpc. Ist meine Antwort sinnvoll?
Simpadjo

Antworten:

4

Ich habe das Problem gelöst, indem ich mehrere ManagedChannelInstanzen pro Ziel erstellt habe. Obwohl Artikel besagen, dass eine ManagedChannelDose selbst genügend Verbindungen erzeugen kann, reicht eine Instanz aus, was in meinem Fall nicht der Fall war.

Die Leistung entspricht der Implementierung von akka-tcp.

Simpadjo
quelle
1
ManagedChannel (mit integrierten LB-Richtlinien) verwendet nicht mehr als eine Verbindung pro Backend. Wenn Sie also einen hohen Durchsatz mit wenigen Backends haben, können Sie die Verbindungen zu allen Backends sättigen. Die Verwendung mehrerer Kanäle kann in diesen Fällen die Leistung steigern.
Eric Anderson
@ EricAnderson danke. In meinem Fall hat es geholfen, mehrere Kanäle sogar auf einen einzelnen Backend-Knoten zu bringen
simpadjo
Je weniger Backends und je höher die Bandbreite, desto wahrscheinlicher ist es, dass Sie mehrere Kanäle benötigen. "Single Backend" würde es also wahrscheinlicher machen, dass mehr Kanäle hilfreich sind.
Eric Anderson
0

Interessante Frage. Computernetzwerkpakete werden unter Verwendung eines Protokollstapels codiert , und solche Protokolle bauen auf den Spezifikationen des vorherigen auf. Daher ist die Leistung (Durchsatz) eines Protokolls an die Leistung des Protokolls gebunden, mit dem es erstellt wurde, da Sie zusätzlich zum zugrunde liegenden Protokoll zusätzliche Codierungs- / Decodierungsschritte hinzufügen.

Zum Beispiel gRPCwird darauf aufgebaut HTTP 1.1/2, was ein Protokoll auf der Anwendungsschicht ist , oder L7, und als solches ist seine Leistung an die Leistung von gebunden HTTP. Jetzt wird HTTPselbst darauf aufgebaut TCP, was sich auf der Transportebene befindet , oder L4wir können daraus schließen, dass der gRPCDurchsatz nicht größer sein kann als ein äquivalenter Code, der in der TCPEbene bereitgestellt wird.

Mit anderen Worten: Wenn Ihr Server in der Lage ist, Rohpakete zu verarbeiten TCP, wie würde das Hinzufügen neuer Komplexitätsebenen ( gRPC) die Leistung verbessern?

Batato
quelle
Aus genau diesem Grund verwende ich den Streaming-Ansatz: Ich bezahle einmal für den Aufbau einer http-Verbindung und sende damit ~ 300 Millionen Nachrichten. Es werden Websockets unter der Haube verwendet, von denen ich einen relativ geringen Overhead erwarte.
Simpadjo
Für gRPCSie zahlen auch einmal eine Verbindung für den Aufbau, aber Sie haben die zusätzliche Belastung der Parsen protobuf hinzugefügt. Wie auch immer, es ist schwierig, ohne zu viele Informationen zu raten, aber ich würde wetten, dass die gRPCImplementierung im Allgemeinen langsamer ist als die entsprechende Web-Socket-Implementierung , da Sie zusätzliche Codierungs- / Decodierungsschritte in Ihre Pipeline aufnehmen .
Batato
Akka summiert sich ebenfalls zu einem gewissen Overhead. Auf jeden Fall sieht die x5-Verlangsamung zu viel aus.
Simpadjo
Ich denke, Sie finden das vielleicht interessant: github.com/REASY/akka-http-vs-akka-grpc , in seinem Fall (und ich denke, das erstreckt sich auch auf Ihren) kann der Engpass auf eine hohe Speichernutzung in protobuf (de) zurückzuführen sein ) Serialisierung, die wiederum mehr Aufrufe an den Garbage Collector auslöst.
Batato
danke, interessant, obwohl ich mein Problem bereits gelöst habe
simpadjo
0

Ich bin ziemlich beeindruckt davon, wie gut Akka TCP hier abgeschnitten hat: D.

Unsere Erfahrung war etwas anders. Wir haben mit Akka Cluster an viel kleineren Instanzen gearbeitet. Für das Akka-Remoting haben wir mithilfe von Artery von Akka TCP zu UDP gewechselt und eine viel höhere Rate + niedrigere und stabilere Reaktionszeit erzielt. Es gibt sogar eine Konfiguration in Artery, die hilft, den CPU-Verbrauch und die Reaktionszeit nach einem Kaltstart auszugleichen.

Mein Vorschlag ist, ein UDP-basiertes Framework zu verwenden, das auch die Übertragungszuverlässigkeit für Sie übernimmt (z. B. das Artery UDP), und nur mit Protobuf zu serialisieren, anstatt Vollfleisch-gRPC zu verwenden. Der HTTP / 2-Übertragungskanal ist nicht wirklich für Zwecke mit hohem Durchsatz und geringer Antwortzeit geeignet.

Wang Xian
quelle