2012年12月16日

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

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

PostgreSQLのアーキテクチャやパフォーマンスを議論する際、「ストレージ(ファイル)が追記型のストレージアーキテクチャを採用している」ということは、PostgreSQL特有の大きな特徴として認識している方も多いでしょう。

少し前にも、ネット上でPostgreSQLと他のRDBMSのストレージのアーキテクチャの違いについて話題になったこともありました。

PostgreSQLとMySQLはどちらかに明確な優位性がありますか? - QA@IT
http://qa.atmarkit.co.jp/q/2395

優位性云々の議論はとりあえず置いておくとして、まずはPostgreSQLの実際の仕組みをきちんと理解するために「追記型のストレージ」というものがどのように動いているのかを覗いてみます。

■「追記型のストレージアーキテクチャ」とは


PostgreSQLにおける「追記型のストレージアーキテクチャ」というのは、簡単に言えば、「レコードの更新処理を行う際に、ブロック内の以前のレコードを上書きするのではなく、別のレコードとして作成する」という仕組みのことです。


以降では、この追記型のアーキテクチャについてについて、テーブル内のレコードがどのように変化していくのか、実際の動作を追いながら解説していきます。

■pageinspectモジュールのインストール


今回は、テーブル内のレコードの状態を見るためにpageinspectモジュールを利用します。

pageinspectモジュールは、PostgreSQLのテーブルやインデックスのブロックの、さらにその中にある「タプル(内部的にはitemと呼ばれる)」の状態を取得するための関数群を提供するcontribモジュールです。

F.20. pageinspect
http://www.postgresql.jp/document/9.0/html/pageinspect.html

9.0以前はインストールスクリプトを使ってインストール、9.1以降はEXTENSIONとしてインストールすることになりますので、必要に応じてDay2のエントリも参照にしてインストールしてください。

■ブロック内部における新規レコードの状態


それでは、実際にテーブルの更新処理においてブロック内のレコードがどのように変化していくのかを見てみましょう。

まず、integerとtextのカラムを持つテーブルt1を作成し、レコードを一件INSERTします。
testdb=# CREATE TABLE t1 ( uid INTEGER PRIMARY KEY, uname TEXT NOT NULL );
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "t1"
CREATE TABLE
testdb=#
testdb=# INSERT INTO t1 VALUES ( 101, 'insert 1' );
INSERT 0 1
testdb=# select * from t1;
 uid |  uname
-----+----------
 101 | insert 1
(1 row)

testdb=#
この状態でテーブルのブロック内のレコードの状態を見てみましょう。

レコードの状態を見るには、pageinspectモジュールで提供される二つの関数 get_raw_page() と heap_page_items() を使います。get_raw_page() はテーブルのブロックをbytea形式で取得します。heap_page_items()は、bytea形式のバイナリを受け取って、その内部にあるレコードの状態を表示します。
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 |   8152 |        1 |     37 |   1859 |      0
(1 row)

testdb=#
「lp」はブロック内のアイテムのオフセットID、「lp_off」はブロック内におけるレコード本体のオフセット(アドレス)です。「lp_len」はレコードの長さです。


なお、テーブルファイルのブロック内部は上記のような配置になっており、データ本体はブロック内の空き領域を後ろの方から使用します。ですので、lp_offの値が8152と、8kBブロックのギリギリ後ろの方になっています。

また、「t_xmin」はそのレコードを作成したトランザクションのトランザクションIDを示しており、「t_xmax」は逆にそのレコードを削除したトランザクションのトランザクションIDを示しています。

上記の場合、t_xminの値が1859で、t_xmaxの値が0になっていますので、このレコードを作成したトランザクションのトランザクションIDが1859であり、かつまだ削除されていない(生きている)レコードであることが分かります。

■レコードに対する更新処理


次に、このレコードを更新して、ブロックの内部がどのように変化するか見てみます。
testdb=# UPDATE t1 SET uname = 'update 1' 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 |   8152 |        1 |     37 |   1859 |   1860
  2 |   8112 |        1 |     37 |   1860 |      0
(2 rows)

testdb=#
レコードを更新すると、先ほどのレコード(lp==1)のt_xmaxが1860に設定され、新しいレコード(lp==2)が作成されました。

この時、新しく作成されたレコードのt_xminの値(1860)が古いレコードのt_xmaxの値(1860)と同じになっていることが分かります。つまり、古いレコード(lp==1)を削除するのと同時に新しいレコード(lp==2)を追加しているのです。

このように、PostgreSQLのUPDATEの処理では、古いレコードにt_xmaxを設定することで「削除したことにして」、新しいレコードを作成することによって、あたかも「更新処理」を行っているように動作するのです。

これが、PostgreSQLの「追記型のストレージアーキテクチャ」の基本的な構造です。

この状態で更新処理を行うと、更に新しいレコードが追加され、t_xminとt_xmaxを使ってチェーンのようにつながっていきます。
testdb=# UPDATE t1 SET uname = 'update 2' 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 |   8152 |        1 |     37 |   1859 |   1860
  2 |   8112 |        1 |     37 |   1860 |   1861
  3 |   8072 |        1 |     37 |   1861 |      0
(3 rows)

testdb=#

■不要領域の解放(VACUUM処理)


このように、更新処理を続けていると、PostgreSQLでは削除されたレコードの領域が増えていきます。この「削除されたレコードの領域」のことを俗に「不要領域」と呼んだりします。

この不要領域が増えてくると、「実際に生きているレコード数は少ないのにファイルサイズが大きい」という状況が発生し、パフォーマンスが低下する原因になります。

この不要領域を解放(回収)する仕組みが、PostgreSQLで有名な「VACUUM」です。

VACUUMの基本的なしくみは上記の通りなのですが、実際にブロック内部のレコードがどのように変化するのかを見てみます。

先ほどのテーブルt1に対してVACUUMを行ったの結果が以下のものです。
testdb=# VACUUM t1;
VACUUM
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 |      3 |        2 |      0 |        |
  2 |      0 |        0 |      0 |        |
  3 |   8152 |        1 |     37 |   1861 |      0
(3 rows)

testdb=#
先ほどまで存在していた古いレコード(lp==1とlp==2)のlp_lenがゼロになり、t_xmin/t_xmaxも削除されてlp_flagsが0に設定されています。これが「テーブルがVACUUMされた状態」になります。

なお、lp_flags==0の領域は「未使用領域」となっていて、すぐに再利用できる領域であることを意味しています。

この状態で、さらに更新処理(UPDATE)を行ってみます。
testdb=# UPDATE t1 SET uname = 'update 3' 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 |      3 |        2 |      0 |        |
  2 |   8112 |        1 |     37 |   1862 |      0
  3 |   8152 |        1 |     37 |   1861 |   1862
(3 rows)

testdb=#
すると今度は以前のレコードが使っていたlp==2の領域が使われました。

つまり、VACUUM処理で解放(回収)したことで領域が空き、新しいレコードがそこを利用できるようになった、ということです。

■まとめ


今回はPostgreSQLの「追記型のストレージアーキテクチャ」について、実際の動作を追いかけながら、その基本的な仕組みを解説しました。

VACUUM処理は、現在は自動VACUUMプロセスによって自動的に実施されるため、昔ほど気にする必要は無くなってきました。

とは言え、どのような仕組みで動いているのかを理解しておくことは、トラブルシューティングやパフォーマンスチューニングの際には重要になってきますので、PostgreSQLを使いこなしたいという方は、ぜひこの辺りを理解しておいていただければと思います。

明日は、このPostgreSQLの「追記型のストレージアーキテクチャ」の弱点を克服するべく実装されている工夫について紹介します。

では、また。

0 件のコメント:

コメントを投稿