Greenplum内核源码分析-分布式事务(二)
目录
前言
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 */
服务端收到如上消息的处理流程可以参考 。服务端发送给客户端的消息有如下类型(不完全),
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 的实现 。
消息通信的过程
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的代码里面有一个,很详细的描述了事务的各种操作关系和函数调用关系,我们取其中的一段简单介绍下。
For example, consider the following sequence of user commands:
1) BEGIN
2) SELECT * FROM foo
3) INSERT INTO foo VALUES (...)
4) COMMIT
In the main processing loop, this results in the following function call
sequence:
/ StartTransactionCommand;
/ StartTransaction;
1) < ProcessUtility; << BEGIN
\ BeginTransactionBlock;
\ CommitTransactionCommand;
/ StartTransactionCommand;
2) / PortalRunSelect; << SELECT ...
\ CommitTransactionCommand;
\ CommandCounterIncrement;
/ StartTransactionCommand;
3) / ProcessQuery; << INSERT ...
\ CommitTransactionCommand;
\ CommandCounterIncrement;
/ StartTransactionCommand;
/ ProcessUtility; << COMMIT
4) < 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