Bootstrap

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

相关阅读: