Disclaimer: this is not another one gRPC hate article... Oh, wait...
Начнем издалека - знаете, всегда было интересно, а почему, собственно, для 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) и 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.Response{}, 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ломается (хотя это хак, как ни крути), иначе таких твитов бы не было, я думаю.