首发于 Nebula Graph 官方博客:https://nebula-graph.com.cn/posts/task-management-design-in-nebula-graph/git
讲解 Task Manager 以前,在这里先介绍一些 Task Manager 会使用到的概念术语。github
图数据库 Nebula Graph 中,存在一些长期在后台运行的任务,咱们称之为 Job。存储层存在的 DBA 使用的部分指令,好比:数据完成导入后,想在全局作一次 compaction,都是 Job 范畴。数据库
做为一个分布式的系统,Nebula Graph 中 Job 由不一样的 storaged 完成,而咱们管一个 storaged 上运行的 Job 子任务叫作 Task。Job 的控制由 metad 上的 Job Manager 负责,而 Task 的控制由 storaged 上的 Task Manager 负责。安全
在本文中,咱们着重讲述如何对长耗时的 Task 进行管理与调度进一步提高数据库性能。微信
Task Manager 要解决的问题
上文说到 storaged 上的 Task Manager 控制的 Task 是 meta 控制的 Job 的子任务,那 Task Manager 它本身具体解决什么问题呢?在 Nebula Graph 中 Task Manager 主要解决了如下 2 个问题:多线程
- 将以前经过 HTTP 的传送方式改成 RPC(Thrift) 通常用户在搭建集群时,知道 storaged 之间通讯使用 Thrift 协议,会为 Thrift 所需端口开放防火墙,可是可能意识不到 Nebula Graph 还须要使用 HTTP 端口,咱们遇到过屡次社区用户实践忘记开放 HTTP 端口的事情。
- storaged 对于 Task 有调度能力 这块内容将在本文下面章节展开讲述。
Task Manager 在 Nebula Graph 中的位置
Task Manager 体系中的 meta
在 Task Manager 体系中, metad(JobManager)的任务是根据 graphd 中传过来的一个 Job Request,选出对应的 storaged host,并拼组出 Task Request 发给对应的 storaged。不难发现,体系中 meta 接受 Job Request,拼组 Task Request , 发送 Task Request 及接受 Task 返回结果,这些逻辑的套路是稳定的。而如何拼组 TaskRequest,将 Task Request 发给哪些 storaged 则会根据不一样的 Job 有所变化。JobManager 用 模板策略
+ 简单工厂
以应对将来的扩展。并发
让将来的 Job 一样继承于 MetaJobExecutor,并实现 prepare() 和 execute() 方法便可。分布式
Task Manager 的调度控制
以前提到的,Task Manager 的调度控制但愿作到 2 点:高并发
- 系统资源足够时,尽量的高并发执行 Task
- 系统资源吃紧时,让全部运行中的 Task 占用的资源不要超过某一个设定的阈值。
高并发执行 Task
Task Manager 将系统资源中本身持有的线程称之为 Worker。Task Manager 有一个现实中的模拟原型——银行的营业厅。想象一下, 咱们去银行办业务时会有如下几步:post
- 场景 1:在门口的排号机拿一个号
- 场景 2:在大厅找个位置, 边玩手机边等叫号
- 场景 3:等叫到号时, 到指定窗口办理
同时, 你还会碰到这样那样的问题:
- 场景4:VIP 能够插队
- 场景5:你可能排着队, 由于某些缘由, 放弃了本次业务
- 场景6:你可能排着排着队, 银行就关门了
那么, 整理一下, 这也就是 Task Manager 的基本需求
- Task 按 FIFO 顺序执行:不一样的 Task 有不一样的优先级,高优先级的能够插队
- 用户可取消一个排队中的 Task
- storaged 随时 shutdown
- 一个 Task,为了使其尽量高的并发,会被拆分为多个 SubTask,SubTask 是每一个 Worker 真正执行的任务
- Task Manager 是全局惟一实例,要考虑多线程安全性
因而, 有了以下实现:
- 实现 1:用 Thrift 结构中的 JobId 和 TaskId,肯定一个 Task,称为 Task Handle。
- 实现 2:TaskManager 会有一个 Blocking Queue,负责让 Task 的 Handle 排队执行(排号机),而 Blocking Queue 自己线程安全。
- 实现 3:Blocking Queue 同时支持不一样的优先级, 高优先级先出队(VIP 插队的功能)。
- 实现 4:Task Manager 维持一个全局惟一的 Map,key 是 Task Handle,value 是具体的 Task(银行的大厅)。在 Nebula Graph 中采用了 folly 的 Concurrent Hash Map,线程安全的 Map。
- 实现 5:若是有用户 cancel Task,直接在根据 Handle 找到 Map 中对应的 Task,并标记 cancel,对 queue 中的 Handle 不作处理。
- 实现 6:若是有正在运行的 Task,对于 storaged 的 shutdown 会等到这个 Task 正在执行的 subTask 执行完毕才返回。
限定 Task 占用的资源阈值
保证不超过阈值仍是很简单的,由于 Worker 就是线程,只要让全部的 Worker 都出自一个线程池,就能够保证最大的 Worker 数。麻烦的是将子任务平均地分配到 Worker 中, 咱们来讨论下方案:
方法一:使用 Round-robin 添加任务
最简单的方法是用 Round-robin 的方式来添加任务。也就是将 Task 分解为 Sub Task 以后, 依次追加到如今的各个 Worker 中。
可是可能会有问题, 好比说, 我有 3 个 Worker, 2 个 Task(蓝色为 Task 1,黄色为 Task 2):
Round-robin 图 1
假如 Task 2 中的 Sub Task 执行远快于 Task1 的, 那么好的并行策略应该是这样:
Round-robin 图 2
简单粗暴的 Round-robin 会让 Task 2 的完成时间依赖于 Task 1(见 Round-robin 图1)。
方法二:一组 worker 处理一个 Task
针对方法一可能会出现的状况,设定专门的 Worker 只处理指定的 Task,从而避免多个 Task 相互依赖问题。可是依然不够好, 好比说:
很难保证每一个 Sub Task 执行时间基本相同,假设 Sub Task 1 的执行明显慢于其余的 Sub Task,那么好的执行策略应该是这样的:
这个方案仍是避免不了 1 核有难,10 核围观的问题 👀。
方法三:Nebula Graph 采用的解决方案
在 Nebula Graph 中 Task Manager 会将 Task 的 Handle 交给 N 个 Worker。N 由总 Worker 数、总 Sub Task 数,以及 DBA 在提交 Job 时指定的并发参数共同决定。
每一个 Task 内部维护一个 Blocking Queue(下图的 Sub Task Queue),存放 Sub Task。Worker 在执行时,根据本身持有的 Handle 先找到 Task,再从 Task 的 Block Queue 中获取 Sub Task。
设计补充说明
问题 1: 为何不直接将 Task 放到 Blocking Queue 排队,而是拆成两部分,将 Task 保存在 Map 里,让 Task Handle 排队?
主要缘由是 C++ 多线程基础设施很差支持这种逻辑。Task 须要支持 cancel。假设 Task 放在 Blocking Queue 中,就须要 Blocking Queue 支持定位到其中的某一个 Task 的能力。而当前 folly 中的 Blocking Queue 都没有此类接口。
问题 2: 什么样的 Job 有 VIP 待遇?
当前 Task Manager 支持的 compaction / rebuild index 对执行时间并不敏感,支持相似 count() 查询操做功能尚在开发中。考虑到用户但愿在一个相对短的时间内完成 count() ,那么假如正好碰上了 storaged 在作多个 compaction,仍是但愿 count(*) 能够优先运行,而非在全部 compaction 以后再开始作。
本文中若有任何错误或疏漏欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向咱们提 issue 或者前往官方论坛:https://discuss.nebula-graph.com.cn/ 的 建议反馈
分类下提建议 👏;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot
做者有话说:Hi,我是 我是 lionel.liu,是图数据 Nebula Graph 研发工程师,对数据库查询引擎有浓厚的兴趣,但愿本次的经验分享能给你们带来帮助,若有不当之处也但愿能帮忙纠正,谢谢~