记踩Etcd的一个坑

背景

最近一个服务引入了etcd,用于主从,编译能够正常通过,但是在run阶段出现如下panic:

2020-12-14 13:36:42.799342 I | WARNING: proto: message authpb.User is already registered
A future release will panic on registration conflicts. See:
https://developers.google.com/protocol-buffers/docs/reference/go/faq#namespace-conflict

2020-12-14 13:36:42.799365 I | WARNING: proto: message authpb.Permission is already registered
A future release will panic on registration conflicts. See:
https://developers.google.com/protocol-buffers/docs/reference/go/faq#namespace-conflict

2020-12-14 13:36:42.799373 I | WARNING: proto: message authpb.Role is already registered
A future release will panic on registration conflicts. See:
https://developers.google.com/protocol-buffers/docs/reference/go/faq#namespace-conflict

panic: proto: duplicate enum registered: authpb.Permission_Type

goroutine 1 [running]:
github.com/golang/protobuf/proto.RegisterEnum(0x70a4aac, 0x16, 0xc0005c4f30, 0xc0005c4f60)
	/Users/pingd/Go/pkg/mod/github.com/golang/protobuf@v1.4.2/proto/registry.go:104 +0x11f
go.etcd.io/etcd/auth/authpb.init.0()
	/Users/pingd/Go/pkg/mod/go.etcd.io/etcd@v0.5.0-alpha.5.0.20200821141407-46a0a44f9539/auth/authpb/auth.pb.go:115 +0x10a

通过上面提示,大致意思是:服务在启动时,有枚举出现了重复注册的情况,通过给出的链接往下跳,此次panic在这里:

// https://github.com/etcd-io/etcd/blob/v3.3.10/auth/authpb/auth.pb.go#L103
func init() {
	proto.RegisterType((*User)(nil), "authpb.User")
	proto.RegisterType((*Permission)(nil), "authpb.Permission")
	proto.RegisterType((*Role)(nil), "authpb.Role")
	proto.RegisterEnum("authpb.Permission_Type", Permission_Type_name, Permission_Type_value)
}

寻找原因

通过上面的一些信息已经能够初步给出一个panic的结论:protobuf里面的注册事件引发了这个报错。但是为何会出现这种情况呢?有点想不明白了,因为引入etcd来进行选主不是这个服务独有的,其它服务早就存在了,有点开始抓脑壳了。
首先使用go mod why命令,查一下新引入的etcd报是被谁在用:

$ go mod why github.com/coreos/etcd
# github.com/coreos/etcd
(main module does not need package github.com/coreos/etcd)

没找到,咱们继续使用go mod graph命令详细的查看一下,但是因为这个命令会直接把所有依赖包都打印出来,如果项目依赖的包比较少的话还好,如果很多的话就有点大海捞针了。于是我们使用grep来过滤掉不需要的信息,既然这次是因为引入了新的github.com/coreos/etcd包引起的,那就首先看看这次引入的包的情况吧:

$ go mod graph|grep " github.com/coreos/etcd"
my-server github.com/coreos/etcd@v3.3.10+incompatible // my-server 是我们这次的服务名,为防敏感做了特殊处理
github.com/spf13/viper@v1.3.2 github.com/coreos/etcd@v3.3.10+incompatible
github.com/spf13/viper@v1.4.0 github.com/coreos/etcd@v3.3.10+incompatible

发现除了my-server本身调用外,也在被github.com/spf13/viper调用,那github.com/spf13/viper又在被哪个包调用呢?我们继续查找

$ go mod graph|grep " github.com/spf13/viper"
github.com/spf13/cobra@v0.0.5 github.com/spf13/viper@v1.3.2
github.com/spf13/cobra@v1.0.0 github.com/spf13/viper@v1.4.0

.... 就这样一步一步的查找,过滤掉一些无用的包,大致出现了以下的依赖图关系:

relation


看到上面的图,凭直觉就感觉有些不对头的地方了,同是etcd,为何是两个包的数据源呢?

github.com/coreos/etcd	// 这个是我新引入的
go.etcd.io/etcd		// 这个是pingcap依赖的

其实问题就是出在这个上面,虽然是两个地址,但是后面代码都是一样的,在两个不同的组件引用了这两个不同的数包的时候,他们内部都触发了proto.buf的初始化,所以出现了最开始的那个报错。etcd最开始是共同存放在github.com/coreos下的,后面因为etcd发展的太好,就准备独立出来,可是这时因为项目已经名声在外,不能随便的更换域名,所以你会发现在github上面和新的go.etcd.io上面同时在维护,我们从github上面的main函数里面的代码也能看到:

链接:https://github.com/etcd-io/etcd/blob/ae7862e8bc8007eb396099db4e0e04ac026c8df5/server/main.go#L23
// Package main is a simple wrapper of the real etcd entrypoint package
// (located at go.etcd.io/etcd/etcdmain) to ensure that etcd is still
// "go getable"; e.g. `go get go.etcd.io/etcd` works as expected and
// builds a binary in $GOBIN/etcd
//
// This package should NOT be extended or modified in any way; to modify the
// etcd binary, work in the `go.etcd.io/etcd/etcdmain` package.
//
package main

import (
	"os"

	"go.etcd.io/etcd/server/v3/etcdmain"
)

func main() {
	etcdmain.Main(os.Args)
}

解决方案

找到上面的原因后再来处理就比较快速了,直接把我们新引入的包由github.com/coreos/etcd替换成go.etcd.io/etcd就好了,比如

	"github.com/coreos/etcd/clientv3"
	"github.com/coreos/etcd/mvcc/mvccpb"

替换为:

	"go.etcd.io/etcd/clientv3"
	"go.etcd.io/etcd/mvcc/mvccpb"

小结

在使用etcd的过程中坑算是比较多的了,鸟哥曾也说过这个问题,其中也有很多是etcd本身的锅,很多人早就在抱怨了,对go module的支持貌似不是很积极,不过据鸟哥说:据说,这个问题会在 etcd v3.5.0版本中解决,大家拭目以待吧, 毕竟, 你们已经都等了几个月了,不在乎再多等些日子。
那我们就等等看吧。
谨以此记录下这个问题,防止忘掉。

PingD

PingD

一个阶段一个目标,多冷静下来思考思考
Chengdu.China