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ломается (хотя это хак, как ни крути), иначе таких твитов бы не было, я думаю.