BeyondTP architecture 架构梳理

1 设计概述

BeyondTP 产品的设计初衷是想要支持在任意两个服务之间迁移数据,由于底层是基于 go-storage 来实现的,所以天然的支持多服务。在此基础之上,我们还需要有一个 UI 页面,来提高用户的使用体验。

2 产品架构

2.1 beyondtp server

通过 beyondtp server 命令启动一个 server,其中包括了:

  • web server (基于 gin 实现)
  • graphQL server (基于 github.com/99designs/gqlgen 实现)
  • flutter UI (编译时调用 flutter build web,并通过 embed.FS 嵌入 go 二进制文件)
  • task manager (grpc server,负责下发 task 到 staff)

在执行 beyondtp server 命令后,会先根据 db flag,新建一个 BadgerDB 实例,并启动一个 grpc server 监听 grpc-port flag 指向的端口,用于进行 staff 与 manager 的 rpc 通信。

除此以外,还会利用同样的 BadgerDB 实例,启动一个 web server,用于渲染前端页面,并提供 api 接口服务供调用。

其中 /graphql 路径被用于把 gqlgen 中的 server 注册到 web server 中来,原理是利用 gin 提供的 gin.WrapFgin.WrapH 方法。

2.2 beyondtp staff

通过 beyondtp staff 命令启动一个 staff(UI 页面上被称为 agent),主要逻辑是根据 manager flag 与 db flag 初始化一个 staff,并执行 Start 方法启动。

在启动流程中,根据 staff 配置 New 一个 grpc client,将其注册到指定的 manager grpc address (通过 server 提供的 Register rpc 方法)。并通过 rpc 调用 Poll 方法流式拉取 task 信息,并进一步进行后续的处理。

3 task 设计

3.1 相关数据结构

Task 是一次完整的迁移任务,结构如下:

type Task struct {
	ID        string     `json:"id"`
	Name      string     `json:"name"`
	Type      TaskType   `json:"type"`
	Status    TaskStatus `json:"status"`
	CreatedAt time.Time  `json:"created_at"`
	UpdatedAt time.Time  `json:"updated_at"`
	Storages  []*Storage `json:"storages"`
	Options   []*Pair    `json:"options"`
	Staffs    []*Staff   `json:"staffs"`
}

其中:

  • Storages 为进行迁移的 Storager 数组,我们会将第 0 个作为 source storager,将第 1 个作为 destination storager
  • Options 为本次迁移中的选项,体现为 go-storage 中的 Pair
  • Staffs 为本次迁移 task 最终会落在哪些 staff 上执行

3.2 task 分发

一个 Task 被插入 DB 后,最初都只是 server db 中的一条数据而已,用于前端展示。

但是在 RunTask 操作中,才会将具体的 Task 分发到指定的 staff,这部分逻辑可以参考 proposal: https://github.com/beyondstorage/beyond-tp/blob/cf9cfcaeeb19cf7c45ba397634a1a126997e267f/docs/specs/110-refactor-task-distribute.md

大致是:staff 在启动时会通过 2.2 章节中提到的 Poll 方法,订阅 server db 中分配给本 staff 的 staff_task key 的变化,从而达到 server 端启动任务时,staff 端能够做出响应。

3.3 leader 与 worker

manager 与 staff 对应的是 task 级别的映射关系,而一个 task 实际对应的可能是多个操作的集合,比如 copy dir 就是一边 list,一边 copy file;copy large file 包含了 create multipart upload, 并发执行 copy multipart, 最终执行 complete multipart。所以我们引入了 job 的概念,并将 task 拆分为多个 job。

在 3.2 章节中我们提到过,staff 在启动的时候会监听 task 的变化,如果 task 被执行了,staff 这边会先进行选举(逻辑是 rpc call Elect 方法,由 manager 选出 leader),如果自己被选为 leader,则会另起一个 rpc server,其他的 staff 则自动成为 worker,与 leader 的 rpc server 建立连接,并通过 PollJob 方法,获取 leader 分发的 job(只需要把 job 插入 DB 便可以触发 leader 分发操作,因为 leader 在启动时便订阅了 job key 的修改)。在 worker 接收到 job 之后,就会启一个 goroutine,并新建 runner 来执行这个 job。其中 runner 中封装了如下方法:

  • Handle,根据 job 的类型调用不同的 job 逻辑
  • Async,异步执行,rpc call CreateJob,启 goroutine 去 rpc call WaitJob 方法
  • Sync,同步执行,rpc call CreateJob,并同步 rpc call WaitJob 方法
  • Await,手动同步,调用 wg.Wait() 方法
  • Finish,将 job 标记为完成,状态有 成功/失败 两种,并 rpc call FinishJob 方法

3.4 task 流程总结

  1. Manager run task
  2. 对应 staff 通过 rpc call 选举 leader,根据选举结果 staff 分别扮演 leader 与 worker 角色
  3. leader 将 task 转为 rootJob,并插入 leader DB
  4. worker 启动时,通过 rpc PollJob 从 leader 的 job channel 获取 job
  5. 将 4 中得到的 job,通过启用 goroutine 的方式,异步的交给 runner 去执行
  6. runner 在执行的过程中,会根据实际情况,将 job 进一步拆分,并将拆分后的 job 通过 rpc call CreateJob 的方式,插入 leader DB,并触发 leader 对 job key 的订阅,将 job 分发给 worker,重复 5 的步骤
  7. 每个 job 执行完成后,都会调用 Finish 来标记完成。在 rootJob 完成后,关闭 done channel 与 job channel,该 task 完成。

由于每个 task 下发到 job 后,都是 goroutine 来异步执行,所以我们可以不停的从 manager 启动并下发新的 task。

4 其他

4.1 GraphQL

我们使用 github.com/99designs/gqlgen 库来处理 GraphQL 相关的逻辑,除了 2.1 章节中提到的 server 以外,还有通过 GraphQL 中的 schema 定义生成脚手架代码的部分。

具体的行为可以参考 https://gqlgen.com/getting-started/,其中 GraphQL schema 相关的内容可以参考 https://graphql.cn/learn/

4.2 BadgerDB

BadgerDB 是高性能的 kv 数据库,由 go 语言编写的,正如他的介绍那样:

BadgerDB is an embeddable, persistent, and fast key-value (KV) database written in pure Go. It is the underlying database for Dgraph, a fast, distributed graph database. It’s meant to be a performant alternative to non-Go-based key-value stores like RocksDB.

可以参考 https://dgraph.io/docs/badger/get-started/ 了解常用的操作。

4.3 grpc

grpc 是一个开源的,高性能的通用 rpc 框架。我们利用 rpc 作为 manager 与 staff 之间,以及 leader 与 worker 间的通信机制。

除此之外,我们还使用 protobuf 来生成 rpc 框架代码,以及序列化结构体为 []byte 存入 DB,从 DB 中得到 []byte 并反序列化为结构体。

2 Likes