やあ、元気にしてるかい?
突然だけど、PostgreSQLのパフォーマンスチューニングって聞くと、真っ先に「`shared_buffers`を増やすべし!」って思い浮かべる人、結構いるんじゃないかな。もちろん、それは間違いじゃない。共有バッファはPostgreSQLの「心臓部」と言っても過言じゃないくらい、重要なメモリ領域だ。
でもさ、ただバッファを大きくすればいいってもんじゃないんだよね。その広大なメモリ空間に、ディスクから読み込んだページ(データブロックのことね)がわんさか格納されるわけだけど、じゃあ、「今、俺が欲しいページは、共有バッファのどこにあるんだ?」って、PostgreSQLはどうやって瞬時に見つけ出してると思う?
もし、その探し方が非効率だったら、せっかくメモリ上にデータがあっても、それを探すのに時間がかかって、結局ディスクに読み込みに行くのと大差ない、なんてことになりかねない。
そこで今日、君に紹介したいのが、PostgreSQLの高速アクセスを支える、「バッファマッピングテーブル」という、ちょっと地味だけどめちゃくちゃ重要な仕組みなんだ。ぶっちゃけ、PostgreSQLがPostgreSQLたる所以の一つと言ってもいいかもしれないね。
—
PostgreSQLの「脳みそ」?バッファマッピングテーブルって何者だ?
本棚の索引みたいなものだよ
いきなり専門用語から入ると頭が痛くなるから、まずは例え話からいこうか。
君が図書館で特定の「本」を探しているとしよう。その本がもう貸し出されていて、今書架にないかもしれないし、誰かが読んでいて閲覧室にあるかもしれない。でも、もしも誰かが読んでいて、それが返却されたばかりで、図書館の「新着本コーナー」に置いてあるとしたら?
君は、まず「新着本コーナー」を片っ端から見て回る?それとも、図書館の「検索端末」で本の場所を調べる?当然、検索端末を使うよね。
この「検索端末」こそが、PostgreSQLにおける「バッファマッピングテーブル」のイメージに近いんだ。
PostgreSQLにとっての「本」が「データページ(ブロック)」で、図書館の「書架」が「ディスク」だとしたら、「新着本コーナー」や「閲覧室」が「共有バッファ(メモリ)」に当たる。
バッファマッピングテーブルの役割は、「ディスク上の特定の場所にあるデータページが、今、共有バッファのどのスロット(場所)にいるのか?」を高速に教えてくれる「索引」みたいなものなんだ。
もう少し具体的に見てみよう
PostgreSQLの内部では、このバッファマッピングテーブルは「ハッシュテーブル」として実装されている。なぜハッシュテーブルか?そりゃあ、高速な検索にはハッシュが一番得意だからだよ。O(1)で検索できる、ってやつだね。
このハッシュテーブルは、こんな情報をキーと値として持っているんだ。
- キー:
- `Relation ID (relfilenode)`: どのテーブル(やインデックス)のファイルかを示すID。
- `Block Number`: そのファイル内の何番目のブロックかを示す番号。
- (これらを合わせて`BufferTag`と呼ぶこともあるよ。)
- 値:
- `共有バッファのスロット番号`: 共有バッファ内のどこにそのデータページが格納されているかを示す番号。
つまり、PostgreSQLが「おい、`pg_class`テーブルのブロック番号100番が欲しいんだけど!」ってなったら、まずバッファマッピングテーブルに「`pg_class`の100番ブロックって、共有バッファにいる?」って問い合わせるわけだ。
もしテーブルが「いるよ!スロット番号23番にいるぜ!」って教えてくれたら、PostgreSQLはすぐに23番スロットにアクセスして、そのデータページを使うことができる。これでディスクI/Oをせずに済む、ってわけだ。
どうしてそんなものが「必要」なの?〜高速ページ検索の実現〜
なかったら地獄絵図だよ
もし、このバッファマッピングテーブルがなかったら、どうなると思う?
PostgreSQLは、データページが必要になるたびに、共有バッファの最初から最後まで、格納されている全ページをスキャンして「あ、これだ!」って探す羽目になる。想像してみてよ、何千、何万、場合によっては何十万ものページが格納されている共有バッファを、毎回毎回端から端まで探し回るんだよ?
それはもう、I/O地獄ならぬ「メモリ検索地獄」だ。ディスクI/Oを減らすために共有バッファを使っているのに、メモリ検索に膨大な時間がかかってしまっては本末転倒だよね。
バッファマネージャとの連携プレイ
バッファマッピングテーブルは、バッファマネージャ(共有バッファを管理するPostgreSQLの内部コンポーネント)と密接に連携している。データページが必要になった時の一般的な流れはこうだ。
1. ページ要求: 何らかの処理(SELECT、UPDATEなど)が、特定のテーブルの特定のブロックを要求する。
2. バッファマッピングテーブル検索: バッファマネージャは、まずバッファマッピングテーブルに、要求されたページが共有バッファ内に存在するかどうかを問い合わせる。
3. ヒットした場合(キャッシュヒット):
- テーブルが「いるよ!」と教えてくれたら、そのスロット番号を使って、共有バッファから直接ページを読み込む。
- これでディスクI/Oは発生しない。高速!
4. ミスした場合(キャッシュミス):
- テーブルが「いないね…」と答えたら、ディスクから該当するページを読み込む。
- ディスクから読み込んだページは、共有バッファ内の空いているスロット(またはLRUなどのアルゴリズムで選択された追い出し対象のスロット)に格納される。
- そして、そのページの`BufferTag`と格納されたスロット番号の組み合わせが、バッファマッピングテーブルに新しく追加される。
この一連の流れを、ハッシュテーブルが高速に処理してくれるおかげで、PostgreSQLは高いパフォーマンスを維持できるんだ。まさに縁の下の力持ちだよね。
ちょっぴりコードを覗いてみよう
「で、実際、PostgreSQLのコードだとどうなってるの?」って興味を持った君は、なかなか筋がいいね!全部を読み解くのは大変だけど、雰囲気だけでも掴んでみよう。
PostgreSQLのソースコードで、この辺りの核心を担っているのは `src/backend/storage/buffer/buf_mgr.c` あたりだ。
バッファマッピングテーブルの各エントリは、`BufferDescriptor`という構造体で表現されていることが多い。そして、その`BufferDescriptor`を管理するためのハッシュテーブルが、内部的に使われているんだ。
ちょっとだけ、関連する部分の雰囲気を抜粋するね(実際のコードはもっと複雑だけど、イメージとして)。
// BufferTagの定義(キーとなる情報)
// これがRelation IDとBlock Numberを内部的に持つ
typedef struct BufferTag
{
RelFileNode rnode; // どのファイルか(relfilenode)
ForkNumber forkNum; // どのフォークか(main, fsm, vmなど)
BlockNumber blockNum; // ブロック番号
} BufferTag;
// BufferDescriptorの定義(バッファのメタデータ)
// 共有バッファ内の特定のスロットに関する情報
typedef struct BufferDesc
{
BufferTag tag; // このバッファが持っているページの情報
int buf_id; // 共有バッファ内のスロット番号(0からshared_buffers-1)
// …他にも多くの状態管理情報(ピンカウント、ダーティフラグ、ロックなど)
// volatile uint32 buf_state;
// int refcount;
// LWLock content_lock;
// …
} BufferDesc;
// バッファマッピングテーブル(ハッシュテーブル)の初期化や検索関数
// BufferTagを元に、共有バッファ内のスロットを探す
extern BufferDesc Get”);
// BufferTagの定義(キーとなる情報)
// これがRelation IDとBlock Numberを内部的に持つ
typedef struct BufferTag
{
RelFileNode rnode; // どのファイルか(relfilenode)
ForkNumber forkNum; // どのフォークか(main, fsm, vmなど)
BlockNumber blockNum; // ブロック番号
} BufferTag;
// BufferDescriptorの定義(バッファのメタデータ)
// 共有バッファ内の特定のスロットに関する情報
typedef struct BufferDesc
{
BufferTag tag; // このバッファが持っているページの情報
int buf_id; // 共有バッファ内のスロット番号(0からshared_buffers-1)
// …他にも多くの状態管理情報(ピンカウント、ダーティフラグ、ロックなど)
// volatile uint32 buf_state;
// int refcount;
// LWLock content_lock;
// …
} BufferDesc;
// バッファマッピングテーブル(ハッシュテーブル)の初期化や検索関数
// BufferTagを元に、共有バッファ内のスロットを探す
extern BufferDesc Get BufferDescriptor (BufferTag tag, bool foundPtr);
`BufferTag`が、まさに「どのファイルのどのブロックか」を特定するためのキーになっているのがわかるよね。そして`BufferDesc`が、そのページが共有バッファのどのスロット(`buf_id`)に入っているか、そしてそのページの現在の状態(ロックされているか、ダーティか、参照されている回数など)を管理しているんだ。
`GetBufferDescriptor`関数などが、このハッシュテーブルを使って、高速に目的の`BufferDesc`を探し出す役割を担っているわけだ。もちろん、複数のプロセスから同時にアクセスされる可能性があるから、ハッシュテーブルへのアクセスには適切なロック(`BufferMappingLock`という専用の軽量ロックがあるよ)がかけられているんだ。
現場で役立つ実践的な視点
さて、ここまで内部の仕組みを見てきたけど、「で、これが俺たちの仕事にどう役立つの?」って思うよね。もちろん、現場で役立つ知識として活用できるんだ。
1. `shared_buffers`のサイジングの理解を深める
「`shared_buffers`はメモリの25%くらいがいい」なんて言われることがあるけど、その根拠の一つに、このバッファマッピングテーブルの存在がある。
- `shared_buffers`が小さすぎると:
- 共有バッファに格納できるページ数が少ない。
- バッファマッピングテーブルのヒット率が下がり、キャッシュミスが増える。
- 結果として、ディスクI/Oが増加し、パフォーマンスが低下する。
- `shared_buffers`が大きすぎると:
- バッファマッピングテーブル自体が大きくなり、その管理(検索、更新、ロック)にかかるオーバーヘッドが増える。
- OSのページキャッシュとの競合が発生しやすくなり、二重キャッシュによる効率低下やメモリ不足を招く可能性がある。
つまり、バッファマッピングテーブルが効率的に機能し、かつシステム全体のリソースとバランスが取れるような、適切な`shared_buffers`のサイジングが重要なんだ。この仕組みを理解していれば、ただ「大きくすれば速くなる」という単純な発想から一歩踏み込めるはずだよ。
2. キャッシュヒット率のモニタリング
バッファマッピングテーブルの効率は、直接的に共有バッファのキャッシュヒット率に現れる。PostgreSQLは、このキャッシュヒット率を監視するための便利なビューを提供してくれている。
SELECT
datname,
blks_read, — ディスクから読み込まれたブロック数
blks_hit, — 共有バッファから読み込まれたブロック数 (キャッシュヒット)
(blks_hit 100) / (blks_read + blks_hit) AS cache_hit_ratio — キャッシュヒット率
FROM
pg_stat_database
WHERE
datname = ‘your_database_name’;
このクエリで得られる`cache_hit_ratio`が低い場合(一般的には90%以下なら検討の余地ありと言われることが多いかな)、それはバッファマッピングテーブルが「いないね…」と答える回数が多い、つまりキャッシュミスが多いことを意味している。
その原因は、`shared_buffers`が不足しているのかもしれないし、あるいはクエリのI/Oパターンが非効率で、何度も同じページをディスクから読み込んでいるのかもしれない。この指標をトリガーに、次のアクション(`shared_buffers`の見直し、クエリチューニング、インデックスの見直しなど)を検討できるわけだ。
3. I/Oパターンの理解を深める
データアクセスが「シーケンシャルスキャン」なのか「インデックススキャン」なのか、これによってバッファマッピングテーブルの使われ方も少し変わってくる。
- シーケンシャルスキャン: 基本的にデータファイルを頭から順に読み込んでいく。同じページが頻繁にリクエストされることは少ないため、キャッシュヒット率は低めになる傾向がある。ただし、テーブル全体が小さければ、全部共有バッファに乗って、結果的に高速になることもある。
- インデックススキャン: 特定の行にアクセスするためにインデックスを経由する。インデックスページや、そこから参照されるテーブルページは、特定のデータ範囲にアクセスするたびに何度も参照される可能性があるため、キャッシュヒットしやすい。
自分のアプリケーションがどのようなI/Oパターンを持っているのかを理解することは、バッファマッピングテーブルを介したキャッシュの挙動を予測し、より効果的なチューニング戦略を立てる上で非常に役立つよ。
—
まとめ
どうだったかな?今日はPostgreSQLの「バッファマッピングテーブル」という、普段あまり意識されないかもしれないけれど、PostgreSQLの高速性を根底から支える重要な仕組みについて話してみた。
- バッファマッピングテーブルは、ディスク上のブロックと共有バッファ内のスロットを対応付けるハッシュテーブルだ。
- これにより、欲しいデータページが共有バッファにあるかどうかを瞬時に判断し、ディスクI/Oを回避している。
- その存在を理解することは、`shared_buffers`の適切なサイジングや、キャッシュヒット率のモニタリング、さらにはアプリケーションのI/Oパターンの理解を深め、実務でのパフォーマンスチューニングに直結するんだ。
PostgreSQLは、本当に奥が深いデータベースだ。ただ外側から使うだけでなく、こうして内部の仕組みを少しずつ紐解いていくと、その賢さや工夫に感動するし、それがまた、より良いシステムを設計・運用するためのヒントになるんだ。
今日話したことが、君のPostgreSQLとの付き合い方を少しでも豊かなものにしてくれたら嬉しいな。じゃ、またね!
コメント