2012年6月8日金曜日

ネイティブ API と抽象化 API のパフォーマンス

こんにちは。サイオステクノロジー 渡辺です。

今回は PostgreSQL へのアクセスにおけるネイティブ API と抽象化 API のパフォーマンスについて紹介をします。

はじめに
PostgreSQL は libpq と呼ばれる Cライブラリを提供しています。Cプログラムの場合、libpq ライブラリで提供された API を用いて PostgreSQL サーバを操作します。Ruby や PHPなど C言語で作成されたプログラムは、libpq を利用したモジュールを提供しています。Ruby では ruby-pg, PHP では pgsql や PDO の PostgreSQL ドライバが libpq を利用しています。

ruby-pg と pgsql は基本的に libpq ライブラリの機能をそのままモジュール化しています。PHP の PDO は複数のデータベースシステムにアクセスできる様に設計された抽象化レイヤーです。Ruby にも Ruby/DBI という似た機能を持つライブラリがあります。どちらも複数のデータベースへのアクセスを同じ API を利用して行えるようになっています。Ruby/DBI は Ruby スクリプトで内部的に ruby-pg を利用しています。この為、ネイティブ API と抽象化 API のパフォーマンスの違いを比較するのは妥当ではありません。スクリプトを利用した API が圧倒的に不利です。

今回は PostgreSQL の libpq API をそのまま用いた PHP のネイティブ API(pgsql) と抽象化を行ったAPI(PDO の PostgreSQL ドライバ)の違いを検証します。

libpq
libpq がどのようなライブラリか簡単に紹介します。PostgreSQL ユーザに欠かせない psql コマンドも libpq を利用して作られています。PostgreSQL 9.1 の libpq マニュアル(http://www.postgresql.jp/document/9.1/html/libpq.html) は以下の目次を持っています。
31.1. データベース接続制御関数
31.2. 接続状態関数
31.3. コマンド実行関数
31.3.1. 主要な関数
31.3.2. 問い合わせ結果の情報の取り出し
31.3.3. 他の結果情報の取り出し
31.3.4. SQLコマンドに含めるための文字列のエスケープ処理
31.4. 非同期コマンドの処理
31.5. 処理中の問い合わせのキャンセル
31.6. 近道インタフェース
31.7. 非同期通知
31.8. COPYコマンド関連関数
31.8.1. COPYデータ送信用関数
31.8.2. COPYデータ受信用関数
31.8.3. 廃れたCOPY用関数
31.9. 制御関数
31.10. 雑多な関数
31.11. 警告処理
31.12. イベントシステム
31.12.1. イベントの種類
31.12.2. イベントコールバックプロシージャ
31.12.3. イベントサポート関数
31.12.4. イベント事例
31.13. 環境変数
31.14. パスワードファイル
31.15. 接続サービスファイル
31.16. 接続パラメータのLDAP検索
31.17. SSLサポート
31.17.1. サーバ証明書のクライアント検証
31.17.2. クライアント証明書
31.17.3. 異なるモードで提供される保護
31.17.4. SSLクライアントファイル使用法
31.17.5. SSLライブラリの初期化
31.18. スレッド化プログラムの振舞い
31.19. libpqプログラムの構築
31.20. サンプルプログラム
目次からデータベースの接続、一連の操作ができるようになっており、非同期クエリ、イベント、COPYコマンド、様々な接続・認証オプションが利用できる事がわかります。目次には載っていませんが、プリペアードクエリもサポートしています。非同期クエリとはクエリを実行したクライアントをブロックせずにクエリを実行する機能です。つまり、PostgreSQL がクエリ実行に時間がかかっても、クライアントはクエリを送信するだけで別の作業を行えます。イベントとは接続やクエリ結果にイベントハンドラを登録する仕組みです。COPY コマンドは psql でもお馴染みの COPY コマンド用の API です。

基本的な libpq のプログラムは次のようなコードを書いて利用します。

1. PQconnectdb または PQconnectdbParams を用いてデータベースに接続する。
2. PQexec や PQexecParams を用いてクエリを実行する
3. Pqntuples, PQnfields で行数、フィールド数を取得し、PQgetvalue で値を取得する

プリペアードクエリの場合、PQprepare でプリペアード文を準備し、PQexecute で実行します。libpq の API は非同期実行の API が多く用意されています。非同期 API を利用すれば、データベースへの接続、クエリの実行などでは、クライアントは操作の完了を待つこと無くプログラムの実行を継続する事ができます。

PHP の PostgreSQL インターフェース
PHP の PostgreSQL 用のインターフェースは 2種類用意されています。

PDO PostgreSQL ドライバ
PDO は複数のデータベースにアクセスできるインターフェースです。アクセスを抽象化しているため、全ての機能をサポートしている訳ではありません。現在の PostgreSQL の PDO ドライバは、PostgreSQL の独自機能としてラージオブジェクトをサポートしています。PDO はプリペアードクエリをサポートしているので、PostgreSQL ドライバも libpq のプリペアードクエリ API を利用して実装しています。PDO は PostgreSQL のプリペアードクエリとは異なる形式のプリペアード文をサポートしています。この為、内部的に変換してからプリペアードクエリを実行します。

PDO は多くのデータベースに共通する機能を集約した API である為、PostgreSQL 固有の非同期クエリや COPY コマンドに相当する機能はサポートしていません。

pgsql
pgsql は libpq の API をほぼ一対一で実装したモジュールです。libpq にある非同期接続など一部の機能をサポートしていませんが、ほとんどの機能をサポートしています。

PDO PostgreSQL に含まれてない機能
・非同期クエリ
・パラメータクエリ
・COPY 機能
・PostgreSQL 独自のエスケープ処理機能
・トレース機能

抽象化 API の制限
PHP の抽象化 API である PDO には 3つの大きな制限があります。
・プリペアードクエリをサポートしているが PostgreSQL のネイティブ形式のプリペアードクエリではない。
・プリペアードクエリを保存する PDOStatement オブジェクトは自動的にプリペアした文を削除する。
・クライアントとサーバの実行を最適化する非同期クエリをサポートしていない。
この3つの制限はシステムの性能にかなり影響します。

ベンチマーク
ネイティブのプリペアードクエリをサポートしない制限、非同期クエリをサポートしない制限がパフォーマンスにどのように影響するかベンチマークしてみましょう。ベンチマークは全て同じ一台のPCで行いました。

テスト環境
CPU: Core2Quad Q9400 @ 2.66GHz
メモリ: 8GB
HDD: SATA2 2TB
OS: Linux 2.6 x86_64
Apache: 2.2
PHP: 5.4
PostgreSQL: 9.1
テスト方法
今回は Apache+PHP の環境と PostgreSQL のツールである pgbench も利用します。

テストデータ作成
例えば、PostgreSQL は 5491 ポートで待機している状態なら、以下のコマンドでテスト用のテーブルを作成します。
# pgbench -p 5491 -i -s 100
psql でテーブルを確認すると、次の様なテーブルが作成されています。
user@[local] ~=# \d
List of relations
Schema | Name | Type | Owner 
--------+------------------+-------+---------
public | pgbench_accounts | table | user
public | pgbench_branches | table | user
public | pgbench_history | table | user
public | pgbench_tellers | table | user
(4 rows)
pgbench は通常モード(TPC-B類似のトランザクション処理)と SELECT モードがあります。各モードで実行される SQL 文は次の通りです。通常モードではトランザクション一つが一つの処理として、SELECT モードでは一つの SELECT 文が一つの処理として実行されます。今回は通常モードのみ利用します。

通常モードの SQL 実行ログ
LOG: statement: select count(*) from pgbench_branches
LOG: statement: vacuum pgbench_branches
LOG: statement: vacuum pgbench_tellers
LOG: statement: truncate pgbench_history
LOG: statement: BEGIN;
LOG: statement: UPDATE pgbench_accounts SET abalance = abalance + -2258 WHERE aid = 8552415;
LOG: statement: SELECT abalance FROM pgbench_accounts WHERE aid = 8552415;
LOG: statement: UPDATE pgbench_tellers SET tbalance = tbalance + -2258 WHERE tid = 889;
LOG: statement: UPDATE pgbench_branches SET bbalance = bbalance + -2258 WHERE bid = 9;
LOG: statement: INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (889, 9, 8552415, -2258, CURRENT_TIMESTAMP);
LOG: statement: END;
テストスクリプト
PDO ではプリペアードクエリ、pgsql ではプリペアードクエリ、非同期クエリが実行できるようになっています。pgbench も -M prepare でプリペアードクエリモードを利用できるのでプリペアードクエリを利用します。

PHP のテストスクリプトは pgbench コマンドと同等のクエリを送信するスクリプトを作成しました。
pgbench.php
---
<?php
// ドライバを設定
$driver = ($_GET['driver'] === 'pdo' ? 'pdo' : 'pgsql');

// 変数を設定
$aid = mt_rand(0, 10000000);
$bid = mt_rand(0, 100);
$tid = mt_rand(0, 9999);
$delta = mt_rand(-10000, 10000);


function my_pg_execute($db, $sql, $param) {
if (!@pg_execute($db, md5($sql), $param)) {
trigger_error('Failed to pg_execute(). '.$sql);
}
}


function my_pg_send_execute($db, $sql, $param) {
if (!@pg_send_execute($db, md5($sql), $param)) {
trigger_error('Failed to pg_execute(). '.$sql);
}
}


/*** main ***/

if ($driver == 'pgsql') {
// 送信するクエリ
$s1 = 'UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid = $2;';
$s2 = 'SELECT abalance FROM pgbench_accounts WHERE aid = $1;';
$s3 = 'UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid = $2;';
$s4 = 'UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2;';
$s5 = 'INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP);';

/* 永続的接続を利用 */
$db = pg_pconnect('host=localhost port=5491 user=user');
/* トランザクションを利用していない場合、適宜プリペア可能だが
プリペアしてない場合、トランザクションが無効になる。この為、
プリペア済みかチェックが必要。*/
$result = pg_query_params($db, 'SELECT name FROM pg_prepared_statements WHERE name = $1', [md5($s5)]);
if (pg_num_rows($result) == 0) {
pg_prepare($db, md5($s1), $s1);
pg_prepare($db, md5($s2), $s2);
pg_prepare($db, md5($s3), $s3);
pg_prepare($db, md5($s4), $s4);
pg_prepare($db, md5($s5), $s5);
}

pg_query($db, 'BEGIN;');
my_pg_execute($db, $s1, [$delta, $aid]);
my_pg_execute($db, $s2, [$aid]);
my_pg_execute($db, $s3, [$delta, $tid]);
my_pg_execute($db, $s4, [$delta, $bid]);
my_pg_execute($db, $s5, [$tid, $bid, $aid, $delta]);
pg_query($db, 'END;');
echo 'pgsql';

} else { /* PDO */
// 送信するクエリ
$s1 = 'UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;';
$s2 = 'SELECT abalance FROM pgbench_accounts WHERE aid = :aid;';
$s3 = 'UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;';
$s4 = 'UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;';
$s5 = 'INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);';

/* 永続的接続を利用 */
$pdo = new PDO('pgsql:host=localhost;port=5491', 'user', 'user', [PDO::ATTR_PERSISTENT => true]);
if ($mode == 'async') {
die('PDO does not have async query');
}

$pdo->beginTransaction();
/* PDOは永続的接続を持つが文オブジェクトは毎回生成しなければならない。
この為に結局毎回クエリの準備が必要。*/
$st = $pdo->prepare($s1); $st->execute([':delta'=>$delta, ':aid'=>$aid]);
$st = $pdo->prepare($s2); $st->execute([':aid'=>$aid]);
$st = $pdo->prepare($s3); $st->execute([':delta'=>$delta, ':tid'=>$tid]);
$st = $pdo->prepare($s4); $st->execute([':delta'=>$delta, ':bid'=>$bid]);
$st = $pdo->prepare($s5); $st->execute([':tid'=>$tid, ':bid'=>$bid, ':aid'=>$aid, ':delta'=>$delta]);
$pdo->commit();
echo 'pdo postgresql';

}
---

ベンチマーク結果
pgbench の実行結果にはゆらぎが大きいので、3回実行して中間の結果を記載します。-M オプションで prepared を指定してプリペアードクエリを利用すると、通常のクエリよりも2割ほど良い実行結果となっていました。
[user@dev ~]$ pgbench -c 10 -t 1000 -s 1 -M prepared -p 5491
Scale option ignored, using pgbench_branches table count = 100
starting vacuum...end.
transaction type: TPC-B (sort of)
scaling factor: 100
query mode: prepared
number of clients: 10
number of transactions per client: 1000
number of transactions actually processed: 10000/10000
tps = 2556.416273 (including connections establishing)
tps = 2575.527339 (excluding connections establishing)

pgbench.php スクリプトを ab コマンドで実行してみます。次が PDO の実行結果です。
[user@dev ~]$ ab -c 10 -n 10000 'http://192.168.100.50:1122/pgbench.php?driver=pdo'
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.100.50 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software: Apache/2.2.22
Server Hostname: 192.168.100.50
Server Port: 1122

Document Path: /pgbench.php?driver=pdo
Document Length: 14 bytes

Concurrency Level: 10
Time taken for tests: 12.111 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 2160000 bytes
HTML transferred: 140000 bytes
Requests per second: 825.72 [#/sec] (mean)
Time per request: 12.111 [ms] (mean)
Time per request: 1.211 [ms] (mean, across all concurrent requests)
Transfer rate: 174.17 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 1
Processing: 3 12 14.6 9 630
Waiting: 3 11 14.4 9 630
Total: 3 12 14.6 9 631

Percentage of the requests served within a certain time (ms)
50% 9
66% 13
75% 15
80% 17
90% 22
95% 27
98% 35
99% 41
100% 631 (longest request)

次は pgsql の実行結果です。
[user@dev ~]$ ab -c 10 -n 10000 'http://192.168.100.50:1122/pgbench.php?driver=pgsql'
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.100.50 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software: Apache/2.2.22
Server Hostname: 192.168.100.50
Server Port: 1122

Document Path: /pgbench.php?driver=pgsql
Document Length: 5 bytes

Concurrency Level: 10
Time taken for tests: 9.738 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 2060000 bytes
HTML transferred: 50000 bytes
Requests per second: 1026.92 [#/sec] (mean)
Time per request: 9.738 [ms] (mean)
Time per request: 0.974 [ms] (mean, across all concurrent requests)
Transfer rate: 206.59 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 1
Processing: 3 10 7.5 8 106
Waiting: 3 9 6.9 7 106
Total: 3 10 7.5 8 106

Percentage of the requests served within a certain time (ms)
50% 8
66% 10
75% 12
80% 13
90% 17
95% 21
98% 28
99% 35
100% 106 (longest request)

ベンチマーク結果
PDO と pgsql の実行結果を比べるとおよそ 2割ほど pgsql が高速でした。
PDO: Requests per second: 825.72 [#/sec] (mean)
pgsql: Requests per second: 1026.92 [#/sec] (mean)

pgbench の場合
tps = 2575.527339 (excluding connections establishing)

約2500tps ほどであるので Web サーバ経由しない方が圧倒的に速いです。しかし、pgbench の場合はWebサーバを経由するオーバーヘッドなどが無いので速くて当然です。ベンチマーク中は PostgreSQL のプロセスも Apache のプロセスも CPU を十分使っていたので稼働プロセスが倍となる Apache+PHP の性能が半分以下になるのは予想通りでした。

PDO と pgsql でおよそ 2割の高速化が可能となったのは、PDO の仕様による部分が大きいです。PHP は永続的接続をサポートしているので、プリペアード文はリクエストをまたいで再利用する事ができます。しかし、PDOは永続的接続をサポートしていますが、プリペアされた文はリクエストをまたいで再利用できません。PDO オブジェクトから生成された PDOStatement オブジェクトは消滅する時、自動的に利用したプリペア文を削除します。

この為、PDO を利用した場合は永続的接続を利用しても、予めプリペアした文を再利用する事ができません。pgbench でプリペアードクエリを利用しなかった場合、およそ2割ほど性能が劣っていました。PDO でプリペアードクエリの再利用できない仕様が大きく影響しています。PDO がプリペアードクエリを再利用できない事を差し引いても、若干(数%)は pgsql の方が良い性能となっています。これは PDO がプリペアードクエリをパースしなければならい分のオーバーヘッドも影響していると考えられます。同じ動作を実現する為には、PDO の方が全般的に複雑な処理を行い、多くの C のコードが必要となっています。これらの積み重ねが pgsql が PDO より若干速い結果となって現れていると考えられます。

まとめ
本来プリペアードクエリはクエリを高速に実行する為に開発された機能ですが、PHP の PDO を利用した場合は同じセッション中で何度もプリペアされた文を使わない限り、通常のクエリと性能的には変わらない事がわかります。

この様に、全く同じデータ操作を行う場合でも、利用するモジュールを変えるだけで何割もの速度差が発生する事があります。性能要件が厳しいアプリケーションの場合、どのようなモジュールを経由して PostgreSQL にアクセスするかは重要なポイントとなります。

今回は筆者が利用している環境の不具合の為、非同期クエリを利用した場合の性能向上を紹介できませんでした。PostgreSQL の非同期クエリは多くのデータベースアクセス抽象化ライブラリで利用できません。しかし、非同期クエリはアプリケーションの体感速度と全体のスループット向上に高い効果があります。PostgreSQL を利用した高性能なシステムを構築する場合、非同期クエリは必須と言える機能です。利用された事がない方は是非一度利用してみてください。


リファレンス

libpq マニュアル
http://www.postgresql.org/docs/current/static/libpq.html

ruby-pg
https://bitbucket.org/ged/ruby-pg/wiki/Home

Ruby/DBI
http://ruby-dbi.rubyforge.org/

pgsql
http://jp.php.net/manual/en/book.pgsql.php

PDO
http://jp.php.net/manual/en/book.pdo.php


記事の作成にあたり、エレクトロニック・サービス・イニシアチブ社 大垣 靖男様に執筆協力をいただきました。

0 件のコメント:

コメントを投稿