Code Day's Night

ichikawayのブログ

Ubuntu22.04にすると古いPostgreSQLのインターフェースと接続できなくなった

問題の現象

今回の環境は非常にレアケースのため世界中でこの問題に直面したのは自分だけかもしれない。。

今回の問題の最初の挙動は、Ubuntu22.04とPHP8.1、pdo_pgsqlで古いDB(PostgreSQLと互換性のあるインターフェースを持つ)に接続すると下記のエラーが出た。

PHP Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 7 server sent data ("D" message) without prior row description ("T" message) in xxx

 

この謎のメッセージはPostgreSQLのプロトコルの資料を読むとわかる。
https://www.postgresql.jp/document/10/html/protocol-message-formats.html

 

D messageはデータを扱うプロトコルのフラグ、Tメッセージはパラメータや行のカラム情報を扱うフラグ。今回はselectまではできて結果をDBサーバからPHPに返す時にTメッセージがこない状態でデータが返されたのがエラーの内容。

実際にtcpdumpしてみると、select結果までは返ってきててTメッセージもきてるように見えたので本当のエラー原因はよくわからない。 -> 原因はTメッセージが2回きてしまうDBサーバ側の不具合でした。

 

問題の切り分け

AmazonLinux2ではこの現象が出ず、Ubuntu20でもでなかった。問題はUbuntu22.04と考え、22.04のPHP8.1が悪いのかまずは切り分けた。

Ubuntu22.04にPHP7.4とpdo_pgsqlを入れて問題を再現させると、みごと同じエラーとなった。これでまずはPHP側の問題ではないとわかる。

そこでpdo_pgsqlが依存するlibpq5のバージョンがUbuntu20と22で異なると判明。このlibpq5が問題点と考えそこを調査した。

Ubuntu20からlibpq5.12をUbuntu22にコピーし、libssl1.1もUbuntu20のパッケージからインストールしたが、他にもいくつか古いライブラリが必要となったためこの回避策の検証は中止した。

 

回避方法

今回はPHPからプリペアードステートメントでDB側にクエリとバインドさせるパラメータを送っていたがこれだとエラーとなった。
しかしPHP側でバインド済みのクエリを作り、DB側にそのクエリを送るとこのエラーがでなくなることがわかった。
具体的には 
pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
のようにしてPHP側で仮想プリペアードをエミュレートさせる。

この方法は回避策として持っておくが、そもそもDB側でプリペアードステートメントができるような方法をこれから探る。今回はここまでが結論。

原因が特定できたのでDBサーバ側をアップデートすることにした。

 

原因

Ununtu20.04はPostgreSQLのライブラリlibpq5.12でver.12なのだが、Ubuntu22.04はlibpq5.14でver.14になった。

ver14からは、古いPostgreSQLのプロトコルがサポートされなくなったのが原因かもしれない。

サーバと libpq で、バージョン 2 プロトコルがサポートされなくなりました。 (Heikki Linnakangas) (14)
これは 2002年リリースの PostgreSQL 7.3 ではデフォルトでしたが、PostgreSQL 7.4 以降はバージョン 3 プロトコルがデフォルトになりました
https://www.sraoss.co.jp/tech-blog/pgsql/14-0/

調査が進み原因が特定できました。DBサーバ側(PostgreSQL互換のAPI)がクエリを受け取って検査結果を返す際に、T -> T -> Dというメッセージを送るため(バグ)でした。このDBサーバの最新版では修正がされており問題は自分の環境だけでした。

Tメッセージが2回送信される不具合でしたがPostgreSQL13まではlibpqでよしなに処理されて問題が発生していませんでした。
今回はPostgreSQL14から導入されたパイプラインモードの影響で、パイプラインモードを導入する際に、T->T->Dのような間違った順序のものは厳密にエラーになるようになりました。パイプラインモードを無理やりオフにしてもこの影響を受けました。
PostgreSQL14のパイプラインモードはこのコミットで入ったようです。

今回の問題はPostgreSQL14の拡張問い合わせの場合にのみ影響がありシンプル問い合わせの場合は影響がありません。SQL文をクライアントで完成させてクエリを送るのはシンプルの方で、DB側でバインドさせる命令は拡張問い合わせです。そのため上記の回避方法で回避できた理由がわかりました。

 

単純にUbuntu20のlibpq5.12をUbuntu22にもってきて、ldconfigでライブラリパスを変えればいけるかと思ったが、libpq5.12はlibssl1.1に依存している。そしてUbuntu22.04はlibssl3系になっていてlibssl1系は入らない。

libsslのギャップは今回の件とは関係なく他のミドルウェアでも影響があるかもしれないのでUbuntu20から22に上げるときは注意が必要。

 

libpq5.12のみを差し替えるのはダメだったのですが、postgresql12や13をコンパイルして/usr/local/postgresql12のようにmake installした場合は問題なくPHP8.1からlibpq5.12が利用できました。ldconfigで読み込むライブラリパスを、/usr/local/postgresql12/libのように指定する必要があります。

例えばUbuntuの場合は /etc/ld.so.conf.d/x86_64-linux-gnu.conf がデフォルトであり、その中で /usr/local/lib/x86_64-linux-gnu がライブラリパスとして定義してあるので

sudo ln -s /usr/local/postgres12/lib /usr/local/lib/x86_64-linux-gnu

みたいにしてシンボリックリンクをはって、 sudo ldconfig とすれば/usr/local/postgres12/libが利用されます。

 

さいごに

非常にレアな環境でエラーとなったため単純にエラーメッセージで検索しても問題の解決に至らなかった。ここからtcpdumpやバージョンごとの再現環境の構築、DB側のトレースログ、など色々な方法で切り分けながら原因を探っていった。

途中から趣味の世界になり始めたが、やはり何か問題があってその原因を探しながら解決策を見つける過程が面白い。