PostgreSQLプロトコル実装に必要な柔軟性
最近、PostgresSQL互換サーバーを実装する目的で、Go言語向けのPostgreSQL互換サーバー構築用フレームワークである「go-postgresql」と、それを活用したマルチプロトコル/マルチモデルをコンセプトとする「PuzzleDB」を開発する過程で、PostgreSQLのプロトコル実装を調査しました。
以前、RDBMSの代表的なPostgreSQL・MySQLの通信プロトコルの概要を調査[3]をしましたが、今回は、実装におけるポイントや、異なるクライアント実際についても考察し、PostgreSQLプロトコルの実装における柔軟性とその必要性について整理してみます。
メッセージの共通形式
PostgreSQL仕様書では、データベースのクライアント側をフロントエンド(Frontend)、サーバー側をバックエンド(Backend)とした用語の定義があります[3]。
PostgreSQLの通信プロトコルは、フロントエンドからの要求パケット、バックエンドからの応答パケットのいずれにおいても、基本的には、以下に示すパケット仕様に準じています[3]。
PostgreSQLの通信メッセージは、最初の1バイトはメッセージ種別(Cmd)、次の4バイト(Length of message)は自身を含むメッセージ部(Message body)長を示す共通ヘッダから始まります。また、後述するように、クライアントからのコネクション確立時においては、いくつかの例外があります。
メッセージ形式の特徴
特徴としては、MySQL[1]やMongoDB[2]のようにメッセージの識別子および応答の識別子となるメッセージ番号(ID)が含まれないことです。そのため、基本的にはフロントエンドからのメッセージ要求順に、バックエンドからのメッセージを順次応答する形式となります。
PostgreSQLプロトコルによる通信は、要求および応答共に複数のメッセージを合成的に組み合わせます。MySQLにおいては、その組み合わせは厳密に定義[7][8]されていますが、PostgreSQLでは厳密な定義はなく、順次要求されるメッセージおよび、それに対する応答メッセージは、各自の実装において、省略される場合があるのが特徴です。
メッセージの処理フロー
PostgreSQLのサーバー側のコネクション確立以降の基本フローについては、公式ドキュメント[4]に概要の説明があります。この公式ドキュメントから今回実装してみた、正常系の主要メッセージを整理すると、以下のような状態マシン図となります。
基本的には後述するスタートアップ時の特殊通信メッセージによる認証および認可終了後は、バックエンド側は、クライアント側からの、静的な文字列である単純なクエリー(Simple Query)と拡張クエリー(Extended Query)などのメッセージ要求を処理をして、待機(Ready)状態への遷移繰り返えすのが基本フローとなります。
スタートアップ時の例外
メッセージの共通形式には例外があり、歴史的な経緯[3]から、クライアントからのコネクション確立時の最初のメッセージはからは、メッセージ種別(Cmd)が省略されています。その例外として、以下に示す、スタートアップメッセージ(StartupMessage)と、SSL要求メッセージ(SSLRequest)があります。
いずれも、共通形式と比較して、最初のメッセージ種別(Cmd)が省略されています。メッセージ種別(Cmd)はありませんが、スタートアップメッセージ(StartupMessage)はメッセージ内容とプロトコルバー順により可変ですが、SSL要求メッセージ(SSLRequest)は固定値であるため、SSL要求メッセージのヘッダ部で識別となります。
SSL要求メッセージの長さは8で固定、SSL要求(Request)部は80877103(=04D2162F)、つまり、1234(0x04D2)と5679(0x162F)の2つのマジックナンバーの固定値による識別となります。いずれにしろ、ネクション確立後のスタートアップは、このような例外的な処理となります。
拡張クエリーの処理フロー
PostgreSQLでは、クライアントからの単純なクエリー(Simple Query)要求に加えて、拡張クエリー(Extended Query)が定義されています。拡張クエリー(Extended Query)は、SQL文を複数のステップに分割して送信するクエリー形式です。拡張クエリーにつき、公式ドキュメント[4]から今回実装してみた、正常系の主要メッセージを整理すると、以下のような状態マシン図となります。
拡張クエリプロトコルは上記図の一連のコマンドで実現されますが、フライアント側はバックエンド側の応答を待たずに、一連のクエリを送信するパイプライン処理を可能とします。また、パイプラインの目的としてはメッセージ往復回数が削減があり[4]、実際の実装でもバックエンド側からの応答が省略される場合があります。
プロトコル実装のポイント
PostgreSQLプロトコルの実装においては、以下に示す「Frontend/Backend Protocol」公式資料を参照しながら作業を進めます。とは言え、MySQLのように厳密な定義[7][8]はなく、RFC形式のような必須・推奨・任意の定義もなく、実装においては、解釈の余地があります。
- [3] Overview - PostgreSQL: Documentation: Frontend/Backend Protocol
- [4] Message Flow - PostgreSQL: Documentation: Frontend/Backend Protocol
- [5] Message Data Types - PostgreSQL: Documentation: Frontend/Backend Protocol
- [6] Message Formats - PostgreSQL: Documentation: Frontend/Backend Protocol
結論としては、必須の仕様が不明確なため、すべてを推奨・任意である仮定を置いて、クライアントおよび互換サーバー共に、柔軟に実装する必要があります。サーバー側は現状公式な実装は一つですが、クライアントは各言語毎に様々な実装が存在し、その実装方針や仕様の解釈が異なります。
拡張クエリーの送受信例
柔軟な実装の必要性を、単純なDMLクエリーである「CREATE TABLE」での各クライアントでの実装を交えて説明してみます。DMLクエリーは一般的には静的、固定文字列でのクエリーとなります。
PostgreSQL付属の標準コマンド(psql,pgbench)や、Go言語のライブラリであるpqなどでは、想定通り単純なクエリー(Simple Query)が発行されます。Go言語のライブラリであるpgがDMLも極力単純なクエリーに展開するのに対して意、pgxにおいては、DDLのような明らかに静的なクエリーであっても、拡張クエリー(Extended Query)に展開するよう実装されています。
CREATE TABLEの送受信例
ここからは、上記の拡張クエリー(Extended Query)事例を通じて、一般的なフロントエンド側とバックエンド側の実装のポイントを整理してみます。左図はフロントエンド側からの要求で、黄色枠が送信が必須、灰色部が任意と思われるメッセージを表しています。右図はバックエンド側からの応答で、同じく黄色枠が送信が必須、灰色部が任意と思われるメッセージを表しています。
まず、左側図となる、フロントエンド側ですが、黄色部は必須として、灰色部のParseやBindo結果を確認するDescribe(D)については、公式仕様ではオプション操作に区分され、ParseかBindいずれかの確認が推奨されています[4]。ただし確認できた実装においては、pgxおよびpqのいずれもParseおよびBindの双方を確認しており、省略はされていませんでした。
次に、右側の図となる、バックエンド側ですが、ReadyForQueryは、フロントエンド側が安全に新しいコマンドを送信できることを通知するメッセージです。「1つ以上の応答後に、最後にReadyForQueryを応答する」[4]とされており、右図は便宜的に要求毎にReadyForQueryによる応答を図示しています。公式仕様には、Sync要求のみ間接的に必須の記載[4]がありますが、明確な省略に関する規定はありません。公式のPostgreSQLの実装では適度にReadyForQuery応答は省略されていますが、試しに各コマンド毎にReadyForQueryを応答してもクライアント側は適切に処理してくれるようです。
また右図は、SyncへのReadyForQuery応答はもとより、他のコマンドに対しては必ず対応する応答を返すよう図示していますが、Exexute要求に対応するCommandCompleteについては省略される可能性も記載[4]されており、フロントエンド側の実装には、結構な柔軟性が求められる印象です。
最後に - 実装の柔軟性とその必要性
今回、PostgresSQL互換サーバーを実装する過程で、PostgreSQLのプロトコル実装を調査しました。PostgreSQLの通信プロトコルは、フロントエンドとバックエンド間のメッセージ交換を規定しています。メッセージは一般的にメッセージ種別とメッセージ本体長を含むヘッダから始まりますが、コネクション確立時には例外が存在します。
通信プロトコルは、MySQLやMongoDBとは異なり、メッセージIDを含まず、基本的には要求と応答が順序通りに行われます。また、拡張クエリーではSQL文を複数ステップに分割して送信し、フロントエンドとバックエンドは一連のメッセージを交換して通信を行います。プロトコルの実装には公式資料を基にしつつも、厳密な定義がなく、実装において解釈の余地があるため、クライアントや互換サーバーは柔軟な実装が求められます。
参考資料
- [1] PostgreSQL・MySQLの問い合わせ(Query)プロトコルの比較 – Cyber Garage (Memo)
- [2] MongoDB プロトコル実装調査 – v3.6以降の実情 – Cyber Garage (Memo)
- [3] Overview - PostgreSQL: Documentation: Frontend/Backend Protocol
- [4] Message Flow - PostgreSQL: Documentation: Frontend/Backend Protocol
- [5] Message Data Types - PostgreSQL: Documentation: Frontend/Backend Protocol
- [6] Message Formats - PostgreSQL: Documentation: Frontend/Backend Protocol
- [7] MySQL: Client/Server Protocol
- [8] MySQL: COM_QUERY Response
Discussion