天天看點

PostgreSQL 通信協定

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

PostgreSQL 通信協定
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

 階段的兩種 "子協定" 在下一節詳細介紹。

消息

消息格式

用戶端和服務端所有通信都通過消息流進行。消息的第一個位元組辨別消息類型,随後四個位元組辨別消息内容的長度(該長度包括這四個位元組本身),具體的消息内容由消息類型決定。

PostgreSQL 通信協定

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

PostgreSQL 通信協定

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

 階段是用戶端和服務端建立連接配接的階段,消息流如下:

PostgreSQL 通信協定

用戶端首先發送

startup

 消息至服務端,服務端判斷是否需要授權資訊,如若需要,則發送

AuthenticationRequest

 ,用戶端随後發送密碼至服務端,權限驗證之後,服務端給用戶端發送一些參數資訊,即

ParameterStatus

 ,包括

server_version

 ,

client_encoding

DateStyle

 等。最後,服務端發送一個

ReadyForQuery

 消息,告知用戶端一切就緒,可以發送請求了。至此,連接配接建立成功。

取消請求

startup

 階段,服務端還會給用戶端發送一個

BackendKeyData

 消息,該消息中包含服務端的程序 ID 和一個取消碼(

MyCancelKey

)。如果用戶端想取消目前正在執行的請求,則可以發送一個

CancelRequset

 消息,該消息中包括

startup

 階段服務端提供的程序 ID 和取消碼。

取消請求并不是通過目前正在處理請求的連接配接發送的,而是會建立一個新的連接配接,建立該連接配接發送的消息與之前建立連接配接的消息不同,不再發送

startup

 消息,而是發送一個

CancelReqeust

 消息,該消息同樣沒有消息類型字段。

PostgreSQL 通信協定

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

發送請求

連接配接建立之後,通信協定進入

normal

 階段,該階段的大體流程是:用戶端發送查詢請求,服務端接收請求、處理請求并将結果傳回給用戶端。上文提到,該階段有兩種 "子協定",本節分别介紹這兩種 "子協定" 的消息流。

Simple Query

用戶端通過

Query

 消息發送一個文本指令給服務端,服務端處理請求,回複查詢結果。查詢結果通常包括兩部分内容:結構和資料。結構通過

RowDescription

 消息傳遞,包括列名、類型 OID 和長度等;資料通過

DataRow

 消息傳遞,每個

DataRow

 消息中包含一行資料。

PostgreSQL 通信協定

每個指令的結果發送完成之後,服務端會發送一條

CommandComplete

 消息,表示目前指令執行完成。用戶端的一條查詢請求可能包含多條 SQL 指令,每個 SQL 指令執行完都會回複一條

CommandComplete

 消息,查詢請求執行結束後會回複一條

ReadyForQuery

 消息,告知用戶端可以發送新的請求。消息流如下:

PostgreSQL 通信協定

注意,一個請求中的多條 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 通信協定

PostgreSQL 服務端收到該消息後,調用

exec_parse_message

 函數進行處理,進行文法分析、語義分析和重寫,同時會建立一個 Plan Cache 的結構,用于緩存後續的執行計劃。

Bind

用戶端發送

Bind

 消息,該消息攜帶具體的參數值、參數格式和傳回列的格式,如下:

PostgreSQL 通信協定

PostgreSQL 收到該消息後,調用

exec_bind_message

 函數進行處理。為之前儲存的 Prepared Statement 建立執行計劃并将其儲存在 Plan Cache 中,建立一個

Portal

 用于後續執行。關于 Plan Cache 的具體實作和複用邏輯在此不細述,以後單獨撰文介紹。

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

Describe

用戶端可以發送

Describe

 消息擷取 Statment 或 Portal 的元資訊,即傳回結果的列名,類型等資訊,這些資訊由

RowDescription

 消息攜帶。如果請求擷取 Statement 的元資訊,還會傳回具體的參數資訊,由

ParameterDescription

 消息攜帶。

PostgreSQL 通信協定

Execute

Execute

 消息告知服務端執行請求,服務端收到消息後,執行

Bind

 階段建立的 Portal,執行結果通過

DataRow

 消息傳回給用戶端,執行完成後發送

CommandComplete

 。

PostgreSQL 通信協定

Execute

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

Sync

使用 Extended Query 協定時,一個請求總是以

Sync

 消息結束,服務端接收到

Sync

 消息後,關閉隐式開啟的事務并回複

ReadyForQuery

 消息。

Extended Query 完整的消息流如下:

PostgreSQL 通信協定

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 通信協定

總結

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