PG16:Add pg_stat_io view, providing more detailed IO statistics

# はじめに

にゃーん。趣味でポスグレをやっている者だ。
今回はPostgreSQL 16に追加予定の収集済み統計情報ビュー``pg_stat_io``について調べてみました。

# 概要

``pg_stat_io``ビューの追加自体は、Commitfest項目にはなかったのですが、2023-02-11のコミットで入ったようです。
[Add pg_stat_io view, providing more detailed IO statistics
](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=a9c70b46dbe152e094f137f7e6ba9cd3a638ee25)
収集済み統計情報ビューとして、どういった内容が表示されるのか、またどういう監視に使えそうかを考えてみます。

# 変更内容

``pg_stat_io``ビューは、ざっくりいうと、データベースクラスタ全体のI/O統計が表示するものです。このビューで表示される項目を以下に示します。

### pg_stat_ioの内容

|列名|型|意味|
|:--|:--|:--|
|backend_type |text |バックエンドのタイプ（例：バックグラウンドワーカー、autovacuumワーカー）。詳細は[pg_stat_activity](https://www.postgresql.org/docs/devel/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW)を参照。一部のbackend_typesはI/O操作の統計情報を蓄積しないため、ビューに含まれない |
|object   |text |I/O操作の対象オブジェクト。値域は以下。<br>relation<br>temp relation|
|context  |text |I/O操作のコンテキスト。値域はnormal, vacuum, bulkread, bulkwrite。<br>詳細は、[contextの意味](#contextの意味)を参照。|
|reads  |bigint |op_bytesで指定されたサイズの読み出し操作の数。 |
|read_time  |double precision |読み取り操作に費やした時間（ミリ秒）<br>track_io_timingが有効な場合のみ設定される。無効の場合は0になる。 |
|writes  |bigint |op_bytesで指定されたサイズの書き込み操作の数。 |
|write_time  |double precision |書き込み操作に費やした時間（ミリ秒） <br>track_io_timingが有効な場合のみ設定される。無効の場合は0になる。|
|extends  |bigint |op_bytesで指定されたリレーションのサイズ拡張操作の数。 |
|extend_time  |double precision |拡張操作に費やした時間（ミリ秒）。<br>track_io_timingが有効な場合のみ設定される。無効の場合は0になる。|
|op_bytes|bigint |読み出し、書き込み、拡張操作I/Oの単位あたりのバイト数。<br>関係データの読み取り、書き込み、拡張は、構築時パラメータBLCKSZから得られるblock_size単位で行われ、デフォルトでは8192である。 |
|hits  |bigint |共有バッファで目的のブロックが見つかった回数。 |
|evictions  |bigint |ブロックが別の用途に利用できるようにするために、共有バッファまたはローカルバッファから書き出された回数。io_contexrにより数値の意味が異なる。詳細は、[evictionsの意味](#evictionsの意味)を参照。 |
|reuses  |bigint |共有バッファ以外のサイズ制限のあるリングバッファの既存のバッファが、bulkread、bulkwrite、またはvacuum io_contextsでI/O操作の一部として再使用された回数。 |
|fsyncs  |bigint |fsyncコールの数。これらはio_contextがnormalの場合のみ追跡される。 |
|fsync_time  |double precision |fsync操作に費やした時間（ミリ秒）。 |
|stats_reset   |timestamp with time zone |統計が最後にリセットされた時刻。 |

### contextの意味

|値域|意味|
|:--|:--|
|normal|標準のcontext。例えば、デフォルトでは、関係データは共有バッファに読み込まれ、共有バッファから書き出される。したがって、共有バッファへのリレーションデータの読み出しと書き込みは、context normalで追跡される。 |
|vacuum|パーマネントリレーションのバキュームと分析中に共有バッファの外で行われるI/O操作。一時的なテーブルのバキュームは、他の一時的なテーブルIO操作と同じローカルバッファプールを使用し、context normalで追跡される。 |
|bulkread |共有バッファの外で行われる特定の大きな読み取りI/O操作（例えば、大きなテーブルの順次スキャンなど）。 |
|bulkwrite |COPYなど、共有バッファの外で行われる特定の大きな書き込みI/O操作。 |

### evictionsの意味

|io_contextの値|意味|
|:--|:--|
|normal  |ブロックがバッファから追い出され、別のブロックに置き換えられた回数をカウントする。   |
|vacuum<br>bulkread<br>bulkwrite|バルクI/Oオペレーションで使用するために、共有バッファをサイズ制限のある別のリングバッファに追加するために、ブロックが共有バッファから退避した回数をカウントする。  |


#  検証
:::note info
今回の検証は(commit 414d66220adb9189951fb4d410470f9f36f9cbd1)時点(2023-04-15)のリポジトリからビルドして検証しています。
:::

## データベースクラスタ生成・起動直後

データベースクラスタを生成し、起動した直後にこのビューを参照すると、30行の情報が表示されます。

```bash
$ psql -p 16001 postgres -c "TABLE pg_stat_io"
Null display is "(null)".
    backend_type     |    object     |  context  | reads  | read_time | writes | write_time | extends | extend_time | op_bytes |  hits  | evictions | reuses | fsyncs | fsync_time |          stats_reset
---------------------+---------------+-----------+--------+-----------+--------+------------+---------+-------------+----------+--------+-----------+--------+--------+------------+-------------------------------
 autovacuum launcher | relation      | bulkread  |      0 |         0 |      0 |          0 |  (null) |      (null) |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 autovacuum launcher | relation      | normal    |      0 |         0 |      0 |          0 |  (null) |      (null) |     8192 |      0 |         0 | (null) |      0 |          0 | 2023-04-23 14:41:40.622437+09
 autovacuum worker   | relation      | bulkread  |      0 |         0 |      0 |          0 |  (null) |      (null) |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 autovacuum worker   | relation      | normal    |    181 |         0 |      0 |          0 |       9 |           0 |     8192 |  12197 |         0 | (null) |      0 |          0 | 2023-04-23 14:41:40.622437+09
 autovacuum worker   | relation      | vacuum    |    144 |         0 |      0 |          0 |       0 |           0 |     8192 |    113 |         0 |    107 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 client backend      | relation      | bulkread  |      0 |         0 |      0 |          0 |  (null) |      (null) |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 client backend      | relation      | bulkwrite |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 client backend      | relation      | normal    |     69 |         0 |      0 |          0 |       0 |           0 |     8192 |   1342 |         0 | (null) |      0 |          0 | 2023-04-23 14:41:40.622437+09
 client backend      | relation      | vacuum    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 client backend      | temp relation | normal    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 | (null) | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 background worker   | relation      | bulkread  |      0 |         0 |      0 |          0 |  (null) |      (null) |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 background worker   | relation      | bulkwrite |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 background worker   | relation      | normal    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 | (null) |      0 |          0 | 2023-04-23 14:41:40.622437+09
 background worker   | relation      | vacuum    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 background worker   | temp relation | normal    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 | (null) | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 background writer   | relation      | normal    | (null) |    (null) |      0 |          0 |  (null) |      (null) |     8192 | (null) |    (null) | (null) |      0 |          0 | 2023-04-23 14:41:40.622437+09
 checkpointer        | relation      | normal    | (null) |    (null) |     42 |          0 |  (null) |      (null) |     8192 | (null) |    (null) | (null) |      9 |          0 | 2023-04-23 14:41:40.622437+09
 standalone backend  | relation      | bulkread  |      0 |         0 |      0 |          0 |  (null) |      (null) |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 standalone backend  | relation      | bulkwrite |      0 |         0 |      0 |          0 |       8 |           0 |     8192 |      7 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 standalone backend  | relation      | normal    |    536 |         0 |   1009 |          0 |     661 |           0 |     8192 |  84327 |         0 | (null) |      0 |          0 | 2023-04-23 14:41:40.622437+09
 standalone backend  | relation      | vacuum    |     10 |         0 |      0 |          0 |       0 |           0 |     8192 |    903 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 startup             | relation      | bulkread  |      0 |         0 |      0 |          0 |  (null) |      (null) |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 startup             | relation      | bulkwrite |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 startup             | relation      | normal    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 | (null) |      0 |          0 | 2023-04-23 14:41:40.622437+09
 startup             | relation      | vacuum    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 walsender           | relation      | bulkread  |      0 |         0 |      0 |          0 |  (null) |      (null) |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 walsender           | relation      | bulkwrite |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 walsender           | relation      | normal    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 | (null) |      0 |          0 | 2023-04-23 14:41:40.622437+09
 walsender           | relation      | vacuum    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 |      0 | (null) |     (null) | 2023-04-23 14:41:40.622437+09
 walsender           | temp relation | normal    |      0 |         0 |      0 |          0 |       0 |           0 |     8192 |      0 |         0 | (null) | (null) |     (null) | 2023-04-23 14:41:40.622437+09
(30 rows)
```

おそらく、一番良く使いそうな情報は、バックエンドプロセスからのI/O情報だと思うので、``backend_type = 'client backend'``で絞り込んで、``reads``,``writes``をを表示させたりするのかな。

```bash
$ psql -p 16001 postgres -c "SELECT context, reads, writes FROM  pg_stat_io WHERE backend_typ
e = 'client backend'"
Null display is "(null)".
  context  | reads | writes
-----------+-------+--------
 bulkread  |     0 |      0
 bulkwrite |     0 |      0
 normal    |    91 |      0
 vacuum    |     0 |      0
 normal    |     0 |      0
(5 rows)
```

``backend_type``が``normal``の``reads``が0でないのは、キャッシュに上がっていなかったシステムカタログへのI/O数が表示されているのかもしれません。

## pgbench初期化後の状態

I/Oを発生させるために、pgbenchを使ってみます。
まずは初期化から。scale factor = 10でテキトーに初期化しておきます。

```bash
$ createdb -p 16001 testdb
$ pgbench -p 16001 -i -s 10 -q testdb
dropping old tables...
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
1000000 of 1000000 tuples (100%) done (elapsed 2.27 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 3.59 s (drop tables 0.00 s, create tables 0.01 s, client-side generate 2.65 s, vacuum 0.15 s, primary keys 0.79 s).
$
```
この直後に、backend_type = 'standalone backend'で絞り込んで``pg_stat_io``を参照します。

```bash
$ psql -p 16001 postgres -c "SELECT context, reads, writes FROM  pg_stat_io WHERE backend_typ
e = 'client backend'"
Null display is "(null)".
  context  | reads | writes
-----------+-------+--------
 bulkread  |  7781 |      0
 bulkwrite |     0 |  14346
 normal    |   119 |      0
 vacuum    | 14346 |      0
 normal    |     0 |      0
(5 rows)
```

この結果から以下のような動作が推測できます。

* pgbenchの初期化においてCOPYによるロードを行うため、bulkwrite行のwritesがカウントされる。
* データロード後にVACUUMを行うため、vacuum行のreadsがカウントされる。この時点ではデッドタプルはないので、writesはカウントされない。
* 初期化処理の最後に、primary key設定による暗黙のインデックス生成が行われる。このときには共有バッファを経由せずにデータを読み込むため、bulkread行のreadsがカウントされる。

``context``がnormalの行が2行表示されていますが、下に表示されているものは、``object``がtemp（中間テーブル）のものだと思われます。(pgbenchでは中間テーブルアクセスはないので0のまま）

続いて、pgbenchのデフォルトトランザクションを接続数2, 5000回指定（総計、10000トランザクション実行）してみます。pgbenchが動作することで``vacuum``と``normal``の``reads``や``writes``が変わるはず。

```bash
$ pgbench -p 16001 -c 2 -t 5000 testdb
pgbench (16devel)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 10
（中略）
latency average = 2.087 ms
initial connection time = 4.435 ms
tps = 958.282053 (without initial connection time)
$
```
pgbench実行後にまたさっきと同じクエリで``pg_stat_io``を参照します。

```bash
$ psql -p 16001 postgres -c "SELECT context, reads, writes FROM  pg_stat_io WHERE backend_type = 'client backend'"
Null display is "(null)".
  context  | reads | writes
-----------+-------+--------
 bulkread  |  7781 |      0
 bulkwrite |     0 |  14346
 normal    |  9338 |      0
 vacuum    | 14346 |      0
 normal    |     0 |      0
(5 rows)
```
``context``がnormalの行（の最初のほう）の``reads``が増加しています。
``wites``が増えていないのは、共有バッファ上での更新のみが行われたためと考えられます。

## チェックポイント実行後の状態

この状態で手動チェックポイントを実行します。
チェックポイントの前後に、``backend_type = checkpointer``の行を表示します。

```bash
$ psql -p 16001 postgres -c "SELECT context, reads, writes FROM  pg_stat_io WHERE backend_type = 'checkpointer'"
Null display is "(null)".
 context | reads  | writes
---------+--------+--------
 normal  | (null) |   5548
(1 row)

$ psql -p 16001 postgres -c "CHECKPOINT"
Null display is "(null)".
CHECKPOINT
$ psql -p 16001 postgres -c "SELECT context, reads, writes FROM  pg_stat_io WHERE backend_type = 'checkpointer'"
Null display is "(null)".
 context | reads  | writes
---------+--------+--------
 normal  | (null) |  12465
(1 row)

$
```

``backend_type = 'checkpointer'``の場合、``io_context``は``normal``だけのようですね。
チェックポイントの前後で``writes``の数値が増加しているのが確認できました。

### 統計情報のリセット
``pg_stat_io``の情報は、``pg_stat_reset()``関数ではリセットされません。

``pg_stat_io``のリセットには、``pg_stat_reset_shared('io')``を使えば良いというコメントを、[@fujii_masao](https://twitter.com/fujii_masao)さんからもらいました。ありがとうございます！
なるほど、こうやってリセットするのか・・・。

```bash
$ psql -p 16001 postgres -c "SELECT context, reads, writes, stats_reset FROM  pg_stat_io WHERE backend_type = 'client backend'"
Null display is "(null)".
  context  | reads | writes |          stats_reset
-----------+-------+--------+-------------------------------
 bulkread  |  8084 |      0 | 2023-04-23 18:47:22.431422+09
 bulkwrite |     0 |  14346 | 2023-04-23 18:47:22.431422+09
 normal    |  9278 |      0 | 2023-04-23 18:47:22.431422+09
 vacuum    | 14346 |      0 | 2023-04-23 18:47:22.431422+09
 normal    |     0 |      0 | 2023-04-23 18:47:22.431422+09
(5 rows)

$ psql -p 16001 postgres -c "SELECT pg_stat_reset_shared('io')"
Null display is "(null)".
 pg_stat_reset_shared
----------------------

(1 row)

$ psql -p 16001 postgres -c "SELECT context, reads, writes, stats_reset FROM  pg_stat_io WHERE backend_type = 'client backend'"
Null display is "(null)".
  context  | reads | writes |          stats_reset
-----------+-------+--------+-------------------------------
 bulkread  |     0 |      0 | 2023-04-23 18:57:48.408184+09
 bulkwrite |     0 |      0 | 2023-04-23 18:57:48.408184+09
 normal    |     7 |      0 | 2023-04-23 18:57:48.408184+09
 vacuum    |     0 |      0 | 2023-04-23 18:57:48.408184+09
 normal    |     0 |      0 | 2023-04-23 18:57:48.408184+09
(5 rows)
```

# おわりに
PostgreSQL 16に入る予定の``pg_stat_io``について調べてみました。
このビューの内容を定期的に収集することで、データベースクラスタ全体でI/O量が増加していないか等の監視ができそうです。このビューの使い方については、まだまだ調べることが多そうですね。

なお、pg_statsinfo がPostgreSQL 16に対応したときにも、たぶん収集されるんじゃないかと想像しています。