数据库内核月报

数据库内核月报 - 2020 / 03

PgSQL · 引擎特性 · PostgreSQL 通信协议

Author: lingce

我们在使用数据库服务时,通常需要使用客户端连接数据库服务端,以 PostgreSQL 为例,常用的客户端有自带的 psql,JAVA 应用的数据库驱动 JDBC,可视化工具 PgAdmin 等,这些客户端都需要遵守 PostgreSQL 的通信协议才能与之 “交流”。所谓协议,可以理解为一套信息交互规则或者规范,最为我们熟知的莫过于 TCP/IP 协议和 HTTP 协议。

protocol

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 阶段。 normal 阶段,客户端发送请求至服务端,服务端执行命令并将结果返回给客户端。客户端请求结束后,可以主动发送消息断开连接。

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

本节介绍通信协议的基础,包括消息格式和基本的消息流, normal 阶段的两种 “子协议” 在下一节详细介绍。

消息

消息格式

客户端和服务端所有通信都通过消息流进行。消息的第一个字节标识消息类型,随后四个字节标识消息内容的长度(该长度包括这四个字节本身),具体的消息内容由消息类型决定。

message

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

startup

startup 消息的处理流程可以参考 ProcessStartupPacket

消息类型

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 */

服务端收到如上消息的处理流程可以参考 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

消息流

Startup

startup 阶段是客户端和服务端创建连接的阶段,消息流如下:

startup-flow

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

取消请求

startup 阶段,服务端还会给客户端发送一个 BackendKeyData 消息,该消息中包含服务端的进程 ID 和一个取消码(MyCancelKey)。如果客户端想取消当前正在执行的请求,则可以发送一个 CancelRequset 消息,该消息中包括 startup 阶段服务端提供的进程 ID 和取消码。

取消请求并不是通过当前正在处理请求的连接发送的,而是会创建一个新的连接,创建该连接发送的消息与之前创建连接的消息不同,不再发送 startup 消息,而是发送一个 CancelReqeust 消息,该消息同样没有消息类型字段。

cancel

取消请求不保证一定成功,可能服务端接收到取消请求时,当前的查询请求已经结束。取消请求只能在一定程度上加速当前查询结束,如果当前请求被取消,客户端会收到一条错误消息。

发送请求

连接创建之后,通信协议进入 normal 阶段,该阶段的大体流程是:客户端发送查询请求,服务端接收请求、处理请求并将结果返回给客户端。上文提到,该阶段有两种 “子协议”,本节分别介绍这两种 “子协议” 的消息流。

Simple Query

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

simple-query

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

simple-query-flow

注意,一个请求中的多条 SQL 命令会被当做一个事务来执行,如果有命令执行失败,整个事务都会回滚。用户可以在请求中显式添加 BEGIN 和 COMMIT ,将一个请求划分为多个事务,避免事务全部回滚。显式添加事务控制语句的方式无法避免请求有语法错误的情况,如果请求有语法错误,整个请求都不会被执行。

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

'I';			/* idle --- not in transaction */
'T';			/* in transaction */
'E';			/* in failed transaction */

Extended Query

Extended Query 协议将以上 Simple Query 的处理流程分为若干步骤,每一步都由单独的服务端消息进行确认。该协议可以使用服务端的 perpared-statement 功能,即先发送一条参数化 SQL,服务端收到 SQL(Statement)之后对其进行解析、重写并保存,这里保存的 Statement 也就是所谓 Prepared-statement,可以被复用;执行 SQL 时,直接获取事先保存的 Prepared-statement 生成计划并执行,避免对同类型 SQL 重复解析和重写。

如下例, SELECT * FROM users u, logs l WHERE u.usrid=$1 AND u.usrid=l.usrid AND l.date = $2; 是一条参数化 SQL,执行 PREPARE 时,服务端对该 SQL 进行解析和重写;执行 EXECUTE 时,为 Prepared Statement 生成计划并执行。第二次执行 EXECUTE 时无需再对 SQL 进行解析和重写,直接生成计划并执行即可。PostgreSQL Prepared Statement 的具体细节可以参考[3],PostgreSQL JDBC 的相关介绍可以参考[4]。

PREPARE usrrptplan (int) AS
    SELECT * FROM users u, logs l WHERE u.usrid=$1 AND u.usrid=l.usrid
    AND l.date = $2;
EXECUTE usrrptplan(1, current_date);
EXECUTE usrrptplan(2, current_date);

可见,Extended Query 协议通过使用服务端的 Prepared Statement,提升同类 SQL 多次执行的效率。但与 Simple Query 相比,其不允许在一个请求中包含多条 SQL 命令,否则会报语法错误。

Extended Query 协议通常包括 5 个步骤,分别是 Parse,Bind,Describe,Execute 和 Sync。以下分别介绍各个阶段的处理流程。

Parse

客户端首先向服务端发送一个 Parse 消息,该消息包括参数化 SQL,参数占位符以及每个参数的类型,还可以指定 Statement 的名字,若不指定名字,即为一个 “未命名” 的 Statement,该 Statement 会在生成下一个 “未命名” Statement 时予以销毁,若指定名字,则必须在下次发送 Parse 消息前将其显式销毁。

parse

PostgreSQL 服务端收到该消息后,调用 exec_parse_message 函数进行处理,进行语法分析、语义分析和重写,同时会创建一个 Plan Cache 的结构,用于缓存后续的执行计划。

Bind

客户端发送 Bind 消息,该消息携带具体的参数值、参数格式和返回列的格式,如下:

bind

PostgreSQL 收到该消息后,调用 exec_bind_message 函数进行处理。为之前保存的 Prepared Statement 创建执行计划并将其保存在 Plan Cache 中,创建一个 Portal 用于后续执行。关于 Plan Cache 的具体实现和复用逻辑在此不细述,以后单独撰文介绍。

在 PostgreSQL 内核中,Portal 是对查询执行状态的一种抽象,该结构贯穿执行器运行的始终。

Describe

客户端可以发送 Describe 消息获取 Statment 或 Portal 的元信息,即返回结果的列名,类型等信息,这些信息由 RowDescription 消息携带。如果请求获取 Statement 的元信息,还会返回具体的参数信息,由 ParameterDescription 消息携带。

desc

Execute

客户端发送 Execute 消息告知服务端执行请求,服务端收到消息后,执行 Bind 阶段创建的 Portal,执行结果通过 DataRow 消息返回给客户端,执行完成后发送 CommandComplete 。

exec

Execute 消息中可以指定返回的行数,若行数为 0,表示返回所有行。

Sync

使用 Extended Query 协议时,一个请求总是以 Sync 消息结束,服务端接收到 Sync 消息后,关闭隐式开启的事务并回复 ReadyForQuery 消息。

Extended Query 完整的消息流如下:

extended

Copy 子协议

为高效地导入/导出数据,PostgreSQL 支持 COPY 命令, COPY 操作会将当前连接切换至一种截然不同的子协议。

Copy 子协议对应三种模式:

copy-in 为例,服务端收到 COPY 命令后,进入 COPY 模式,并回复 CopyInResponse。随后客户端通过 CopyData 消息传输数据,CopyComplete 消息标识数据传输完成,服务端收到该消息后,发送 CommandComplete 和 ReadyForQuery 消息,消息流如下:

copy

总结

本文简要介绍了 PostgreSQL 的通信协议,包括消息格式、消息类型和常见通信过程的消息流。一般通信过程分为两个阶段: startup 阶段创建连接, normal 阶段发送请求并返回结果。 normal 阶段又包括两种子协议, Simple Query 一次性发送查询请求; Extended Query 分阶段发送请求,利用服务端的 prepared statement 特性,提升反复执行同类请求的效率。

PostgreSQL 通信协议中,除本文介绍的 COPY 子协议,还有一些其他的子协议,如主备流复制子协议,限于篇幅,本文并未给出详尽的描述,感兴趣的同学可以参考相关文档[5]。

最后,本文严重参考了 2014 年 PG 大会这篇[6]分享,推荐大家阅读。

参考文献

  1. https://www.net.t-labs.tu-berlin.de/teaching/computer_networking/01.02.htm
  2. https://www.postgresql.org/docs/current/protocol.html
  3. https://www.postgresql.org/docs/12/sql-prepare.html
  4. https://jdbc.postgresql.org/documentation/head/server-prepare.html
  5. https://www.postgresql.org/docs/current/protocol-replication.html
  6. https://www.pgcon.org/2014/schedule/attachments/330_postgres-for-the-wire.pdf