etcdv3 与 protoc-gen-go 对 grpc 依赖的冲突

1. 背景

在 go 的项目中同时引用较新版本的 etcd 和 protoc-gen-go 会产生一些问题,具体表现为:

1
2
3
4
5
6
7
8
9
10
11
go: finding module for package google.golang.org/grpc/naming
go: finding module for package github.com/coreos/bbolt
go: found github.com/coreos/bbolt in github.com/coreos/bbolt v1.3.6
go: node/watch imports
go.etcd.io/etcd/clientv3 tested by
go.etcd.io/etcd/clientv3.test imports
github.com/coreos/etcd/auth imports
github.com/coreos/etcd/mvcc/backend imports
github.com/coreos/bbolt: github.com/coreos/bbolt@v1.3.6: parsing go.mod:
module declares its path as: go.etcd.io/bbolt
but was required as: github.com/coreos/bbolt

或者

1
2
3
4
5
6
uapm-agent/watch imports
go.etcd.io/etcd/clientv3 tested by
go.etcd.io/etcd/clientv3.test imports
github.com/coreos/etcd/integration imports
github.com/coreos/etcd/proxy/grpcproxy imports
google.golang.org/grpc/naming: module google.golang.org/grpc@latest found (v1.45.0), but does not contain package google.golang.org/grpc/naming

这都是由于 etcdv3 依赖了较低版本的 grpc(v1.26.0) 导致的。

想立刻知道解决办法的可以直接拉到第三节查看。

2. 探索

首先来看一下我所使用的 etcd 和 protobuf 的版本:

1
2
etcd: go.etcd.io/etcd v3.3.27
proto-gen-go: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

最开始我生成 proto 代码是通过执行 protoc -I=infra --go-grpc_out=. --go_out=. infra/library/proto/service/{agent}/*.proto ,这个语句包含 2 个 out 模块,其中 –go_out=. 用于生成 go 语言的 proto 代码。 –go-grpc_out=. 用于生成定于在 proto 文件中 service 相关的绑定和注册[^1],如果不使用该模块,则不会生成服务注册相关的逻辑代码,只会生成 message 定义的对象相关的代码。但是通过对比发现,正是 go-prgc_out 生成的 proto.go 文件会引用最新版本的 grpc ,而最新版本的 grpc 缺失了一部分 etcdv3 中依赖的模块,例如 naming。因此 etcd 一直锁定了对 grpc 1.26.0 版本的依赖,当我们生成代码的时候会导致修改 go.mod 中对 grpc 的依赖,也就有了上面的问题。

但实际上 protoc-gen-go-grpc 在早起使用 protoc 生成 go 代码文件的时候并不是必须的,而是通过执行 protoc -I=infra --go_out=plugins=grpc:. infra/library/proto/service/agent/*.proto 来完成的,这个命令同样会生成 service 和 client 相关的逻辑代码。

那既然只使用 protoc-gen-go 再绑定 plugin 的方式就能生成我们需要源代码,google 为什么还要搞出个 protoc-gen-go-grpc 来专门生成 service 注册的代码呢,其实看上面[^1] 的官方说明就明白了:新的 protoc-gen-go-grpc 在生成 service 中定义的 method 对应的代码之外,还会生成一个名为 UnimplementedXXXServer 的结构体,该结构体同样包含了定义在 service 中的 method,只不过 method 的内容都是返回 Unimplemented error,这么做是为了向未来兼容。这样做的好处是如果目标 server 端并没有注册实现该 service 也不会导致 panic,并且会向 client 返回 UnimplementedXXXServer error。

不够这一点其实并不是刚需,所以完全可以抛弃掉 protoc-gen-go-grpc 这个组件,仅使用 protoc-gen-go 即可。

3. 解决

办法其实很简单,不需要给 protobuf 也就是 protoc 降级,只需要:

  1. 将 gomod 中对 grpc 的依赖强制改成 1.26.0 保证 etcd 正常使用;
    • replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
  2. 将 etcd 找不到的 bbolt 包帮它放回它期望的位置:
    • replace github.com/coreos/bbolt v1.3.6 => go.etcd.io/bbolt v1.3.6
  3. 把 protoc-gen-go 降级到 1.3.0
  4. 然后改一下 protoc 执行的语句即可:
    • protoc -I=infra –go_out=plugins=grpc:. infra/library/proto/service/agent/*.proto

然后在项目路径下执行 go mod tidy 就会自动适配好其他依赖的库版本,然后,搞定~

4. 结语

这个问题其实很苟,命名 etcd 只要把最新版本 grpc 确实的 naming 模块抽出来独立实现就能保持第三方库的 update to date,就是偏不,就是要守旧,才导致我们广大开发者左右为难,即使现在通过降级保证能用了,但也失去了对最新版本的 grpc 使用的机会,也会老版本的 grpc 存在较为严重的 bug 的时候,我们就更为难了,有时候这也算是某种程度的技术制裁吧。希望 etcd 能够早日更新 grpc 版本把,etcd 更新日,家祭无忘告翁。

5. 注脚

[^1]:见 protoc-gen-go-grpc 官网说明;

6. 参考