vacuum (7)

HeapTupleSatisfiesVacuum

Vacuumを実行するためにタプルの状態をチェックする。タプルが実行中の全てのトランザクションから参照できない場合、HEAPTUPLE_DEADを返す。HEAPTUPLE_DEADだったらVacuumが削除できる。
タプルがdeleteされているが、実行中のいくつかのトランザクションから参照可能な場合は、HEAPTUPLE_RECENTLY_DEADを返す。

    if (!TransactionIdPrecedes(HeapTupleHeaderGetXmax(tuple), OldestXmin))
    {
        /* deleting xact is too recent, tuple could still be visible */
        return HEAPTUPLE_RECENTLY_DEAD;
    }

scan_heap (続き)

HeapTupleSatisfiesVacuumがHEAPTUPLE_RECENTLY_DEADを返したとき、タプルがupdateで更新されたタプルだったら、vtlinksに登録する。(タプルがupdateされるとtuple.t_data->t_ctidにupdate後のタプルのTIDが設定されるので、自分のTID(tuple.t_self)と比較して違っていたらupdateされたタプルだと判定できる)
update後のタプルの位置を移動するとき、update前のタプルからのリンクも考慮する必要があるため、更新前後のTIDを記録している。

                    if (do_shrinking &&
                        !(ItemPointerEquals(&(tuple.t_self),
                                            &(tuple.t_data->t_ctid))))
                    {
                        if (free_vtlinks == 0)
                        {
                            free_vtlinks = 1000;
                            vtlinks = (VTupleLink) repalloc(vtlinks,
                                           (free_vtlinks + num_vtlinks) *
                                                 sizeof(VTupleLinkData));
                        }
                        vtlinks[num_vtlinks].new_tid = tuple.t_data->t_ctid;
                        vtlinks[num_vtlinks].this_tid = tuple.t_self;
                        free_vtlinks--;
                        num_vtlinks++;
                    }

vacuum (6)

VacPage

vacuumするページの情報を記録する。offsets_used以外の情報は、scan_heapで設定される。freeにはvacuumでdeadタプルを削除してページの断片化を解消した後の空き領域(pd_upperとpg_lowerの間のスペース)の大きさが設定される。offsets配列にはdeadタプルのOffsetNumberが登録される。offsets_freeにoffsets配列に登録されたdeadタプルの数が設定される。

typedef struct VacPageData
{
    BlockNumber blkno;          /* BlockNumber of this Page */
    Size        free;           /* FreeSpace on this Page */
    uint16      offsets_used;   /* Number of OffNums used by vacuum */
    uint16      offsets_free;   /* Number of OffNums free or to be free */
    OffsetNumber offsets[1];    /* Array of free OffNums */
} VacPageData;

typedef VacPageData *VacPage;

vacuum (5)

scan_heap

この関数はfull vacuumするときにfull_vacuum_relから実行される。onerelで指定されたテーブルの全てのページを調べ、以下の処理を行う。

  • タプルのcommit status(Xmin)を設定する
  • 削除できるタプルがあるページのリスト(vacuum_pages)を作成する
  • ページの断片化を解消して空き領域を増やせるページのリスト(fraged_pages)を作成する

empty_pagesには空のページ数が設定される。empty_end_pagesにはテーブルの末尾にある連続する空きページ数が設定される。
free_spaceにはテーブル全体で使用されていない領域のサイズが設定される。usable_free_spaceには断片化を解消した結果、使用可能になる領域のサイズが設定される。

(1)作業用のvacpageを作成する。削除するタプルを登録する配列が足りなくならないように、MaxOffsetNumberのタプルが登録できるようにメモリを確保している。vacuum_pagesやfraged_pagesに登録するときは、vacpageのコピーをcopy_vac_pageで作成するが、このときはvacpageに登録されているタプルが登録できるだけのメモリを確保する。(コピー後のオブジェクトの方が小さい)

(2)forループで全てのページを調べる

  • 未初期化のページが見付かったらPageInitでページを初期化して、vacuum_pages, fraged_pagesに登録する。未初期化のページはテーブルにページを追加した後、ページを初期化する前にバックエンドが異常終了したときなどに発生する可能性がある。
  • 空のページが見付かったらvacuum_pages, fraged_pagesに登録する。
  • 上記以外(タプルがあるページ)は、ページ内の全タプルについてHeapTupleStatisfiesVacuumでタプルの状態をチェックする。HEAPTUPLE_DEADだったらvacpageに登録する。また、ページのコピーを作成してコピーページのItemIdのLP_USEDフラグをおろす。(コピーページはページの断片化を解消したときの空き領域を計算するために使う。scan_pageではタプルの削除は実行せず、削除するタプルの情報を集める処理を行うため、ページのコピーを作成してから操作している)

ページに削除できるタプルがあったらvacuum_pagesに登録する。また、ページの断片化を解消して空き領域を増やせる場合はfraged_pagesに登録する。

(3)vacrelstatsに後で使う情報を設定する

vacuum (4)

vac_open_indexes

relに指定したテーブルのindexを全てopenする。テーブルと同じlock modeを使う。

vac_close_indexes

vac_open_indexesでopenしたindexをcloseする。

vpage_insert

vacpagelist配列にvacpageを追加する。vacpagelist配列が初期化されていないときは、PG_NPAGEDESC(=1024)の大きさの配列を作成して、vacpageを追加する。vacpagelist配列が一杯になっていたら、repallocで配列サイズを2倍にしてからvacpageを追加する。

copy_vac_page

vacpageのコピーを作成する。コピー元のvacpageはscan_heapやrepair_flagで作成され、vacpage->offsets配列にはMaxOffsetNumberの数のoffsetを登録できるようにメモリを確保している。コピーするときは配列に登録されているoffsetの数(vacpage->offsets_free)のメモリを確保してコピーするので、コピーしたvacpageのサイズはコピー元よりも小さくなることが多い。

vacuum_rel

relidで指定したテーブルをvacuumする。vacuumが成功したらtrueを返す。permissionなどの理由でvacuumできなかったらfalseを返す。falseが返されたときは、トランザクションIDの状態を変更できない(commit logをtruncateできない)

(1)StgartTransactionCommandでトランザクションを開始。StrategyHintVacuumをtrueにする。

(2)テーブルが削除されていたらtrueを返して終了。

(3)ロックモードを決める。full vacuumのときはAccessExclusiveLock、その他はShareUpdateExclusiveLock。

(4)テーブルをopenして、テーブルのpermissionをチェックする。
vacuumを実行したユーザーがsuper user, tableのowner, databaseのownerのいづれかであれば、vacuumできる。

(5)テーブルの種類をチェックする。この関数では、通常のテーブルかTOASTテーブルのみvacuumできる。

(6)テーブルが他のバックエンドが使用している一時テーブルの場合、vacuumを実行せずにtrueを返す。

(7)LockRelationForSessionでセッションロック(commitしても開放されないロック)を取得する。テーブルにTOASTテーブルがある場合、TOASTテーブルをvacuumするまで、ロックを保持するためにこのロックを取得している。

(8)full vacuumのときはfull_vacuum_rel、その他はlazy_vacuum_relを実行する

(9)テーブルをcloseしてcommitする。

(10)StrategyHintVacuumをfalseにする。

(11)TOASTテーブルがある場合、vacuum_relを再帰的に呼び出してTOASTテーブルをvacuumする。

(12)セッションロックを開放する。

strategy_hint_vacuum

vacuumの実行中にtrueに設定されるフラグ。このフラグはバッファマネージャのUnpinBufferの動作に影響を与える。UnpinBufferはバッファを参照する必要がなくなったときに実行され、バッファの参照カウンタを減らす。
vacuumが実行されていないときは、使用回数をインクリメントし、バッファが最近使用されたことを記録する。
vacuum実行中は参照カウンタ(refcount)と使用回数(usage_count)が0で、trashOKにtrueが指定された場合、vacuum以外のトランザクションではこのバッファはすぐに必要にならないと判定し、バッファをfreelistに登録して別のページのために使用できるようにする。

       if (!strategy_hint_vacuum)
       {
           if (buf->usage_count < BM_MAX_USAGE_COUNT)
               buf->usage_count++;
       }
       else if (trashOK && 
                buf->refcount == 0 &&
                buf->usage_count == 0)
           trash_buffer = true;

vacuum (3)

vacuum_delay_point

vacuumを実行するプロセスよりも通常のトランザクションを実行するプロセスを優先させるために、sleepを実行してvacuumプロセスを遅延させる。この仕組みはpostgresql.confでvacuum_cost_delayを1以上に設定したときに有効になる。この関数はvacuum中に1ページ処理するごとなど、頻繁に実行され、sleepするかどうか判定している。

sleepする条件は以下のとおり。
(1)VacuumCostActiveがtrue
VacuumCostActiveはvacuumの開始時に設定される。VacuumCostDelay(postgresql.confのvacuum_cost_delay)が1以上のときtrueになる。

        VacuumCostActive = (VacuumCostDelay > 0);

(2)InterruptPendingがfalse
InterruptPendingは、プロセスの中断要求があったときにtrueになる。中断要求があった場合は中断させるのが優先なので、sleepしない。

(3)VacuumCostBalanceがVacuumCostLimitに達した時
VacuumCostBalanceはvacuumの開始時に0に設定され、以下の処理が実行されたときに加算される。

  • ReadBufferでキャッシュヒットしたとき、vacuum_cost_page_hitが加算される
  • ReadBufferでキャッシュミスしたとき、vacuum_cost_page_missが加算される
  • WriteBufferでページをdirtyにしたとき、vacuum_cost_page_dirtyが加算される

(これらのパラメータについては、PostgreSQLのマニュアルに詳しい記述がある)

sleepする時間は以下の式で計算される。最大値はVacuumCostDelay * 4に制限している。

        msec = VacuumCostDelay * VacuumCostBalance / VacuumCostLimit;
        if (msec > VacuumCostDelay * 4)
            msec = VacuumCostDelay * 4;

この仕組みを有効にした場合、vacuumプロセスの優先度を下げることにより、他のトランザクションを実行するプロセスの性能が悪くならない効果があると考えられる。しかし、full vacuumのときでもvacuumが遅延するため注意が必要。full vacuumはテーブルをAccessExclusiveLockするのでテーブルの検索もブロックされてしまう。full vacuum中のテーブルを使用するトランザクションはvacuumが終るまで待たされることになるので、vacuum_cost_delayを大きくしすぎると逆効果になるかもしれない。

vacuum (2)

lazy_vacuum_heap

vacrelstats->dead_tupleに登録されたタプルを、テーブルから削除する。
(1)LockBufferForCleanupでページをロックする
(2)lazy_vacuum_pageでページ内のアイテムを削除する
(3)lazy_record_free_spaceでページの空き領域を登録
(4)ロックを開放
(5)WriteBufferでページをdirtyにする

lazy_vacuum_page

指定されたページのタプルを削除する。
(1)vacrelstats->dead_tupleに登録されたタプルのItemIdのLP_USEDフラグをおろす。
(2)PageRepairFragmentationでページの断片化を修正する
(3)一時テーブルではない場合、log_heap_cleanでWALレコードを出力する

lazy_record_free_space

vacrelstats->free_pages配列にページの空き容量の情報を登録する。
free_pages配列が一杯になった場合、配列から一番容量の少ないページの情報を追い出して登録していく。(heap-based priority queuesというアルゴリズムで一番容量の少ないページが配列の先頭にくるように調整している)

PageRepairFragmentation

ページの断片化を修正する。
(1)ItemId配列を順にチェックして、使用中のアイテムの数(nused)を数える。また、削除するアイテムはunused配列にアイテムの番号を登録する。
(2)使用中のアイテムがない場合(nused == 0)の場合、ページをemptyにする。(ItemIdのlp_lenを0にして、pageのpd_upperを初期化する)
(3)使用中のアイテムがある場合、使用中のアイテムデータのoffsetをitemidbase配列に登録し、offset順にソートする。次にoffset順にデータを並べ変える。

log_heap_clean

ページから複数のタプルを削除したときのWALレコードを出力する。PageRepairFragmentationが作成したunused配列をログとして出力する。複数のタプルを削除した情報を1つのログレコードで出力することにより、ログの容量を少なく抑えている。
以下のように配列をそのままログに出力している。

    if (uncnt > 0)
    {
        rdata[1].data = (char *) unused;
        rdata[1].len = uncnt * sizeof(OffsetNumber);
    }

_bt_check_uniqueのバグ修正

_bt_check_uniqueでbufferをdirtyにするコードは以下で正解だった。

    if (nbuf == InvalidBuffer)
        SetBufferCommitInfoNeedsSave(buf);
    else
        SetBufferCommitInfoNeedsSave(nbuf);

http://archives.postgresql.org/pgsql-patches/2005-10/msg00088.php
http://archives.postgresql.org/pgsql-committers/2005-10/msg00177.php