📖

内部構造から学ぶPostgreSQL Part1

に公開

https://gihyo.jp/book/2022/978-4-297-13206-4

本記事ではこの本の内容を追って、自分の理解をまとめていきます。メモ書き程度に残すのですが、間違っているところもあると思うので、気になった方は調べてみてください

PostgreSQL"超"入門

PostgreSQL(以下postgre)はRDB(Relational Database)とODB(Object Database)の双方の機能を兼ね備えたOSSのORDBMS(Object-Relational Dababase Management System)である
RDBの機能としてはMySQLやOracle DatabaseなどのRDBと遜色ないレベルで実装されており、ODBとしてさらにユーザー定義による拡張が可能となっている。この拡張性がpostgreの大きな特徴である

postgreは以下のようなバージョン番号体系を取っており、postgre9.6以前とpostgre10以降でバージョン体系番号が異なっている。

  • 9.6以前
    最初の二つの数字がメジャーバージョン番号と呼び、3つ目の数字がマイナーバージョン番号と呼ぶ

    9.3.1
    --- -
    

    年1のメジャーバージョンアップがなされ、マイナーバージョンはメジャーバージョンのたびに0に戻り、マイナーバージョンアップの度にカウントアップされる。マイナーバージョンアップでは新機能の追加などはなく、バグの修正やセキュリティの改善などを行っている。なお、異なるマイナーバージョン間ではデータベースの構成ファイルに互換性があるので、極力最新のものを使う方が良い

  • 10以降
    10以降は一つのメジャーバージョン番号と一つのマイナーバージョン番号になった。位置付けとしては9.6以前と変わらない

    10.3
    -- -
    

postgreのライセンスはBSDライセンスと類似したPostgreSQLライセンスという名称で、使用、変更、配布を個人利用、商用利用、学術利用など、目的を限定せずに無償で使用できる。また、GPL(General Public Licence)のようなソース公開義務がなく、商用システムでも利用しやすい

postgreはコミュニティ活動が活発で、日本のpostgreコミュニティのJPUGは海外でも高く評価されている。
開発コミュニティではメジャーバージョンのサポート期間は5年と定められており、サポート期間が終了したバージョンを使用している場合、バグ修正やセキュリティホールの修正が行われないため、サポート中のバージョンに移行することが進められている。

鉄則

  • postgresqlはエンタープライズ領域でも使用できるDBである
  • マイナーバージョンは極力アップデートして最新化する

アーキテクチャの基本

アーキテクチャ構成

image.png

RDBMSはクエリの処理だけでなく、バッファの管理、ストレージへの書き込み制御、統計情報の収集など様々な制御を行っている。それを複数プロセスを立ち上げることで可能にしている。各プロセスはps aux | grep postgresなどで検索できる

postgreはマルチプロセスタイプのアーキテクチャであり、よく比べられるMySQLはマルチスレッドタイプのアーキテクチャである。MySQLではコネクションが確立された際にスレッドを生成するが、postgreではプロセスを生成する。
これは一見非効率のように感じるかもしれない。実際プロセスとスレッドを比べるとスレッドの方が軽量である。しかし、DBの性能はそれだけで決まるのではなく、ロックの待ち時間などに比べたらそれらは誤差と言っても過言ではない。

元々PostgreSQLはIngresというDBから派生したもので、その設計思想を引き継いでいる。当時のUnix系OSではスレッドよりもプロセスの方が安定しており、マルチプロセスアーキテクチャが普通であった。そのためPostgreSQLもマルチプロセス設計となった。

メリット

  • プロセスにすることで各コネクションが独立して動作するため、一つのコネクションがクラッシュしても他のコネクションに影響を及ぼさない
  • 各プロセスが独立したメモリ空間を持つため、メモリーリークやバッファオーバーフローなどのメモリ破壊による影響を局所化しやすい
  • マルチプロセスなので、OSのスケジューラーが各プロセスをマルチコアCPUに分散させることで並列に処理することができる(マルチスレッドでもできるが、スレッド間のロックやスケジューリング管理が難しくなる)
  • スレッド間の競合(グローバル変数へのアクセスなど)を最小限に抑えることができる(MySQLのInnoDBでは高速なスレッドモデルを採用しているが、その分管理のロジックなどが複雑になっている)

デメリット

  • コネクション数が増えるとプロセス管理のオーバーヘッドが増える
  • プロセスの方がスレッドよりも作成や廃棄のコスト(メモリ使用量や運用コスト)が高い

スレッドとプロセスの違い

image.png
最初にあげた画像でもいいのだが、こちらの方がよりわかりやすいので、こちらの画像を参考にしながら簡単にそれぞれのプロセスなどについて見ていく。左側の四角で囲われた部分がプロセスで右上の四角がメモリ、右下の四角がデータ領域などのファイルである。

プロセス構成

最初の画像でのサーバープロセスはバックエンドプロセスと思っていいと思う。自分の認識ではサーバープロセス(postmaster)が最初にワーカープロセス(バックグラウンドプロセス)などをフォークして生成し、その後、各コネクションごとにサーバープロセス内のリスナープロセスがクライアントの要求に応じてコネクションを確立し、クエリの解析や実行などを行うバックエンドプロセスを生成する、と思ってます

では、まずは全体の流れを見ていく。先ほども述べたようにpostgreを立ち上げるとまず初めにマスタープロセス(postmaster)が立ち上がる。これがDB全体を制御するための親プロセスである。その後共有メモリ領域の確保(動的)やデータ領域の確認(静的)などを行う。そして待機状態に入り、マスタープロセス内のリスナープロセスというのでクライアントからの接続を検知し、その度にバックエンドプロセスを作成する。
クライアントからのコネクション要求はpgAdmin(GUIベースの管理ツール)やpsql(postgre公式のCLI)、libpq(C言語の公式ライブラリ)などから行える。フロントエンドなどとも言い、クエリの受け取りをしてバックエンドにクエリを送信したり、結果を画面に表示したり、コネクションの管理などを行っている。なお、バックエンドとフロントエンドの通信は専用のプロトコルを使用している

さて、話を戻す。postgreを起動しマスタープロセスが立ち上がると、WAL(write ahead log)ライタやautovacuumランチャなどのワーカープロセスと呼ばれるプロセスがフォーク(fork)される。これらはバックグラウンドで常に動いており、一つだけ存在する。つまりクライアントごとに生成されるのではなく、起動時にフォークされ、クライアントごとに存在するバックエンドプロセスがこのプロセスにアクセスしたり、バックグラウンドで動いて自動でDBを効率化したりなどをしている。

この画像内のプロセスだけ見ていく

  • WALライタ
    • WALファイルと呼ばれる復旧の際や効率化のために使用するファイルがあるのだが、そのファイルに対して、WALバッファに溜まった内容を書き込むためのプロセス( WALとは )
  • ライタ(バックグラウンドライタ)
    • 共有バッファに書き込まれたデータ(ダーティーページ)をデータファイルに書き込む。これによりメモリ内のデータをディスクに書き込んで永続化する
  • チェックポインタ
    • 定期的にチェックポイント操作を実行し、全てのダーティーページの内容をデータファイルに書き込む。これにより、クラッシュした後にWALファイルから再生するのに必要なデータの量を減らし、リカバリ時間を短縮する
  • 統計情報コレクタ
    • DBの使用頻度やテーブルのアクセス頻度、行数、インデックスなどの使用状況を収集し、クエリプランナーが最適な実行計画を作成できるようにデータを提供する
  • 自動VACUUMランチャ
    • postgreでは追記型アーキテクチャを採用しており、データの削除や更新の際に古いデータには削除フラグのようなものを立てて使用しないものとし、新しい行(タプル)を追加している。なので、時間が経ったらそれらの使用しない行を削除し、ディスクを解放する必要がある。それらを行うためのVACUUM処理を開始するためのプロセスで、テーブルの更新や、削除活動などに基づいて自動で実行される

最後にクライアントごとに作成されるバックエンドプロセスについて少しだけ触れる。ここでは、クエリの解析(構文解析、トークン化)を行い、実行するクエリに対してクエリプラン(最適化)を行う。先ほど書いた統計情報コレクタというプロセスなどの情報を参考にし、オプティマイザーが最適な実行計画を選択する。そしてトランザクションの管理などを行い、フロントエンドへの結果の返却などを行っている。永続化するためにデータをディスクへ書き込んだりするが、それらの操作はWALライターなどのプロセスが担当しており、ここではWALバッファなどへの書き込みのみを行っている。

メモリ管理

image.png

postgreで使用されるメモリはpostgreサーバープロセス全体で共有される共有メモリ域とバックエンドプロセスで使用されるプロセスメモリ域に分けられる

共有メモリ域

これはバックグラウンドプロセス(ワーカープロセスなど全てのプロセス)とバックエンドプロセスの全てから参照や更新される共有領域である。この領域はpostgreが起動する時にOSのシステムコールにより予約される。また、起動時に確保したい共有メモリ領域の方がLinuxのカーネルパラメーターのshmmaxより大きかった場合はエラーが出力される

  • 共有バッファ(wal_buffers)
    • テーブルやインデックスのデータをキャッシュする領域。postgreのメモリ管理の中心的な役割で、DBがディスク上のデータにアクセスする頻度を下げたり、効率的にアクセスするためによく使用するデータなどをメモリ上にキャッシングする
  • WALバッファ(shared_buffers)
    • ディスクに書き込まれていないトランザクションログ(WAL)をキャッシュする領域。postgreではバックエンドプロセスでクエリの実行を行う時にこのWALバッファに変更を書き込む。その後ワーカープロセスのWALライタによってその変更内容がWALファイルに書き込まれる。WALはDBの永続性と回復性を保証するためにデータをログとして保存している。これによりディスクが壊れてもクラッシュリカバリを可能にしACID特性のDurabilityを保証している
  • 空き領域マップ(Free Space Map)
    • テーブル上の利用可能な領域を指し示す情報を扱う領域である。postgreは追記型アーキテクチャなので使用されなくなった行(タプル)を自動VCUUMランチャが自動的に空き領域として再利用可能にしてくれる。そして追加や更新時に空き領域マップを探索して、再利用可能な領域に対して新しい行を挿入する。これにより、効率的にデータを配置することができる
  • 可視性マップ(Visibility Map)
    • テーブルのデータが可視であるかどうかを管理するために使用される情報を扱う領域。可視であるとは、削除や更新などにより必要ではなくなったタプルに対して削除フラグのようなものを立てるのだが、それである。自動VACUUMされるまでは存在はしているが、参照することはないので不可視である、という認識になる。それらを管理するための情報を扱っている。これにより、バキューム処理の高速化やインデックスの検索などでも不要な行をスキップするなどして、効率的にデータを参照できる

プロセスメモリ域

バックエンドプロセスごとに確保される作業用のメモリがプロセスメモリである。メモリ領域を確保したプロセスのみが参照可能。

  • 作業メモリ(work_mem)
    • クエリ実行時に一時的に使用されるメモリ領域。ソートやハッシュ結合などの際に使用される。このメモリサイズを超えるとデータはディスク上の一時ファイルに書き出されるため遅くなったり、大きすぎるとシステム全体のメモリを圧迫してしまう可能性があるので、work_memの値の設定は注意が必要
  • メンテナンス用作業メモリ(maintenance_work_mem)
    • バキュームやインデックス作成、外部キーの追加などのDBメンテナンスの際に使用される領域。通常ではこれらの操作が同時に多数発生することはないのでメンテナンス時間の短縮を目指すためにwork_memより大きい値を設定することが多い
  • 一時バッファ(tmp_buffers)
    • バックエンドプロセスごとに作成される一時テーブルに使用するための領域。CREATE TEMP TABLEコマンドで一時テーブルを作成可能

ファイル

postgreで使用されるファイルの多くはデータベースクラスと呼ばれるディレクトリ配下に作成される。

https://www.postgresql.jp/docs/9.4/creating-cluster.html

initdbコマンドによってディスク上に保存するDB格納領域を初期化する。すると以下のようなデータベースクラスタが構成される(dataディレクトリ配下)

image.png

主なディレクトリ

  • baseディレクトリ

    • 各データベースの物理データを保存するディレクトリである。データベースごとに識別子(oid:Object ID)を示す数字のディレクトリ(データベースディレクトリ)が作成される。つまりCREATE DATABASE mydb;などとすると、それに対して12345などのようなoidが自動で割り振られ、その後別のDBを作成すると12346のようなディレクトリがbase/配下に加わる。そして、そのDB内にテーブルごとにテーブルファイルやインデックスファイル、Free Space MapやVisibility Mapなどがoidと紐づいて作成される
    $PGDATA/
    ├── base/         # 各データベースのデータディレクトリを格納
    │   ├── 12345/    # データベース "mydb" のディレクトリ (OID: 12345)
    │   │   ├── 23456       # "users" テーブルのデータ (OID: 23456)
    │   │   ├── 23456_fsm   # "users" の Free Space Map(空き領域管理)
    │   │   ├── 23456_vm    # "users" の Visibility Map(可視性情報)
    │   │   ├── 23457       # "orders" テーブルのデータ (OID: 23457)
    │   │   ├── 23457_fsm   # "orders" の Free Space Map
    │   │   ├── 23457_vm    # "orders" の Visibility Map
    │
    ├── global/      # クラスタ全体で共有されるデータ (ユーザー情報など)
    ├── pg_wal/      # WAL (Write-Ahead Logging) のログファイルを格納
    ├── pg_xact/     # トランザクション状態情報を管理
    ├── pg_tblspc/   # テーブルスペース情報を管理
    
  • globalディレクトリ

    • ここにはDBクラスタ全体で使用される情報が保管されている。例えば各DBの名前やoid、ユーザー情報やpassword、システムカタログ(postgre自身の情報:例えばDB、テーブル、インデックス、権限などを管理するテーブルのこと)などが格納されている
  • pg_walディレクトリ

    • WALファイルを保存するディレクトリ。postgreではDBの整合性を保証するために、実際にディスクに書き込む前にWALファイルというところにログを保存する。これによってクラッシュなどが発生しても直前のトランザクションを復元できたり、レプリケーションの際などに使用される。DBクラスタ作成時のオプションによってはシンボリックリンクとなることもある
  • pg_xactディレクトリ

    • トランザクションのコミット状態を管理するファイルを保存するディレクトリ。トランザクションID(XID)の状態を記録し、クラッシュリカバリの際に参照される
  • pg_tblspcディレクトリ

    • postgreのデフォルトではbaseディレクトリにデータが保存されるが、それを異なるストレージ(ディスク)に保存することもできる。その際に使用されるのがテーブルスペースというもので、異なるディスク上($PGDATA以外)にデータを保存することができるようになる。しかし、$PGDATA以外の場所をpostgreでは検知できないので、その場所を把握するためにシンボリックリンクを作成し、それをこのディレクトリに保存する

HDDよりもSSDの方がランダムアクセスが高速なので、頻繁にアクセスするテーブルやインデックス、WALファイルなどをSSDに設置し、I/Oの上昇を見込むケースなどがある
HDDとSSDの違い

主なファイル

  • PG_VERSIONファイル
    • 所属ディレクトリ:$PGDATA/
    • postgreのデータベースクラスタのメジャーバージョン番号が書き込まれている。ただのtxtファイルなのでcatコマンドなので参照できる。postgreはメジャーバージョンが異なるとデータベースクラスタの互換性が保証されず、無理に起動するとデータベースクラスタ内に作成されるファイルやフォーマットが異なり誤作動を発生する原因となってしまう。そのため、起動したpostgreのバージョンと指定したデータベースクラスタ配下のPG_VERSIONファイルの値を比較して、メジャーバージョンが異なる場合には起動しないようにしている
    $ cat PG_VERSION
    16
    
  • テーブルファイル
    • 所属ディレクトリ:$PGDATA/base/[データベースOID]/
    • テーブルデータの実際の値が格納されている場所で、複数の8192バイト(2^13)のページで構成されている。バイナリ形式で保存されている
  • インデックスファイル
    • 所属ディレクトリ:$PGDATA/base/[データベースOID]/
    • テーブルのインデックスデータを保存するファイル。検索性能の向上などに使用され、8192バイトのページという単位で構成されている
  • TOASTファイル
    • 所属ディレクトリ:$PGDATA/base/[データベースOID]/
    • テーブル内に長大なタプル(通常は2KBを超えるサイズ)を格納する際に生成される特殊なファイルで、メインのテーブルを逼迫しないようにTOASTファイルに分割して収納される。テーブルの格納領域にはTOAST用のoidが格納される
  • Free Space Mapファイル
    • 所属ディレクトリ:$PGDATA/base/[データベースOID]/
    • テーブルやインデックスの空き領域を示すファイルで、新しいデータの挿入時にどこに保存すれば効率的かを判断するのに使用される。テーブルおよびインデックスを示す数字_fsmという名前になる
  • Visibility Mapファイル
    • 所属ディレクトリ:$PGDATA/base/[データベースOID]/
    • テーブルの可視性を管理するファイルで、バキュームプロセスのやインデックスの検索の最適化のために使用される。テーブルを示す数字_vmという名前で保存される
  • WALファイル
    • 所在ディレクトリ: $PGDATA/pg_wal/(PostgreSQL 10以降)、$PGDATA/pg_xlog/(PostgreSQL 9以前)
    • postgreに対して行われた変更操作を記録するファイルであり、データベースの永続性やクラッシュリカバリの際に使用される。データの内容がまずWALファイルに書き込まれ、その後ワーカープロセスによって実際のデータファイルに変更が書き込まれる。WALファイルは固定長のファイルでinitdb実行時にサイズを指定する。デフォルトは16MBで作成され、max_wal_sizeに設定したサイズ程度のファイル群が生成される
  • postgresql.conf, pg_hba.conf
    • 所属ディレクトリ:$PGDATA/
    • postgresql.conf: PostgreSQLの全体的な設定(リソース割り当て、ログ出力、接続設定など)を定義する主要な設定ファイル
    • pg_hba.conf (host-based authentication): クライアントの認証方法やアクセス制御を設定するファイルで、どのホストからの接続を許可するか、どの認証方式を使用するかを指定する
    • 詳しくは次の章で
  • postmaster.pid
    • 所属ディレクトリ:$PGDATA/
    • postgreのサーバープロセスのプロセスID(PID)を記録するファイルで、サーバーが起動中であることを示し、複数のpostgreインスタンスが同じデータベースクラスタを使用しないようするためのロックファイルである

鉄則

PostgreSQLの構成要素を把握し、設計・運用計画に活かす

各種設定ファイルと基本設定

ここは一旦無視。もし時間が出来たらまとめる

まとめ

どんなプロセスが動いているのか、メモリ領域はどのように使用しているのか、どのようなディレクトリ構成で、それぞれのファイルがどのようなものを保持しているのかなんとなくわかってよかった

参照記事

https://www.postgresql.jp/document/16/html/index.html

https://gihyo.jp/dev/feature/01/dex_postgresql

https://www.fujitsu.com/jp/products/software/resources/feature-stories/postgres/article-index/architecture-overview/

https://thinkit.co.jp/free/compare/1/3/1.html

Discussion