为什么不用 gRPC-Go: VictoriaTraces 中实现 OTLP/gRPC 的幕后故事 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
RedisMasterNode
V2EX  nbsp; Go 编程语言

为什么不用 gRPC-Go: VictoriaTraces 中实现 OTLP/gRPC 的幕后故事

  •  1
     
  •   RedisMasterNode 55 天前 2969 次点击
    这是一个创建于 55 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我们不妨先从结论开始,不使用 gRPC-Go 来构建 OTLP/gRPC 的 gRPC Server ,可以让:

    • 二进制包的体积:降低 25%
    • CPU 使用率:降低 36%

    背景

    OpenTelemetry 协议( OTLP )是 “有 OpenTelemetry 插装的应用” 与 “OpenTelemetry (兼容的) Collector/后端” 之间进行数据传输所用的协议。

    现在假设你就有这样一个应用,想要传输数据到 Collector ,那么你可以配置通过以下 Exporter 完成:

    • OTLP/gRPC Exporter
    • OTLP/HTTP Exporter, 其中 Protobuf 的 Payloads 可以编码成:
      • - binary 格式
      • - JSON 格式

    出于一些原因,VictoriaTraces 仅暴露了一个 HTTP 接口,通过 OTLP/HTTP 接收 binary 或者 JSON 格式的数据。然而,有很多的应用只支持通过 OTLP/gRPC Exporter 输出数据,kube-apiserver 就是其中的一个典型例子。所以,完善对 OTLP/gRPC 的支持是当前的刚需。

    目标

    我们的目标是实现一个 gRPC Server,它作为 TraceService 服务提供 Export 方法给远程调用方进行调用,这些信息是定义在 OpenTelemetry Proto 中的。

    // Service that can be used to push spans between one Application instrumented with // OpenTelemetry and a collector, or between a collector and a central collector (in this // case spans are sent/received to/from multiple Applications). service TraceService { rpc Export(ExportTraceServiceRequest) returns (ExportTraceServiceResponse) {} } 

    那么,是什么原因让我们考虑弃用 gRPC-Go ,或者更准确地说,弃用整套 protoc 工具链呢?

    原因 1: Protoc 工具链不好用

    以 Go 为例,最常见的构建 gRPC Server 的方式就是:

    • protocprotoc-gen-go 生成 .proto 中定义的 Message 对应的结构体。
    • protocprotoc-gen-go-grpc 生成 .proto 中定义的服务 Interface 并实现它。

    说起来简单,不妨先试想一下这些步骤具体是怎么做的。假设我(对 Protobuf 熟悉程度一般)现在 git clone 了一个项目,然后想往其中的 .proto 添加几个新的 Message 和方法:

    1. 首先想起来 protoc 并不是 Ubuntu/MacOS 自带的;
    2. Release 页下载最新版本的 protoc
    3. 诶,光靠 protoc 不能生成 Go 代码,所以还需要 go install protoc-gen-gogo install protoc-gen-go-grpc
    4. 都准备好了,生成代码的命令是什么来着? Google 一下 “gRPC 如何编译 Go 代码”;
    5. 终于,抄到了命令在本地运行,回车。Boom !弹了个依赖 Error ,因为 .proto 里面有一些 import,还要将这些引用的内容的目录在命令中指定清楚;
    6. 整理好所有目录和命令,重新运行,终于成功生成出了代码。

    不过,别高兴得太早,protoc 可能还有惊喜在等你。

    “为什么这些新生成的代码看起来和老代码好像不太一样?”

    新代码:

    type TracesData struct { state protoimpl.MessageState `protogen:"open.v1"` ResourceSpans []*ResourceSpans `protobuf:"bytes,1,rep,name=resource_spans,json=resourceSpans,proto3" json:"resource_spans,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } 

    老代码:

    type TracesData struct { ResourceSpans []*ResourceSpans `protobuf:"bytes,1,opt,name=resource_spans,proto3" json:"resource_spans,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } 

    7.Google 对应原因,行,然后用不同版本的 protoc 工具链重新走了遍步骤 2-6 。(甚至有时候还要 git log 找之前的作者问用的是哪个版本。)

    幸好,这些步骤只是个玩笑,在 VictoriaMetrics 里面我们不这么干。这只是想说明,编译 Protobuf 相关的内容并不是那么直来直去的,不像写个 HTTP JSON 接口那么简单。

    当然,它可以变得简单一点:

    1. 把所有的命令都写到 Makefile
    2. 或者使用 Buf CLI,把代码生成完全在线化。

    不过仍然有很多工程师就是喜欢 HTTP JSON 。

    尽管麻烦不断,但是这些仍不足以说服我们弃用 protoc 工具链。还有别的理由 (借口) 吗?

    写在原因 2 之前

    原因 2 或许能给你新的启发,不过需要声明,因为 VictoriaTraces 中存在一些历史包袱,它才能在该项目中作为一个 “原因”。也就是说,这个 “原因” 并不是每个项目都该纳入考虑的。

    原因 2: easyproto 已经代替了 golang/protobuf

    在 VictoriaMetrics ,VictoriaLogs 和 VictoriaTraces 中,对于 Protobuf 的内容,我们是用 easyproto 来进行 Marshal 和 Unmarshal 操作的,而非常见的 golang/protobuf 包。原因在 easyprotoREADME 中写得很清楚:

    • easyproto 不需要 protoc 或者 go generate
    • easyproto 不会像传统的 protoc 那样让编译的二进制包的体积增大。
    • easyproto 正确使用的话可以达成零内存分配。

    不过,如果真的要实现 OTLP/gRPC 支持,我们得考虑:

    1. 如果用了 protoc 生成 gRPC Server ,那编译的二进制包会大多少
    2. 有可能将 easyproto 和 gRPC 结合起来用吗?protoc 只需要生成 gRPC 服务的代码,而 Protobuf Message 的结构体还是用 easyproto 处理。
      • 注意这仍然需要引入 gRPC 相关的包,这会弱化使用 easyproto 的第二个理由(减小二进制包体积)。
    3. 还有其他办法可以复用现有的 easyproto,而不用引入任何新的包吗

    邪门歪道: 用 HTTP/2 Server 代替 gRPC Server

    按理来说

    gRPC 是个实现在 HTTP/2 上的协议,所以按理来说,实现一个 HTTP/2 Server 来处理对应接口的请求就可以了。

    gRPC 也可以实现在 HTTP/3 (QUIC) 或者 HTTP/1.1 之上,不过这已经超过了这篇博客讨论的范围,我们把它留给读者探索。

    根据 gRPC over HTTP2,Data Frame 的格式是这样的:

    // +------------+---------------------------------------------+ // | 1 byte | 4 bytes | // +------------+---------------------------------------------+ // | Compressed | Message Length | // | Flag | (uint32) | // +------------+---------------------------------------------+ // | | // | Message Data | // | (variable length) | // | | // +----------------------------------------------------------+ 

    同时很容易知道,TraceService 服务的 Export 方法对应的 HTTP 接口是 /opentelemetry.proto.collector.trace.v1.TraceService/Export

    下面这段代码简单展示了如何用 HTTP/2 Server 处理 gRPC 请求:

    // Init 启动一个 HTTP Server 。 func Init() { logger.Infof("starting OTLP gPRC server at :4317...") go httpserver.Serve( []string{":4317"}, OTLPGRPCRequestHandler, httpserver.ServeOptions{UseProxyProtocol: nil, DisableBuiltinRoutes: true, EnableHTTP2: true}, ) } // OTLPGRPCRequestHandler 管理 gRPC 请求的路由。 func OTLPGRPCRequestHandler(r *http.Request, w http.ResponseWriter) bool { switch r.URL.Path { case `/opentelemetry.proto.collector.trace.v1.TraceService/Export`: otlpExportTracesHandler(r, w) default: grpc.WriteErrorGrpcResponse(w, grpc.StatusCodeUnimplemented, fmt.Sprintf("gRPC method not found: %s", r.URL.Path)) } return true } // otlpExportTracesHandler 处理 OTLP Export 请求。 func otlpExportTracesHndler(r *http.Request, w http.ResponseWriter) { // gzip 解压缩 ... // 解析前 5 bytes ,用 easyproto unmarshalling 剩余 []bytes ... // 写数据等操作 ... writeExportTraceServiceResponse() } 

    完整的代码可以查看 VictoriaTraces #59

    代价是什么?

    这个实现看起来简单直接,那作为交换,一定有什么代价吧?

    到目前为止这个实现只在 Unary RPC 中测试过,而对于 Streaming RPC ,我们没有场景和动力去做相关的测试,所以暂且认为它是只能处理 Unary RPC 请求的。

    不过这样的实现足以覆盖我们在 OTLP/gRPC 上的需求,它可能在其他场景不适用,如果你知道具体是哪些场景,非常欢迎在评论中发表看法!

    对比测试

    我们做了一轮测试来对比 VictoriaTraces 中不同 OTLP/gRPC 实现方案的二进制包的体积资源使用率,这些方案包括:

    1. 用原生 HTTP/2 Server 处理请求,用 easyproto 来处理 Protobuf Message 。
    2. protoc 生成 gRPC Server 代码,用 gRPC 原生的 Encoder 和 Decoder 来处理 Protobuf Message 。

    另外,生成的 gRPC Server 也支持通过以下代码自定义 Encoder 和 Decoder ,所以我们也用将 easyproto 设为 Encoder 和 Decoder 作为另一组对比。

    import ( "google.golang.org/grpc/encoding" ) func init() { encoding.RegisterCodec(&easyProtoCodec{}) } 

    结果如下:

    编译二进制包体积:

    • Release 的所有二进制包总体积 (tar.gz):

      • HTTP/2 + easyproto: 87M
      • gRPC + easyproto: 113M (+29%)
      • gRPC: 113M (+29%)
    • 单个 linux-amd64 包体积:

      • 参考基准 (v0.4.0): 21M
      • HTTP/2 + easyproto: 21M (+0%)
      • gRPC + easyproto: 28M (+33%)
      • gRPC: 28M (+33%)

    请求处理的资源使用率( CPU 使用率, no-op: 对每个请求仅进行 Decompression 和 Unmarshalling):

    • HTTP/2 + easyproto: 31.3%
    • gRPC + easyproto: 45.6% (+45%)
    • gRPC: 49.1% (+56%)

    • 资源监控 Snapshot 可以查看这里

    • CPU 和内存的 Profiles 可以在这里下载。

    从这些结果可以看出,HTTP/2 + easyproto 确实更占优一些。

    总结

    这篇博客分享的是“为什么 VictoriaTraces 用 HTTP/2 + easyproto 来实现 OTLP/gRPC 所需的 gRPC Server”。它的关键实现是由 JayiceZ 完成的,而最初的想法来自于 @makasim

    这个实现的背后有很多特定的原因,我们并不是想说服你也这样做,但是我们在测试中确实看到了这种方案的潜力和价值。

    VictoriaStack 的亮点是高性能和资源优化,所以每一分 CPU 、内存和网络流量都很重要。当然,这同样也适用于二进制包、Docker 镜像的体积等等,就如这些要素也曾在 Aliaksandr Valialkin( VictoriaMetrics 的作者)写的这篇博客中被提到,一直以来它们都没变过。

    第 1 条附言    52 天前

    PS: 再次强调一下结论哈哈,以免又没细看内容的伙伴误会

    • 这是个好玩的实验,证明一些特殊玩法适合特殊(历史背景和使用场景的)项目,以及 gRPC-Go 是(相对)低效的。
    • 但是没有推荐普通业务中去这样做,特别是在没有资源效率诉求的前提下直接去这样做 :)
    23 条回复    2025-10-25 23:29:32 +08:00
    JimLee0921
        1
    JimLee0921  
       55 天前   1
    最近刚好学 Go 学到这里,收藏一下
    czyt
        2
    czyt  
       55 天前   1
    Desdemor
        3
    Desdemor  
       55 天前   1
    我们内部通信一直走的是 http2
    qW7bo2FbzbC0
        4
    qW7bo2FbzbC0  
       55 天前   1
    AI 翻译的?
    swananan
        5
    swananan  
       55 天前
    CPU 使用率:降低 36%

    我没太细看帖子内容,我就好奇一个点,为啥 CPU 性能差这么多,这块有分析过细节原因吗,感觉成熟的轮子,不应该有这么大优化空间。
    xiuming
        6
    xiuming  
       55 天前
    看了半天也没看懂在说什么
    mcfog
        7
    mcfog  
       55 天前   1
    某些特殊服务自己封装 grpc 实现跑了几年了,客户端服务端都有
    Google 的实现一直都是神鬼两面性
    mcfog
        8
    mcfog  
       55 天前   1
    @swananan 如果 http 服务是 10 个功能点,http 上叠上 grpc unary 是 12 个,叠上完整 grpc 是 20 个功能的话,估计 Google 的 grpc-go 实现大概有 50 个功能点吧
    RedisMasterNode
        9
    RedisMasterNode  
    OP
       55 天前 via Android
    @mcfog 100%
    RedisMasterNode
        10
    RedisMasterNode  
    OP
       55 天前 via Android
    @czyt 确认过好用~!
    eijnix
        11
    eijnix  
       55 天前   1
    你说的几个确实是 protobuf go 的痛点,非常痛,但是确实也不会用这个 easyproto ,毕竟没维护保障。为啥国内没有个公司做 buf 的事,那个 proto 注册中心的收费有点离谱,按类型收费
    RedisMasterNode
        12
    RedisMasterNode  
    OP
       55 天前 via Android
    @eijnix 用就不必用,因为这个也不是纯面相使用体验的,要手写很多结构体的序列化反序列化只会比自动生成更辛苦。

    我们要用是因为''很多(反)序列化和结构体都已经实现好了''。

    至于 buf.build ,之前个人使用体验感觉挺好的,而且注册之后可以解除限流,个人使用量肯定是够的(企业级调用量的话就不确定了,不过为什么有这么多需要持续编译 proto 的场景?)
    DefoliationM
        13
    DefoliationM  
       55 天前   1
    有同样的问题,protobuf 和 grpc 都占很多体积,还有内存泄漏,但现在没有太多选择。我后面可能会直接把 protobuf 换掉,一直在找更好的 idl 替代品。
    iyaozhen
        14
    iyaozhen  
       55 天前   1
    楼主有个小建议,可以简单说下背景

    大部分人还是只知道 http ,看你这文章可能有点懵。
    而且 OpenTelemetry 协议( OTLP )很多人也不知道 日志、指标和跟踪这一套,小公司根本没这一块,大公司也是专门的 SRE 来支持

    关于 pb 编解码可以试试字节的 https://github.com/cloudwego/prutal 不要看 star 少,因为场景有限。内部已经大量应用了 和你们 easyproto 对比下
    RedisMasterNode
        15
    RedisMasterNode  
    OP
       55 天前
    @iyaozhen 感谢!这篇博客的目标用户确实是窄了,稍微补充背景可以让普通的 gRPC 用户更容易明白。 <3

    谢谢提醒 prutal 的方案!不过很可惜应该不太可能将已有的( easyproto 的)使用再作替换,作为对比是很不错的,如果未来还需要再次调研的话会加入比较 :)
    pluswu1986
        16
    pluswu1986  
       55 天前
    @iyaozhen 这玩意支持 optional 么。。gogoprotobuff 不维护了导致没有 optional 支持一直是我们这是用 gogopb 的一个痛点
    ericFork
        17
    ericFork  
       55 天前   1
    恰好工作中 OpenTelemetry, gRPC 和 VictoriaMetrics 和 VictoriaLogs 都在用,读下来感觉不仅收获很大,而且文章的组织也很舒适
    slowman
        18
    slowman  
       55 天前
    君 markdown 本当上手
    crime1024
        19
    crime1024  
       55 天前
    一般企业喜欢造轮子,内部私有的协议
    iyaozhen
        20
    iyaozhen  
       55 天前
    @pluswu1986 细节不知道,是 rpc 框架底层集成了。我之前自己实现了套不需要生成代码 可以(反)序列化的方式,想迁移到 prutal ,但我的场景不在乎性能,就没多少动力
    zhaoahui
        21
    zhaoahui  
       53 天前
    别啥都自己造轮子 全是技术债
    RedisMasterNode
        22
    RedisMasterNode  
    OP
       53 天前
    @zhaoahui 哈哈
    RedisMasterNode
        23
    RedisMasterNode  
    OP
       53 天前
    @zhaoahui 可能你只看了标题 估计是
    关于     帮助文档     自助推广系统     博客         FAQ     Solana     2905 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 14:06 PVG 22:06 LAX 06:06 JFK 09:06
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86