ぱと隊長日誌

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

デブサミ2020「【13-E-5】Googleにおける「ソフトウェア×インフラ」デザイン~マイクロサービス・アーキテクトの視点から~」聴講メモ

はじめに

Developers Summit 2020 (Developers Summit 2020)
Googleにおける「ソフトウェア×インフラ」デザイン~マイクロサービス・アーキテクトの視点から~
スピーカー:中井 悦司 [グーグル・クラウド・ジャパン]
の聴講メモです。

Twitterのつぶやきがtogetterでまとめられています。併せてご参照ください。
デブサミ2020【13-E-5】Googleにおける「ソフトウェア×インフラ」デザイン〜マイクロサービス・アーキテクトの視点から〜 #devsumiE #devsumi - Togetter

挿入した図は講演資料を思い出して描いたものです。実際とは異なります。。

【参考】としている箇所は私が挿入しています(補足や参考資料など)。登壇者の講演内容ではありませんので、その旨ご了承ください。

聴講メモ

時代はインフラの次を考えるようになっているのではないか。

マイクロサービスとアーキテクトを切り離すことはできない。表題の「マイクロサービス・アーキテクト」を司会者が読み上げるときに中点で区切らないでほしいとお願いしたのにはそういう思いを込めている。

GCPGoogleが社内で利用していたインフラ環境をpublicに使えるように公開したものだ。

資料で示した専用線の図はインターネット回線のように見えるが、全てGoogleのプライベートな回線となっている。このため、Googleのインフラエンジニアはネットワークにどんなパケットが流れているかを把握しているし、パケットの種類に応じて優先度をコントロールすることもできる。


【参考】
2018/01の記事で少し古いですが、Googleが海底ケーブルの図を公表していました。
新リージョンと海底ケーブルの増設でグローバル インフラストラクチャを拡張 | Google Cloud Blog
講演資料ではもっと線があったような印象を受けました。ケーブルがますます増えたということかもしれません。

Google社内ではBorgによるコンテナオーケストレーションにてデータセンターを管理している。


【参考】
Googleが作った分散アプリケーション基盤、Borgの論文を読み解く -導入編- - inductor's blog

GCPGoogle社内のエンジニアが使っている環境と同等といえる。
Google転職にチャレンジしたい方は試してみると良いかも!

GCPをいざ使い始めようとすると悩むのが、どう組み合わせる?どう使う?ということだ。
Googleのように全世界を相手にサービスを提供するわけではない。自分たちの規模に合わせてどう使えば良いか?ここでマイクロサービスとコンテナが出てくる。

なお、Google社内ではマイクロサービスという言葉が出てこない。マイクロサービスで開発することは検索サービス初期からのことであり、全世界向けのサービス提供のためには当たり前のことだから。

マイクロサービスのメリットはスケーラブルなことだけでない。サービスの規模によらずマイクロサービスを利用できる。

Googleのインフラの構成は論文などで公開されているが、アプリの構成は公開されていない。なので今回の発表はどこまで話せるか探りながらとなる(クビにならない範囲で…)。よって、今回の発表では写真撮影もお断りさせていただいた。

3層構造と対比したマイクロサービスWebアーキテクチャを示す。
3層構造になっていることは今も昔も変わらないが、Viewがクライアントに寄り、サーバがデータを管理するスタイルになってきた。
View相当部分はクライアント側のJavaScriptにて実装している。

f:id:pato_taityo:20200215214912p:plain

BFF (Backends For Frontends) でサービスをアグリゲーションしてクライアントに返す。以前はクライアントが直接サービスを呼んでいたが、複雑になりすぎるためBFFを挟んだ。また、サービスは他のサービスを呼びだすこともある。


【参考】
BFF(Backends For Frontends)超入門――Netflix、Twitter、リクルートテクノロジーズが採用する理由:マイクロサービス/API時代のフロントエンド開発(1)(1/2 ページ) - @IT
この記事ではBFFが登場した経緯について説明しています。

ロードバランサーは1つの巨大なバランサーで全てをまかなっている。これは社内もGCPも同じ。


【参考】
『1つの巨大なバランサー』とは以下の記載を指しているように思えます。

GCPの「Cloud Load Balancing」を利用すると、1つのグローバルIPアドレス(VIP: Virtual IP)を用いて、地球上のすべてのリージョンにアクセスを分散することができます。

コラム - グーグルのクラウドを支えるテクノロジー | 第1回 分散型ロードバランサーを実現するMaglev(パート1)|CTC教育サービス 研修/トレーニング


GCPを組み合わせることでGoogleのマイクロサービスアーキテクチャを実現するのは難しくない。

ただ、スクラッチで作るなら、モノリシックで設計された既存システムをどうするかについて考える必要がある。

マイクロサービスの利点を以下に挙げる。

  • スケーラビリティの実現。サービス単位で独立した開発デプロイを可能にすることで新機能のリリースを安全に素早くできる。
  • 想定外の影響を減らせる。多数のサービスにまたがる依存関係を気にせず、特定機能の改善に集中できる。
  • チーム間の依存関係を減らせる。チーム毎の独立性を高めて、機能毎の並行開発・並行リリースを可能とする。
  • スケーラブルなインフラの活用。
  • SREメンバーはサービスの粒度でアプリケーションアーキテクチャが把握できるので、問題発生時の切り分けが迅速にできる。

マイクロサービスによるスクラッチ開発の課題として、サービスの独立性を確保した設計を初期段階で確実に行うことは難しいことが挙げられる。
⇒ある程度成長し終えた既存アプリから独立性の高い部分をマイクロサービスとして切り出す方が設計的には上手くいくかも?

参考文献として下記の本を挙げる。

モノリシックなところからマイクロサービスにどうやって切り出すかを解説した本。
今後はこういう本が流行るかも?

一例として既存アプリがMVCモデルで構築されてクラウドで動いているとする。
アプリの中身を外部サービスとして切り出し、コントローラが切り出した外部サービスを呼びだすようにする。

オンプレの塩漬けシステムを活用する例として、サービスの一つとして見せてしまうという手もある。新規クライアントとオンプレシステムの間にBFFを挟んで実現する。
新しい機能はサービスとして実装し、これもBFFから呼びだす。

f:id:pato_taityo:20200215214933p:plain

マイクロサービス基盤を設計するとGoogleの構成に似てくるのは、世の中で必要とする物がGoogleが必要としていた物に近づいた、ということではないか。

マイクロサービスによるシステム設計とは。

  • アプリケーションの機能を適切にマイクロサービスに分割する作業
  • それぞれのマイクロサービスを実装する方法を考える作業

求められるスキルや知見。

  • SREはシステム運用の方法論。
  • マイクロサービスアーキテクトはアプリケーションデザインの知見。
  • 共通してクラウドやインフラの知見。

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 という選択肢もあるかもしれません。