我们在使用数据库服务时,通常需要使用客户端连接数据库服务端,以 PostgreSQL 为例,常用的客户端有自带的 psql,JAVA 应用的数据库驱动 JDBC,可视化工具 PgAdmin 等,这些客户端都需要遵守 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
阶段。 normal
阶段,客户端发送请求至服务端,服务端执行命令并将结果返回给客户端。客户端请求结束后,可以主动发送消息断开连接。
normal
阶段,客户端可以通过两种 “子协议” 来发送请求,分别是 simpel query
和 extened query
。使用 simple query
时,客户端发送字符串文本请求,后端收到后立即处理并返回结果;使用 extened query
时,发送请求的过程被分为若干步骤,通常包括 Parse,Bind 和 Execute。
本节介绍通信协议的基础,包括消息格式和基本的消息流, normal
阶段的两种 “子协议” 在下一节详细介绍。
消息
消息格式
客户端和服务端所有通信都通过消息流进行。消息的第一个字节标识消息类型,随后四个字节标识消息内容的长度(该长度包括这四个字节本身),具体的消息内容由消息类型决定。
需要注意的是,客户端创建连接时,发送的第一条消息,即启动(startup)消息格式有所不同。它没有最开始的消息类型字段,以消息长度开始,随后紧跟协议版本号,然后是键值对形式的连接信息,如用户名、数据库以及其他 GUC 参数和值。
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
消息至服务端,服务端判断是否需要授权信息,如若需要,则发送 AuthenticationRequest
,客户端随后发送密码至服务端,权限验证之后,服务端给客户端发送一些参数信息,即 ParameterStatus
,包括 server_version
, client_encoding
和 DateStyle
等。最后,服务端发送一个 ReadyForQuery
消息,告知客户端一切就绪,可以发送请求了。至此,连接创建成功。
取消请求
在 startup
阶段,服务端还会给客户端发送一个 BackendKeyData
消息,该消息中包含服务端的进程 ID 和一个取消码(MyCancelKey
)。如果客户端想取消当前正在执行的请求,则可以发送一个 CancelRequset
消息,该消息中包括 startup
阶段服务端提供的进程 ID 和取消码。
取消请求并不是通过当前正在处理请求的连接发送的,而是会创建一个新的连接,创建该连接发送的消息与之前创建连接的消息不同,不再发送 startup
消息,而是发送一个 CancelReqeust
消息,该消息同样没有消息类型字段。
取消请求不保证一定成功,可能服务端接收到取消请求时,当前的查询请求已经结束。取消请求只能在一定程度上加速当前查询结束,如果当前请求被取消,客户端会收到一条错误消息。
发送请求
连接创建之后,通信协议进入 normal
阶段,该阶段的大体流程是:客户端发送查询请求,服务端接收请求、处理请求并将结果返回给客户端。上文提到,该阶段有两种 “子协议”,本节分别介绍这两种 “子协议” 的消息流。
Simple Query
客户端通过 Query
消息发送一个文本命令给服务端,服务端处理请求,回复查询结果。查询结果通常包括两部分内容:结构和数据。结构通过 RowDescription
消息传递,包括列名、类型 OID 和长度等;数据通过 DataRow
消息传递,每个 DataRow
消息中包含一行数据。
每个命令的结果发送完成之后,服务端会发送一条 CommandComplete
消息,表示当前命令执行完成。客户端的一条查询请求可能包含多条 SQL 命令,每个 SQL 命令执行完都会回复一条 CommandComplete
消息,查询请求执行结束后会回复一条 ReadyForQuery
消息,告知客户端可以发送新的请求。消息流如下:
注意,一个请求中的多条 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
消息前将其显式销毁。
PostgreSQL 服务端收到该消息后,调用 exec_parse_message
函数进行处理,进行语法分析、语义分析和重写,同时会创建一个 Plan Cache 的结构,用于缓存后续的执行计划。
Bind
客户端发送 Bind
消息,该消息携带具体的参数值、参数格式和返回列的格式,如下:
PostgreSQL 收到该消息后,调用 exec_bind_message
函数进行处理。为之前保存的 Prepared Statement 创建执行计划并将其保存在 Plan Cache 中,创建一个 Portal
用于后续执行。关于 Plan Cache 的具体实现和复用逻辑在此不细述,以后单独撰文介绍。
在 PostgreSQL 内核中,Portal 是对查询执行状态的一种抽象,该结构贯穿执行器运行的始终。
Describe
客户端可以发送 Describe
消息获取 Statment 或 Portal 的元信息,即返回结果的列名,类型等信息,这些信息由 RowDescription
消息携带。如果请求获取 Statement 的元信息,还会返回具体的参数信息,由 ParameterDescription
消息携带。
Execute
客户端发送 Execute
消息告知服务端执行请求,服务端收到消息后,执行 Bind
阶段创建的 Portal,执行结果通过 DataRow
消息返回给客户端,执行完成后发送 CommandComplete
。
Execute
消息中可以指定返回的行数,若行数为 0,表示返回所有行。
Sync
使用 Extended Query 协议时,一个请求总是以 Sync
消息结束,服务端接收到 Sync
消息后,关闭隐式开启的事务并回复 ReadyForQuery
消息。
Extended Query 完整的消息流如下:
Copy 子协议
为高效地导入/导出数据,PostgreSQL 支持 COPY
命令, COPY
操作会将当前连接切换至一种截然不同的子协议。
Copy 子协议对应三种模式:
- copy-in 导入数据,对应命令 COPY FROM STDIN
- copy-out 导出数据,对应命令 COPY TO STDOUT
- copy-both 用于 walsender,在主备间批量传输数据
以 copy-in
为例,服务端收到 COPY
命令后,进入 COPY 模式,并回复 CopyInResponse
。随后客户端通过 CopyData
消息传输数据,CopyComplete
消息标识数据传输完成,服务端收到该消息后,发送 CommandComplete
和 ReadyForQuery
消息,消息流如下:
总结
本文简要介绍了 PostgreSQL 的通信协议,包括消息格式、消息类型和常见通信过程的消息流。一般通信过程分为两个阶段: startup
阶段创建连接, normal
阶段发送请求并返回结果。 normal
阶段又包括两种子协议, Simple Query
一次性发送查询请求; Extended Query
分阶段发送请求,利用服务端的 prepared statement 特性,提升反复执行同类请求的效率。
PostgreSQL 通信协议中,除本文介绍的 COPY
子协议,还有一些其他的子协议,如主备流复制子协议,限于篇幅,本文并未给出详尽的描述,感兴趣的同学可以参考相关文档[5]。
最后,本文严重参考了 2014 年 PG 大会这篇[6]分享,推荐大家阅读。
参考文献
- https://www.net.t-labs.tu-berlin.de/teaching/computer_networking/01.02.htm
- https://www.postgresql.org/docs/current/protocol.html
- https://www.postgresql.org/docs/12/sql-prepare.html
- https://jdbc.postgresql.org/documentation/head/server-prepare.html
- https://www.postgresql.org/docs/current/protocol-replication.html
- https://www.pgcon.org/2014/schedule/attachments/330_postgres-for-the-wire.pdf