我們在使用資料庫服務時,通常需要使用用戶端連接配接資料庫服務端,以 PostgreSQL 為例,常用的用戶端有自帶的 psql,JAVA 應用的資料庫驅動 JDBC,可視化工具 PgAdmin 等,這些用戶端都需要遵守 PostgreSQL 的通信協定才能與之 "交流"。所謂協定,可以了解為一套資訊互動規則或者規範,最為我們熟知的莫過于 TCP/IP 協定和 HTTP 協定。

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