Golang RPC und alles-alles-alles … / Sudo Null IT News

Haftungsausschluss: Dies ist kein weiterer gRPC-Hassartikel … Oh, was …

Fangen wir von weitem an – wissen Sie, es war schon immer interessant, warum es tatsächlich eine so große Vielfalt an Bibliotheken für Golang gibt, für einige häufig verwendete Entitäten, irgendwie – http-Router (schneller httprouter vergessen, wie in den Kommentaren vorgeschlagen) oder Zwischenspeicher?

Mit der Wahl von RPC scheint alles einfach zu sein, gRPC ist unser Ein und Alles (Sie wissen übrigens, dass g hier nicht Google ist plötzlich). Aber es war nicht da…

Alle sind verrückt nach Mary gRPC (nein).

Beginnen wir mit der Tatsache, dass golang ursprünglich net/rpc mit seinem gob-Serializer implementiert hat. Typ gibt es einen Bedarf – in Golang gibt es eine Lösung “out of the box” (die gleiche Geschichte wie beim http-Router – es ist, aber jeder verwendet Lösungen von Drittanbietern aufgrund parametrisierter Anforderungspfade). Und hier ist ein Hinterhalt – dieser RPC kann nur zwischen Golang-Anwendungen verwendet werden. Dann haben sie gRPC eingeführt und alles eingepackt … Kurz gesagt – gRPC verwendet http/2 und protobuf für die Serialisierung (denken Sie daran, dass rpc ein Austauschprotokoll plus Serializer ist). Darüber hinaus ist die gRPC-Implementierung für viele Sprachen verfügbar, tatsächlich gibt es keine Bindung, was die Client- und Serverteile zu schreiben sind. So weit, ist es gut…

Es ist jedoch nicht alles so glatt … Es ist verständlich, dass Google bestrebt ist, alle möglichen Fälle zu erfassen, aber! Im Laufe der Zeit tauchten viele Fragen zur ursprünglichen Implementierung von gRPC auf. Wie sonst lässt sich erklären, dass eine Reihe von Firmen damit begonnen haben, ihre eigenen RPC-Implementierungen (und / oder Serialisierer) zu schneiden? Außerdem stellte sich plötzlich heraus, dass die Anforderungen für RPC innerhalb der Cloud (Lesen zwischen Microservices) und RPC zwischen Clients außerhalb der Cloud / Rechenzentrum und Diensten darin (hinter dem Ingress / Proxy / Load Balancer – wie auch immer Sie es nennen wollen ) sind irgendwie “ein wenig” anders? Und die Wahl von http/2 als Transportmittel – na ja, wer hätte gedacht, dass die Implementierung wie erwartet (schnell) schief gehen würde.

Beginnen wir mit Serialisierern, der allgemein akzeptierte Favorit ist gogo/protobuf (Gabel golang/protobuf), generiert schnelleren Serialisierungscode durch Wiederverwendung des Speichers und Reflektion/Zeigerablehnung sowie andere Optimierungen, aber warten Sie – auch bekannt als Deprecated (und now auf der Suche nach neuen Eigentümern)? Und das liegt daran, dass nach der Umstellung von Google auf protobuf-API v2entschieden sich die Gogo-Entwickler dafür, das Projekt aufzugeben (was bedauerlich ist), anstatt den Code fast vollständig neu zu schreiben. Obwohl hier ein Beispiel dafür ist, wie sie von gogo auf API v2 umgestiegen sind – Erkenntnisse aus dem Versuch, von gogoprotobuf auf die Protobuf V2-API zu migrieren (bisher).

Aber es gibt immer noch Enthusiasten – check it out vtprotobuf. Jungs aus Vitesse verwirrt wurden und trotzdem einen eigenen Serializer unter der protobuf-API v2 geschrieben haben, siehe die Gründe und Zahlen im Artikel Ein neuer Protokollpuffergenerator für Go.

Übrigens – kein einziger Protobuf, wie sie zum Beispiel sagen, das gleiche Google einmal getrübt Flatbuffer Serialisierer. Interessanterweise unterstützt gRPC tatsächlich benutzerdefinierte Serialisierer, nicht nur protobuf von Haus aus. Hier ist ein Beispielprojekt dgraph (der mit net/rpc mit flatbuffers statt gob angefangen hat) und dann auf gRPC umgestiegen ist, aber auch mit flatbuffers – Benutzerdefinierte Codierung: Go-Implementierung in net/rpc vs. grpc und warum wir gewechselt haben.

Im Allgemeinen gibt es hier, wie bereits erwähnt, 100500 verschiedene Implementierungen einzelner Entitäten (wahrscheinlich ist dies immer noch kein Golang-spezifisches Problem). github Rübeder die Leistung aller (wahrscheinlich) existierenden Golang-Serializer vergleicht, obwohl die Ergebnisse dort im Moment ziemlich seltsam sind (gob ist langsamer als JSON – wie ist es im Allgemeinen?), wenn man sie über die Jahre vergleicht:

05.09.2022 – Go 1.16.5 Linux/Amd64 i7-3630QM

Benchmark

iter

Zeit/Iter

Byte/op

Zuweisungen/Op

Json_Marschall-8

189709

6090

151

208

Json_Unmarshal-8

92833

12751

151

383

Gob_Marschall-8

71692

16463

163

1616

Gob_Unmarshal-8

14772

84385

163

7688

Goprotobuf_Marschall-8

1405010

854

53

64

Goprotobuf_Unmarshal-8

973688

1255

53

168

Gogoprotobuf_Marshal-8

3359550

354

53

64

Gogoprotobuf_Unmarshal-8

1908633

619

53

96

Musgo_Marschall-8

4294477

280

46

48

Musgo_Unmarshal-8

2498404

480

46

96

21.06.2021 – Go 1.16.5 linux/amd64 i7-3630QM

Benchmark

iter

Zeit/Iter

Byte/op

Zuweisungen/Op

Json_Marschall-8

501478

2538

151

208

Json_Unmarshal-8

226456

5023

151

383

Gob_Marschall-8

1320562

882

63

40

Gob_Unmarshal-8

1000000

1041

63

112

Goprotobuf_Marschall-8

3247056

378

53

64

Goprotobuf_Unmarshal-8

1839267

651

53

168

Gogoprotobuf_Marshal-8

5886194

204

53

64

Gogoprotobuf_Unmarshal-8

3464098

345

53

96

Musgo_Marschall-8

12882543

86

0

0

Musgo_Unmarshal-8

3381966

343

96

96

An anderer Stelle mehr “relevante” Ergebnisse gefunden:

19.03.2022 Go 1.17.8 Darwin/arm64 Apple M1 Max

Benchmark

iter

Zeit/Iter

Byte/op

Zuweisungen/Op

Json_Marschall-8

1440837

822

148

208

Json_Unmarshal-8

653754

1817

148

399

Gob_Marschall-8

2750721

440

63

40

Gob_Unmarshal-8

2918254

410

63

112

Goprotobuf_Marschall-8

6831308

176

53

64

Goprotobuf_Unmarshal-8

5746256

210

53

168

Gogoprotobuf_Marshal-8

16528346

72

53

64

Gogoprotobuf_Unmarshal-8

12764978

94

53

96

Musgo_Marschall-8

22535546

53

48

0

Musgo_Unmarshal-8

12952696

90

48

96

Im Allgemeinen ist gogo doppelt so schnell wie die Implementierung von Google. Übrigens sieht man in der Tabelle ein gewisses muss – zeigt sich sehr gut (weil codegen). Wahrscheinlich hat es sich gelohnt, einen Bekannten in die Tabelle einzufügen Nachrichtenpaket – ein Projekt aus der Open-Source-Community, das immer noch nicht so startet, wie es sollte (aber es scheint einige Fortschritte zu geben). Für zusätzliche Lektüre – Zoo in Golang MSA. Protobuf, MessagePack, Gob – was soll man wählen?

Mach weiter. Immer öfter fragen sich Entwickler, warum golang gRPC so monströs in Bezug auf den Overhead von Abhängigkeiten ist? Und warum hat es unter der Haube eine eigene Implementierung des http/2-Stacks und nicht die Wiederverwendung des Pakets “golang.org/x/net/http2” (naja, ja, Typen und Konfigurationen davon werden verwendet, aber nein mehr). Und im Allgemeinen – nicht alles ist so glatt mit der Weiterleitung von http/2 über Load Balancer.

Um die beiden genannten Probleme zu lösen – Abhängigkeiten vom Code (lesen Sie, der ständige Krieg mit Fehlern und Breaking Changes, was bei Google anscheinend ein “normales” Phänomen ist) und Unterstützung für http 1.1, in Zucken reichte Ihren Rahmen ein Zwitschern (http/2 wird übrigens auch von der Golang-Standardbibliothek unterstützt) – Twirp: ein süßes neues RPC-Framework für Go, darüber wurde auch auf Habr – Twirp vs. gRPC geschrieben. Lohnt es sich?

Aus den gleichen Gründen in Storj auch eine eigene Alternative zu gRPC entwickelt – DRPCsiehe Artikel Wir stellen DRPC vor: Unser Ersatz für gRPCund sie betrachteten Twirp als mögliche Lösung, aber es hatte nicht die notwendige Funktion – Streaming (wie in gRPC), das auch in DRPC implementiert war.

Warten Sie eine Minute, bisher drehte sich alles um RPC zwischen, relativ gesehen, der Cloud und PC/Mobile-Clients. Und warum so viel Schnickschnack für das Zusammenspiel von Microservices? Warum nicht einfaches TCP (oder sogar UDP, sie machen das manchmal in Netzwerkspielzeugen)? Ach ja – net/rpc ist da (was braucht man sonst noch, als würde man Google fragen).

Benötigen Sie mehr Leistung und Funktionen! So erschien die Bibliothek zuerst. valyala/gorpcund dann valyala/fastrpc von Alexander Valyalkin, Autor schnell http (Lesen Sie dazu hier auf Habr – Sünden der Leistungsoptimierung).

Bei näherer Betrachtung stellt sich heraus, dass es tatsächlich viele RPC-Implementierungen gibt (z. rpcx, kitex, Arpcvergleicht ihre Leistung mit gRPC und net/rpc – 2022 Go Ecosystem rpc Framework-Benchmark), aber gRPC ist allen als eine Art „Wunderwaffe“ bekannt.

Und über UDP-basiertes RPC – es gibt ein Projekt Hprose (High Performance Remote Object Service Engine) von chinesischen Genossen, es wird für viele Sprachen und für unterstützt golang hat auch eine Implementierung, und so – es gibt UDP-Unterstützung. Außerdem unterstützt das obige rpcx TCP, HTTP, SCHNELL (das sich unter der Haube von UDP befindet) und KCP (sozusagen die chinesische Version von QUIC, ebenfalls auf UDP).

Und schließlich zu der Frage, wie gRPC unter der Haube funktioniert … Es stellt sich heraus, dass es eine einfache Möglichkeit gibt, es zu beschleunigen. Hier sind einige Slowpacks, die 2022 etwas schreiben Das mysteriöse Problem der gRPC-Stream-Performancewir verwenden diesen Trick in PROD bereits seit 4 Jahren: Wie Sie wissen, hat gRPC einfache Aufrufe und Streaming-Aufrufe und so – wenn Sie statt eines einfachen Aufrufs einen Pool von Streams erstellen, funktioniert alles etwa doppelt so schnell ( mit sequentiellen oder gleichzeitigen Anfragen – egal ), abstraktes Beispiel:

api.proto

syntax=”proto3″; Paket pb Nachricht Anfrage {} Nachricht Antwort {} Service Service { rpc Unary (Anfrage) gibt zurück (Antwort); rpc Stream (Stream Request) gibt zurück (Stream Response); }

server.go

func (s *grpcServer) Unary(ctx context.Context, req *pb.Request) (*pb.Response, error) { return &pb.ResponseDomain{}, nil } func (s *grpcServer) Stream(stream pb.Service_StreamServer) error { ctx := stream.Context() for { select { case <-ctx.Done(): return ctx.Err() default: } req, err := stream.Recv() if err == io.EOF { break } if err != nil { return err } bzw. _ := s.Unary(ctx, req) if err := stream.Send(resp); err != nil { return err } } return nil }

client.go

func (c *grpcClient) Call(ctx context.Context, req *pb.Request) (*pb.Response, error) { if !c.streams { return c.client.Unary(ctx, req) } stream := c .getStreamFromPool() if stream == nil { return nil, fmt.Errorf(“no stream”) } if err := stream.Send(req); err != nil { stream, err = c.client.Stream(ctx) if err != nil { return nil, err } } defer c.putStreamToPool(stream) return stream.Recv() }

Aber niemand weiß, wann es kaputt geht (obwohl dies ein Hack ist, was auch immer man sagen mag), sonst so Tweets wäre nicht, denke ich.

Similar Posts

Leave a Reply

Your email address will not be published.