PostgreSQL のメモリ管理関数の解説

作成日:2017.03.26
修正日:2017.04.02

この記事は PostgreSQL 9.5 に基づいて記述している。

このページでは PostgreSQL のエクステンション(extension)を開発する人向けに、プログラム中で用いるメモリ管理関数を紹介する。

PostgreSQL の他の記事へのインデックスはここ


更新履歴
(2017.03.26) 作成。
(2017.04.02) 共有メモリの利用方法を追加。


目次

1. はじめに

PostgreSQL の C 言語で記述するユーザー定義関数やエクステンションでのメモリ管理手法を解説する。

まず PostgreSQL はマルチプロセス構成のシステムであり、1つのインスタンスに対して複数のプロセスが作成される。 このプロセス群は DBMS デーモン 通称 postmasterを起動プロセスとし、posmaster プロセスの子プロセスとして作成される。 原則として孫プロセスは作成しない。

fig-1: 共有メモリとメモリアドレス
共有メモリとメモリアドレス
(バックエンド・プロセスはコネクション毎に作成されるセッション毎のSQL処理を担当するプロセス)

PostgreSQL 内部で利用可能な表1にまとめる。

表1: PostgreSQL のメモリの種類
種類共有・私用アドレスタイミングプログラマバブル説明・用途
共有メモリ(Shared Memory) 共有固定起動時Yes インスタンス内の複数のプロセスやロックの管理のために利用される。
ShmemInitStruct()RequestAddinShmemSpace() を使うことでエクステンションからも利用可能。
共有メモリバッファ(Shared Buffer) 共有浮動起動時No いわゆる DB バッファ。 リレーションを読み書きする場合に自動的に利用されるが、明示的にメモリとして利用することは非常に困難。 詳細はPostgreSQL のテーブルとブロックのデータ構造を参照のこと。
動的共有メモリ(Dynamic Shared Memory) 共有浮動動的Yes dsm_create()dsm_attach() を用いることで動的に確保できる共有メモリ。 通常はバックグラウンド・ワーカー(Background Worker)と一緒に用いる。
メモリ・コンテキスト(Memory Context) 私用 動的Yes トランザクション、SQL 文字列のパース、プラン実行など様々な処理に利用するメモリ空間。 基本的にこのカテゴリーのメモリを最初に使う。
エクステンション開発でも多用する。
malloc() 私用 動的Yes 標準関数の malloc() を直接使うことができる。 エクステンションでも Memory Context API では確保できない一部のケースで使用する。
その他    Yes その他、PostgreSQL の構造を理解すれば mmap() などを OS のメモリ管理機構を使うことも可能。 ただし PostgreSQL 側の機構とのすり合わせが必要。

PostgreSQL の基本的な思想として共有メモリで渡すものは最小限に留めているように見える。 そのためマルチスレッド構成の RDBMS であればメモリ渡しであろう情報がファイル経由の受け渡しになっている。 例えばテーブル(リレーション)のデータサイズは、プラン最適化のためにかかせない情報だが、リレーションのファイル群 に OS 的にアクセスしてファイルサイズを取得するという効率の悪いやり方をしている。 やれやれ。

2. MemoryContext を使ったメモリ・アロケーター

ユーザー定義関数やエクステンションを C 言語で記述する場合、専用のメモリアロケーターを使う。 このメモリアロケーターは malloc(size_t size) の替わりにpalloc(Size size) を、free() の替わりに pfree() を呼び出す。

PostgreSQL メモリアロケーターの特徴は以下の通り。

メモリ・コンテキスト(Memory Context)
PostgreSQL の専用メモリアロケーターはリージョンベースメモリ管理(region-based memory management)を行う。 メモリ・コンテキスト(Memory Context) と呼ばれる領域を作成し、palloc() によるオブジェクトはメモリ・コンテキストと関連付けられる。 オブジェクトの解放は pfree() でも可能だが、メモリ・コンテキストに紐付けられたオブジェクトを一斉解放(MemoryContextReset()することもできる。
メモリ・コンテキストはプログラム中で作成・削除できる。 メモリ・コンテキストが削除(MemoryContextDelete())される場合、メモリ・コンテキストに紐付けられたオブジェクトも一斉解放される。
メモリ・コンテキストは親子関係を持ち、親のメモリ・コンテキストが削除される時には自動的に子のメモリ・コンテキストも削除される。 これを使って大規模なメモリ回収が可能となる。
メモリ確保に失敗した場合はエラー送出
メモリ・コンテキストは作成時に最大メモリサイズを設定し、palloc() もメモリ・コンテキスト毎に使用メモリサイズがカウントされる。 そして palloc() が最大メモリサイズを越えた場合には、PostgreSQL のエラーが送出される。 このエラーは C++ の例外のように try-catch 構造で捕捉することができる。

2.1 Memory Context API

2.1.1 AllocSetContextCreate

PostgreSQL メモリ・アロケーターを使うにはメモリ・コンテキストを作成するのだが、実は MemoryContext は C++ や Java 言語で言うところの抽象クラスであり、実装クラスとして AllocSet が定義されている。 MemoryContext の作成だけは AllocSetContextCreate() を使う。 AllocSetContextCreate() は utils/memutils.h に定義されている。

MemoryContext AllocSetContextCreate(MemoryContext parent, const char *name,
                                    Size minContextSize, Size initBlockSize, Size maxBlockSize);

MemoryContext は MemoryContextData 型のポインターである。 ただし MemoryContextData 型のメンバー変数に直接触る必要はないので、ハンドルのようにして扱うことができる。

initBlockSizemaxBlockSize は「ブロック」と言っているが、これは 8 KiB の DB ページ・ブロックではなく、メモリ・アロケーター内部のメモリ割り当て単位となる「ブロック」の指す。

主に以下のように使用する。

#include "postgres.h"
#include "utils/memutils.h"

MemoryContext mcontext;

mcontext = AllocSetContextCreate(CurrentMemoryContext, "my memory context",
                                 ALLOCSET_DEFAULT_MINSIZE,
                                 ALLOCSET_DEFAULT_INITSIZE,
                                 ALLOCSET_DEFAULT_MAXSIZE);

いくつか注意事項を述べる。

2.1.2 MemoryContext からのメモリ割り当て・解放

MemoryContext の中にメモリを割り当て・解放する関数は utils/palloc.h に定義されている。 ただし utils/palloc.h はpostgres.h または postgres_fe.h からインクルードされるので、自分でインクルードする必要はない。

以下の表はメモリ・コンテキストを指定してメモリを割り当てる関数の抜粋である。

API戻り値の型説明
MemoryContextAlloc(MemoryContext context, Size size) void * contextで指定したメモリ・コンテキストから、size で指定したバイト数のメモリを割り当てる。 割り当てられたメモリは未初期化状態となる。
MemoryContextAllocZero(MemoryContext context, Size size) void * contextで指定したメモリ・コンテキストから、size で指定したバイト数のメモリを割り当てる。 割り当てられたメモリはゼロ初期化されている。

ただし普段は MemoryContextAlloc() は使わずに、palloc()pfree() を使う。 palloc() らは CurrentMemoryContext というグローバル変数に設定されたメモリ・コンテキストを暗黙のうちに参照している。

API戻り値の型説明
palloc(Size size) void * size で指定したバイト数のメモリを割り当てる。 割り当てられたメモリは未初期化状態となる。
palloc0(Size size) void * size で指定したバイト数のメモリを割り当てる。 割り当てられたメモリはゼロ初期化されている。
pfree(void *pointer) void PostgreSQL のメモリ・アロケーターで割り当てられたメモリを解放する。 pfree()CurrentMemoryContext を参照しない。 pointer はそれを割り当てたメモリ・コンテキストに回収される。
repalloc(void * pointer, Size size) void * PostgreSQL のメモリ・アロケーターで割り当てられたメモリに対して、標準関数の realloc() のようにサイズを size に変更する。 変更後のメモリが元のメモリサイズよりも大きくなった場合、その部分は未初期化状態となる。 残念ながら repalloc0() に相当する関数は用意されていない。
repalloc()CurrentMemoryContext を参照せず、pointer を割り当てたメモリ・コンテキストに回収され、そのメモリ・コンテキストから割り当てられる。

CurrentMemoryContext グローバル変数は MemoryContextSwitchTo() を使って切り替える。 典型的なメモリ割り当ては以下のようになる。

MemoryContext oldcontext;
void *obj;

oldcontext = MemoryContextSwitchTo(newcontext);

obj = palloc(size)

pfree(obj);

MemoryContextSwitchTo(oldcontext);

いくつか注意を述べる。

palloc()CurrentMemoryContext グローバル変数を参照する前提として、PostgreSQL がシングルスレッドで動作するマルチプロセス構成だという点がある。 CurrentMemoryContext グローバル変数が、他のスレッドが勝手に書き換えることはないのでこのようなことが可能になっている。

2.1.3 MemoryContext の解放・削除

以下の表はメモリ・コンテキストに紐付けられたオブジェクトの一斉解放と、メモリ・コンテキスト自身の削除する関数の抜粋である。

API戻り値の型説明
MemoryContextReset(MemoryContext context) void context で指定されたメモリ・コンテキストに紐付けられたオブジェクトも一斉に解放する。 もし子メモリ・コンテキストがあれば、それらも再帰的に MemoryContextDelete() を適用する。
context は以降も利用することが可能だが、子メモリ・コンテキストは使えなくなる。
MemoryContextDelete(MemoryContext context) void context で指定されたメモリ・コンテキストに紐付けられたオブジェクトも一斉解放した後、context 自身を削除する。 同時にメモリ・コンテキストに付属するデータも全て解放される。 もし子メモリ・コンテキストがあれば、それらも再帰的に MemoryContextDelete() を適用する。
context は以降利用できない。
MemoryContextResetChildren(MemoryContext context) void context に子メモリ・コンテキストがあれば、再帰的に MemoryContextReset() を適用する。
context 自身は影響を受けない。
MemoryContextDeleteChildren(MemoryContext context) void context に子メモリ・コンテキストがあれば、再帰的に MemoryContextDelete() を適用する。
context と子メモリ・コンテキスト間の親子関係はなくなる。

基本的にメモリ・コンテキストは AllocSetContextCreate() で作成し MemoryContextDelete() で削除するという非対称性があることに注意せよ。

2.2 Out-of-memory エラーの捕捉

MemoryContextAlloc()palloc() が指定したサイズのメモリを確保できなかった場合、PostgreSQL システムの「エラー」が送出される。 これは C++ の例外のように PG_TRY()/PG_CATCH()/PG_END_TRY() で囲った構文でエラーは捕捉できる。

Catch 節に当たる部分では、geterrcode()ERRCODE_OUT_OF_MEMORY を返せば、メモリ割り付けに失敗したことが判定できる。

void * volatile p1 = NULL;
void * volatile p2 = NULL;

PG_TRY();
{
    p1 = palloc(1024 * 1024);
    p2 = palloc(1024 * 1024);
}
PG_CATCH();
{
    if (geterrcode() == ERRCODE_OUT_OF_MEMORY)
    {
        /*
         * メモリ割り当て時にエラーが出た場合、ここに到着する。
         * 後始末処理をここに書く。
         */
        if (p1)
           pfree(p1);

        /*
         * エラーを再送しない場合、最新のエラーをフラッシュする。
         * (フラッシュしない場合、エラー情報を保持するスタックが枯渇する) 
         */
        FlushErrorState();
    }
    else
    {
        /*
         * エラーを再送する。
         * もしこのコードの外側にも PG_TRY()〜PG_CATCH() があれば次のネストの PG_CATCH() へ処理が移る。
         */
        PG_RE_THROW();
    }
}
PG_END_TRY();

if (p2)
    pfree(p2);

if (p1)
    pfree(p1);

C++ と異なり、PostgreSQL の PG_TRY()/PG_CATCH()/PG_END_TRY()setjump()/longjmp() で実装されている。 そのためコンパイラの最適化によって try 節でエラーが発生した場合、それまで単なるローカル変数に書き込んだ内容は、catch 節では書き込んだ値が消えてしまうことがある。 これを防ぐには volatile 修飾が必要となる。

この際に volatile 修飾子を付ける位置は重要である。 p1 のように volatile を付けても解決しない。p2 のようにする必要がある。

volatile void *p1;
void * volatile p2;

p1 の場合には *p1 = value のような p1 の指している領域へのアクセスが volatile になり必ず実効メモリへの読み書きになることが保証される。 ただし p1 = value のような p1 自身のアクセスは volatile ではないのでレジスタ経由のアクセスになるかもしれず setjmp() で書き込んだ内容が消えるかもしれないためである。

2.3 既定のメモリ・コンテキスト

PostgreSQL は幾つかのメモリ・コンテキストが用意されており、AllocSetContextCreate()parent (2.1.1節)を使って子メモリ・コンテキストを作成したり、MemoryContextAlloc() で直接オブジェクトを割り当てることができる。 ただしこれらのメモリ・コンテキストはプロセス毎に独立しており、同じメモリ・コンテキストであってもデータを共有することはできない。

名前 内容
TopMemoryContext 各プロセスごと起点となるメモリ・コンテキストで、他のメモリ・コンテキストは TopMemoryContext の子孫となる。 TopMemoryContext は削除されることもリセットされることもないので、TopMemoryContext で割り当てたオブジェクトはプロセスが終了するまで永続する。 そのため TopMemoryContext から割り当てたオブジェクトは、注意深く削除する必要がある。

ユーザーは原則として本当に必要な場合以外は TopMemoryContext からオブジェクトを割り当てるのは避けるべき。 また CurrentMemoryContextTopMemoryContext に設定するのも避けるべき。
PostmasterContext PostgreSQL の DBMS デーモンプロセス(一般的に postmaster プロセス)が通常使っているメモリ・コンテキスト。 セッション(コネクション)毎に作成するバックエンドプロセスでは、PostmasterContext は削除する。 バックエンドプロセスは postmaster プロセスを fork して作成するが、PostmasterContext を削除することによって postmaster 固有のオブジェクトを消去している。
ユーザーは shared_preload_library を指定して読み込んだライブラリの初期化処理などでのみ利用できる。
CacheMemoryContext relcache や catcache などのを保持するメモリ・コンテキスト CacheMemoryContext は削除されることもリセットされることもない。

ユーザーが直接利用することがないメモリ・コンテキストである。
MessageContext 各コマンド実行の度にフロントエンドの処理使われるメモリ・コンテキスト。 元となる SQL コマンド列、パース・ツリー、プラン・ツリーなどが、このメモリ・コンテキストから確保され、コマンド終了後にリセットと子メモリ・コンテキストの削除が行われる。
通常のクエリー実行ではクエリー実行と MessageContext はほぼ同じ寿命を持つが、 Prepared statment を作成した場合、フロントエンド処理が MessageContext で行われるが、完成したプランツリーは prepared statment のプライベートのメモリ・コンテキストにコピーされ、MessageContext はリセットされること。

ユーザーが planner_hookExecutorStart_hook/ExecutorRun_hook/ExecutorFinish_hook/ExecutorEnd_hook などを使う場合、MessageContext から子メモリ・コンテキストを作って行うのがよい。
TopTransactionContext トップレベル・トランザクションと同一のサイクルを持つメモリ・コンテキスト。 TopTransactionContext はトランザクションの終わり(COMMIT/ROLLBACK)でリセットと子メモリ・コンテキストの削除する。 エラーが発生した場合、すぐに TopTransactionContext がリセットされるかどうかは不定である。 トップレベル・トランザクションは、サブトランザクションではない通常のトランザクションのことである。

ユーザーは TopTransactionContext よりも CurTransactionContext を使ったほうがよい。
CurTransactionContext 現在のトランザクションまたはサブトランザクションに所属するメモリ・コンテキスト。
サブトランザクションが生成されるとトップレベル・トランザクションの子メモリ・コンテキストが作成される。 サブトランザクションがネストした場合も、多段のメモリ・コンテキストができる。 CurTransactionContext はその最下位のトランザクション・サブトランザクションのメモリ・コンテキストを指す。
よってサブトランザクションが終了した場合は、CurTransactionContext が指すメモリ・コンテキストが削除され、その上のトランザクション・サブトランザクションのメモリ・コンテキストに CurTransactionContext に置き換わる。
ただし CurTransactionContext がサブトランザクションであった場合、サブトランザクション内でエラーが発生した場合にはサブトランザクションに属するメモリ・コンテキストの内容は放置され、TopTransactionContext がリセットされる時に回収される。

ユーザーは普通に使ってよい。
PortalContext 現在のアクティブ・エグゼキューション・ポータル(Active Execution Portal)を指すグローバル変数。
ErrorContext エラーのリカバリー処理を行うために設けられた専用メモリ・コンテキストである。 エラーのリカバリー処理が終わるとリセットされる。
エラー処理の専用メモリ・コンテキストがあるのは、CurrentMemoryContext が容量オーバーで out-of-memory エラーを発生させた場合、リカバリー処理にも CurrentMemoryContext を使ってしまうとリカバリー処理のためにメモリの割り当てができなくなってしまうからである。

ユーザーが直接利用することがないメモリ・コンテキストである。

ポータル(Portal)は 1つの SQL クエリーと同じサイクルを持つメモリ・コンテキストである。 単純な SELECT- クエリーの場合はクエリー=コマンド=ポータルになるが、カーソルを用いた場合は DECLARE 〜 CLOSE までがポータルの寿命となる。 またカーソルは同時に複数生成できるので、ポータルが重なることもある。 Active execution portal は存在するポータルの中で現在の処理を実行することになっているポータルにあたる。

DECLARE liahona1 CURSOR FOR SELECT * FROM tbl1;
DECLARE liahona2 CURSOR FOR SELECT * FROM tbl2;

FETCH 5 FROM liahona1;
FETCH 5 FROM liahona2;

CLOSE liahona2;
CLOSE liahona1;

3. 共有メモリの利用方法

ここでは(動的でない)共有メモリの利用方法を説明する。 共有メモリを使うには PostgreSQL インスタンスの起動シーケンスを知る必要がある。 またサンプルコードを https://github.com/nminoru/misc/tree/master/postgresql/shm_test に配置しておく。

まず第一にエクステンションで共有メモリを使うには、postmaster 内で必要サイズ分のメモリの確保を行う必要がある。 このために C 言語のモジュールを postmaster 内で起動する必要がある。 通常のユーザー定義関数(UDF)を C 言語で書いた場合、その UDF が呼び出される時に初めてロードされる。 この処理はバックエンドプロセスなので、共有メモリにマップするのは手遅れである。 そこで postgresql.conf の shared_preload_libraries の中に起動時にロードするモジュール名を指定する。 例えばモジュール名が shm_test.so なら shared_preload_libraries = 'shm_test' と入力する。

postgresql.conf:
shared_preload_libraries = 'shm_test'

shm_test というモジュール内のコードは以下のように行う。 PostgreSQL のモジュールは起動時に _PG_init() という固定の名前の関数を実行する。 ただしこの段階では共有メモリ関数を確保することはできず、自分が必要なメモリサイズを RequestAddinShmemSpace() で予約する。 また shmem_startup_hook 変数に共有メモリを確保後に行うフック関数を登録しておく。

static shmem_startup_hook_type shmem_startup_prev = NULL;
static void shmem_test_setup(void);

void
_PG_init(void)
{
    /* shared_preload_libraries 以外からロードされた場合には何もしない */
    if (!process_shared_preload_libraries_in_progress)
        return;

    /* 必要な共用メモリのサイズを予約 */
    RequestAddinShmemSpace(requested-shared-memory-size);
    
    /* 共有メモリの確保後に行う処理をフックに登録 */
    shmem_startup_prev = shmem_startup_hook;
    shmem_startup_hook = shmem_test_setup;
}

複数のプリロードライブラリが RequestAddinShmemSpace() を呼び出すので、その合算分の共有メモリが PostgreSQL 内部に確保される。 その後、shmem_startup_hook 変数でフックされた関数が一つづつ実行される。 この中で ShmemInitStruct() を呼び出して、共有メモリから事前に RequestAddinShmemSpace() で予約したメモリを切り分けてもらう。 この際に ShmemInitStruct() の第1引数として共有メモリの名前を付ける必要がある。 この名前はインスタンス内で衝突しない名前を付ける必要がある。

static void * volatile shared_memory_p;

static void
shmem_test_setup(void)
{
    void *address;
    bool found;

    /* 他のプリロードライブラリのフック関数を呼び出す */
    if (shmem_startup_prev)
        shmem_startup_prev();

    address = ShmemInitStruct("shared test memory", requested-shared-memory-size, &found);

    Assert(!found);

    shared_memory_p = address;
}
fig-2: 共有メモリの確保の仕方
共有メモリの確保の仕方

fig-2 の処理を行うと共有メモリは postmaster プロセス内では shared_memory_p を介してアクセスすることができるようになる。 ただし postmaster 以外のプロセス、例えばバックエンドプロセスは shared_memory_p が共有メモリを指しているとは限らない。 これは Unix 系 OS では postmaster 以外のプロセスは postmaster を fork() して作るのだが、Windows では CreateProcess API で子プロセスを作成するため shared_memory_p の内容が引き継がれないためである。

そこで子プロセス側でも ShmemInitStruct() をもう一度呼び出す。 第1引数の名前とサイズが一致すれば、すでに切り出された共有メモリのアドレスが返ってくるので、これを利用すればよい。

static void * volatile shared_memory_p;

PG_FUNCTION_INFO_V1(show_shm_test);
Datum
show_shm_test(PG_FUNCTION_ARGS)
{
    int32 result;

    if (!shared_memory_p)
    {
        void *address;
        bool found;

        address = ShmemInitStruct("shared test memory", requested-shared-memory-size, &found);
        shared_memory_p = address;
    }

    /* ここで shared_memory_p を介して共有メモリにアクセスする */

    PG_RETURN_INT32(result);
}

コメント

コメントを書き込む

TOP    掲示板    戻る
Written by NAKAMURA Minoru, Email: nminoru atmark nminoru dot jp, Twitter:@nminoru_jp