Disclaimer: this is not another one gRPC hate article… Oh, whait…
Начнем издалека — знаете, всегда было интересно, а почему, собственно, для golang существует такое большое разнообразие библиотек, для каких-то часто используемых сущностей, как-то — роутеры http (fasthttprouter забыли, как подсказали в коментах) или cache?
С выбором RPC вроде все просто, gRPC — наше всё (вы, кстати, в курсе, что g здесь — это не Google внезапно). Но не тут-то было…
Все просто без ума от Мэри gRPC (нет).
Начнем с того, что в golang изначально реализовали net/rpc со своим сериализатором gob. Типа есть потребность — в golang есть решение из коробки (так же история, что и с роутером http — он есть, но все используют сторонние решения из-за параметризованных путей запросов). И тут засада — этот rpc можно только между golang приложениями использовать. Потом выкатили gRPC и все заверте… Вкратце — gRPC использует http/2 и protobuf для сериализации (запомним, rpc — это протокол обмена плюс сериализатор). Причем gRPC реализация доступна для многих языков, фактически нет привязки, на чем писать клиентскую и серверную часть. So far, so good…
Однако не все так гладко… Понятно стремление Google объять все возможные кейсы, но! К оригинальной реализации gRPC со временем появилось куча вопросов. Иначе как объяснить, что куча контор начали пилить свои собственные реализации RPC (и/или сериализаторов)? Также, внезапно, выяснилось, что требования к RPC внутри облака (читай между микросервисами) и RPC между клиентами за пределами облака/датацентра и сервисами внутри него (за ingress/proxy/load balancer — как хотите называйте) как бы «немножко» разные? Да и выбор http/2 в качестве транспорта — ну кто-же знал, что внедрёж пойдет не так (быстро), как ожидалось.
Начнем с сериализаторов, общепризнанный фаворит — gogo/protobuf (форк golang/protobuf), генерирует более быстрый код сериализации за счет переиспользования памяти и отказа о рефлексии/указателей, а так же других оптимизаций, но постойте — он же Deprecated (и теперь ищут new ownership)? А это потому, что после перехода Google на protobuf API v2, разработчики gogo предпочли забить на проект (это прискорбно), чем переписать его код почти целиком. Хотя вот пример, как с gogo на API v2 переходили — Things Learned From Trying to Migrate To Protobuf V2 API from gogoprotobuf (So Far).
Но есть еще энтузиасты — зацените vtprotobuf. Парни из Vitess заморочились, и таки написали свой сериализатор под protobuf API v2, причины и цифры смотрим в статье A new Protocol Buffers generator for Go.
Кстати — не protobuf единым, как говорится, например та же Google когда-то замутила flatbuffers сериализатор. Интересно то, что gRPC вообще-то поддерживает кастомные сериализаторы, а не только protobuf из коробки. Вот пример проекта Dgraph (которые начинали как раз с net/rpc с flatbuffers вместо gob), а потом перешли на gRPC, но тоже с flatbuffers — Custom encoding: Go implementation in net/rpc vs grpc and why we switched.
Вообще, как упоминалось ранее — есть 100500 разных реализаций отдельных сущностей (наверное, это все-таки не проблема конкретно golang), вот github репа, где сравнивается производительность всех (наверное) существующих сериализаторов для golang, правда результаты там довольно странные по состоянию на сейчас (gob медленнее JSON — это как вообще?), если сравнивать по годам:
2022/09/05 — Go 1.16.5 linux/amd64 i7-3630QM
|
benchmark |
iter |
time/iter |
bytes/op |
allocs/op |
|
Json_Marshal-8 |
189709 |
6090 |
151 |
208 |
|
Json_Unmarshal-8 |
92833 |
12751 |
151 |
383 |
|
Gob_Marshal-8 |
71692 |
16463 |
163 |
1616 |
|
Gob_Unmarshal-8 |
14772 |
84385 |
163 |
7688 |
|
Goprotobuf_Marshal-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_Marshal-8 |
4294477 |
280 |
46 |
48 |
|
Musgo_Unmarshal-8 |
2498404 |
480 |
46 |
96 |
2021/06/21 — Go 1.16.5 linux/amd64 i7-3630QM
|
benchmark |
iter |
time/iter |
bytes/op |
allocs/op |
|
Json_Marshal-8 |
501478 |
2538 |
151 |
208 |
|
Json_Unmarshal-8 |
226456 |
5023 |
151 |
383 |
|
Gob_Marshal-8 |
1320562 |
882 |
63 |
40 |
|
Gob_Unmarshal-8 |
1000000 |
1041 |
63 |
112 |
|
Goprotobuf_Marshal-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_Marshal-8 |
12882543 |
86 |
0 |
0 |
|
Musgo_Unmarshal-8 |
3381966 |
343 |
96 |
96 |
В другом месте нашлись более «релевантные» результаты:
2022/03/19 Go 1.17.8 Darwin/arm64 Apple M1 Max
|
benchmark |
iter |
time/iter |
bytes/op |
allocs/op |
|
Json_Marshal-8 |
1440837 |
822 |
148 |
208 |
|
Json_Unmarshal-8 |
653754 |
1817 |
148 |
399 |
|
Gob_Marshal-8 |
2750721 |
440 |
63 |
40 |
|
Gob_Unmarshal-8 |
2918254 |
410 |
63 |
112 |
|
Goprotobuf_Marshal-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_Marshal-8 |
22535546 |
53 |
48 |
0 |
|
Musgo_Unmarshal-8 |
12952696 |
90 |
48 |
96 |
В общем, gogo быстрее в два раза реализации от Google. Кстати, можно заметить в таблице некий musgo — очень даже неплохо себя показывает (ибо codegen). Вероятно, в таблицу стоило вставить достаточно известный msgpack — проект от opensource сообщества, который все никак не взлетит как следует (но подвижки вроде есть). Для дополнительного чтения — Зоопарк в Golang MSA. Protobuf, MessagePack, Gob – что выбрать?
Идем дальше. Все чаще разрабы задаются вопросом, а чой-та golang gRPC такой монструозный в плане оверхеда на зависимости? И почему под капотом у него собственная реализация http/2 стека, а не переиспользование пакета «golang.org/x/net/http2» (ну да, типы и конфиги из него используются, но не более). И вообще — не так все гладко с пробросом http/2 через load balancers.
Дабы решить две упомянутые проблемы — зависимости от кода (читай, постоянной войны с багами и breaking changes, которые в Google, видимо — «нормальное» явление) и поддержки http 1.1, в Twitch запилили свой фреймворк Twirp (кстати, http/2 тоже поддерживается из стандартной библиотеки golang) — Twirp: a sweet new RPC framework for Go, о нем и на Habr тоже писалось — Twirp против gRPC. Стоит ли?
По тем же причинам в Storj тоже разработали свою альтернативу gRPC — DRPC, см. статью Introducing DRPC: Our Replacement for gRPC, причем они рассматривали Twirp, как возможное решение, но в нем не оказалось нужной фичи — стриминга (как в gRPC), которую в DRPC тоже реализовали.
Постойте-ка, до сих пор все разговоры велись о RPC между, условно говоря, облаком и клиентами на PC/Mobile. А зачем такие навороты для взаимодействия микросервисов? Почему не plain TCP (или даже UDP, в сетевых игрушках так делают иногда)? Ах, да — net/rpc же есть (что вам еще нужно-то, как бы спрашивает Google).
Нужно больше производительности и фич! Так появилась сначала библиотека valyala/gorpc, а затем и valyala/fastrpc от Александра Валялкина, автора fasthttp (читать про неё тут на Habr — Грехи оптимизации производительности).
При ближайшем рассмотрении оказывается, что на самом деле RPC реализаций много (например rpcx, kitex, arpc, сравнение их производительности с gRPC и net/rpc — 2022 Go Ecosystem rpc Framework Benchmark), но на слуху у всех gRPC как некая «серебряная пуля».
И про UDP based RPC — есть проект Hprose (High Performance Remote Object Service Engine) от китайских товарищей, он поддерживается для многих языков, и для golang тоже есть реализация, так вот — там есть поддержка UDP. Кроме того, вышеупомянутый rpcx поддерживает TCP, HTTP, QUIC (который под капотом UDP) and KCP (так сказать китайский вариант QUIC, тоже на UDP).
Ну и напоследок, к вопросу как работает gRPC под капотом… Оказывается, есть простой способ его ускорить. Тут вот какие-то слоупоки пишут в 2022 году-то The Mysterious Gotcha of gRPC Stream Performance, у нас такой трюк в PROD уже года 4 используется: как известно, в gRPC есть простые вызовы и стриминговые, так вот — если сделать пул стримов вместо простого вызова, то все работает быстрее приблизительно в два раза (с последовательными или конкурентными запросами — неважно), абстрактный пример:
api.proto
syntax = "proto3"; package pb message Request {} message Response {} service Service { rpc Unary (Request) returns (Response); rpc Stream (stream Request) returns (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 } resp, _ := 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() }
Но никто не знает, когда это cломается (хотя это хак, как ни крути), иначе таких твитов бы не было, я думаю.

ссылка на оригинал статьи https://habr.com/ru/post/688756/
Добавить комментарий