我們在使用數據庫服務時,通常需要使用客戶端連接數據庫服務端,以 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