乐趣区

关于postgresql:PostgreSQL技术内幕十三探究MPP数据库分布式查询分发Dispatcher

Dispatcher(分布式查问散发器)是 MPP 数据库的外围组件,所有的查问工作都要通过其进行散发,起着沟通用户到协调者(Coordinator,即 QD)和执行调度的关键作用。

在这次的直播中,咱们为大家介绍了 Dispatcher 基本原理和实现机制,并结合实际用例进行了操作演示。以下内容依据直播文字整顿而成。

Slice 与 Gang 的基本概念与分类
传统 MPP 数据库采纳无共享 Shared-Nothing 架构来存储数据,节点之间不共享存储和计算资源,须要应用其余节点的数据时通常利用网络重散发。

                 图 1:Greenplum 数据库查问示意图

(图片来自 Greenplum: A Hybrid Database for Transactional and Analytical Workloads,SIGMOD ’21,序号和箭头系本文作者所加)

以 Greenplum 为例,如图 1 所示,当用户连贯到 Coordinator(协调者节点)进行查问操作时,会通过 Dispatcher 组件将查问任务分配到不同的 Segment,各 Segment 之间通过 Interconnect 模块来传输数据。当各节点查问执行实现后,由 QD 节点对查问后果进行收集和整顿,再回传给用户。

须要留神的是,在查问工作执行时,用户不会和 QE 产生任何的连贯,所有音讯都是通过 QD 来直达传递,这也是 MPP 数据库的重要特色。整个过程中,波及到两个重要的概念:

Slice:为了在查问执行期间实现最大的并行度,Greenplum 会将查问打算工作划分为 Slices。Slice 是查问打算中能够独立进行解决的局部。查问打算会为 Motion 生成 Slice,Motion 的每一侧都有一个 Slice。正是因为 Motion 算子将查问打算宰割为一个个 Slice,上一层 Slice 对应的过程会读取下一层各个 Slice 过程播送或重散布操作生成的数据,而后进行计算。

Gang:属于同一个 Slice 然而运行在不同的 Segment 上的过程,称之为 Gang。在 Greenplum 中,共有 Unallocated、Reader Gang、Writer Gang、Entry Reader、Singleton Reader 五种类型的 Gang。其中:

Unallocated 运行在 QD,个别只在 Gather Motion 将各个 QE 回传的后果收取并集时才会用到。

Reader Gang 和 Writer Gang 会常常用到,而且相干的查问打算会很简单。
● 只读的查问仅蕴含 Reader Gang,蕴含写操作的查问才会应用 Writer Gang。
● 一些既读又写的查问(例如 Create table as、Update Returning 等)可能同时蕴含这两类 Gang。
● 这两类 Gang 都只有 1-Gang 和 N-Gang 的状况(其中 N 为 Segment 数量),优化器不会进一步划分出更粗疏的结构。

Entry Reader 和 Singleton Reader 不太常见,通常在解决子查问或者其它须要保证系统中只有一份值的情景下呈现。

MPP 数据库分布式的特点,使其存储容量和计算能力冲破了单机的下限,而成为多个 Segment 的总和。MPP 数据库的存储和计算紧耦合,对某一部分数据的计算应尽量在这一部分数据的存储节点上间接实现,这也使得所有表在创立时都有指定的分布模式,对该表的存储和计算都应依照该模式在集群的参与者节点上散布开来,从而造成“分布式”。

                 图 2:Greenplum 查问执行调度示意图(起源同图 1)

许多 MPP 数据库的初学者,常常会遇到一个问题,Slice 和 Gang 是什么关系?

从二者的作用来看,Slice 是对 Plan 的天然切割,Gang 是对 Slice 的天然实现,两者之间是互相对应的。事实上,在内核代码中两者常常会交替应用。如果是优化器,会更多应用 Slice;而在执行器、Dispatcher 局部的代码就更多应用 Gang。咱们能够认为,Slice 和 Gang 是齐全对应的,前者偏重布局,后者偏重执行

须要特别强调的是,Writer Gang ≠ Writer QE。事实上,QE 并不关怀本人是不是 Writer Gang,它的行为只与 QD 连贯时下发的 isWriter 参数相干,这个参数通过 libpq 协定握手时 Startup 音讯中的 gpqeid 参数下发到 QE 上,QE 会据此设置 Gp_is_writer 全局变量,从而管制 QE 的行为。

在 Greenplum 中,任何查问都只有一个 Writer QE,而只有写查问才会有 Writer Gang。因而,咱们时常听到的“任何查问都有一个 Writer Gang”的论断是不谨严的。

查问散发
查问散发通常分为以下四个阶段:
1. 创立 Gang 并初始化链接;
2. 散发查问打算并执行;
3. 回收 Gang;
4. 会话断开时,敞开链接并销毁 Gang。

在所有查问中,Plan 类查问最为简单,它涵盖了其余三类查问的共用办法。接下来,咱们以 Plan 类查问为例,开展介绍查问散发的实现流程。

💡Plan 是指 Select、Insert 这种可由 explain 指令查看执行打算的查问。这里有一个特例,CTAS(create table as)不属于 Plan,而是属于 Utility 类别。

Plan 类查问散发的入口点是 cdbdisp_dispatchX 函数,它会调用 AssignGangs 办法,通过两次先序遍历查问打算树,创立所有的 Writer Gang,而后再创立其余 Gang:

bool AssignWriterGangFirst(ExecSlice *slice, ...) [if (slice->gangType == GANGTYPE_PRIMARY _WRITER) {slice->primaryGang = AllocateGang(...);
    return true;
  } else {
    ListCell *cell;
    foreach(cell, slice-›children) {if (AssignWriterGangFirst(cell->element, ...))
        return true;
    }
  }
  return false;
}
                   第一次遍历代码示例
bool InventorySliceTree(ExecSlice *slice, ...) {if (slice->gangType == GANGTYPE_UNALLOCATED) {slice->primaryGang = NULL;} else if (!slice->primaryGang) {slice->primaryGang = AllocateGang(...);
  }

  ListCell *cell;
  foreach(cell, slice->children) {InventorySliceTree(cell->element, ...);
  }
}
                   第二次遍历代码示例

留神:AssignWriterGangFirst 有一个“提前终止”的个性,表明查问打算只能有一个 Writer Gang。

咱们看到这两段代码都是调用 AllocateGang 函数实现具体一个 Gang 的初始化,上方的代码只能调用一次,下方的代码能够调用屡次。

创立并初始化 Gang
这里引入了另一个概念:Segment Type,因为 AllocateGang 函数并不间接基于 Gang Type 去工作,须要将 Gang Type 转化为 Segment Type。二者之间对应关系如下:

● Writer Gang 肯定要求一个 Writer QE,即一个 isWriter 为 true 的 Gang;
● extended query(libpq 规定的扩大查问协定,即游标 CURSOR)肯定要求一个 Reader QE,即 isWriter 为 false 的 Gang;
● 其余状况(包含 Reader Gang 在内),实用任何节点。

通过以上的对应关系可能提供更好的兼容性,并进一步提高 Gang 过程的复用率。下一步会调用:cdbgang_createGang_asynccdbcomponent_allocateIdleQE 这两个函数。从名字能够看出,Gang 的创立是一个异步的过程,会一次创立一个 Gang 中的所有链接,而后通过轮询期待创立实现。在创立过程中,首先判断 freelist 中回收上来的 QE 的相容性,如果相容则间接应用;否则异步调配新的 Gang。

异步的过程如下所示:

// cdbgang_createGang_async
int size = list_length(segments);
for (i = 0; i < size; i++) {segdbDesc = newGangDefinition->db_descriptors[i];
  cdbconn_doConnectStart(segdbDesc, ...); // -> PQconnectStartParams -> PQconnectPoll

  connStatusDone[i] = false;
  pollingStatus[i] = PGRES_POLLING_WRITING;
}

for (;;) {for (i = 0; i < size; i++) {segdbDesc = newGangDefinition->db_descriptors[i];
    if (connStatusDone[i]) continue;
    switch (pollingStatus[i]) {
      case PGRES_POLLING_OK:
        ... // -> cdbconn_doConnectComplete
        connStatusDone[i] = true;
        continue;
      case PGRES_POLLING_READING: 
        ... // set poll to wait till ready to read
        break;
      case PGRES_POLLING_WRITING:
        ... // set poll to wait till write won't get blocked
        break;
      case PGRES_POLLING_FAILED: ... // throw exception
    }
  }
  
  nready = poll(fds, nfds, poll_timeout);
  if (nready > 0) {for (i = 0; i < size; i++)
      segdbDesc = newGangDefinition->db_descriptors[i];
      pollingStatus[i] = PQconnectPoll(segdbDesc->conn);
  }
}

散发查问打算并执行
在这一阶段,以 Gang 为单位异步散发 M 类型音讯(QD 到 QE 的音讯为 M 类型音讯,调用 exec_mpp_query 入口办法,而用户收到 QD 的音讯则为 Q 类型音讯,调用 exec_simple_query 入口办法)到所有 Gang,并再次通过轮询期待散发实现。

回收 Gang
回收 Gang 时,对每个 Gang 中的所有 QE 调用 cdbcomponent_recycleIdleQE 办法,将回收的 QE 放到 freelist 中。在下次调配时,会尽量应用曾经创立好的链接。这种办法保障了 Writer QE 永远在 freelist 的前部,同时因为 Writer QE 具备良好的相容性,就使得 freelist 具备“有序”的性质,在调配 Gang(allocateIdleQE)时就无需遍历整个 freelist,晋升了 Gang 的调配效率。

销毁 Gang
销毁 Gang 时,通过 libpq 对每个 idle QE 进行终止握手,并开释状态。

其它类型查问
● Plan:到所有 QE;parsed statement
● Command:到 Writer QE;raw SQL
● Utility Statement:到 Writer QE;parsed statement
● Set Command:到所有 QE;raw SQL

把握了 Plan 类型查问散发机制之后,就能更好地了解其余的三类查问,它们是 {到所有 QE,到 Writer QE} 和对 {parsed statement,raw SQL} 两两组合造成的四种后果。这里的 parsed statement 指通过查问解析和布局后的 PlannedStmt。

SharedSnapshot
Postgres 有本地快照机制,保障本地过程的一致性,而 Greenplum 作为一个分布式系统,必须通过分布式快照,来确保跨 Segment 的全局一致性。

本地快照保障过程本地一致性,分布式快照保障跨 Segment 一致性,但在 MPP 数据库中,一个 Segment 可能有多个过程,这就须要一个新的机制来保障跨过程的 Segment 本地一致性。

SharedSnapshot 作为基于共享内存的快照同步,成为沟通本地快照和分布式快照的一个桥梁。Postgres 中的 SnapshotData 自带了过程本地一致性,在这根底之上通过 SharedSnapshot 实现跨过程的 Segment 本地一致性,最终通过分布式快照 DistributedSnapshot,实现跨 Segment 的零碎全局一致性,从而向用户提供全局统一的视图。

退出移动版