書籍「改訂新版-内部構造から学ぶPostgreSQL-設計・運用計画の鉄則」のまとめ - 理論編

# はじめに
ここ最近、環境構築や障害調査、他RDBへの移行といった作業を行う中で、データベース内部の仕組みについて一度じっくりと学ぶ必要があるなと感じました。
色々とネットで調べたところ、PostgreSQLに関しては
[[改訂新版]内部構造から学ぶPostgreSQL 設計・運用計画の鉄則](https://www.amazon.co.jp/%E6%94%B9%E8%A8%82%E6%96%B0%E7%89%88-%E5%86%85%E9%83%A8%E6%A7%8B%E9%80%A0%E3%81%8B%E3%82%89%E5%AD%A6%E3%81%B6PostgreSQL-%E8%A8%AD%E8%A8%88%E3%83%BB%E9%81%8B%E7%94%A8%E8%A8%88%E7%94%BB%E3%81%AE%E9%89%84%E5%89%87-Software-Design/dp/4297100894)
が良さそうだったので、この書籍を読んで学んだことをいくつかピックアップし自分なりにまとめます。
**※あくまで個人の理解なので誤りがある可能性があります。ご了承ください。**

# 基本的なアーキテクチャ
![pg_architecture.png](https://qiita-image-store.s3.amazonaws.com/0/22736/3a1adf1e-0ce7-7839-38f1-e8282039dcb8.png)

## プロセス
PostgreSQLではマルチプロセスモデルを採用。
プロセスは大きく分けてマスタサーバプロセス、バックグラウンドプロセス、バックエンドプロセスが存在する。

- PostgreSQL起動時にマスタサーバプロセスが生成される
- 生成されたマスタサーバプロセスが各制御用プロセス（バックグラウンドプロセス）をフォークする
- クライアントからの接続要求時には、まずマスタサーバプロセスがリクエストを受け付けて、バックエンドプロセスをフォークし、以降、クライアントとバックエンドプロセスがやり取りする
- つまりクライアントの数だけバックエンドプロセスが生成される

## メモリ
### 共有メモリ
共有メモリとはすべてのプロセスで共有する領域。
大きく分けて共有バッファとWALバッファが存在する。

#### 共有バッファ

- テーブルやインデックスの情報をキャッシュする領域
- キャッシュすることでテーブル情報などが格納されてるデータファイルに直接アクセスしないで済む（I/Oが発生しない）
- 初回アクセス時にテーブルやインデックスの情報をキャッシュしておいて、２回目以降はキャッシュを参照する
- UPDATEなどの更新処理が発生した際は、キャッシュしている共有バッファのデータが更新され、後からデータファイルに更新情報が反映される

#### WALバッファ

- WAL（ログ先行書き込み：Write Ahead Logging）と呼ばれるPostgreSQLに対する更新情報をキャッシュする領域
- UPDATEなどの更新処理が発生した際は、更新した内容が先にWALバッファに書き込まれる

#### 空き領域マップ
テーブル上の利用可能な領域を指し示す領域

#### 可視性マップ
テーブルのデータが参照できるか否かを管理する情報を扱う領域

### プロセスメモリ
バックエンドプロセスから利用される領域で、バックエンドプロセスごとに存在する
#### 作業メモリ
問い合わせ時のソートやハッシュテーブルの操作に使用する領域
#### メンテナンス用作業メモリ
バキュームやインデックスの作成など、データベースメンテナンスの操作で使用する領域
#### 一時バッファ
`CREATE TEMP TABLE` で作成される一時テーブルにアクセスする際に使用する領域

## ファイル
### データファイル
![pg_datafile.png](https://qiita-image-store.s3.amazonaws.com/0/22736/ce84b57e-2e9c-8450-2d60-e90fb7c10966.png)

データファイルには、テーブルごとの実データを管理するテーブルファイルと、インデックスの情報を管理するインデックスファイルが存在する。

- 8192バイトの「ページ」単位で構成され最大1Gバイト
- 1Gバイトを超えると「ファイルノード番号.連番」の命名規則に則って分割管理される
- ライタプロセスやチェックポインタによって共有バッファのデータが書き込まれる
 
### WALファイル
![pg_walfile.png](https://qiita-image-store.s3.amazonaws.com/0/22736/df129ae7-b5b4-e23c-190f-98bbcea8ba0d.png)

WALファイルにはPostgreSQLに対する更新情報が記録されていて、この情報を用いてレプリケーションやリカバリを行う

- 更新内容が書かれている「WALレコード」単位で構成され最大16Mバイト
- WALレコードにはLSN（Log Sequence Number）とよばれる番号が振られている
- `max_wal_size` の値によりファイルの総数が決まり、ファイルの総数に達すると、古いファイルから上書かれていく
- 上書きによるWALの消失を防ぐために、WALアーカイブファイルとして残すこともできる
- WALライタプロセスによってWALバッファのデータが書き込まれる

## プロセス・メモリ・ファイルの関係
### WALバッファ -> WALライタプロセス -> WALファイル
- UPDATEなどの更新処理が発生した場合、まずWALバッファに更新情報が書き込まれる
- COMMITを行った契機でWALバッファの情報がWALライタプロセスによってWALファイルに書き込まれる

### 共有バッファ -> ライタプロセス（or チェックポインタ） -> テーブルファイル・インデックスファイル
- UPDATEなどの更新処理が発生した場合、まず共有バッファ上のデータが更新される
- この更新された情報（ダーティーページ）はすぐにはデータファイルには反映されない
  - 随時データファイルへの書き込みを行うとI/Oが頻発してパフォーマンス上よろしくない
- ダーティーページのデータファイルへの書き込みはライタプロセスおよびチェックポインタで行われる
  - 書き込み時はI/Oが発生するので、問い合わせが遅くなる原因にもなる

# ストリーミングレプリケーション
![pg_streamingreplication.png](https://qiita-image-store.s3.amazonaws.com/0/22736/5766026c-2181-ebf6-7178-72af59fed7be.png)

ストリーミングレプリケーションとは、マスタースレーブ構成において、プライマリの更新内容ごとに（WALレコードごとに）スタンバイへと送る機能

- スタンバイでは送られてきたWALを実行することで、プライマリと同じ状態を保つことができる。
- 実際にはプライマリ側のwalsenderプロセスと、スタンバイ側のwalreceiverプロセスでやりとりを行う


# バックアップ・リカバリ
![pg_backup.png](https://qiita-image-store.s3.amazonaws.com/0/22736/c5e1f3a9-5923-9666-8e40-183593ca579b.png)


障害が発生した際に、障害前の状態に戻すために必要となるファイル群をとっておき（＝バックアップ）、
それらのファイル群をリストアすることで障害前の状態に戻すこと（＝リカバリ）

必要なファイル群は以下のとおり

- ベースバックアップ（テーブルやインデックスの情報）
- アーカイブWALファイル
- 直前のWALファイル

# VACUUMについて
## 追記型アーキテクチャ
PostgreSQLでは追記型アーキテクチャを採用している。
例えばある行に対してUPDATE文を実行した場合、既存の行の領域は不要領域として残したまま、更新後の行の領域が新たに追加される。
そのためUPDATE処理を行っていくと、その分だけ物理データが増えていく。

## VACUUMとは
バキュームとは更新や削除によって発生したデータファイル内の不要領域をクリアし、再利用できるようにすること。
定期的にバキューム処理を行うことで不要領域が大量に残ることによる性能低下を防ぐ。

![pg_vacuum.png](https://qiita-image-store.s3.amazonaws.com/0/22736/76a31719-328a-bccc-bb2f-f006af9ec5e3.png)

1. BBBをBBB'に更新。ページ2が不要領域となり、ページ4が新たに追加される
2. ページを上から順に確認
3. 2で取得したページが必要領域か不要領域かを確認
4. 3で不要領域となっているページを再利用対象ページとして空き領域マップに登録

# パラメータチューニングの勘所
## CPU
クライアントからの接続数だけバックエンドプロセスが生成されるので、プロセスの数が多い分、CPUによるプロセスの切り替え（コンテキストスイッチ）が発生する。
`max_connections` などのパラメータに適切な値を設定しCPUの負荷をおさえる。

|パラメータ|説明|
|---|---|
|max_connections|最大同時接続数|
|superuser_reserved_connections|スーパーユーザの最大同時接続数。<br><br>一般ユーザの最大同時接続数は、`max_connections - superuser_reserved_connecitons - スタンバイの数` となる|

## メモリ
直接ディスクへアクセスせずにメモリ上に展開されているデータにアクセスすることで、パフォーマンスの向上をはかっている。
そのためどのくらいのメモリ領域を確保するかがポイントになる。

### 共有メモリ
#### OS側の設定
共有メモリの領域は、サーバの起動時にOSのシステムコールにより確保される。
実際には下記のカーネルパラメータにてサーバ全体で使用できる共有メモリのサイズが決まる。

|パラメータ|説明|
|---|---|
|shmmax|共有メモリの最大容量|
|shmall|使用可能な共有メモリの総量|

#### PostgreSQL側の設定
PostgreSQL側の設定としては主に以下の２つ。
ちなみに、PostgreSQLの起動時に設定パラメータのチェックが行われ、`shmmax < shared_buffers` の場合はエラーメッセージが表示される

|パラメータ|説明|
|---|---|
| shared_buffers|共有バッファのサイズを設定。推奨値はサーバ実メモリの25%。|
| wal_bufferes|WALバッファのサイズを設定|

### プロセスメモリ

|パラメータ|説明|
|---|---|
| work_mem|問い合わせ時のソートやハッシュデータ格納時に使用するメモリのサイズ|
| maintenance_work_mem|Vacuum文やALTER TABLEなどのメンテナンス操作時に使用するメモリのサイズ|

## ディスク
### OS側の設定
I/Oスケジューラの設定として `deadline` が推奨。
ただしハードウェアの環境によって異なる。

### PostgreSQL側の設定
#### ライタプロセス
ライタプロセスによるテーブルファイルへの書き込みが大量に発生すると、問い合わせ性能がおちる。
以下のパラメータに適切な値を設定し書き込みの周期や上限を設定する

|パラメータ|説明|
|---|---|
|bgwriter_delay|ライタプロセスの動作周期|
|bgwriter_lru_maxpages|一度にライタプロセスが書き込むページ数の上限|
|bgwriter_lru_multiplier|書き込みが必要になったページのうち、どのくらいの割合を書き込むべきか計算する際に使用|

#### チェックポインタ
以下のパラメータのいずれかの閾値に達すると、共有バッファの情報がデータファイルへ書き込まれる。
書き込み処理の間はI/O負荷が高くなり問い合わせ性能に影響を及ぼす。
適切な値を設定し書き込みの周期や上限を設定する。

|パラメータ|説明|
|---|---|
|checkpoint_segments|最後のチェックポイントからWALセグメントがこの数に達するとチェックポイント処理を行う（PostgreSQL9.4まで）|
|max_wal_size|最後のチェックポイントからWALバッファがこのサイズに達するとチェックポイント処理を行う（PostgreSQL9.5から）|
|checkpoint_timeout|チェックポインタの動作周期|

#### WALファイル

|パラメータ|説明|
|---|---|
|wal_writer_delay|WALライタプロセスの動作周期|
|max_wal_size|生成されるWALファイルの総数|

# おわりに
ざっくりとではありますが、今回は理論編ということで基本的な内部の仕組みについてまとめました。
次回は実際に手を動かしてPostgreSQLのインストールからクラスタ構成の構築、アプリとの連携など、実践編としてまとめる予定です。