本文经授权转载自公众号 PostgreSQL 中文社区,主要介绍了 Greenplum 集群概述、分布式数据存储和分布式查询优化。
现在有了分布式数据存储机制,也生成了分布式查询计划,下一步是如何在集群里执行分布式计划,最终返回结果给用户。
先看一个 SQL 例子及其计划:
test=# CREATE TABLE students (id int, name text) DISTRIBUTED BY (id);
test=# CREATE TABLE classes(id int, classname text, student_id int) DISTRIBUTED BY (id);
test=# INSERT INTO students VALUES (1, 'steven'), (2, 'changchang'), (3, 'guoguo');
test=# INSERT INTO classes VALUES (1, 'math', 1), (2, 'math', 2), (3, 'physics', 3);
test=# explain SELECT s.name student_name, c.classname
test-# FROM students s, classes c
test-# WHERE s.id=c.student_id;
QUERY PLAN
----------------------------------------------------------------------------------------------
-
Gather Motion 2:1 (slice2; segments: 2) (cost=2.07..4.21 rows=4 width=14)
-> Hash Join (cost=2.07..4.21 rows=2 width=14)
Hash Cond: c.student_id = s.id
-> Redistribute Motion 2:2 (slice1; segments: 2) (cost=0.00..2.09 rows=2 width=10)
Hash Key: c.student_id
-> Seq Scan on classes c (cost=0.00..2.03 rows=2 width=10)
-> Hash (cost=2.03..2.03 rows=2 width=12)
-> Seq Scan on students s (cost=0.00..2.03 rows=2 width=12)
Optimizer status: legacy query optimizer
这个图展示了上面例子中的 SQL 在 2 个 segment 的 Greenplum 集群中执行时的示意图。
QD(Query Dispatcher、查询调度器):Master 节点上负责处理用户查询请求的进程称为 QD(PostgreSQL 中称之为 Backend 进程)。 QD 收到用户发来的 SQL 请求后,进行解析、重写和优化,将优化后的并行计划分发给每个 segment 上执行,并将最终结果返回给用户。此外还负责整个 SQL 语句涉及到的所有的 QE 进程间的通讯控制和协调,譬如某个 QE 执行时出现错误时,QD 负责收集错误详细信息,并取消所有其他 QEs;如果 LIMIT n 语句已经满足,则中止所有 QE 的执行等。QD 的入口是 exec_simple_query()。
QE(Query Executor、查询执行器):Segment 上负责执行 QD 分发来的查询任务的进程称为 QE。Segment 实例运行的也是一个 PostgreSQL,所以对于 QE 而言,QD 是一个 PostgreSQL 的客户端,它们之间通过 PostgreSQL 标准的 libpq 协议进行通讯。对于 QD 而言,QE 是负责执行其查询请求的 PostgreSQL Backend 进程。通常 QE 执行整个查询的一部分(称为 Slice)。QE 的入口是 exec_mpp_query()。
Slice:为了提高查询执行并行度和效率,Greenplum 把一个完整的分布式查询计划从下到上分成多个 Slice,每个 Slice 负责计划的一部分。划分 slice 的边界为 Motion,每遇到 Motion 则一刀将 Motion 切成发送方和接收方,得到两颗子树。每个 slice 由一个 QE 进程处理。上面例子中一共有三个 slice。
Gang:在不同 segments 上执行同一个 slice 的所有 QEs 进程称为 Gang。上例中有两组 Gang,第一组 Gang 负责在 2 个 segments 上分别对表 classes 顺序扫描,并把结果数据重分布发送给第二组 Gang;第二组 Gang 在 2 个 segments 上分别对表 students 顺序扫描,与第一组 Gang 发送到本 segment 的 classes 数据进行哈希关联,并将最终结果发送给 Master。
下图展示了查询在 Greenplum 集群中并行执行的流程。该图假设有 2 个 segments,查询计划有两个 slices,一共有 4 个 QEs,它们之间通过网络进行通讯。
QD 和 QE 都是 PostgreSQL backend 进程,其执行逻辑非常相似。对于数据操作(DML)语句(数据定义语句的执行逻辑更简单),其核心执行逻辑由 ExecutorStart, ExecutorRun, ExecutorEnd 实现。
QD:
QE 上的 ExecutorStart/ExecutorRun/ExecutorEnd 函数和单节点的 PostgreSQL 代码逻辑类似。主要的区别在 QE 执行的是 Greenplum 分布式计划中的一个 slice,因而其查询树的根节点一定是个 Motion 节点。其对应的执行函数为 ExecMotion,该算子从查询树下部获得元组,并根据 Motion 的类型发送给不同的接收方。低一级的 Gang 的 QE 把 Motion 节点的结果元组发送给上一级 Gang 的 QE,最顶层 Gang 的 QE 的 Motion 会把结果元组发送给 QD。Motion 的 Flow 类型确定了数据传输的方式,有两种:广播和重分布。广播方式将数据发送给上一级 Gang 的每一个 QE;重分布方式将数据根据重分布键计算其对应的 QE 处理节点,并发送给该 QE。
QD 和 QE 之间有两种类型的网络连接:
有一类特殊的 SQL,执行时只需要单个 segment 执行即可。譬如主键查询:SELECT * FROM tbl WHERE id = 1;
为了提高资源利用率和效率,Greenplum 对这类 SQL 进行了专门的优化,称为 Direct Dispatch 优化:生成查询计划阶段,优化器根据分布键和 WHERE 子句的条件,判断查询计划是否为 Direct Dispatch 类型查询;在执行阶段,如果计划是 Direct Dispatch,QD 则只会把计划发送给需要执行该计划的单个 segment 执行,而不是发送给所有的 segments 执行。
Greenplum 使用两阶段提交(2PC)协议实现分布式事务。2PC 是数据库经典算法,此处不再赘述。本节概要介绍两个 Greenplum 分布式事务的实现细节:
在分布式环境下,SQL 在不同节点上的执行顺序可能不同。譬如下面例子中 segment1 首先执行 SQL1,然后执行 SQL2,所以新插入的数据对 SQL1 不可见;而 segment2 上先执行 SQL2 后执行 SQL1,因而 SQL1 可以看到新插入的数据。这就造成了数据的不一致。
Greenplum 使用分布式快照和本地映射实现跨节点的数据一致性。Greenplum QD 进程承担分布式事务管理器的角色,在 QD 开始一个新的事务(StartTransaction)时,它会创建一个新的分布式事务 id、设置时间戳及相应的状态信息;在获取快照(GetSnapshotData)时,QD 创建分布式快照并保存在当前快照中。和单节点的快照类似,分布式快照记录了 xmin/xmax/xip 等信息,结构体如下所示:
typedef struct DistributedSnapshot
{
DistributedTransactionTimeStamp distribTransactionTimeStamp;
DistributedTransactionId xminAllDistributedSnapshots;
DistributedSnapshotId distribSnapshotId;
DistributedTransactionId xmin; /* XID < xmin 则可见 */
DistributedTransactionId xmax; /* XID >= xmax 则不可见 */
int32 count; /* inProgressXidArray 数组中分布式事务的个数 */
int32 maxCount;
/* 正在执行的分布式事务数组 */
DistributedTransactionId *inProgressXidArray;
} DistributedSnapshot;
执行查询时,QD 将分布式事务和快照等信息序列化,通过 libpq 协议发送给 QE。QE 反序列化后,获得 QD 的分布式事务和快照信息。这些信息被用于确定元组的可见性(HeapTupleSatisfiesMVCC)。所有参与查询的 QEs 都使用 QD 发送的同一份分布式事务和快照信息判断元组的可见性,因而保证了整个集群数据的一致性,避免前面例子中出现的不一致现象。
在 QE 上判断一个元组对某个快照的可见性流程如下:
和 PostgreSQL 的提交日志 clog 类似,Greenplum 需要保存全局事务的提交日志,以判断某个事务是否已经提交。这些信息保存在共享内存中并持久化存储在 distributedlog 目录下。
为了提高判断本地 xid 可见性的效率,避免每次访问全局事务提交日志,Greenplum 引入了本地事务-分布式事务提交缓存,如下图所示。每个 QE 都维护了这样一个缓存,通过该缓存,可以快速查到本地 xid 对应的全局事务 distribXid 信息,进而根据全局快照判断可见性,避免频繁访问共享内存或磁盘。
Greenplum 中一个 SQL 查询计划可能含有多个 slices,每个 Slice 对应一个 QE 进程。任一 segment 上,同一会话(处理同一个用户 SQL)的不同 QE 必须有相同的可见性。然而每个 QE 进程都是独立的 PostgreSQL backend 进程,它们之间互相不知道对方的存在,因而其事务和快照信息都是不一样的。如下图所示。
为了保证跨 slice 可见性的一致性,Greenplum 引入了“共享本地快照(Shared Local Snapshot)”的概念。每个 segment 上的执行同一个 SQL 的不同 QEs 通过共享内存数据结构 SharedSnapshotSlot 共享会话和事务信息。这些进程称为 SegMate 进程组。
Greenplum 把 SegMate 进程组中的 QE 分为 QE writer 和 QE reader。QE writer 有且只有一个,QE reader 可以没有或者多个。QE writer 可以修改数据库状态;QE reader 不能修改数据库的状态,且需要使用和 QE writer 一样的快照信息以保持与 QE writer 一致的可见性。如下图所示。
“共享”意味着该快照在 QE writer 和 readers 间共享,“本地”意味着这个快照是 segment 的本地快照,同一用户会话在不同的 segment 上可以有不同的快照。segment 的共享内存中有一个区域存储共享快照,该区域被分成很多槽(slots)。一个 SegMate 进程组对应一个槽,通过唯一的会话 id 标志。一个 segment 可能有多个 SegMate 进程组,每个进程组对应一个用户的会话,如下图所示。
QE Writer 创建本地事务后,在共享内存中获得一个 SharedLocalSnapshot 槽,并它自己的本地事务和快照信息拷贝到共享内存槽中,SegMate 进程组中的其他 QE Reader 从该共享内存中获得事务和快照信息。Reader QEs 会等待 Writer QE 直到 Writer 设置好共享本地快照信息。
只有 QE writer 参与全局事务,也只有该 QE 需要处理 commit/abort 等事务命令。
相邻 Gang 之间的数据传输称为数据洗牌(Data Shuffling)。数据洗牌和 Slice 的层次相吻合,从下到上一层一层通过网络进行数据传输,不能跨层传输数据。根据 Motion 类型的不同有不同的实现方式,譬如广播和重分布。
Greenplum 实现数据洗牌的技术称为 interconnect,它为 QEs 提供高速并行的数据传输服务,不需要磁盘 IO 操作,是 Greenplum 实现高性能查询执行的重要技术之一。interconnect 只用来传输数据(表单的元组),调度、控制和错误处理等信息通过 QD 和 QE 之间的 libpq 连接传输。
Interconnect 有 TCP 和 UDP 两种实现方式,TCP interconnect 在大规模集群中会占用大量端口资源,因而扩展性较低。Greenplum 默认使用 UDP 方式。UDP interconnect 支持流量控制、网络包重发和确认等特性。
分布式集群包含多个物理节点,少则四五台,多则数百台。管理如此多机器的复杂度远远大于单个 PostgreSQL 数据库。为了简化数据库集群的管理, Greenplum 提供了大量的工具。下面列出一些常用的工具,关于更多工具的信息可以参考 Greenplum 数据库管理员官方文档。
上面概要介绍了把单个 PostgreSQL 数据库变成分布式数据库涉及的 6 个方面的工作。若对更多细节感兴趣,最有效的方式是动手改改代码实现某些新特性。下面几个项目可以作为参考:
对这些项目有兴趣者可以联系 yyao AT pivotal DOT io 提供更多咨询或帮助。实现以上任何一个功能者,可以走快速通道加入 Greenplum 内核开发团队,共应挑战共享喜悦 :)
想了一会,本想写一点打鸡血的话吸引更多人加入数据库内核开发行列,然觉不合自性,作罢。
Greenplum 酒文化比较浓厚,还是分享一个与酒有关的小故事收尾吧。
13/14 年左右有幸和数据库老前辈 Dan Holle(Teradata CTO,第七号员工)有诸多交集。老爷子谈吐优雅而不失幽默,从事 MPP 数据库已有 30 余年。每次喝酒至少放两瓶酒于面前,且同时喝两瓶酒,笑谈此为并行处理;若某一瓶喝的多了,必拿起另一瓶再喝一点,以确保两瓶余量保持一致,笑谈此为避免倾斜。经常左瓶喝的多了点,拿起右瓶补一口,补多了,再拿起左瓶补一口。如此左右互补,无需山东人劝酒,自己很快进入状态。
老先生对 MPP 数据库之爱已融入生活中,令人敬佩。而正是许多这样数十年如一日的匠人成就了当今数据库领域的辉煌。期待更多人加入,幕天席地把酒言欢!
关于作者
姚延栋, Greenplum 研发总监,Greenplum 中文社区发起人。致力于 Greenplum/PostgreSQL 开源数据库产品、社区和生态的发展。
作者介绍:
姚延栋,山东大学本科,中科院软件所研究生。PostgreSQL 中文社区委员,致力于 Greenplum/PostgreSQL 开源数据库产品、社区和生态的发展。
相关阅读:
领取专属 10元无门槛券
私享最新 技术干货