ぱと隊長日誌

ブログ運用もエンジニアとしての生き方も模索中

PostgreSQLのトランザクション分離レベル毎のパフォーマンス測定(に失敗しました)

要約

PostgreSQLベンチマーク試験コマンドである pgbench を利用して、トランザクション(TX)分離レベル毎のパフォーマンス測定にチャレンジしました。

残念ながら、今回の検証手法でTX分離レベルのパフォーマンスを比較することに意味がないと言わざるを得ない結果となりました。

ただ、この過程で得られた知見は今後の検証の糧になると考えています。そこで、検証記録をここに公開します。

検証環境

WindowsHyper-V による仮想マシンを検証環境としました。本来は物理マシンを使うのが理想なのですが、機材を準備できなかったためです。

ホスト

プロセッサ Intel Core i5-6600 CPU @ 3.30GHz
メモリ 20.0 GB
OS Windows 10 Pro バージョン 1909

Hyper-V

プロセッサ 4個の仮想プロセッサ
メモリ 8.0 GB
OS CentOS 8.0.1905
DB PostgreSQL 12.1

PostgreSQL設定 (postgresql.conf)

デフォルトの設定から変更した箇所を示します。

logging_collector = on
autovacuum = off
default_transaction_isolation = {'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable'}

autovacuum は pgbench のマニュアルを参考に OFF としました。

自動バキュームが有効な場合、性能を測定する上で結果は予測できないほど変わる可能性があります。

pgbench

default_transaction_isolation は試験条件に応じて都度変更しました。

マニュアル

現時点で PostgreSQL 12 の日本語版マニュアルは公開されていません。
ただ、英語版マニュアルの差分から今回の検証の範囲では 11 と 12 で大きな差異はないと判断し、11 の日本語版マニュアルを引用することにしました。

検証方法

pgbench のデフォルトである TPC-B に似た試験シナリオを採用しました。

以下の条件を組み合わせて測定を行いました。

  • TX分離レベル
    • read uncommitted
    • read committed
    • repeatable read
    • serializable
  • クライアント数及びワーカースレッド数
    • 1
    • 4

測定の度にテーブルの初期化をやり直しています。

デフォルトの試験シナリオはまた、テーブルを初期化してからの経過時間に非常に敏感です。 テーブル内の不要行や不要空間の累積により結果が変わります。

pgbench

初期データの scale は 4 で固定としました。4 としたのは測定条件のクライアント数の最大値が 4 であることからです。

デフォルトのTPC-Bのような試験シナリオでは、初期倍率(-s)を試験予定のクライアント数(-c)の最大値と同程度にしなければなりません。 pgbench_branchesテーブルには-s行しかありません。 また、全トランザクションはその内の1つを更新しようとします。 ですので、-c値を-sより大きくすると、他のトランザクションを待機するためにブロックされるトランザクションが多くなることは間違いありません。

pgbench

pgbench をデータベースサーバ上で実行しています。マニュアルでは pgbench とデータベースサーバは分けることを推奨するような記述があることに注意が必要です。

pgbenchの制限は、多くのクライアントセッションを試験しようとする際にpgbench自身がボトルネックになる可能性があることです。 これは、データベースサーバとは別のマシンでpgbenchを実行することで緩和させることが可能です。

pgbench

初期化時に unlogged-tables オプションを指定しました。これにより WAL ファイルが出力されなくなります。
当初はオプション無し(通常通り WAL ファイルが出力される)で試験を行いましたが、ディスクアクセスがボトルネックとなりました。今回のTX分離レベルによるボトルネックがディスクアクセスになるとは想定しがたいため、無用なボトルネックを回避するために unlogged-tables オプションを指定しました。

測定は複数回行い、ばらつきが大きい結果については妥当と思われる結果を採用しました。
なお、今回の測定ではクライアント数及びワーカースレッド数が大きいときにばらつきが大きかったようです。また、実行・測定タイミングによっても結果は上下しました。

TPSは "excluding connections establishing" の値を採用しています。

検証結果

TX分離レベル クライアント数 完了 / 予定TX数 平均レイテンシー TPS
read uncommitted 4 2,000,000 / 2,000,000 1.049 ms 3,813
read uncommitted 1 500,000 / 500,000 0.832 ms 1,202
read committed 4 2,000,000 / 2,000,000 1.050 ms 3,809
read committed 1 500,000 / 500,000 0.840 ms 1,191
repeatable read 4 500,001 / 2,000,000 3.343 ms 1,196
repeatable read 1 500,000 / 500,000 0.837 ms 1,195
serializable 4 500,000 / 2,000,000 3.405 ms 1,175
serializable 1 500,000 / 500,000 0.851 ms 1,175

read uncommitted と read committed の比較

read uncommitted と read committed ではほとんど同じ結果となりました。これはPostgreSQLの実装を踏まえると妥当な結果といえます。

PostgreSQLでは、4つの標準トランザクション分離レベルを全て要求することができます。 しかし、内部的には3つの分離レベルしか実装されていません。 つまり、PostgreSQLのリードアンコミッティドモードは、リードコミッティドのように動作します。

13.2. トランザクションの分離

クライアント数が増えるとTPSも増えますが、クライアント数が4倍に増えたからといってTPSが4倍になるわけではありません。これはTX間で処理の競合があることを踏まえると妥当と思われます。

repeatable read, serializable の比較

repeatable read, serializable の クライアント数 = 4 における完了TX数はクライアントごとに割り当てられたTX数 ( = 予定TX数 / 4 ) とほぼ同じになっています。また、実行時の記録からは直列化異常が発生していることが分かります。

これを踏まえ pgbench の実装を確認したところ、コマンドの実行に失敗するとクライアントが終了し、そのクライアントに割り当てられた残りのTXが実行されないようです。今回の場合では早々に1クライアントしか残らず、実質的にシングル処理となっています。よって、今回の結果をパラレル処理の評価には使うことができません。

また クライアント数 ≧ 2 かつ ワーカースレッド数 = 1 でも直列化異常は起きます。当初はワーカースレッド数が 1 であれば逐次的な実行になるのではと推測したのですが、pgbench のデバッグオプション (--debug) で実行して確認したところ、各クライアントのステップが互い違いに実行されていました。
⇒これに関しては私のC言語のリーディング力が足りず、コードレベルでは読み切れていません。そんなに難しいことではないはずなのですが…。残念です。

クライアント数 = 1 であれば直列化異常は起きませんが、TX分離の目的を考えるとあまり意味のある測定とは言えないでしょう。

全てのTX分離レベルの比較

repeatable read, serializable でクライアント数 ≧ 2 だと直列化異常が発生して abort するため、read uncommitted, read committed とパフォーマンスの比較をすることができません。直列化異常時にリトライする処理を含んだシナリオとする必要があります。

ただ、直列化異常が発生しているということはアノマリーが発生しているということです。アノマリーが発生するシナリオでTX分離レベル間のパフォーマンスを比較することに意味がないと思われます。

アノマリーについては以下の記事を参照ください。
いろんなAnomaly - Qiita

クライアント数 = 1 では各TX分離レベルとも大差のない結果となっています。ただ、クライアント数 = 1 であればTXも1つであり、分離する必要がありません。また、現実的なアプリケーションでは想定しがたい状況です。よって、このケースのパフォーマンスを議論することはあまり意味がないと思われます。

まとめ

今回採用した「TPC-B に似た試験シナリオ」による検証では以下の問題点がありました。

  • アノマリーが発生するシナリオであること。
  • 直列化異常が発生した時にリトライしていないこと。
  • クライアントでエラーが発生すると残りのTXが実行されないこと。

このことを踏まえると、今回の手法で異なるTX分離レベルのパフォーマンスを比較することは不適切と考えられます。

TX分離レベルのパフォーマンス測定を意味のあるものとするためには、アノマリーが発生しない、もしくは発生しても許容できるシナリオである必要があります。また、それが実際のアプリケーションに近いシナリオである必要があります。

もしくはTX分離レベルに応じて測定シナリオを変えることも考えられます。TX分離レベルが異なれば実際のアプリケーションでも設計が異なるはずだからです。ただ、一般化した測定シナリオを作ることはかなり困難でしょう。

測定記録

本節では実際の測定データを掲載します。

実際のコマンド、測定結果、発生したエラーなどの参考になさってください。

read uncommitted

$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 2000000/2000000
latency average = 1.022 ms
tps = 3912.509879 (including connections establishing)
tps = 3912.558738 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 2000000/2000000
latency average = 1.024 ms
tps = 3906.769702 (including connections establishing)
tps = 3906.787669 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 2000000/2000000
latency average = 1.049 ms
tps = 3813.112466 (including connections establishing)
tps = 3813.152381 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 2000000/2000000
latency average = 1.050 ms
tps = 3808.567856 (including connections establishing)
tps = 3808.588580 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 1 -j 1 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 500000
number of transactions actually processed: 500000/500000
latency average = 0.831 ms
tps = 1204.027044 (including connections establishing)
tps = 1204.043443 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 1 -j 1 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 500000
number of transactions actually processed: 500000/500000
latency average = 0.832 ms
tps = 1201.563732 (including connections establishing)
tps = 1201.569588 (excluding connections establishing)

read committed

$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 2000000/2000000
latency average = 1.043 ms
tps = 3834.281733 (including connections establishing)
tps = 3834.307153 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 2000000/2000000
latency average = 1.060 ms
tps = 3772.517305 (including connections establishing)
tps = 3772.546641 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 2000000/2000000
latency average = 1.052 ms
tps = 3803.228359 (including connections establishing)
tps = 3803.268465 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 2000000/2000000
latency average = 1.050 ms
tps = 3809.376424 (including connections establishing)
tps = 3809.413435 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 1 -j 1 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 500000
number of transactions actually processed: 500000/500000
latency average = 0.839 ms
tps = 1191.747452 (including connections establishing)
tps = 1191.761725 (excluding connections establishing)
pgbench -i -s 4 --unlogged-tables testdb

pgbench -c 1 -j 1 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 500000
number of transactions actually processed: 500000/500000
latency average = 0.840 ms
tps = 1190.629978 (including connections establishing)
tps = 1190.640484 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 1 -j 1 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 500000
number of transactions actually processed: 500000/500000
latency average = 0.840 ms
tps = 1190.574792 (including connections establishing)
tps = 1190.590501 (excluding connections establishing)

repeatable read

$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
client 2 script 0 aborted in command 8 query 0: ERROR:  could not serialize access due to concurrent update
client 3 script 0 aborted in command 8 query 0: ERROR:  could not serialize access due to concurrent update
client 0 script 0 aborted in command 8 query 0: ERROR:  could not serialize access due to concurrent update
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 500001/2000000
latency average = 3.343 ms
tps = 1196.411218 (including connections establishing)
tps = 1196.424449 (excluding connections establishing)
Run was aborted; the above results are incomplete.
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
client 3 script 0 aborted in command 8 query 0: ERROR:  could not serialize access due to concurrent update
client 2 script 0 aborted in command 8 query 0: ERROR:  could not serialize access due to concurrent update
client 1 script 0 aborted in command 8 query 0: ERROR:  could not serialize access due to concurrent update
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 500001/2000000
latency average = 3.345 ms
tps = 1195.807328 (including connections establishing)
tps = 1195.822348 (excluding connections establishing)
Run was aborted; the above results are incomplete.
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 1 -j 1 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 500000
number of transactions actually processed: 500000/500000
latency average = 0.858 ms
tps = 1165.395653 (including connections establishing)
tps = 1165.411007 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 1 -j 1 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 500000
number of transactions actually processed: 500000/500000
latency average = 0.837 ms
tps = 1195.096627 (including connections establishing)
tps = 1195.108248 (excluding connections establishing)

serializable

$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
client 1 script 0 aborted in command 8 query 0: ERROR:  could not serialize access due to concurrent update
client 2 script 0 aborted in command 9 query 0: ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during conflict in checking.
HINT:  The transaction might succeed if retried.
client 3 script 0 aborted in command 9 query 0: ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during conflict in checking.
HINT:  The transaction might succeed if retried.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 500000/2000000
latency average = 3.409 ms
tps = 1173.402161 (including connections establishing)
tps = 1173.414763 (excluding connections establishing)
Run was aborted; the above results are incomplete.
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 4 -j 4 -t 500000 testdb
starting vacuum...end.
client 1 script 0 aborted in command 9 query 0: ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during conflict in checking.
HINT:  The transaction might succeed if retried.
client 3 script 0 aborted in command 9 query 0: ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during conflict in checking.
HINT:  The transaction might succeed if retried.
client 0 script 0 aborted in command 7 query 0: ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during write.
HINT:  The transaction might succeed if retried.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 4
number of threads: 4
number of transactions per client: 500000
number of transactions actually processed: 500000/2000000
latency average = 3.405 ms
tps = 1174.618390 (including connections establishing)
tps = 1174.623363 (excluding connections establishing)
Run was aborted; the above results are incomplete.
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 1 -j 1 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 500000
number of transactions actually processed: 500000/500000
latency average = 0.850 ms
tps = 1175.877155 (including connections establishing)
tps = 1175.886246 (excluding connections establishing)
$ pgbench -i -s 4 --unlogged-tables testdb

$ pgbench -c 1 -j 1 -t 500000 testdb
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 4
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 500000
number of transactions actually processed: 500000/500000
latency average = 0.851 ms
tps = 1174.909483 (including connections establishing)
tps = 1174.917468 (excluding connections establishing)

pgbench の scale オプションを知る

要約

PostgreSQLベンチマーク試験コマンドである pgbench の scale オプションは初期化処理で重要です。ベンチマーク実行時のオプションとしては有効ではないことに注意が必要です。

$ pgbench -s 4 -c 1 -j 1 -t 10000 testdb
scale option ignored, using count from pgbench_branches table (1)

このようなワーニングが出力されたときはオプションの意味を勘違いしている可能性が高いため、注意が必要です。

対象バージョン

今回は PostgreSQL 11 で調査を行いました。

pgbench のマニュアルは以下を参照しています。
https://www.postgresql.jp/document/11/html/pgbench.html

「初期化用のオプション」と「ベンチマーク用オプション」の違い

pgbench は組み込みの TPC-B に似たシナリオを使うことも、独自のシナリオを使用することもできます。

組み込みのシナリオを実行するためには初期データ投入の初期化処理を実行する必要があります。これは pgbench コマンドを initialize (-i) オプション付きで実行します。

pgbench -i [ other-options ] dbname

PostgreSQL の pgbench のマニュアルには「初期化用のオプション」の記載がありますが、これはこの初期化処理で指定するオプション (other-options) に相当します。

初期化処理実行後はベンチマークを実行することができます。pgbench コマンドを initialize (-i) オプション無しで実行します。

pgbench [ options ] dbname

PostgreSQL の pgbench のマニュアルには「ベンチマーク用オプション」の記載がありますが、これはこの初期化処理で指定するオプション (options) に相当します。

scale オプション

初期化用オプション

scale は初期データの「倍率」を指定します。デフォルトの「倍数」の 1 では以下の初期データ(行数)が生成されます。

テーブル 行数
pgbench_branches 1
pgbench_tellers 10
pgbench_accounts 100,000
pgbench_history 0

「倍率」を 100 (-s 100) に指定すると各テーブルの行数が100倍で生成されます。

テーブル 行数
pgbench_branches 100
pgbench_tellers 1,000
pgbench_accounts 10,000,000
pgbench_history 0

ベンチマーク用オプション

ドキュメントの説明を引用します。

pgbenchの出力で指定した倍率を報告します。 これは組み込みの試験では必要ありません。 正確な倍率がpgbench_branchesテーブルの行数を数えることで検出されます。 しかし、独自ベンチマーク(-fオプション)のみを試験している場合、このオプションを使用しない限り、倍率は1として報告されます。

pgbench

この説明を読むだけだと分かり辛い点があるので、組み込み試験の実行例を用いて説明します。

$ pgbench -i -s 10 testdb
dropping old tables...
creating tables...
generating data...
100000 of 1000000 tuples (10%) done (elapsed 0.07 s, remaining 0.62 s)
200000 of 1000000 tuples (20%) done (elapsed 0.44 s, remaining 1.76 s)
300000 of 1000000 tuples (30%) done (elapsed 0.78 s, remaining 1.83 s)
400000 of 1000000 tuples (40%) done (elapsed 1.03 s, remaining 1.54 s)
500000 of 1000000 tuples (50%) done (elapsed 1.20 s, remaining 1.20 s)
600000 of 1000000 tuples (60%) done (elapsed 1.72 s, remaining 1.14 s)
700000 of 1000000 tuples (70%) done (elapsed 2.08 s, remaining 0.89 s)
800000 of 1000000 tuples (80%) done (elapsed 3.17 s, remaining 0.79 s)
900000 of 1000000 tuples (90%) done (elapsed 3.46 s, remaining 0.38 s)
1000000 of 1000000 tuples (100%) done (elapsed 3.79 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done.

$ pgbench -s 100 testdb
scale option ignored, using count from pgbench_branches table (10)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 10
query mode: simple
number of clients: 1
number of threads: 1
number of transactions per client: 10
number of transactions actually processed: 10/10
latency average = 47.884 ms
tps = 20.883699 (including connections establishing)
tps = 20.980622 (excluding connections establishing)

初期化処理の scale オプションを "10" に指定して実行しました。

次にベンチマーク実行の scale オプションを "100" に指定して実行したところ、以下のワーニングが出力されました。

scale option ignored, using count from pgbench_branches table (10)

これは scale オプションを無視して pgbench_branches テーブルの行数を参照するとしています。()内はその行数を示しています。

そして、出力結果の "scaling factor" にはこの行数(つまり初期化処理の倍率)が出力されます。

scaling factor: 10

つまり、組み込みの試験でベンチマークを実行する際に scale オプションを指定することは意味がないとわかりました。

では組み込みの試験でなく、独自のシナリオで試験した場合にこの scale オプションがどう使われるかというと、単に "scaling factor" の出力に使われるだけのようです。
ソースコードを読んで判断しましたが、もし異なるようであればツッコミをお願いします。

scale オプションの値をどのように決定すべきか?

ドキュメントには「優れた実践 (Good Practices)」として以下の記載があります。

デフォルトのTPC-Bのような試験シナリオでは、初期倍率(-s)を試験予定のクライアント数(-c)の最大値と同程度にしなければなりません。 pgbench_branchesテーブルには-s行しかありません。 また、全トランザクションはその内の1つを更新しようとします。 ですので、-c値を-sより大きくすると、他のトランザクションを待機するためにブロックされるトランザクションが多くなることは間違いありません。

pgbench

ただ、私としては『初期倍率(-s)を試験予定のクライアント数(-c)の最大値と同程度にしなければなりません』が正しいかについて確信を持てないでいます。マニュアルではpgbench_branches テーブルの更新による競合を避けることを理由に挙げていますが、組み込み試験ではトランザクションの実行毎に参照若しくは更新する行を選択しており、どれほど初期倍率を大きくしたとしてもクライアント数が複数あれば競合する可能性を否定できないためです。

組み込みのTPC-Bのようなトランザクションの完全な定義を示します。

\set aid random(1, 100000 * :scale)
\set bid random(1, 1 * :scale)
\set tid random(1, 10 * :scale)
\set delta random(-5000, 5000)
BEGIN;
UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;
UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;
INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
END;

このスクリプトにより、トランザクションを繰り返す度に異なる、ランダムに選ばれた行を参照することができます。

pgbench

よって、マニュアルの推奨は一つの目安にしつつ、どのようなシナリオでベンチマークしたいのか?というのを都度考えるしかないと思われます。それによっては倍率オプションが 1 という選択肢もあるかもしれません。

Database Concurrency Control Papadimitriou 読書会 第16回 議論メモ

勉強会について

Database Concurrency Control Papadimitriou 読会 第16回 - connpass の議論メモです。

自分のメモをベースにまとめています。発言の聞き間違い、解釈違いの可能性があることをご了承ください。

特記の無い引用は本で議論した箇所を示しています。

Theory of Database Concurrency Control

Theory of Database Concurrency Control

  • 作者:Christos Papadimitriou
  • 出版社/メーカー: Computer Science Pr
  • 発売日: 1986/07/01
  • メディア: ハードカバー

[memo]は私のメモです。勉強会で議論された内容ではないことにご注意ください。

2.6 CONFLICT SERIALIZABILITY

Conflicts and Commutativity

s ∼* s' if s can be transformed to s' via zero or more switchings of non-conflicting steps.

Proof では "Among the pairs of steps which are out of order in s', choose a pair of steps which are consecutive in s'." としており、交換するステップは consecutive であることが必要なはず。Theorem 2.9 でも省略せずに記載すべきだ。

Theorem 2.9 Proof

Let k be the number of pairs of steps of the two schedules which occur in different order in s than in s'.

k = 0 は s と s' が同じとき。
k = 1 は隣り合うステップが1つだけひっくり返ったとき(例:123 → 132)。

conflict を定義すれば conflict serializable か否かを判定できる。read / write で考える必要がない。
これにより、mono-version なのか multi-version なのかを意識する必要がなくなる。

Example 2.6 のスケジュールにおける "c" は "c" であり、commit の意味ではない。


[memo]
Example 2.6 について。
conflict はステップの交換ができない。なので、オリジナルのスケジュールのオーダーを保つことになる。
これを踏まえ、conflict をスケジュールのオーダーで記述すると以下の結果となる。
{b1, a2}, {a2, c1}, {b1, c1}, {c4, b3}
これをグラフにすると figure 2.11 となる。

PostgreSQLのストアドプロシージャはループがあったとしても1つのTXで処理する。ただ、ループ内の処理がパラレルに実行可能であれば、パラレルに実行したい。これが今は実現できない。

Definition 2.7 の "set of transactions" の "set" は部分集合のこと。

monotonic であれば、一部のTXを抜いても conflict serializable といえるが、view が変わることになるので view serializable とは言えない。
view serializable で monotonic なケースを考えると、一部のTXを抜き出せばそれまでの view が壊れる。だが、残されたTXだけで考えると view serializable ではあるし、monotonic でもある。


[memo]

view serializability is not a monotonic property.

これは view serializable でも monotonic なときはあるが(例:conflict serializable)、必ずしもそうとは言えない、と言いたいのではないか。



Not only is conflict serializability a monotonic property and view serializability is not, but the set of conflict serializable schedules is the largest monotonic subset of view serializability.

CSR でない VSR は monotonic ではない。

f:id:pato_taityo:20191214005159p:plain

monotonic であれば abort の処理が楽になる。


[memo]
以前に monotone について調べてまとめた記事があります。
SerializabilityとMonotonicityとRigorousnessの関係 - ぱと隊長日誌

スケジュールの "c" を commit ではなく、「TXのコマンドがこれ以降無い(最後である)」という pre-commit として扱うことも出来るのではないか。つまり、pre-commit は commit する意思を示すもので commit できることを確約するものではない。依存関係を表すものとして扱わないということだ。

関連記事

papa本第16回参加メモ - 誰にも見えないブログ
@yuyabu2 さんの読書会ノート。