GRPC: tworzenie wysokowydajnego klienta w Javie / Scali

9

Mam usługę, która przesyła wiadomości z dość wysoką prędkością.

Obecnie jest obsługiwany przez akka-tcp i wytwarza 3,5 miliona wiadomości na minutę. Postanowiłem dać grpc szansę. Niestety spowodowało to znacznie mniejszą przepustowość: ~ 500 000 wiadomości na minutę, a nawet mniej.

Czy mógłbyś polecić jak to zoptymalizować?

Moja konfiguracja

Sprzęt komputerowy : 32 rdzenie, stos 24 Gb.

wersja grpc grpc: 1.25.0

Format wiadomości i punkt końcowy

Wiadomość to w zasadzie binarny obiekt blob. Klient przesyła strumieniowo 100K - 1M i więcej wiadomości do tego samego żądania (asynchronicznie), serwer nic nie odpowiada, klient używa obserwatora bez operacji

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

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

message DummyResponse {
}

Problemy: Szybkość wiadomości jest niska w porównaniu do implementacji akka. Obserwuję niskie zużycie procesora, więc podejrzewam, że połączenie grpc faktycznie blokuje wewnętrznie, mimo że mówi inaczej. Calling onNext()rzeczywiście nie wraca natychmiast, ale na stole jest także GC.

Próbowałem odrodzić więcej nadawców, aby złagodzić ten problem, ale nie uzyskałem dużej poprawy.

Moje ustalenia Grpc faktycznie przydziela bufor bajtów 8 KB na każdą wiadomość, gdy jest ona serializowana. Zobacz stacktrace:

java.lang.Thread.State: BLOCKED (na monitorze obiektów) w com.google.common.io.ByteStreams.createBuffer (ByteStreams.java:58) w com.google.common.io.ByteStreams.copy (ByteStreams.java: 105) w io.grpc.internal.MessageFramer.writeToOutputStream (MessageFramer.java:274) w io.grpc.internal.MessageFramer.writeKnownLengthUncompressed (MessageFramer.java:230) w io.grpc.internal.MessageFramer.write : 168) w io.grpc.internal.MessageFramer.writePayload (MessageFramer.java:141) w io.grpc.internal.AbstractStream.writeMessage (AbstractStream.java:53) w io.grpc.internal.ForwardingClientStream.reamiteream (ForwardingClientStream. java: 37) w io.grpc.internal.DelayedStream.writeMessage (DelayedStream.java:252) w io.grpc.internal.ClientCallImpl.sendMessageInternal (ClientCallImpl.java:473) pod adresem io.grpc.internal.ClientCallImpl.sendMessage (ClientCallImpl.java:457) pod adresem io.grpc.ForwardingClientCall.sendMessage (ForwardingClientCall.orva.grva37) (ForwardingClientCall.java:37) w io.grpc.stub.ClientCalls $ CallToStreamObserverAdapter.onNext (ClientCalls.java:346)

Doceniono wszelką pomoc dotyczącą najlepszych praktyk w budowaniu wysokoprzepustowych klientów grpc.

simpadjo
źródło
Czy używasz Protobuf? Tę ścieżkę kodu należy pobrać tylko wtedy, gdy InputStream zwrócony przez MethodDescriptor.Marshaller.stream () nie implementuje Drainable. Protobuf Marshaller obsługuje Drainable. Jeśli używasz Protobuf, czy jest możliwe, że ClientInterceptor zmienia MethodDescriptor?
Eric Anderson,
@EricAnderson dziękuję za odpowiedź. Próbowałem standardowego protobufa z gradle (com.google.protobuf: protoc: 3.10.1, io.grpc: protoc-gen-grpc-java: 1.25.0), a także scalapb. Prawdopodobnie ten stacktrace rzeczywiście był od kodu generowanego przez scalapb. Usunąłem wszystko związane ze skalapb, ale nie pomogło to w dużej wydajności wrt.
simpadjo
@EricAnderson Rozwiązałem problem. Śledzę cię jako programistę grpc. Czy moja odpowiedź ma sens?
simpadjo,

Odpowiedzi:

4

Rozwiązałem problem, tworząc kilka ManagedChannelinstancji dla każdego miejsca docelowego. Mimo artykułów mówi się, że sam ManagedChannelmoże wygenerować wystarczającą liczbę połączeń, więc wystarczy jedna instancja, co nie było prawdą w moim przypadku.

Wydajność jest na równi z implementacją akka-tcp.

simpadjo
źródło
1
ManagedChannel (z wbudowanymi zasadami LB) nie używa więcej niż jednego połączenia na jeden backend. Jeśli więc masz dużą przepustowość z kilkoma backendami, możesz nasycić połączenia ze wszystkimi backendami. Korzystanie z wielu kanałów może zwiększyć wydajność w takich przypadkach.
Eric Anderson
@EricAnderson dzięki. W moim przypadku pomogło
odrodzenie
Im mniej backendów i wyższa przepustowość, tym większe prawdopodobieństwo, że potrzebujesz wielu kanałów. Tak więc „pojedynczy backend” zwiększyłby prawdopodobieństwo, że więcej kanałów jest pomocnych.
Eric Anderson,
0

Interesujące pytanie. Pakiety sieci komputerowej są kodowane przy użyciu stosu protokołów , a takie protokoły są zbudowane zgodnie ze specyfikacjami poprzedniego. Dlatego wydajność (przepustowość) protokołu jest ograniczona wydajnością tego, który został użyty do jego zbudowania, ponieważ dodajesz dodatkowe kroki kodowania / dekodowania na szczycie protokołu bazowego.

Na przykład gRPCjest zbudowany na podstawie HTTP 1.1/2, który jest protokołem w warstwie aplikacji , lub L7, i jako taki, jego wydajność jest związana z wydajnością HTTP. Teraz HTTPsam jest zbudowany na wierzchu TCP, który jest w warstwie Transportu , lub L4, więc możemy wywnioskować, że gRPCprzepustowość nie może być większa niż równoważny kod obsługiwany wTCP warstwie.

Innymi słowy: jeśli Twój serwer jest w stanie obsłużyć TCPpakiety raw , jak dodanie nowych warstw złożoności ( gRPC) poprawiłoby wydajność?

Batato
źródło
Właśnie z tego powodu używam podejścia strumieniowego: płacę raz za nawiązanie połączenia HTTP i za jego pomocą wysyłam ~ 300 mln wiadomości. Korzysta z gniazd sieciowych pod maską, które, jak sądzę, mają stosunkowo niski koszt.
simpadjo,
Poniewaz gRPCzaplacisz jednokrotnie za ustanowienie polaczenia, ale dodales dodatkowe obciazenie parsowania protobuf. W każdym razie trudno zgadywać bez zbyt dużej ilości informacji, ale założę się, że ogólnie, ponieważ dodajesz dodatkowe kroki kodowania / dekodowania w swoim potoku, gRPCimplementacja byłaby wolniejsza niż równoważne gniazdo sieciowe.
Batato,
Akka również dodaje trochę kosztów ogólnych. W każdym razie spowolnienie x5 wygląda zbyt mocno.
simpadjo
Myślę, że może Cię to zainteresować: github.com/REASY/akka-http-vs-akka-grpc , w jego przypadku (i myślę, że dotyczy to twojego), wąskie gardło może wynikać z wysokiego zużycia pamięci w protobuf (de ), która z kolei wyzwala więcej wywołań do modułu wyrzucania elementów bezużytecznych.
Batato,
dzięki, ciekawe, mimo że już rozwiązałem swój problem
simpadjo
0

Jestem pod dużym wrażeniem tego, jak dobrze spisała się tutaj Akka TCP: D

Nasze doświadczenie było nieco inne. Pracowaliśmy nad znacznie mniejszymi instancjami przy użyciu klastra Akka. W przypadku zdalnego sterowania Akka zmieniliśmy z Akka TCP na UDP za pomocą Artery i osiągnęliśmy znacznie wyższą szybkość + niższy i bardziej stabilny czas reakcji. W Artery jest nawet konfiguracja pomagająca zrównoważyć zużycie procesora i czas reakcji od zimnego startu.

Sugeruję, aby użyć frameworka opartego na UDP, który również dba o niezawodność transmisji dla Ciebie (np. Artery UDP), i po prostu serializować przy użyciu Protobuf, zamiast pełnego gRPC. Kanał transmisji HTTP / 2 nie jest tak naprawdę przeznaczony do celów o wysokiej przepustowości i krótkim czasie odpowiedzi.

Wang Xian
źródło