bilibili/discovery
gitbhub 地址: https://github.com/bilibili/discovery
目录结构
目录结构如下,旁边加了注释
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cmd
│ └── discovery # discovery 程序目录
│ ├── discovery-example.toml # 配置例子
│ ├── main.go # main 入口文件
│ └── scheduler-example.json # `多机房流量调度`配置
├── codecov.sh
├── conf
│ └── conf.go # discovery-example.toml 配置对应的 go 解析代码
├── coverage.txt
├── discovery # cmd/discovery 只是个壳;真正 discovery 程序代码在这里
│ ├── discovery.go # discovery 启动时主要流程都在这里,包括读取配置;同步集群中的数据给自己;注册自己;poll 增量拉取集群中的节点信息
│ ├── register.go # register / renew / cannel / ... 所有操作主要流程代码,会调 registry/registry.go 代码
│ ├── register_test.go
│ └── syncup.go # discovery 启动时主要流程实现细节
├── doc # 文档
│ ├── api.md
│ ├── arch.md
│ ├── discovery_arch.png
│ ├── discovery_pod_quit.png
│ ├── discovery_pod_start.png
│ ├── discovery_sdk.png
│ ├── discovery_sdk_self.png
│ ├── discovery_wechat.png
│ ├── discovery_zone_arch.png
│ ├── felixhao_wechat.png
│ ├── intro.md
│ ├── practice.md
│ └── sdk.md
├── errors
│ └── errors.go # 错误号 NotModified / NothingFound / Conflict 等都是很重要的错误号
├── go.mod
├── go.sum
├── http
│ ├── discovery.go # 会调用 discovery/register.go 中代码
│ └── http.go # HTTP 服务, discovery 程序内会初始化 1 个实例,对外提供服务
├── install.sh
├── lib
│ ├── http
│ │ └── client.go # HTTP client 帮助类
│ └── time
│ └── time.go # 字面表达时间的帮助类
├── model # 相关概念数据的数据结构都在这里
│ ├── instance.go # 表示 Service Provider 信息,有 Apps <- App <- Instance 一系列概念及数据维护逻辑
│ ├── node.go # 表示 Discovery Server 信息,有 Node 这样的概念
│ └── param.go # 定义了各操作 HTTP 协议内容
├── naming # discovery 集群客户端 golang 版 SDK 实现(暂未细看,跳过,后续有时间可深究)
│ ├── client.go
│ ├── client_test.go
│ ├── example_test.go
│ ├── grpc
│ │ ├── resolver.go
│ │ └── resolver_test.go
│ └── naming.go
└── registry # discovery 所有操作的细节处理都在这里
├── guard.go # `自我保护`相关内容
├── guard_test.go
├── node.go # 表示 1 个对等节点,与对等节点的交互,在这里发起
├── node_test.go
├── nodes.go # 表示 discovery 集群内所有节点,管理单位 node
├── registry.go # 所有操作的细节处理都在这里
├── registry_test.go
└── scheduler.go # `多机房流量调度`配置加载等
下面详细说明
cmd/discovery 目录
discovery 程序入口
func main() {
// ...
dis, cancel := discovery.New(conf.Conf)
http.Init(conf.Conf, dis)
// init signal
// ...
}
主要做了 2 件事:
- 实例化 discovery ,该类实现 discovery 节点工作流程
- 提供 HTTP 服务,接受 register / renew / … 等等请求处理
discovery 目录
discovery.go
// New get a discovery.
func New(c *conf.Config) (d *Discovery, cancel context.CancelFunc) {
d = &Discovery{
c: c,
client: http.NewClient(c.HTTPClient),
registry: registry.NewRegistry(c),
}
d.nodes.Store(registry.NewNodes(c))
d.syncUp()
cancel = d.regSelf()
go d.nodesproc()
return
}
主要做了如下几件事:
- d.nodes.Store(registry.NewNodes©)
- 读配置,初始化对等节点(Node / Nodes)
- d.syncUp()
- 自发现1,拉取 discovery 集群其他 discovery 节点信息
- cancel = d.regSelf()
- 自发现2,注册自己
- go d.nodesproc()
- 自发现3,循环增量拉取 discovery 集群其他 discovery 节点信息
register.go
// 类似函数略,其他业务操作
// 这里典型摘取 Renew 函数
func (d *Discovery) Renew(c context.Context, arg *model.ArgRenew) (i *model.Instance, err error) {
i, ok := d.registry.Renew(arg)
if !ok {
err = errors.NothingFound
log.Errorf("renew appid(%s) hostname(%s) zone(%s) env(%s) error", arg.AppID, arg.Hostname, arg.Zone, arg.Env)
return
}
if !arg.Replication {
_ = d.nodes.Load().(*registry.Nodes).Replicate(c, model.Renew, i, arg.Zone != d.c.Env.Zone)
return
}
if arg.DirtyTimestamp > i.DirtyTimestamp {
err = errors.NothingFound
} else if arg.DirtyTimestamp < i.DirtyTimestamp {
err = errors.Conflict
}
return
}
// 类似函数略,其他业务操作
- 本文件代码很重要,看懂这里,也就已通关 bilibili/discovery 啦
- 本文件代码函数,相关流程中都会被执行 2 次 (实际情况是 1 + (N-1)次,2次好描述些 )
- 1 次在本 discovery 节点上
- 另外 1 次在 其他对等 discovery 节点上
- 2 次执行,函数内都只执行了部分代码
- 调本文件代码函数的地方皆为 http/http.go 内,但会被本节点、对等节点执行
- registry/node.go 内有相关函数,是本节点与对等节点的交互代码
syncup.go
discovery 自发现细节(略)
http 目录
http.go
func innerRouter(e *gin.Engine) {
group := e.Group("/discovery")
group.POST("/register", register)
group.POST("/renew", renew)
group.POST("/cancel", cancel)
group.GET("/fetch/all", fetchAll)
group.GET("/fetch", fetch)
group.GET("/fetchs", fetchs)
group.GET("/poll", poll)
group.GET("/polls", polls)
group.GET("/nodes", nodes)
group.POST("/set", set)
}
主要提供上述 HTTP 请求
需要注意的事:
- 这些服务不仅给 Service Provider/Consumer 用, Discovery Server 也用
- 因此 1 个操作流程下来,Service Provider/Consumer 调用 1次, Discovery Server 会做 replication 操作,继续调用对等节点的该 HTTP 请求
discovery.go
// 其他代码略
func poll(c *gin.Context) {
arg := new(model.ArgPolls)
if err := c.Bind(arg); err != nil {
result(c, nil, errors.ParamsErr)
return
}
ch, new, err := dis.Polls(c, arg)
if err != nil && err != errors.NotModified {
result(c, nil, err)
return
}
// wait for instance change
select {
case e := <-ch:
result(c, resp{Data: e[arg.AppID[0]]}, nil)
if !new {
dis.DelConns(arg) // broadcast will delete all connections of appid
}
return
case <-time.After(_pollWaitSecond):
result(c, nil, errors.NotModified)
case <-c.Done():
}
result(c, nil, errors.NotModified)
dis.DelConns(arg)
}
// 其他代码略
poll / polls 的实现,就是官方所说的长轮询监听
- 内存中有增量数据,则直接返回,发给客户端
- 否则 30 秒内 chan 阻塞等待增量数据
- 要么30秒超时返回
- 要么 30秒内 chan 增量数据到达,发送数据给客户端
- 这与
Eureka定期30s拉取一次
有细微区别
model 目录
提供了 3 种数据的数据结构及维护代码
- Service Provider 信息
- Apps ,1 个 zone 内 某 appid 的 app 集合
- App ,表示一个微服务
- Instance , 表示一个 Service Provider 数据
- 表示 Discovery Server 信息
- Node ,discovery 节点数据
- 定义了各操作 HTTP 协议内容
naming 目录
可以略, client sdk for golang
registry 目录
guard.go
自我保护
数据统计与判断
node.go
// 其他代码略
func (n *Node) Renew(c context.Context, i *model.Instance) (err error) {
var res *model.Instance
err = n.call(c, model.Renew, i, n.renewURL, &res)
if err == errors.ServerErr {
log.Warningf("node be called(%s) instance(%v) error(%v)", n.renewURL, i, err)
n.status = model.NodeStatusLost
return
}
n.status = model.NodeStatusUP
if err == errors.NothingFound {
log.Warningf("node be called(%s) instance(%v) error(%v)", n.renewURL, i, err)
err = n.call(c, model.Register, i, n.registerURL, nil)
return
}
// NOTE: register response instance whitch in conflict with peer node
if err == errors.Conflict && res != nil {
err = n.call(c, model.Register, res, n.pRegisterURL, nil)
}
return
}
func (n *Node) call(c context.Context, action model.Action, i *model.Instance, uri string, data interface{}) (err error) {
// 其他代码略
if err = n.client.Post(c, uri, "", params, &res); err != nil {
log.Errorf("node be called(%s) instance(%v) error(%v)", uri, i, err)
return
}
if res.Code != 0 {
log.Errorf("node be called(%s) instance(%v) response code(%v)", uri, i, res.Code)
if err = errors.Int(res.Code); err == errors.Conflict {
_ = json.Unmarshal([]byte(res.Data), data)
}
}
return
}
Node 表示 discovery 集群中的一个对等节点,本 discovery 节点通过它与该对等节点交互
上面 Renew 的意思为:
- HTTP 对等节点,请求 Renew
- 返回后 3 种情况
- 情况1 ,该对等节点已坏
- 情况2, errors.NothingFound
- 对等节点没有该 Service Provider 信息
- 于是 HTTP 对等节点,请求 Register 该 Service Provider 信息
- 情况3, errors.Conflict
- 对等节点上的 该 Service Provider 信息比自己的新
- 于是 HTTP 自己,请求 Register 该 Service Provider 信息
nodes.go
维护 Node 对象,表示整个 discovery 集群
代码分析略,简单
registry.go
各 HTTP 请求,在节点的处理细节,内容太多且细,到这里已经基本不涉及到流程、交互上的东西啦
除了以下几点需要单独列下:
- evict
- 本节点上踢除无效 Service Provider 信息操作,均在本文件内处理
- broadcast 与 poll/polls
- 前面 poll/polls 提到内有 ch 阻塞等待增量信息,就是 register / cannel 操作后,调用 broadcast 往 ch 里塞的
以上