PostgreSQL のバックグラウンドワーカー(Background Worker)の使い方

作成日:2017.04.02
修正日:2017.04.05

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

このページでは PostgreSQL のエクステンション(extension)を開発する人向けに、バックグラウンドワーカー(Background Worker)の使い方を解説する。

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


更新履歴
(2017.04.02) 作成。
(2017.04.05) 図を追加。


目次

1. はじめに

PostgreSQL はインスタンスをマルチプロセス構成で管理しており、個々のプロセスはシングルスレッド動作している。

PostgreSQL が自分のエクステンションの中で並列処理(parallel processing)または並行処理(concurrent processing)を行いたい場合、別のプロセスを立てて実行を行うことになる。 このために PostgreSQL が提供している仕組みが バックグラウンドワーカー(Background Worker)である。

PostgreSQL をマルチスレッド動作させることは非常に難しい。 いろいろな箇所でフラグ変数をグローバル変数として公開し、これをフックしたり一時的にトリガーにしている箇所があるため、非同期シグナルの類が動作しない。 またメモリ管理関数もマルチスレッドに対応していない。

バックグラウンドワーカーには 2 つの種類が存在する。

1つ目は無印のバックグラウンドワーカーである。 PostgreSQL のインスタンスを起動時に作成され、インスタンスが存在する間に常駐する。 このバックグラウンドワーカーは postmaster からのみ「登録」ができる。 無印のバックグラウンドワーカーは PostgreSQL 9.3 から利用できる。

2つ目は動的バックグラウンドワーカー(Dynamic Background Worker)である。 動的バックグラウンドワーカーは postmaster 以外から「生成」することができる。 これはユーザー定義関数(User Defined Function; UDF)の中で動的バックグラウンドワーカーを生成し、処理を行ってから、動的バックグラウンドワーカーを終了させることも可能である。 動的バックグラウンドワーカーは PostgreSQL 9.4 から利用できる。

説明が混乱するので、この文書では無印のバックグラウンドワーカーの方を静的バックグラウンドワーカーと呼ぶことにする。 一般的に使われる言葉ではない。

2. 静的バックグラウンドワーカー

2.1 静的バックグラウンドワーカーの登録

静的バックグラウンドワーカーは、postmaster プロセス内で RegisterBackgroundWorker() を使うことで「登録」ができる。 RegisterBackgroundWorker() は以下の形式をとる。

void RegisterBackgroundWorker(BackgroundWorker *worker);

第1引数には登録するバックグラウンドワーカーの情報を BackgroundWorker 構造体で渡す。 BackgroundWorker 構造体のメンバー変数は 表1 のように指定する。 ただし指定していないメンバー変数はゼロパディングしておくこと。

RegisterBackgroundWorker() の引数に渡す BackgroundWorker 構造体はローカル変数でもよい。 その内容はバックグラウンドワーカーのプロセスにコピーされ、グローバル変数の MyBgworkerEntry グローバル変数から参照できるようになる。

RegisterBackgroundWorker() は引数が void 型だが、「登録」に失敗することはある(例えば GUC パラメータの max_worker_process が不足した場合に)。 この場合はエラーが発生する。

RegisterBackgroundWorker() で「登録」したバックグラウンドワーカーは BackgroundWorker 構造体の bgw_start_time の指定に従い起動する。 起動したバックグラウンドワーカーは RegisterBackgroundWorker() とは無関係で動作を開始する。

ユーザーが RegisterBackgroundWorker() を使って静的バックグラウンドワーカーを登録するには、postgresql.conf の shared_preload_libraries の中に起動時にロードするモジュール名を指定し、その _PG_init() 中で呼び出す。 例えばモジュール名が shm_test.so なら shared_preload_libraries = 'shm_test' と入力する。

postgresql.conf:
shared_preload_libraries = 'shm_test'
void
_PG_init(void)
{
    BackgroundWorker worker;

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

    /* 予めゼロパディングする */    
    memset(&worker, 0, sizeof(worker));

    /* worker に値を設定する */

    RegisterBackgroundWorker(&worker);    
}

2.2 BackgroundWorker 構造体

BackgroundWorker 構造体は以下のようになる。

表1: BackgroundWorker 構造体
メンバー名データ型説明
bgw_name char [BGW_MAXLEN(64)] バックグラウンドワーカーの名前。 これはデバッグ情報などにのみ利用されるので適当につけてよい。
bgw_flags int バックグラウンドワーカーに与える機能を以下2つのマクロの論理和で与える。
  • BGWORKER_SHMEM_ACCESS(0x0001)。 バックグラウンドワーカーに共有メモリを割り当てる。 このマクロを指定しないとバックグラウンドワーカー側で共有メモリが使えない。
  • BGWORKER_BACKEND_DATABASE_CONNECTION(0x0002)。 バックグラウンドワーカーからデータベースに接続する。 このマクロは BGWORKER_SHMEM_ACCESS(0x0001) と同時に使う必要がある。
bgw_start_time BgWorkerStartTime バックエンドプロセスが起動するタイミングを BgWorkerStartTime 列挙子のうち1つで指定する。 Postmaster はその起動シーケンスとして、以下のようなフェーズを持っている。
  1. INIT。Postmaster の起動開始。
  2. STARTUP。スタートアッププロセスの終了。
  3. RECOVERY。クラッシュリカバリー開始以降。
  4. HOT STANDBY。Hot standby モードの準備を開始以降。
  5. RUN。通常の運用状態。
bgw_start_time には、postmaster の起動シーケンスのフェーズをあわせて指定する。
  • BgWorkerStart_PostmasterStart。 Postmaster が INIT に遷移にした時点でバックグラウンドワーカーを起動する。 bgw_flagsBGWORKER_BACKEND_DATABASE_CONNECTION を指定した場合は利用できない(指定するとエラーとなる)。
  • BgWorkerStart_ConsistentState。 Postmaster がクラッシュリカバリーを完了し、ホットスタンバイモードの準備を開始したらバックグラウンドワーカーを起動する。
  • BgWorkerStart_RecoveryFinished。 ホットスタンバイモードの同期が終わり通常のコネクションを受け入れる状態になったらバックグラウンドワーカーを起動する。
通常は BgWorkerStart_RecoveryFinished を指定すればよい。
bgw_restart_time int バックグラウンドワーカーがエラーで終了した後に、再起動するまでに待機時間を秒数を指定する。 bgw_restart_timeBGW_NEVER_RESTART(0) を指定した場合、エラー終了した後にバックグラウンドワーカーが再起動しなくなる。
bgw_main bgworker_main_type バックグラウンドワーカーの処理の実体となるコールバック関数。
typedef void (*bgworker_main_type) (Datum main_arg);
bgw_library_name char [BGW_MAXLEN(64)] バックグラウンドワーカーの処理の実体となるコールバック関数の共有メモリライブラリを指定する。 bgw_main が NULL の場合のみ利用される。
bgw_function_name char [BGW_MAXLEN(64)] バックグラウンドワーカーの処理の実体となるコールバック関数の関数名を指定する。 bgw_main が NULL の場合のみ利用される。
bgw_main_arg Datum バックグラウンドワーカーのコールバック関数の第1引数に渡す。
この引数にポインタを渡したい場合、PostgreSQL のメモリ管理関数の解説3章で述べたような共有メモリから渡す必要がある。
bgw_extra char [BGW_EXTRALEN(128)] バックグラウンドワーカー側に渡す余分な情報を渡す。
バックグラウンドワーカー・プロセス側からは MyBgworkerEntry->bgw_extra でアクセスできる。
bgw_notify_pid pid_t 静的バックグラウンドワーカーでは 0 を指定する必要がある。 0 以外を指定すると RegisterBackgroundWorker() はエラーとなる。
これは動的バックグラウンドワーカーで利用する。

コールバック関数は bgw_mainbgw_library_name & bgw_function_name のどちらか一方を指定する。

2.3 コールバック関数

静的バックグラウンドワーカーのコールバック関数は、典型的に以下のように記述する。

void
foo_main(Datum main_arg)
{
    pqsignal(SIGHUP,  sighup_handler);
    pqsignal(SIGTERM, sigterm_handler);
    pqsignal(SIGQUIT, sigterm_handler);
    pqsignal(SIGINT,  sigterm_handler);

    BackgroundWorkerUnblockSignals();

    /*
     * SIGUSR1 シグナルの標準ハンドラである procsignal_sigusr1_handler() は
     * set_latch_on_sigusr1 が true の場合 SIGUSR1 を捕捉すると SetLatch() を呼ぶ。 
     */
    set_latch_on_sigusr1 = true;

    /* データベースへ接続する */
#if 1
    BackgroundWorkerInitializeConnection("dbname", "username");
#else
    BackgroundWorkerInitializeConnectionByOid(dboid, useroid);
#endif

    while (!gotSigterm)
    {
        int rc;

        /*
         * 非同期割り込みをチェック
         * 場合によってはここでエラーが発生しプロセスは終了する。
         */
        CHECK_FOR_INTERRUPTS();

        /* 状態が変化するまで待機 */
        rc = WaitLatch(&MyProc->procLatch,
                       WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
                       100 /* msec */);
        ResetLatch(&MyProc->procLatch);

        /* postmaster が終了した場合は終了する */
        if (rc & WL_POSTMASTER_DEATH)
            proc_exit(1);

        if (gotSigterm)
            goto done;

        /* SIGHUP が来た場合 postgresql.conf をリロードする */
        if (gotSighup)
        {
            gotSighup = false;
            ProcessConfigFile(PGC_SIGHUP);
        }

        /*
         * ここに処理コードを書く。
         * 必要なら処理の切れ目で CHECK_FOR_INTERRUPTS() を呼ぶ。
         */
    }

done:
    /* code=1 を指定するのはリスタート */
    proc_exit(1);
}

static void
sigterm_handler(SIGNAL_ARGS)
{
    int save_errno = errno;

    gotSigterm = true;

    if (MyProc)
        SetLatch(&MyProc->procLatch);

    errno = save_errno;
}

static void
sighup_handler(SIGNAL_ARGS)
{
    int save_errno = errno;

    gotSighup = true;

    if (MyProc)
        SetLatch(&MyProc->procLatch);

    errno = save_errno;
}

3. 動的バックグラウンドワーカー

3.1 動的バックグラウンドワーカーの登録

動的バックグラウンドワーカーは RegisterDynamicBackgroundWorker() を使うことで「登録」ができる。 RegisterDynamicBackgroundWorker() は以下の形式をとる。 静的バックグラウンドワーカーを登録する RegisterBackgroundWorker() は postmaster プロセス内でしか呼びなかったが、RegisterDynamicBackgroundWorker() は逆に postmaster プロセスでは呼び出せない。 バックエンドプロセス、静的バックグラウンドワーカー、他の動的バックグラウンドワーカーから呼び出すのは OK である。

bool RegisterDynamicBackgroundWorker(BackgroundWorker *worker, BackgroundWorkerHandle **handle);

3.2 動的バックグラウンドワーカーの起動の確認

RegisterDynamicBackgroundWorker() で登録された動的バックグラウンドワーカーは postmaster によってプロセスとして生成される。 その進行状態を RegisterDynamicBackgroundWorker() を呼び出したプロセス側で確認する方法が提供されている。

GetBackgroundWorkerPid()RegisterDynamicBackgroundWorker() で渡されたハンドルを使って、動的バックグラウンドワーカーの状態を返す。 この関数は即時状態を返却する。

BgwHandleStatus GetBackgroundWorkerPid(BackgroundWorkerHandle *handle, pid_t *pid);

WaitForBackgroundWorkerStartup()RegisterDynamicBackgroundWorker() で渡されたハンドルを使って、動的バックグラウンドワーカーが 起動済み状態(BGWH_STARTED)になるか、停止状態(BGWH_STOPPEDBGWH_POSTMASTER_DIED)になるまで待機する。 BGWH_STARTED が返る場合は、pid に動的バックグラウンドワーカーの PID も設定される。

BgwHandleStatus WaitForBackgroundWorkerStartup(BackgroundWorkerHandle *handle, pid_t *pid);

3.3 動的バックグラウンドワーカーの終了

TerminateBackgroundWorker() に動的バックグラウンドワーカーのハンドラを指定することで外部から停止を指示する。 ただしこの関数を呼び出しても SIGUSR1 シグナルが送られるだけで、停止を保証しない。 この関数自体は即時復帰する。

void TerminateBackgroundWorker(BackgroundWorkerHandle *handle);

WaitForBackgroundWorkerStartup() のように動的バックグラウンドワーカーの終了を待機する WaitForBackgroundWorkerShutdown() も存在する。

BgwHandleStatus WaitForBackgroundWorkerShutdown(BackgroundWorkerHandle *handle);

3.4 bgw_notify_pid

PostgreSQL はバックグラウンドワーカー・プロセスもバックエンドプロセス・プロセスも postmaster プロセスの子プロセスである。 しかし通常、RegisterDynamicBackgroundWorker() を呼んだプロセスと動的バックグラウンドワーカーの間には、プロセスの親子関係とは別に、特別な呼び出し関係が期待される。 これを bgw_notify_pid メンバー変数を使うことで指定する。

現在のプロセスの PID は MyProcPid で取得できるので、以下のように起動する。

BackgroundWorker worker;
BackgroundWorkerHandle *handle;

/* ... */
worker.bgw_notify_pid = MyProcPid;

if (!RegisterDynamicBackgroundWorker(&worker, &handle))
    ereport(PANIC,
            (errcode(ERRCODE_INSUFFICIENT_RESOURCES),
             errmsg("could not register dynamic background process"),
             errhint("You may need to increase max_worker_processes.")));

以降、bgw_notify_pid が指すプロセスを呼び出し元プロセスと呼ぶことにする。

以降、動的バックグラウンドワーカーは内部に持っているステータス(3.2 節)の変化が起きると、bgw_notify_pid のプロセスに SIGUSR1 シグナルを送信する。 この機構を使って呼び出し元プロセスや動的バックグラウンドワーカーの終了を捕捉する。

図1: bgw_notify_pid と SIGUSR1 シグナルの動作
bgw_notify_pid と SIGUSR1 シグナルの動作

動的バックグラウンドワーカーが終了

動的バックグラウンドワーカーが終了した場合、呼び出し元プロセスに SIGUSR1 が送信される。 SIGUSR1 到着後に自分が作成した動的バックグラウンドワーカーを GetBackgroundWorkerPid() でチェックし、回収処置をとることができる。

呼び出し元プロセスが終了した場合

呼び出し元プロセスが終了した場合でも、動的バックグラウンドワーカーはすぐには終了しない。 ただし呼び出し元プロセスの終了を(親プロセスである) postmaster プロセスが検知すると、動的バックグラウンドワーカーの MyBgworkerEntry->bgw_notify_pid は 0 に上書きされる。

そのため動的バックグラウンドワーカー内で自身の MyBgworkerEntry->bgw_notify_pid を時々確認することで、呼び出し元プロセスの生存または終了を検知できる。

3.5 コールバック関数

動的バックグラウンドワーカーのコールバック関数は、典型的に以下のように記述する。

void
foo_main(Datum main_arg)
{
    pqsignal(SIGTERM, sigterm_handler);
    pqsignal(SIGQUIT, sigterm_handler);
    pqsignal(SIGINT,  sigterm_handler);

    BackgroundWorkerUnblockSignals();

    /*
     * 動的バックグラウンドワーカーが走行中に別プロセスが異常終了した場合、postmaster は
     * 子プロセスを全て殺した上で動的バックグラウンドワーカーのプロセスを再作成してしまう。
     *
     * この時、呼び出し元のプロセスがバックエンドプロセスなら、そちらは再作成されない。
     * おかしな状況なので動的バックグラウンドワーカーを終了する。
     */
    if (MyBgworkerEntry->bgw_notify_pid == 0)
        proc_exit(1);

    /*
     * SIGUSR1 シグナルの標準ハンドラである procsignal_sigusr1_handler() は
     * set_latch_on_sigusr1 が true の場合 SIGUSR1 を捕捉すると SetLatch() を呼ぶ。 
     */
    set_latch_on_sigusr1 = true;

    /* データベースへ接続する */
#if 1
    BackgroundWorkerInitializeConnection("dbname", "username");
#else
    BackgroundWorkerInitializeConnectionByOid(dboid, useroid);
#endif

    while (!gotSigterm)
    {
        int rc;

        /*
         * 非同期割り込みをチェック
         * 場合によってはここでエラーが発生しプロセスは終了する。
         */
        CHECK_FOR_INTERRUPTS();

        /* 状態が変化するまで待機 */
        rc = WaitLatch(&MyProc->procLatch,
                       WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
                       100 /* msec */);
        ResetLatch(&MyProc->procLatch);

        /* postmaster が終了した場合は終了する */
        if (rc & WL_POSTMASTER_DEATH)
            proc_exit(1);

        if (gotSigterm)
            goto done;

        /*
         * SIGHUP シグナルによる postgresql.conf の読み直し対応は不要。
         * 動的バックグラウンドワーカーを再作成した方がよい。
         */ 

        if (MyBgworkerEntry->bgw_notify_pid != 0)
            goto done;

        /*
         * ここに処理コードを書く。
         * 必要なら処理の切れ目で CHECK_FOR_INTERRUPTS() を呼ぶ。
         */
    }

done:
    /* 動的バックグラウンドワーカーは終了 */
    proc_exit(0);
}

static void
sigterm_handler(SIGNAL_ARGS)
{
    int save_errno = errno;

    gotSigterm = true;

    if (MyProc)
        SetLatch(&MyProc->procLatch);

    errno = save_errno;
}

4. 注意事項

4.1 BackgroundWorkerHandle の再利用

postmaster の内部に max_worker_processes 分のバックグランドワーカーのスロットがある。 バックグラウンドワーカーは静的なものも動的なものもこのスロットを消費する。 RegisterDynamicBackgroundWorker() が返す BackgroundWorkerHandle はこのスロットを確保してから動的バックグラウンドワーカーを起動する。

一方、RegisterDynamicBackgroundWorker() によって起動したバックグラウンドワーカーは、RegisterDynamicBackgroundWorker() を呼び出したプロセスとは連動せずに終了しスロットを解放することがある。 この場合、その後に RegisterDynamicBackgroundWorker()max_worker_processes 回呼び出されると、解放したスロットが再利用される。

呼び出し元プロセス #1 が RegisterDynamicBackgroundWorker()GetBackgroundWorkerPid() の間に、動的バックグラウンドワーカーが終了した場合 BGWH_STOPPED が返るが、そのうち第3者となる別のプロセス #2 から RegisterDynamicBackgroundWorker() の呼び出しによって最初のプロセスが参照している(と思っている)スロットを再利用することが考えられる。 この場合、GetBackgroundWorkerPid() はどのような値を返すのか?

図2: BackgroundWorkerHandle の再利用
BackgroundWorkerHandle の再利用

結論から言うとプロセス #1 とプロセス #2 で GetBackgroundWorkerPid() が別の結果を返す。 これは BackgroundWorkerSlot の中の generation によって実現している。 スロットが再利用される度に generation がカウントアップして行く。 BackgroundWorkerHandle の中にも generation あり、一致しない場合はスロットが再利用され世代が進んでいると判断されて BGWH_STOPPED を返す。

struct BackgroundWorkerHandle
{
    int         slot;
    uint64      generation;
};

generation は 64 ビットなので、カウンター自身が一周することは現実的にないので一周した場合はケアしない。

4.2 その他

コメント

コメントを書き込む

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