rewriteheap.cpp(8) - ストレージエンジン

はじめに

前回のブログでは、**rewriteheap.cpp**の学習を開始しました。今回は引き続きrewriteheap.cppの学習とコード解説を進めます。ファイルパス: opengauss-server\\src\\gausskernel\\storage\\access\\heap\\rewriteheap.cpp

一、技術概要

Heap Only Tuple技術概要

Heap Only TupleはPostgreSQL 8.3バージョンで導入された技術であり、update操作の効率化を目的としています。PG9.2.4S上で構築されたOpenguassも同様にこの技術をサポートしています。

テーブルに複数のフィールドがあり、その中にインデックスを持つフィールドが存在する場合、フィールドの値を変更するとPostgreSQLのMVCCメカニズムにより新しいtupleが追加され、前のtupleはdeadとマークされます。このとき、各tupleの物理アドレスctidが変更されます。インデックスはctidアドレスに基づいてデータを検索するため、フィールドの変更はインデックスの変更も意味します。テーブルに多くのインデックスがある場合、すべてのインデックスを変更する必要があり、これにより書き込みが増幅する問題が発生します。hot技術を使用すると、すべてのインデックス属性が変更されておらず、新しいバージョンが元のバージョンと同じページ上に存在する場合、新しいインデックスレコードが生成されません。これらのレコードはhot(heap only tuple)と呼ばれます。以下では、従来の更新とhot更新をそれぞれ簡単に説明します。

従来の更新

tuple1をtuple3にupdateする場合、新しい行へのアクセスパスは以下の通りです。

hot更新

古い行へのポインタlinp1は、古い行のtuple1位置を指します。古い行のtuple1のt_cidは新しい行tuple4の位置を指しており、tuple1内にはこのチェーンが終了していないことを示すフラグ(infomask2)があります。新しい行tuple4内には、このチェーンの末端であることを示すフラグがあり、tupleデータが返されます。

しかし、古いtupleはdead tupleであり、vacuumによって回収されるため、このチェーンに問題が発生します。そのため、pruning剪枝操作が行われ、行ポインタのリダイレクトが実行されます。

Pruning後の更新

古いtupleのポインタが新tupleのポインタに再指向されます。tuple1をtuple4に更新する例で、新しい行へのアクセスパスは以下の通りです。

古い行へのポインタlinp1は、新しい行ポインタlinp4の位置を指します。新しい行ポインタlinp4は、新しい行tuple4の位置を指し、tupleデータが返されます。適切な时机で古いtuple1が回収されます。

二、用語解説

fillfactor

テーブルの填充因子(フィルファクター)は**10から100のパーセンテージ**です。100(完全に埋める)がデフォルト値です。より小さい填充因子を指定すると、INSERT操作はテーブルを指定されたパーセンテージのみでページ埋めします。各ページに残りのスペースを残し、そのページ上の行の更新に使用します。これにより、UPDATEが行の更新コピーを同じページ上に配置する機会が得られ、別のページに配置するよりも効率的になります。**更新されないエントリを持つテーブルでは、填充因子を100(完全に埋める)に設定するのが最適ですが、大量の更新があるテーブルでは、より小さい填充因子が適切です。このパラメータは列ストレージテーブルには意味がありません**。

テーブルのfillfactor値を変更することは、HOTの使用率に影響を与えます。HOTを使用しないupdateは通常2つのデータブロックを更新する必要がありますが、HOTを使用したupdateは1つのデータブロックのみを更新します。以下の条件が満たされた場合にHOT特性がトリガーされます:

  • updateがインデックスフィールドを変更しない
  • update後、新しいtupleが同じheapページ内にある

fillfactor値を下げることの影響:

  • HOTの使用率が向上する
  • ページ内の更新が容易になる
  • ストレージ使用効率が若干低下する可能性がある

三、コード解説

関数名:rewrite_write_one_page

この関数は主に**データベースシステムのデータページ書き換えプロセス**で使用され、データページが変更された後、変更されたデータページをディスクに書き戻す必要があります。このプロセスでは、暗号化、ログ記録などの特殊な操作が含まれる場合があります。

/*
* 関数名:rewrite_write_one_page
* 説明:この関数は主にデータベースシステムのデータページ書き換えプロセスで使用されます。
* データページが変更された後、変更されたデータページをディスクに書き戻す必要があります。
* このプロセスでは、暗号化、ログ記録などの特殊な操作が含まれる場合があります。
*/
static void rewrite_write_one_page(RewriteState state, Page page)
{
    TdeInfo tde_info = {0};
    if (RelationisEncryptEnable(state->rs_new_rel)) {
        GetTdeInfoFromRel(state->rs_new_rel, &tde_info);
    }
    /*
    * 新しいリレーション(state->rs_new_rel)で暗号化が有効な場合、
    * このリレーションからTDE情報を取得します。
    */
    if (IsSegmentFileNode(state->rs_new_rel->rd_node)) {
        //新しいリレーションがセグメントファイルノードかどうかをチェック
        Assert(state->rs_use_wal);
        Buffer buf = ReadBuffer(state->rs_new_rel, P_NEW);
        //新しいリレーションのバッファを読み取る
#ifdef USE_ASSERT_CHECKING
        BufferDesc *buf_desc = GetBufferDescriptor(buf - 1);
        Assert(buf_desc->tag.blockNum == state->rs_blockno);
#endif 
        //USE_ASSERT_CHEKINGが定義されている場合、rs_blocknoをチェック
        LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
        XLogRecPtr xlog_ptr = log_newpage(&state->rs_new_rel->rd_node, MAIN_FORKNUM, state->rs_blockno, page, true, 
            &tde_info);
        //このバッファをロック
        errno_t rc = memcpy_s(BufferGetBlock(buf), BLCKSZ, page, BLCKSZ);
        //ページデータをバッファブロックにコピー
        securec_check(rc, "\0", "\0");
        PageSetLSN(BufferGetPage(buf), xlog_ptr);//バッファブロックのLSNを設定
        MarkBufferDirty(buf);
        UnlockReleaseBuffer(buf);
    } else {
        /* 新しいファイルを拡張する際のテーブルスペースサイズ制限をチェック */
        STORAGE_SPACE_OPERATION(state->rs_new_rel, BLCKSZ);

        if (state->rs_use_wal) {
            log_newpage(&state->rs_new_rel->rd_node, MAIN_FORKNUM, state->rs_blockno, page, true, &tde_info);
        }
        // WALが使用されている場合、新しいページをログ記録
        RelationOpenSmgr(state->rs_new_rel);
        //新しいリレーションのストレージマネージャを開く
        char *bufToWrite = NULL;
        if (RelationisEncryptEnable(state->rs_new_rel)) {
            bufToWrite = PageDataEncryptIfNeed(page, &tde_info, true);
        } else {
            bufToWrite = page;
        }
        //新しいリレーションで暗号化が有効な場合、ページデータを暗号化する必要がある
        PageSetChecksumInplace((Page)bufToWrite, state->rs_blockno);
        //正しい位置にページチェックサムを設定
        rewrite_flush_page(state, (Page)bufToWrite);
        //ページ状態をフラッシュ
    }
}

ローカル変数の初期化

TdeInfo tde_info = {0};

その構造体定義は以下の通りです:

typedef struct {
    char dek_cipher[DEK_CIPHER_LEN];
    char cmk_id[CMK_ID_LEN];
    uint8 iv[RANDOM_IV_LEN];
    uint8 tag[GCM_TAG_LEN];
    uint8 algo;
    uint8 res[RES_LEN];
} TdeInfo;
フィールド 機能
dek_cipher 秘密鍵の保存
cmk_id CMK IDの保存
iv ランダム情報
tag 識別子
algo 暗号化アルゴリズム
res 戻り値の予約情報

WAL位置を記録するポインタ

XLogRecPtr xlog_ptr = log_newpage(&state->rs_new_rel->rd_node, MAIN_FORKNUM, state->rs_blockno, page, true, 
    &tde_info);

XLogRecPtrはPostgreSQLの型で、WAL(Write-Ahead Logging)内の位置ポインタを格納するために使用されます。書き込みが成功した後にログにコミットされ、データベースの復旧に使用されます。

暗号化処理

char *bufToWrite = NULL;
if (RelationisEncryptEnable(state->rs_new_rel)) {
    bufToWrite = PageDataEncryptIfNeed(page, &tde_info, true);
} else {
    bufToWrite = page;
}

この書き換えプロセスでは、書き込み内容の復号化と暗号化が考慮されています。**rs_new_rel**構造体には、暗号化が必要かどうかのフラグ情報が記録されています。

四、まとめ

今回のブログでは、**rewriteheap.cpp**の動作メカニズムについてより深く理解しました。hot技術は、頻繁なデータ挿入を行うデータベースのストレージ効率を大幅に向上させます。fillfactor填充因子は、hot技術の使用により細かい選択肢を提供します。

次回のブログでは、これまでの作業をレビューまとめ、より体系的に学習を展開していきます。これにより、ストレージエンジンの理解を深め、SQLエンジンとの連携を実現していきます。

タグ: OpenGauss ストレージエンジン Heap-Only-Tuple HOT fillfactor

5月17日 11:27 投稿