本文首发于 Nebula Graph Community 公众号

上篇我们讲述了 Query Engine Optimizer 部分的内容,在本文我们讲解下 Query Engine 剩下的 Scheduler 和 Executor 部分。

概述

在执行阶段,执行引擎通过 Scheduler(调度器)将 Planner 生成的物理执行计划转换为一系列 Executor,驱动 Executor 的执行。

Executor,即执行器,物理执行计划中的每个 PlanNode 都会对应一个 Executor。

源码定位

调度器的源码在 src/scheduler 目录下:

src/scheduler
├── AsyncMsgNotifyBasedScheduler.cpp
├── AsyncMsgNotifyBasedScheduler.h
├── CMakeLists.txt
├── Scheduler.cpp
└── Scheduler.h

Scheduler 抽象类定义了调度器的公共接口,可以继承该类实现多种调度器。

目前实现了 AsyncMsgNotifyBasedScheduler 调度器,它基于异步消息通信与广度优先搜索避免栈溢出。

执行器的源码在 src/executor 目录下:

src/executor
├── admin
├── algo
├── CMakeLists.txt
├── ExecutionError.h
├── Executor.cpp
├── Executor.h
├── logic
├── maintain
├── mutate
├── query
├── StorageAccessExecutor.cpp
├── StorageAccessExecutor.h
└── test

执行过程

首先,调度器从执行计划的根节点开始通过使用广度优先搜索算法遍历整个执行计划并根据节点间的执行依赖关系,构建它们的消息通知机制。

执行时,每个节点收到它的所依赖的节点全部执行完毕的消息后,会被调度执行。一旦自身执行完成,又会发送消息给依赖自己的节点,直至整个计划执行完毕。

void AsyncMsgNotifyBasedScheduler::runExecutor(
std::vector<folly::Future<Status>>&& futures,
Executor* exe,
folly::Executor* runner,
std::vector<folly::Promise<Status>>&& promises) const {
folly::collect(futures).via(runner).thenTry(
[exe, pros = std::move(promises), this](auto&& t) mutable {
if (t.hasException()) {
return notifyError(pros, Status::Error(t.exception().what()));
}
auto status = std::move(t).value();
auto depStatus = checkStatus(std::move(status));
if (!depStatus.ok()) {
return notifyError(pros, depStatus);
}
// Execute in current thread.
std::move(execute(exe)).thenTry(
[pros = std::move(pros), this](auto&& exeTry) mutable {
if (exeTry.hasException()) {
return notifyError(pros, Status::Error(exeTry.exception().what()));
}
auto exeStatus = std::move(exeTry).value();
if (!exeStatus.ok()) {
return notifyError(pros, exeStatus);
}
return notifyOK(pros);
});
});
}

每个 Executor 会经历 create-open-execute-close 四个阶段:

create

根据节点类型生成对应的 Executor。

open

在 Executor 正式执行前做一些初始化操作,以及慢查询终止和内存水位的判断。

Nebula 支持手动 kill 掉某个查询语句的执行,因此每个 Executor 执行前需要检查下当前执行计划状态,若被标记为 killed,则终止执行。

每个 Query 类型的 Executor 执行前,还需要检查当前系统所占用内存是否达到内存水位。若达到内存水位,则终止执行,这能在一定程度上避免 OOM。

Status Executor::open() {
if (qctx_->isKilled()) {
VLOG(1) << "Execution is being killed. session: " << qctx()->rctx()->session()->id()
<< "ep: " << qctx()->plan()->id()
<< "query: " << qctx()->rctx()->query();
return Status::Error("Execution had been killed");
}
auto status = MemInfo::make();
NG_RETURN_IF_ERROR(status);
auto mem = std::move(status).value();
if (node_->isQueryNode() && mem->hitsHighWatermark(FLAGS_system_memory_high_watermark_ratio)) {
return Status::Error(
"Used memory(%ldKB) hits the high watermark(%lf) of total system memory(%ldKB).",
mem->usedInKB(),
FLAGS_system_memory_high_watermark_ratio,
mem->totalInKB());
}
numRows_ = 0;
execTime_ = 0;
totalDuration_.reset();
return Status::OK();
}

execute

Query 类型的 Executor 的输入和输出都是一张表(DataSet)。

Executor 的执行基于迭代器模型:每次计算时,调用输入表的迭代器的 next() 方法,获取一行数据,进行计算,直至输入表被遍历完毕。

计算的结果构成一张新表,输出给后续的 Executor 作为输出。

folly::Future<Status> ProjectExecutor::execute() {
SCOPED_TIMER(&execTime_);
auto* project = asNode<Project>(node());
auto columns = project->columns()->columns();
auto iter = ectx_->getResult(project->inputVar()).iter();
DCHECK(!!iter);
QueryExpressionContext ctx(ectx_); VLOG(1) << "input: " << project->inputVar();
DataSet ds;
ds.colNames = project->colNames();
ds.rows.reserve(iter->size());
for (; iter->valid(); iter->next()) {
Row row;
for (auto& col : columns) {
Value val = col->expr()->eval(ctx(iter.get()));
row.values.emplace_back(std::move(val));
}
ds.rows.emplace_back(std::move(row));
}
VLOG(1) << node()->outputVar() << ":" << ds;
return finish(ResultBuilder().value(Value(std::move(ds))).finish());
}

如果当前 Executor 的输入表不会被其他 Executor 作为输入时,这些输入表所用的内存会在执行阶段被 drop 掉,减小内存占用。

void Executor::drop() {
for (const auto &inputVar : node()->inputVars()) {
if (inputVar != nullptr) {
// Make sure use the variable happened-before decrement count
if (inputVar->userCount.fetch_sub(1, std::memory_order_release) == 1) {
// Make sure drop happened-after count decrement
CHECK_EQ(inputVar->userCount.load(std::memory_order_acquire), 0);
ectx_->dropResult(inputVar->name);
VLOG(1) << "Drop variable " << node()->outputVar();
}
}
}
}

close

Executor 执行完毕后,将收集到的一些执行信息如执行时间,输出表的行数等添加到 profiling stats 中。

用户可以在 profile 一个语句后显示的执行计划中查看这些统计信息。


Execution Plan (optimize time 141 us) -----+------------------+--------------+-----------------------------------------------------+--------------------------------------
| id | name | dependencies | profiling data | operator info |
-----+------------------+--------------+-----------------------------------------------------+--------------------------------------
| 2 | Project | 3 | ver: 0, rows: 56, execTime: 147us, totalTime: 160us | outputVar: [ |
| | | | | { |
| | | | | "colNames": [ |
| | | | | "VertexID", |
| | | | | "player.age" |
| | | | | ], |
| | | | | "name": "__Project_2", |
| | | | | "type": "DATASET" |
| | | | | } |
| | | | | ] |
| | | | | inputVar: __TagIndexFullScan_1 |
| | | | | columns: [ |
| | | | | "$-.VertexID AS VertexID", |
| | | | | "player.age" |
| | | | | ] |
-----+------------------+--------------+-----------------------------------------------------+--------------------------------------
| 3 | TagIndexFullScan | 0 | ver: 0, rows: 56, execTime: 0us, totalTime: 6863us | outputVar: [ |
| | | | | { |
| | | | | "colNames": [ |
| | | | | "VertexID", |
| | | | | "player.age" |
| | | | | ], |
| | | | | "name": "__TagIndexFullScan_1", |
| | | | | "type": "DATASET" |
| | | | | } |
| | | | | ] |
| | | | | inputVar: |
| | | | | space: 318 |
| | | | | dedup: false |
| | | | | limit: 9223372036854775807 |
| | | | | filter: |
| | | | | orderBy: [] |
| | | | | schemaId: 319 |
| | | | | isEdge: false |
| | | | | returnCols: [ |
| | | | | "_vid", |
| | | | | "age" |
| | | | | ] |
| | | | | indexCtx: [ |
| | | | | { |
| | | | | "columnHints": [], |
| | | | | "index_id": 325, |
| | | | | "filter": "" |
| | | | | } |
| | | | | ] |
-----+------------------+--------------+-----------------------------------------------------+--------------------------------------
| 0 | Start | | ver: 0, rows: 0, execTime: 1us, totalTime: 19us | outputVar: [ |
| | | | | { |
| | | | | "colNames": [], |
| | | | | "type": "DATASET", |
| | | | | "name": "__Start_0" |
| | | | | } |
| | | | | ] |
-----+------------------+--------------+-----------------------------------------------------+--------------------------------------  

以上,源码解析 Query Engine 相关的模块就讲解完毕了,后续将讲解部分特性内容。

交流图数据库技术?加入 Nebula 交流群请先填写下你的 Nebula 名片,Nebula 小助手会拉你进群~~

【活动】Nebula Hackathon 2021 进行中,一起来探索未知,领取 ¥ 150,000 奖金 →→ https://nebula-graph.com.cn/hackathon/

Nebula Graph 源码解读系列 | Vol.05 Scheduler 和 Executor 两兄弟的更多相关文章

  1. 新手阅读 Nebula Graph 源码的姿势

    摘要:在本文中,我们将通过数据流快速学习 Nebula Graph,以用户在客户端输入一条 nGQL 语句 SHOW SPACES 为例,使用 GDB 追踪语句输入时 Nebula Graph 是怎么 ...

  2. Alamofire源码解读系列(二)之错误处理(AFError)

    本篇主要讲解Alamofire中错误的处理机制 前言 在开发中,往往最容易被忽略的内容就是对错误的处理.有经验的开发者,能够对自己写的每行代码负责,而且非常清楚自己写的代码在什么时候会出现异常,这样就 ...

  3. Alamofire源码解读系列(四)之参数编码(ParameterEncoding)

    本篇讲解参数编码的内容 前言 我们在开发中发的每一个请求都是通过URLRequest来进行封装的,可以通过一个URL生成URLRequest.那么如果我有一个参数字典,这个参数字典又是如何从客户端传递 ...

  4. Alamofire源码解读系列(三)之通知处理(Notification)

    本篇讲解swift中通知的用法 前言 通知作为传递事件和数据的载体,在使用中是不受限制的.由于忘记移除某个通知的监听,会造成很多潜在的问题,这些问题在测试中是很难被发现的.但这不是我们这篇文章探讨的主 ...

  5. Alamofire源码解读系列(五)之结果封装(Result)

    本篇讲解Result的封装 前言 有时候,我们会根据现实中的事物来对程序中的某个业务关系进行抽象,这句话很难理解.在Alamofire中,使用Response来描述请求后的结果.我们都知道Alamof ...

  6. Alamofire源码解读系列(六)之Task代理(TaskDelegate)

    本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...

  7. Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager)

    Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager) 本篇主要讲解iOS开发中的网络监控 前言 在开发中,有时候我们需要获取这些信息: 手机是否联网 ...

  8. Alamofire源码解读系列(八)之安全策略(ServerTrustPolicy)

    本篇主要讲解Alamofire中安全验证代码 前言 作为开发人员,理解HTTPS的原理和应用算是一项基本技能.HTTPS目前来说是非常安全的,但仍然有大量的公司还在使用HTTP.其实HTTPS也并不是 ...

  9. Alamofire源码解读系列(九)之响应封装(Response)

    本篇主要带来Alamofire中Response的解读 前言 在每篇文章的前言部分,我都会把我认为的本篇最重要的内容提前讲一下.我更想同大家分享这些顶级框架在设计和编码层次究竟有哪些过人的地方?当然, ...

  10. Alamofire源码解读系列(十)之序列化(ResponseSerialization)

    本篇主要讲解Alamofire中如何把服务器返回的数据序列化 前言 和前边的文章不同, 在这一篇中,我想从程序的设计层次上解读ResponseSerialization这个文件.更直观的去探讨该功能是 ...

随机推荐

  1. pytest-assume插件-多重校验

    自动化接口测试我们通常会对一条case设置多条断言,这样就会出现一个问题,如果前面一 个校验不通过,那么后面的校验就不会走到,如下图,可以看到校验走到assert False就不往 下走了,这个时候p ...

  2. 浅析大促备战过程中出现的fullGc,我们能做什么?

    作者:京东科技 白洋 前言: 背景: 为应对618.双11大促,消费金融侧会根据零售侧大促节奏进行整体系统备战.对核心流量入口承载的系统进行加固优化,排除系统风险,保证大促期间系统稳定. 由于大促期间 ...

  3. Vue 中keep-alive组件将会被缓存

    动态包裹哈 <keep-alive> <component :is="comName"></component> </keep-alive ...

  4. vue mixin混入 全局混入 局部混入

    <div id="app"> --{{nameName}} </div> // 全局混入 不需要注册 var m1 = Vue.mixin({ data() ...

  5. 【发现一个问题】extjs-gpl-7.0: 当修改 store 对象的字段后,再次 loadPage() 后字段映射错误。

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 因为查询中需要每次返回数目不确定的 fields ,这就需 ...

  6. Ubuntu编译Xilinx的u-boot

    博主这里的是Ubuntu20.04LTS+Vivado2017.4+ZedBoard 注意:本文使用的环境变量导入方法是临时的,只要退出当前终端或者使用其他终端就会失效,出现异常问题,请随时expor ...

  7. 【2】Visual Studio 2017同时配置OpenCV2.4 以及OpenCV4.3

    相关文章: [1]windows下安装OpenCV(4.3)+VS2017安装+opencv_contrib4.3.0配置 [2]Visual Studio 2017同时配置OpenCV2.4 以及O ...

  8. C/C++ 原生套接字抓取FTP数据包

    网络通信在今天的信息时代中扮演着至关重要的角色,而对网络数据包进行捕获与分析则是网络管理.网络安全等领域中不可或缺的一项技术.本文将深入介绍基于原始套接字的网络数据包捕获与分析工具,通过实时监控网络流 ...

  9. 【动态内存】C语言动态内存使用常见错误及其避免方法(初学者避雷)

    C语言动态内存使用常见错误及其避免方法(初学者动态内存避雷手册) 求个赞求个赞求个赞求个赞 谢谢 先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要 ...

  10. 单片机 IAP 功能基础开发篇之APP升级(三)

    1 前言 上一篇单片机 IAP 功能基础开发篇之APP升级(二)讲到了单片机给 APP 程序升级具体的设计方案,这篇介绍的是升级进阶功能,如何在编译后在程序结束地址自动加上校验标志(可以通过脚本工具, ...