bilibili/discovery 介绍与源代码分析 (二)

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
}

主要做了如下几件事:

  1. d.nodes.Store(registry.NewNodes©)
    • 读配置,初始化对等节点(Node / Nodes)
  2. d.syncUp()
    • 自发现1,拉取 discovery 集群其他 discovery 节点信息
  3. cancel = d.regSelf()
    • 自发现2,注册自己
  4. 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 里塞的

以上

发布了129 篇原创文章 · 获赞 73 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/u013272009/article/details/91358453