開發與維運

PostgreSQL 通信協議

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

image.png

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 階段的兩種 "子協議" 在下一節詳細介紹。

消息

消息格式

客戶端和服務端所有通信都通過消息流進行。消息的第一個字節標識消息類型,隨後四個字節標識消息內容的長度(該長度包括這四個字節本身),具體的消息內容由消息類型決定。

image.png

需要注意的是,客戶端創建連接時,發送的第一條消息,即啟動(startup)消息格式有所不同。它沒有最開始的消息類型字段,以消息長度開始,隨後緊跟協議版本號,然後是鍵值對形式的連接信息,如用戶名、數據庫以及其他 GUC 參數和值。

image.png

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 階段是客戶端和服務端創建連接的階段,消息流如下:

image.png

客戶端首先發送 startup 消息至服務端,服務端判斷是否需要授權信息,如若需要,則發送 AuthenticationRequest ,客戶端隨後發送密碼至服務端,權限驗證之後,服務端給客戶端發送一些參數信息,即 ParameterStatus ,包括 server_version , client_encoding 和 DateStyle 等。最後,服務端發送一個 ReadyForQuery 消息,告知客戶端一切就緒,可以發送請求了。至此,連接創建成功。

取消請求

startup 階段,服務端還會給客戶端發送一個 BackendKeyData 消息,該消息中包含服務端的進程 ID 和一個取消碼(MyCancelKey)。如果客戶端想取消當前正在執行的請求,則可以發送一個 CancelRequset 消息,該消息中包括 startup 階段服務端提供的進程 ID 和取消碼。

取消請求並不是通過當前正在處理請求的連接發送的,而是會創建一個新的連接,創建該連接發送的消息與之前創建連接的消息不同,不再發送 startup 消息,而是發送一個 CancelReqeust 消息,該消息同樣沒有消息類型字段。

image.png

取消請求不保證一定成功,可能服務端接收到取消請求時,當前的查詢請求已經結束。取消請求只能在一定程度上加速當前查詢結束,如果當前請求被取消,客戶端會收到一條錯誤消息。

發送請求

連接創建之後,通信協議進入 normal 階段,該階段的大體流程是:客戶端發送查詢請求,服務端接收請求、處理請求並將結果返回給客戶端。上文提到,該階段有兩種 "子協議",本節分別介紹這兩種 "子協議" 的消息流。

Simple Query

客戶端通過 Query 消息發送一個文本命令給服務端,服務端處理請求,回覆查詢結果。查詢結果通常包括兩部分內容:結構和數據。結構通過 RowDescription 消息傳遞,包括列名、類型 OID 和長度等;數據通過 DataRow 消息傳遞,每個 DataRow 消息中包含一行數據。

image.png

每個命令的結果發送完成之後,服務端會發送一條 CommandComplete 消息,表示當前命令執行完成。客戶端的一條查詢請求可能包含多條 SQL 命令,每個 SQL 命令執行完都會回覆一條 CommandComplete 消息,查詢請求執行結束後會回覆一條 ReadyForQuery 消息,告知客戶端可以發送新的請求。消息流如下:

image.png

注意,一個請求中的多條 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 消息前將其顯式銷燬。

image.png

PostgreSQL 服務端收到該消息後,調用 exec_parse_message 函數進行處理,進行語法分析、語義分析和重寫,同時會創建一個 Plan Cache 的結構,用於緩存後續的執行計劃。

Bind

客戶端發送 Bind 消息,該消息攜帶具體的參數值、參數格式和返回列的格式,如下:

image.png

PostgreSQL 收到該消息後,調用 exec_bind_message 函數進行處理。為之前保存的 Prepared Statement 創建執行計劃並將其保存在 Plan Cache 中,創建一個 Portal 用於後續執行。關於 Plan Cache 的具體實現和複用邏輯在此不細述,以後單獨撰文介紹。

在 PostgreSQL 內核中,Portal 是對查詢執行狀態的一種抽象,該結構貫穿執行器運行的始終。

Describe

客戶端可以發送 Describe 消息獲取 Statment 或 Portal 的元信息,即返回結果的列名,類型等信息,這些信息由 RowDescription 消息攜帶。如果請求獲取 Statement 的元信息,還會返回具體的參數信息,由 ParameterDescription 消息攜帶。

image.png

Execute

客戶端發送 Execute 消息告知服務端執行請求,服務端收到消息後,執行 Bind 階段創建的 Portal,執行結果通過 DataRow 消息返回給客戶端,執行完成後發送 CommandComplete 。

image.png

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

Sync

使用 Extended Query 協議時,一個請求總是以 Sync 消息結束,服務端接收到 Sync 消息後,關閉隱式開啟的事務並回復 ReadyForQuery 消息。

Extended Query 完整的消息流如下:

image.png

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 消息,消息流如下:

image.png

總結

本文簡要介紹了 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

Leave a Reply

Your email address will not be published. Required fields are marked *