PostgreSQL 「effective_io_concurrency」の設定について

PostgreSQLの「effective_io_concurrency」という設定をご存知でしょうか?
PostgreSQLのドキュメントには、

「PostgreSQLが同時実行可能であると想定する同時ディスクI/O操作の数を設定します」

とあります。
とりあえず有効にしておけば同時実行でなんか速そう!という印象をうけるのですが、そんなことはありません!
PostgreSQL9.6以下の場合はオフにした方がパフォーマンスが良いケースが多いと思います。
今回はなぜオフにした方が速いのか、みていきたいと思います。


※PostgreSQL10以降で実装されたパラレルビットマップヒープスキャンにはあてはまりません
※常に「オフ」が最適設定ということではありません。システム構成・データ傾向次第です
※検証環境はCentOS7、PostgreSQL9.6です

effective_io_concurrencyとは?

ビットマップヒープスキャン時に、ストレージからのデータ読み込みIOを、データの処理と並行させることでパフォーマンス向上を狙うもので、ビットマップヒープスキャン専用の設定です。
effective_io_concurrencyを1以上に設定することで有効となり、最大は1000です。設定値を上げるほどより多くのデータを非同期で読み込むようになります。
0に設定することで非同期IOを無効化することができます。

どうやって非同期でデータを読み込むのか?

ずばり、posix_fadvise関数です。ソースコードには将来的に別の実装も検討したいとコメントがありますが、現時点ではありません。
そのため、posix_fadvise関数が利用できないシステムでは動作しません。

posix_fadvise関数とは、アプリケーションがカーネルに、次に行う予定のストレージアクセスを知らせるためのものです。
これにより、アプリケーションが実際にストレージアクセスを行うまでの間にカーネルが先行して準備を進めることが可能となります。

たとえば、posix_fadvice(fd, 0, 8192, POSIX_FADV_WILLNEED)とすると、
カーネルがファイルディスクリプタfdのファイルの先頭から8KBをページキャッシュに読み込みます。
このあとにread()すると、すでにページキャッシュに読み込まれているため、ディスクアクセスが発生することなく即座にデータを取得可能となります。
(read()までにカーネルの処理が間に合っていれば)

PostgreSQLはまさにこれを使っています。データ処理を行う際に次に読み込む予定のページをposix_fadviseでカーネルに知らせています。

で、何が問題なの?

とても効率的に見えるのですが、逆にとても非効率になってしまうケースがあるんです。
それは「ビットマップヒープスキャンでアクセスするページの多くが隣接している場合」です。
隣接している、つまり、ストレージアクセスがシーケンシャルリードになる場合です。

PostgreSQLは常にページサイズである8KB単位でread/writeをします。たとえば読み取る予定のページが16個隣接していた場合、128KBのread()一回で済みますが、PostgreSQLでは8KBのread()を16回実行します。
とても非効率に見えますが、実際はOSの機能でカバーされるため、ほとんど問題になりません。
Linuxには「read_ahead_kb」という設定があります。シーケンシャルリードが続く場合、この設定にもとづいてカーネルはread()で指定されたアドレス以降のデータを一括で読み込みます。
そのため、PostgreSQLがread()を16回実行したとしても、実際にストレージに16回IOが発行されるということはありません。おそらくせいぜい2~3回です。

が、ここで問題が発生します。posix_fadviceによる先読みです。
PostgreSQLは先に書いたようにposix_fadviceで先読みを行いますが、これも常に8KB単位です。連続した領域に対してposix_fadviceを実行しても、OSはread()のときのように先読みすることはなく、そのまま8KBでIOを発行します。
結果、本来であればOSの機能により抑えられるIO発行回数が数倍、数十倍にもふくれあがり、パフォーマンスが大きく悪化します。

実際に試してみる

実際に再現させてみます。テストとして以下のテーブルを用います。

テストデータとして500万件投入します。投入するデータはcreated_atが1秒ずつ進み、dataはすべて同じ900Byteの'A'が並んだデータを用意します。
これで1ページにちょうど8行入ることになります。

テストデータ

コピーで投入します。

テーブルファイルを確認します。1ページに8行入るデータを500万件投入したので、計625000ページで、ファイルサイズは625000 * 8192 = 5120000000Byteとなります。

ビットマップヒープスキャンを発生させるため、created_atにbrinインデックスをはります。

いよいよ実験です!
まずはeffective_io_concurrencyを1で試してみます。

約20秒かかりました。このとき、ストレージへのIO発行状況は以下です。
(blktrace/blkparseで取得)

+16となっているのは512Byte計算で、8KBのことです。8KBの読み込みIOが続いているのがわかります。

straceでみてみると、posix_fadviceに対応するシステムコールfadvise64が8KBで連続した領域に実行されているのがわかります。

では次にeffective_io_concurrencyを0で試してみます。(キャッシュ等はすべてクリアしたうえで)

劇的改善です!3秒強になりました!

このときのストレージへのIO発行状況は以下です。

先ほどの8KBのリードが並んでいたのとは違い、512KBのリードが並んでいます。
かなり発行IO数を減らせていそうです。

最後に

今回の実験では確実にデータが隣接しているビットマップヒープスキャンとなるよう意図的にデータを用意したため、顕著に違いがでました。
実際に運用しているシステムではここまでデータが隣接することはないと思いますが、ビットマップヒープスキャンはスキャン開始前にページ位置順にソートするため、極端にランダムアクセスになることも少ないと思います。
そのため、effective_io_concurrency無効化は十分に試す価値があると思います。特定のクエリだけ実行前にSETでオフにするというのもアリですね。

PostgreSQL10以降で試したところ、パラレルビットマップヒープスキャンの場合はeffective_io_concurrencyを1にしてもread_aheadが機能していました。
パラレルでない場合は9系までと同じ挙動となるので注意してください。

Comments are closed, but you can leave a trackback: Trackback URL.