上一篇:开源数据库CockroachDB(二)
转载自:CockroachDB中国社区
集群的每个节点均可接受SQL连接。CockroachDB支持PostgreSQL协议,用户可以复用PostgreSQL客户端driver。同时,支持两种连接方式:一种是使用SSL加密连接,通过证书认证客户端,建议在非加密(不安全)环境下使用;另一种是基于密码验证的连接。
每个连接关联一个SQL Session,该Session维护了服务器端状态。与其他SQL数据库一样,在Session的整个生命周期,客户端可以通过SQL语句设置是否开启显性事务、查询或者设置Session参数。
CockroachDB支持许多PostgreSQL协议的SQL语法,但仍有很多区别:
每个节点上均有一个pgwire服务端进程(goroutine)处理客户端连接,接收传入的命令和返回响应结果如查询和语句结果。该pgwire进程还管理pgwire层面的预处理语句,绑定预处理参数并执行。
同时,SQL连接状态由一个Session对象和一个Planner对象(每个连接对应一个Planner对象)维护,Planner对象协调Session,当前SQL事务状态和底层KV存储的执行。
当接收到查询或语句(直接或者间接通过预处理语句生成的执行命令)时,pgwire进程将SQL文本发送到与连接相关联的Planner对象。然后,SQL代码被转化成SQL查询计划树,树中每个节点对应一种数据操作,例如:join,index join,scan,group等
目前,查询计划嵌入了执行时的run-time状态。SQL查询计划生成后,每个节点对象以“Generator”的方式,执行操作:每个节点Start其子节点,每个子节点像Generator一样把结果集返回给父节点,父节点处理后再返回到其上一级节点。
Planner把查询计划最顶层节点生成的数据,通过pgwire返回给客户端。
在CockroachDB中,每张表均有一个主键(如果建表时没有指定主键,则会自动生成隐式主键)。在底层KV存储中,每行数据的Key前缀由表标识符和该行数据的主键组成。
在底层KV存储中,表中非主键列或Column Family则被编码为Value,列标识符或Column Family标识符则作为后缀被追加到Key中。
例子:
(表格的右侧数值和左侧简化后的Schema Key结构仅为举例说明)每个数据库、表和字段名均被映射成一个自动生成的标识符,以便重命名。
下面是表格中的一行数据:
每一个Key均由表前缀“/51/42”、主键前缀“/Apple”、列或Column Family前缀(“/66,/69”)组成。Value则从SQL语句获取并编码而成。Key的高效存储由底层RocksDB引擎通过前缀压缩的方式来保障。
最后,对于SQL索引,则由索引列组成KV的Key,而Value值则保存被索引行的Key前缀。
Dist-SQL是在2016年第三季度开发的一个新的执行框架,其目的是分布式地处理SQL查询。更多细节可以参考《Distributed SQL RFC》;本章节只做概要性的描述。
选择分布式处理有以下几个原因:
Dist-SQL实现方式最初主要受Sawzaul项目启发(在谷歌,Rob Pike et al. 提出了用“shell”(高级语言解释器)来简化MapReduce的使用)。它清楚地区分本地处理(处理限量数据)和分布式计算(从概念结构的有限集合中抽象出来)。
为了更好地理解SQL语句的分布式执行过程,我们引入了几个概念:
逻辑计划是由一组Aggregator组成的。每个Aggregator接受一个行输入流(或者JOIN的多个输入流),然后生成一个行输出流。输入和输出流都有确定的模式。流是一个逻辑概念,不一定映射到实际计算中的单个数据流。当逻辑计划被转换为物理计划时,Aggregator可能被分布式化;为了标识Aggregator可被分布式并行处理,可在Aggregator上定义一个Grouping操作,对把流经它的数据进行分组,从而区分出哪些数据行需要在相同节点做处理(此机制保证满足一定条件的数据行只在同一个节点上处理)。此概念对需要从部分数据集中输出结果的Aggregator来说很有用,比如SQL聚合函数。不需要Grouping的Aggregator比较特殊,虽然不需要聚合多个数据片,但可能会过滤、转换或重新排序个别数据片。
没有输入的Table Reader Aggregator被用作数据源;Table Reader可被设置成只输出某些需要的列。一个没有输出的Final Aggregator被用于缓存查询/语句的执行结果。
为了保证查询结果的有序性,一些Aggregator(final,limit)要求输入流(具有相应升降序要求的字段列表)也必须有序。一些Aggregator(如Table Reader)可以保证输出流的有序性,称为保序性。所有聚合器都有一个关联的顺序特征函数ord(input_order) -> output_order,它将input_order(输入流的顺序)映射到output_order(输出流的顺序),也就是说,如果输入流中的行是根据input_order排序的,那么输出流中的行将按照output_order排序。
Table Reader的有序性保证以及特征函数可在逻辑计划中传播排序信息。当存在不匹配(聚合器具有与有序性保证不匹配的排序要求)的情况时,我们插入一个排序聚合器。
AGGREGATOR 即SQL语义上的聚合操作。通过group key对数据进行分组,并且计算每个分组的聚合值。AGGREGATOR可以配置一个或多个聚合函数:
也可在group key或者聚合结果上执行过滤操作(甚至可以对中间结果进行过滤)。
在物理计划规划阶段,逻辑计划将会被转换成物理计划。详情请参阅Distributed SQL RFC相应章节。总的来说,在物理计划中每一个聚合器被转化为一个或多个Processor,根据数据分布状况分配Processor – 依据Range数量将TABLE READERS划分为多个实例 – 每个实例被规划至相关Range的租约持有者上。从这时候起,后续的Processor通常要么与它们的输入一起使用,要么被规划为单例(通常是在入口节点上)执行。
当把一个逻辑计划变成一个物理计划时,计划树的执行节点转变成Processor。Processor通常由三个部分组成:
Input Synchronizer将多个输入流合并成一个。类型包括:
Data Processor实现了数据转换或聚合逻辑(以及在某些情况下执行的KV操作)
Output Router将Data Processor的输出分成多个输出流;类型包括:
下面举例说明来自《 Distributed SQL RFC》:
TABLE Orders (OId INT PRIMARY KEY, CId INT, Value DECIMAL, Date DATE)
SELECT CID, SUM(VALUE) FROM Orders
WHERE DATE > 2015
GROUP BY CID
ORDER BY 1 – SUM(Value)
生成以下逻辑计划:
以上的逻辑计划可以转换为以下其中一种物理计划:
或者:
物理计划一旦生成,系统将该执行计划分发到多个节点执行。每个节点负责调度本地的Data Processor 和 Input Synchronizer。节点之间可以通过流式接口将Output Router和Input Synchronizer连接起来。
分布式执行过程:Gateway向执行计划的参与节点发送请求,参与节点负责各自的子计划的调度和执行(on-the-fly flow除外,详情见Dist-SQL设计文档)。一个节点可能负责多个独立的DAG任务——称之为“Flow”。一个Flow包含它所负责的物理计划执行节点的执行顺序,执行节点间的连接(Input Synchronizer 和 Output Router),以及该Flow上层节点输入流和下层节点输出流的标识符。一个节点可能负责调度执行多个异构Flow。通常情况下,当一个节点持有同一个表多个Range的租约,而且这些Range在同一个查询中被访问时,节点将会在本地运行一个TableReader,读取所有Range组成的Span。
因此,节点实现了ScheduleFlows RPC机制来管理Flow,为每个Flow设置输入和输出的Mailbox,并创建本地Processor执行Flow。_Mailboxes
一个节点同时调度多个不同Processor最简单的方法是并发执行:每一个Data Processor,Synchronizer和Router分别在独立的goroutine中运行,通过Channel机制通信。这些Channel自带缓冲,用于协调生产者和消费者。
不同节点上的Flow通过gRPC相互通信。为了能让生产者和消费者异步启动,ScheduleFlows为所有的输入和输出Flow创建命名的Mailbox。Mailbox可以将部分元组缓存至内部消息队列直到gRPC连接建立。当gRPC连接建立后,gRPC流控机制被用来协调生产者和消费者。消费者通过StreamMailbox RPC请求建立gRPC Stream,并携带相应的Mailbox ID(该ID在ScheduleFlows中也会用到)。
使用Mailbox执行简单查询示意图:
为了方便理解上述的处理逻辑,我们画了一个相对复杂查询的物理计划示意图。这个查询的关键是帮助分析日常促销,定位去年花了1000美元以上的客户。我们向表DailyPromotion执行插入数据,其中每行记录代表一个客户及其最近的订单总额。
TABLE DailyPromotion (
Email TEXT,
Name TEXT,
OrderCount INT
)
TABLE Customers (
CustomerID INT PRIMARY KEY,
Email TEXT,
Name TEXT
)
TABLE Orders (
CustomerID INT,
Date DATETIME,
Value INT,
PRIMARY KEY (CustomerID, Date),
INDEX date (Date)
)
INSERT INTO DailyPromotion
(SELECT c.Email, c.Name, os.OrderCount FROM
Customers AS c
INNER JOIN
(SELECT CustomerID, COUNT(*) as OrderCount FROM Orders
WHERE Date >= ‘2015-01-01’
GROUP BY CustomerID HAVING SUM(Value) >= 1000) AS os
ON c.CustomerID = os.CustomerID)
可能的物理计划如下: