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

1 天前
 RedisMasterNode

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

背景

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

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

出于一些原因,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 的方式就是:

说起来简单,不妨先试想一下这些步骤具体是怎么做的。假设我(对 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 otlpExportTracesHandler(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{})
}

结果如下:

编译二进制包体积:

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

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

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

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

总结

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

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

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

1460 次点击
所在节点    Go 编程语言
20 条回复
JimLee0921
1 天前
最近刚好学 Go 学到这里,收藏一下
czyt
1 天前
Desdemor
1 天前
我们内部通信一直走的是 http2
qW7bo2FbzbC0
1 天前
AI 翻译的?
swananan
1 天前
CPU 使用率:降低 36%

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

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

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

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

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

谢谢提醒 prutal 的方案!不过很可惜应该不太可能将已有的( easyproto 的)使用再作替换,作为对比是很不错的,如果未来还需要再次调研的话会加入比较 :)
pluswu1986
22 小时 42 分钟前
@iyaozhen 这玩意支持 optional 么。。gogoprotobuff 不维护了导致没有 optional 支持一直是我们这是用 gogopb 的一个痛点
ericFork
20 小时 51 分钟前
恰好工作中 OpenTelemetry, gRPC 和 VictoriaMetrics 和 VictoriaLogs 都在用,读下来感觉不仅收获很大,而且文章的组织也很舒适 👏
slowman
20 小时 37 分钟前
君 markdown 本当上手
crime1024
20 小时 10 分钟前
一般企业喜欢造轮子,内部私有的协议
iyaozhen
18 小时 2 分钟前
@pluswu1986 细节不知道,是 rpc 框架底层集成了。我之前自己实现了套不需要生成代码 可以(反)序列化的方式,想迁移到 prutal ,但我的场景不在乎性能,就没多少动力

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1168006

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX