2012年12月19日

PostgreSQLのストレージアーキテクチャ(FILLFACTOR編)

PostgreSQL Advent Calendar 2012(全部俺)のDay 19です。

昨日までのエントリで、PostgreSQLの追記型アーキテクチャの基本的な仕組みと、「追記型であるがゆえの課題」にどのように対処しているのかを解説してきました。

ストレージアーキテクチャ小咄シリーズの最終回である今回は、「FILLFACTOR」と呼ばれる仕組みについて、その動作している様子を見ながら解説します。

(他のRDBMSをご存じの方のボキャブラリーに直すと、OracleやDB2で言うところのPCTFREE、SQL ServerのFILLFACTORと似たような役割の機能になります。)

■ブロックに空き領域が無い時の更新処理


ブロック内部が既にいっぱいで空き領域が無い時にレコードを追加または更新しようとした場合、特に前々回に解説したPage Pruningを実施しても空き領域が無かった場合、PostgreSQLではどのように処理されるのでしょうか。

結論から言うと、同一ブロック内に新しいレコードを追記する領域が無いため、他のブロックに新しいレコードを書き込むことになります。

テーブルのブロック構造を思い出しながら、具体的な例で見てみましょう。


以下の例は、テーブルに25件のレコードをINSERTしていった状態のテーブルです。

1つのブロックに25件のレコードが含まれていて、テーブルがその1つのブロックのみでできています。既にブロック全体にレコードが配置されており、これ以上レコードを追記する空き領域が無い状態になっています。
testdb=# INSERT INTO t1 VALUES ( 125, 'insert 125                                                                                                                                                                                                                                                                             ' );
INSERT 0 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
  1 |   7880 |        1 |    311 |  46798 |      0
  2 |   7568 |        1 |    311 |  46799 |      0
  3 |   7256 |        1 |    311 |  46800 |      0
  4 |   6944 |        1 |    311 |  46801 |      0
  5 |   6632 |        1 |    311 |  46802 |      0
  6 |   6320 |        1 |    311 |  46803 |      0
  7 |   6008 |        1 |    311 |  46804 |      0
  8 |   5696 |        1 |    311 |  46805 |      0
  9 |   5384 |        1 |    311 |  46806 |      0
 10 |   5072 |        1 |    311 |  46807 |      0
 11 |   4760 |        1 |    311 |  46808 |      0
 12 |   4448 |        1 |    311 |  46809 |      0
 13 |   4136 |        1 |    311 |  46810 |      0
 14 |   3824 |        1 |    311 |  46811 |      0
 15 |   3512 |        1 |    311 |  46812 |      0
 16 |   3200 |        1 |    311 |  46813 |      0
 17 |   2888 |        1 |    311 |  46814 |      0
 18 |   2576 |        1 |    311 |  46815 |      0
 19 |   2264 |        1 |    311 |  46816 |      0
 20 |   1952 |        1 |    311 |  46817 |      0
 21 |   1640 |        1 |    311 |  46818 |      0
 22 |   1328 |        1 |    311 |  46819 |      0
 23 |   1016 |        1 |    311 |  46820 |      0
 24 |    704 |        1 |    311 |  46821 |      0
 25 |    392 |        1 |    311 |  46822 |      0
(25 rows)

testdb=# SELECT pg_relation_size('t1');
 pg_relation_size
------------------
             8192
(1 row)

testdb=# 
この状態でレコードを更新すると、どうなるのでしょうか。
testdb=# UPDATE t1 SET uname = 'update 101                                                                                                                                                                                                                                                                             ' WHERE uid=101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
  1 |   7880 |        1 |    311 |  46798 |  46823
  2 |   7568 |        1 |    311 |  46799 |      0
  3 |   7256 |        1 |    311 |  46800 |      0
  4 |   6944 |        1 |    311 |  46801 |      0
  5 |   6632 |        1 |    311 |  46802 |      0
  6 |   6320 |        1 |    311 |  46803 |      0
  7 |   6008 |        1 |    311 |  46804 |      0
  8 |   5696 |        1 |    311 |  46805 |      0
  9 |   5384 |        1 |    311 |  46806 |      0
 10 |   5072 |        1 |    311 |  46807 |      0
 11 |   4760 |        1 |    311 |  46808 |      0
 12 |   4448 |        1 |    311 |  46809 |      0
 13 |   4136 |        1 |    311 |  46810 |      0
 14 |   3824 |        1 |    311 |  46811 |      0
 15 |   3512 |        1 |    311 |  46812 |      0
 16 |   3200 |        1 |    311 |  46813 |      0
 17 |   2888 |        1 |    311 |  46814 |      0
 18 |   2576 |        1 |    311 |  46815 |      0
 19 |   2264 |        1 |    311 |  46816 |      0
 20 |   1952 |        1 |    311 |  46817 |      0
 21 |   1640 |        1 |    311 |  46818 |      0
 22 |   1328 |        1 |    311 |  46819 |      0
 23 |   1016 |        1 |    311 |  46820 |      0
 24 |    704 |        1 |    311 |  46821 |      0
 25 |    392 |        1 |    311 |  46822 |      0
(25 rows)

testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 1));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
  1 |   7880 |        1 |    311 |  46823 |      0
(1 row)

testdb=# SELECT pg_relation_size('t1');
 pg_relation_size
------------------
            16384
(1 row)

testdb=# 
空き領域が無い状態でレコードを更新(UPDATE)すると、更新前の古いレコード(lp==1)は削除(t_xmaxが設定される)されますが、同じブロックに空き領域が無い(Page Pruningをしても空き領域を作れない)ため、新しいレコードは異なる新しい2番目のブロックに書き込まれています。新しいブロックが作成されたことで、pg_relation_sizeで取得されるテーブルサイズが8kBから16kBに増加していることが分かります。

これは、もともと25件のレコードが1つのブロックに収まっていたものの、更新する際に(追記するための)空き領域が足りないため新しいブロックを作成した(テーブルファイルを拡張した)ということを示しています。

■「別ブロックへの更新処理」の何が問題か


では、この「別ブロックへの更新処理」の何が問題なのでしょうか。今まで見てきた「同じブロック内での更新」と比べて何が違うのでしょうか。

それは、「更新処理の際に別ブロックへのレコード追記を行うと、余分なI/O処理が発生する」ということです。

同一のブロック内での更新処理であれば、共有バッファ内の(8kBの)読み書きだけで処理が完結することが担保されます。データベースのI/O処理はブロック単位で行われるので、これは当然のことです。

それに対して、異なるブロックへの更新処理になると、「異なるブロックを読み込む」というI/O処理が発生する可能性がある、ということです。もう少し正確に言うと、
  • 空き領域のあるブロックを探す
  • 空き領域のあるブロックが見つからない場合は、ファイルを拡張して新しいブロックを作る
  • ブロックをディスクから共有バッファに読み込む
  • 共有バッファへ読み込んだブロックに対して新しいレコードを書き込む
という処理が行われることになり、(パフォーマンス的に)最悪のケースではディスクをファイルを拡張したり、読み込んだり、といったディスクI/Oが発生することになり、このことがパフォーマンスに大きな悪影響を与えます。なぜなら、メモリへの操作に対してディスクI/Oは、3ケタ近く処理速度が遅いからです。

そもそも、データベースにおいてはストレージが追記型構造であるかどうかに関わらず、ブロックへのアクセスを如何に削減するか、余計なブロックアクセスを減らすか、ということが重要です。

にも関わらずブロックを一杯まで使ってしまうと、異なるブロックへの更新処理が頻発し、ディスクI/Oが増えてしまう可能性が高まります。追記型のストレージアーキテクチャを持つPostgreSQLでは、この可能性が特に高くなります。

また、当然ながら更新の時だけではなく、(前回までに見てきたように)参照の時にもPostgreSQLの内部では古いタプルからチェーンを辿るようにして最新のタプルを探しますので、更新のチェーンが異なるブロックにまたがっているということは、参照の時にも余計なI/O処理を発生させることになり、パフォーマンスに影響を与えます。

■強制的に空き領域を確保して更新(UPDATE)に備える


というわけで、前述のような状況(別ブロックへの追記)が発生すると、「あぁ、同じブロックに空き領域があればなぁ」と考えるのが人情というものでしょう。この「UPDATEに備えて同じブロックに意図的に空き領域を確保しておく」ための仕組みが「FILLFACTOR」と呼ばれるものです。


これは、INSERTをする際には指定した割合の空き領域を確保しておき、UPDATEをする時にその空き領域を使う、という仕組みです。このように領域を予約しておくことによって、「更新処理の際に同じブロックの空き領域を使える」という可能性が飛躍的に高まります。

実際の動作を見てみましょう。

以下は、先の例とまったく同じレコードを25件INSERTしたテーブルの状態です。

先ほどと違うのは、同じ25件のレコードであるにも関わらず、今回は1ブロックに収まらず既に2ブロック使っていること、そして1ブロック目に少し空き領域があることです(最後のlp_offが1000バイト以上あります)。
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
  1 |   7880 |        1 |    311 |  46856 |      0
  2 |   7568 |        1 |    311 |  46857 |      0
  3 |   7256 |        1 |    311 |  46858 |      0
  4 |   6944 |        1 |    311 |  46859 |      0
  5 |   6632 |        1 |    311 |  46860 |      0
  6 |   6320 |        1 |    311 |  46861 |      0
  7 |   6008 |        1 |    311 |  46862 |      0
  8 |   5696 |        1 |    311 |  46863 |      0
  9 |   5384 |        1 |    311 |  46864 |      0
 10 |   5072 |        1 |    311 |  46865 |      0
 11 |   4760 |        1 |    311 |  46866 |      0
 12 |   4448 |        1 |    311 |  46867 |      0
 13 |   4136 |        1 |    311 |  46868 |      0
 14 |   3824 |        1 |    311 |  46869 |      0
 15 |   3512 |        1 |    311 |  46870 |      0
 16 |   3200 |        1 |    311 |  46871 |      0
 17 |   2888 |        1 |    311 |  46872 |      0
 18 |   2576 |        1 |    311 |  46873 |      0
 19 |   2264 |        1 |    311 |  46874 |      0
 20 |   1952 |        1 |    311 |  46875 |      0
 21 |   1640 |        1 |    311 |  46876 |      0
 22 |   1328 |        1 |    311 |  46877 |      0
 23 |   1016 |        1 |    311 |  46878 |      0
(23 rows)

testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 1));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
  1 |   7880 |        1 |    311 |  46879 |      0
  2 |   7568 |        1 |    311 |  46880 |      0
(2 rows)

testdb=# SELECT pg_relation_size('t1');
 pg_relation_size
------------------
            16384
(1 row)

testdb=# 
この状態でUPDATE処理を行ってみます。
testdb=# UPDATE t1 SET uname = 'update 101                                                                                                                                                                                                                                                                             ' WHERE uid=101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
  1 |   7880 |        1 |    311 |  46856 |  46881
  2 |   7568 |        1 |    311 |  46857 |      0
  3 |   7256 |        1 |    311 |  46858 |      0
  4 |   6944 |        1 |    311 |  46859 |      0
  5 |   6632 |        1 |    311 |  46860 |      0
  6 |   6320 |        1 |    311 |  46861 |      0
  7 |   6008 |        1 |    311 |  46862 |      0
  8 |   5696 |        1 |    311 |  46863 |      0
  9 |   5384 |        1 |    311 |  46864 |      0
 10 |   5072 |        1 |    311 |  46865 |      0
 11 |   4760 |        1 |    311 |  46866 |      0
 12 |   4448 |        1 |    311 |  46867 |      0
 13 |   4136 |        1 |    311 |  46868 |      0
 14 |   3824 |        1 |    311 |  46869 |      0
 15 |   3512 |        1 |    311 |  46870 |      0
 16 |   3200 |        1 |    311 |  46871 |      0
 17 |   2888 |        1 |    311 |  46872 |      0
 18 |   2576 |        1 |    311 |  46873 |      0
 19 |   2264 |        1 |    311 |  46874 |      0
 20 |   1952 |        1 |    311 |  46875 |      0
 21 |   1640 |        1 |    311 |  46876 |      0
 22 |   1328 |        1 |    311 |  46877 |      0
 23 |   1016 |        1 |    311 |  46878 |      0
 24 |    704 |        1 |    311 |  46881 |      0
(24 rows)

testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 1));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
  1 |   7880 |        1 |    311 |  46879 |      0
  2 |   7568 |        1 |    311 |  46880 |      0
(2 rows)

testdb=# SELECT pg_relation_size('t1');
 pg_relation_size
------------------
            16384
(1 row)

testdb=# 
UPDATE処理を行った結果を見てみると、1番目のブロックのlp==1のレコードが削除され、同じブロックにlp==24として更新されていることが分かります。つまり、1番目のブロックに空き領域が確保されていたために、UPDATE処理の際にその領域を使うことができている、ということが分かります。

FILLFACTORの設定方法はCREATE TABLEにオプションを付ける方法、ALTER TABLEで設定する方法などがあります。詳細はマニュアルを参照してください。

CREATE TABLE
http://www.postgresql.jp/document/9.0/html/sql-createtable.html
ALTER TABLE
http://www.postgresql.jp/document/9.0/html/sql-altertable.html

なお、FILLFACTORの値は「10~100(%)」で指定しますが、テーブルにおける設定のデフォルトは「100」、つまり「空き領域無しで全部詰め込む」という設定になっています。つまり、何も設定しないとFILLFACTORの恩恵はまったく受けられませんのでご注意ください。

■まとめ


今回は、UPDATE処理の際にできるだけ同一ブロック内の空き領域を使うための「FILLFACTOR」の仕組みについて、その実際の動作を見ながら解説してきました。

FILLFACTORは、特に更新処理の多いワークロードで性能の安定に寄与します。一方で、「空き領域を確保しておく」という機能の特性上、テーブルファイル全体のサイズは大きくなります。

このようなトレードオフが存在しますので、自身のデータベースのワークロードのタイプをよく見極めて、これらの機能をうまく使いこなしていただければと思います。

では、また。

0 件のコメント:

コメントを投稿