-
2023-11-20 PostgreSQL
PostgreSQL の共有バッファのピンについて
共有バッファ
PostgreSQL では、テーブルのデータなどを共有バッファというメモリ領域にキャッシュさせて処理を行います。
共有バッファは名前のとおり、全てのプロセスで共有して扱うものとなっております。
そのため、複数のプロセスで同時に共有バッファを操作するための機能が必要となってくるのですが、これはピンとバッファロックと呼ばれる仕組みで実現しているそうです。
今回は、その共有バッファのピンについて確認した事項を記載します。
共有バッファのピン
共有バッファのピンという概念に関して、公式ドキュメント内を検索しても説明しているページは見つけられませんでした。
その代わり、ソースコード内の README ファイルに、共有バッファのピンに関する説明がありました。
README ファイルの場所は "src/backend/storage/buffer/README" になります。
ピンについて、ごく簡単にまとめると
- 共有バッファのページを参照する前にピンを保持(参照カウントをインクリメント)し、操作が完了したらピンを解放(参照カウントをデクリメント)する。
- 逆にいうと、ピンを保持していないページは参照できない。
- どのプロセスからもピンが保持されていないページは、共有バッファから追い出される(再利用される)可能性がある
ということのようです。
また、ピンを保持するのは ReadBuffer 関数で、解放するのは ReleaseBuffer 関数であるとも説明がありました。
具体的にソースコードを確認しますと PrivateRefCountEntry 構造体の refcount という変数が共有バッファのピンに当たるようです。
ピンを保持する ReadBuffer 関数
ReadBuffer 関数の実装は "src/backend/storage/buffer/bufmgr.c" にありましたので、下記に抜粋します。
Buffer
ReadBuffer(Relation reln, BlockNumber blockNum)
{
return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL);
}
上記の通り、ReadBuffer 関数は ReadBufferExtended 関数の実行結果を返しているだけですので、ピンをに関する情報は ReadBufferExtended 関数の内容を見る必要があります。
ReadBufferExtended 関数は ReadBuffer_common 関数を実行しており、その ReadBuffer_common 関数の中で更に BufferAlloc 関数を実行し、BufferAlloc 関数で PinBuffer 関数を呼び出し、その PinBuffer 関数の最後の方で PrivateRefCountEntry 構造体の refCount 変数をインクリメントしている記述をようやく見つけることができました。
ref->refcount++;
Assert(ref->refcount > 0);
ResourceOwnerRememberBuffer(CurrentResourceOwner, b);
return result;
}
ピンを解放する ReleaseBuffer 関数
ReleaseBuffer 関数の実装も "src/backend/storage/buffer/bufmgr.c" にありましたので、下記に抜粋します。
void
ReleaseBuffer(Buffer buffer)
{
if (!BufferIsValid(buffer))
elog(ERROR, "bad buffer ID: %d", buffer);
if (BufferIsLocal(buffer))
{
ResourceOwnerForgetBuffer(CurrentResourceOwner, buffer);
Assert(LocalRefCount[-buffer - 1] > 0);
LocalRefCount[-buffer - 1]--;
return;
}
UnpinBuffer(GetBufferDescriptor(buffer - 1), true);
}
最終行の UnpinBuffer 関数の中で、refcount をデクリメントしています。
下記に記述を抜粋します。
Assert(ref->refcount > 0);
ref->refcount--;
if (ref->refcount == 0)
{
確認の確認
README の説明通り ReadBuffer 関数で refcount がインクリメントされ、ReleaseBuffer 関数 で refcount がデクリメントされていることを確認できました。
念の為、その他の記述で refcount がどう扱われているかを確認して、本当に refcount が共有バッファのピンかどうかを調べました。
その結果、bufmgr.c の IncrBufferRefCount 関数のコメントに共有バッファのピンについて、言及がありましたので下記に抜粋します。
/*
* IncrBufferRefCount
* Increment the pin count on a buffer that we have *already* pinned
* at least once.
*
* This function cannot be used on a buffer we do not have pinned,
* because it doesn't change the shared buffer state.
*/
void
IncrBufferRefCount(Buffer buffer)
{
Assert(BufferIsPinned(buffer));
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
if (BufferIsLocal(buffer))
LocalRefCount[-buffer - 1]++;
else
{
PrivateRefCountEntry *ref;
ref = GetPrivateRefCountEntry(buffer, true);
Assert(ref != NULL);
ref->refcount++;
}
ResourceOwnerRememberBuffer(CurrentResourceOwner, buffer);
}
- 「ピンをインクリメントする」という旨のコメントが記述されています。
- バッファページがローカルではない場合に refcount をインクリメントしています。
という 2 点が「共有バッファのピン = refcount」である裏付けになりそうです。
また、バッファページがローカルな場合、refcount ではなく LocalRefCount がインクリメントされています。
(横道)LocalRefCount について
BufferIsPinned というマクロに LocalRefCount に関する情報がありましたので、こちらも下記に抜粋します。
/*
* BufferIsPinned
* True iff the buffer is pinned (also checks for valid buffer number).
*
* NOTE: what we check here is that *this* backend holds a pin on
* the buffer. We do not care whether some other backend does.
*/
#define BufferIsPinned(bufnum) \
( \
!BufferIsValid(bufnum) ? \
false \
: \
BufferIsLocal(bufnum) ? \
(LocalRefCount[-(bufnum) - 1] > 0) \
: \
(GetPrivateRefCount(bufnum) > 0) \
)
コメントを読む限り、LocalRefCount は文字通り自プロセスだけの参照カウントのようです。
まとめ
今回は共有バッファのピンについて調べた結果を記載いたしました。
共有バッファのピンについて調べていく中で、少し上で触れましたローカルなバッファピンや、ページの使用状況をカウントしているような変数を見つけたりもしました。
PostgreSQL は C 言語で書かれているので、正直なところ読むのが大変ですが、ソースコードには有益な情報が詰まっていますので、少しずつですが読み進めて、その結果をまたブログ記事にしたいと考えています。