写点什么

Greenplum 内核源码分析 - 分布式事务 (二)

  • 2021 年 12 月 31 日
  • 本文字数:3708 字

    阅读完需:约 12 分钟

目录

  • 前言

  • PostgreSQL 和 Greenplum 的通信协议

  • PostgreSQL 的事务处理简介


前言

因为 Greenplum 是基于 PostgreSQL 二次开发出来的,代码里面沿用了很多 PostgreSQL 的逻辑和函数,所以我会先简单介绍 PostgreSQL 数据库的事务处理函数。

为了后面的代码分析做准备,这一篇还会介绍几个受众之间的通信关系,命令的传输主要是用 libpq 来做通信。如果有复杂的 query,会使用 Greenplum 自研的 Interconnect 机制做数据交互。


PostgreSQL 和 Greenplum 的通信协议

这段分析严重参考了网上分析通信协议的文章,推荐大家自己阅读。


数据库是一个 server 端的服务,需要客户端和它进行通信,然后才能提供服务。以 PostgreSQL 为例,常用的客户端有自带的 psql,JAVA 应用的数据库驱动 JDBC,可视化工具 PgAdmin 等, 这些客户端都需要遵守 PostgreSQL 的通信协议才能使用 PostgreSQL 数据库。 所谓协议,可以理解为一套信息交互规则或者规范,最为我们熟知的莫过于 TCP/IP 协议和 HTTP 协议。



PostgreSQL 在 TCP/IP 协议之上实现了一套基于消息的通信协议。 同时,为避免客户端和服务端在同一台机器时的网络通信代价,也支持在 Unix 域套接字上使用该协议。 PostgreSQL 至今共实现了三个版本的通信协议,现在普遍使用的是从 7.4 版本开始使用的 3.0 版本,其他版本的协议依然支持。 一个 PostgreSQL 数据库实例同时支持所有版本的协议,具体使用那个版本取决于客户端的选择,无论选择哪个版本, 客户端和服务端需要匹配,否则可能无法正常 “交流”。本文介绍 PostgreSQL 3.0 版本的通信协议。

PostgreSQL 是多进程架构,守护进程 Postmaster 为每个连接分配一个后台进程(backend),后台进程的分配是在协议处理之前进行的, 每个后台进程自行负责协议的处理。在 PostgreSQL 源码或者文档中,通常认为 'backend' 和 'server' 是等价的,表示服务端; 同样,'frontend' 和 'client' 是等价的,表示客户端。


协议基础

PostgreSQL 通信协议包括两个阶段: startup 阶段和 normal 阶段。 startup 阶段,客户端尝试创建连接并发送授权信息,如果一切正常,服务端会反馈状态信息,连接成功创建,随后进入 normal 阶段。

startup 阶段,客户端发送请求至服务端,服务端执行命令并将结果返回给客户端。客户端请求结束后,可以主动发送消息断开连接。

normal 阶段,客户端可以通过两种 “子协议” 来发送请求,分别是 simpel query 和 extened query。 使用 simple query 时,客户端发送字符串文本请求,后端收到后立即处理并返回结果; 使用 extened query 时,发送请求的过程被分为若干步骤,通常包括 Parse,Bind 和 Execute。


消息格式

消息的第一个字节标识消息类型,随后四个字节标识消息内容的长度(该长度包括这四个字节本身),具体的消息内容由消息类型决定。



客户端创建连接时,发送的第一条消息,即启动(startup)消息格式有所不同。它没有最开始的消息类型字段,以消息长度开始,随后紧跟协议版本号,然后是键值对形式的连接信息,如用户名、数据库以及其他 GUC 参数和值。


消息类型

PostgreSQL 目前支持如下客户端消息类型:

case 'Q':			/* simple query */case 'P':			/* parse */case 'B':			/* bind */case 'E':			/* execute */case 'F':			/* fastpath function call */case 'C':			/* close */case 'D':			/* describe */case 'H':			/* flush */case 'S':			/* sync */case 'X':case EOF:case 'd':			/* copy data */case 'c':			/* copy done */case 'f':			/* copy fail */
复制代码


这里我们需要注意了,Greenplum 在这个协议的基础上增加了几个消息

case 'M':     /* MPP dispatched stmt from QD */case 'T':     /* MPP dispatched dtx protocol command from QD */
复制代码

服务端收到如上消息的处理流程可以参考 PostgresMain。服务端发送给客户端的消息有如下类型(不完全),

case 'C':		/* command complete */case 'E':		/* error return */case 'Z':		/* backend is ready for new query */case 'I':		/* empty query */case '1':		/* Parse Complete */case '2':		/* Bind Complete */case '3':		/* Close Complete */case 'S':		/* parameter status */case 'K':		/* secret key data from the backend */case 'T':		/* Row Description */case 'n':		/* No Data */case 't':		/* Parameter Description */case 'D':		/* Data Row */case 'G':		/* Start Copy In */case 'H':		/* Start Copy Out */case 'W':		/* Start Copy Both */case 'd':		/* Copy Data */case 'c':		/* Copy Done */case 'R':		/* Authentication Request */
复制代码


客户端处理如上服务端消息的流程可以参考 PostgreSQL libqp 的实现 pqParseInput3


消息通信的过程

1) Startup

客户端首先发送 startup 消息至服务端,服务端判断是否需要授权信息,如若需要,则发送 AuthenticationRequest ,客户端随后发送密码至服务端,权限验证之后,服务端给客户端发送一些参数信息,即 ParameterStatus ,包括 server_version , client_encoding 和 DateStyle 等。最后,服务端发送一个 ReadyForQuery 消息,告知客户端一切就绪,可以发送请求了。至此,连接创建成功。



2)常规请求

连接创建之后,通信协议进入 normal 阶段,该阶段的大体流程是:客户端发送查询请求,服务端接收请求、处理请求并将结果返回给客户端。该阶段有两种 “子协议” simpel query 和 extened query。


simple query:客户端通过 Query 消息发送一个文本命令给服务端,服务端处理请求,回复查询结果。查询结果通常包括两部分内容:结构和数据。结构通过 RowDescription 消息传递,包括列名、类型 OID 和长度等;数据通过 DataRow 消息传递,每个 DataRow 消息中包含一行数据。



每个命令的结果发送完成之后,服务端会发送一条 CommandComplete 消息,表示当前命令执行完成。客户端的一条查询请求可能包含多条 SQL 命令,每个 SQL 命令执行完都会回复一条 CommandComplete 消息,查询请求执行结束后会回复一条 ReadyForQuery 消息,告知客户端可以发送新的请求。



ReadyForQuery 消息会反馈当前事务的执行状态,客户端可以根据事务状态做相应的处理,目前有如下三种事务状态

'I';			/* idle --- not in transaction */'T';			/* in transaction */'E';			/* in failed transaction */
复制代码


大家平时用 linux 的进程工具查看 Greenplum 进程状态的时候,就会进程信息里看到这样的一些状态信息。


Extended Query:Extended Query 协议将以上 Simple Query 的处理流程分为若干步骤,每一步都由单独的服务端消息进行确认。

PostgreSQL 的事务处理简介

PostgreSQL 的代码里面有一个README文件,很详细的描述了事务的各种操作关系和函数调用关系,我们取其中的一段简单介绍下。


For example, consider the following sequence of user commands:
1) BEGIN2) SELECT * FROM foo3) INSERT INTO foo VALUES (...)4) COMMIT
In the main processing loop, this results in the following function callsequence:
/ StartTransactionCommand; / StartTransaction;1) < ProcessUtility; << BEGIN \ BeginTransactionBlock; \ CommitTransactionCommand;
/ StartTransactionCommand;2) / PortalRunSelect; << SELECT ... \ CommitTransactionCommand; \ CommandCounterIncrement;
/ StartTransactionCommand;3) / ProcessQuery; << INSERT ... \ CommitTransactionCommand; \ CommandCounterIncrement;
/ StartTransactionCommand; / ProcessUtility; << COMMIT4) < EndTransactionBlock; \ CommitTransactionCommand; \ CommitTransaction;
复制代码


这是一套简单 begin/commit 操作,每一条语句都会被 StartTransactionCommand 和 CommitTransactionCommand (AbortCurrentTransaction) 包裹起来。

因为是 Begin 命令,所以有 BeginTransactionBlock,然后还会调用 StartTransaction 表示事务开始。

BeginTransactionBlock 是 Begin 命令专有的函数, 表示后续的 SQL 语句是一个完整的事务,所以要做一些状态处理。


StartTransaction 属于底层的事务调用,无论有没有 Begin,都会调用到。


Begin 模块里面,ProcessUtility 是具体执行逻辑的地方,包含了 BeginTransactionBlock,在 Greenplum 代码里,会在 master 和 segment 上面都执行 Begin。


Insert 模块里面,ProcessQuery 是具体执行逻辑的地方,在 Greenplum 代码里,会在这一步把 insert 相关的 SQL 发到对应的 segment 上面。


Commit 模块里面,CommitTransaction 是具体执行逻辑的地方。在 Greenplum 代码里,有两个步骤,第一步发送 DTX_PROTOCOL_COMMAND_PREPARE 到每个 segment,第二步发送 DTX_PROTOCOL_COMMAND_COMMIT_PREPARE 到每个 segment,这就是传说中的两阶段提交。


以上只是简短的描述,后续的段落和文章会进一步深入。

参考文献

https://beta.pgcon.org/2014/schedule/attachments/330_postgres-for-the-wire.pdf

发布于: 1 小时前
用户头像

还未添加个人签名 2021.12.30 加入

https://github.com/ginobiliwang

评论

发布
暂无评论
Greenplum内核源码分析-分布式事务(二)