PostgreSQL 16 vacuum_buffer_usage_limit 拡張時の挙動について

# Overview
PostgreSQL 16 で、VACUUM と ANALYZE で使用できるバッファサイズの指定が可能になりました。
```
Allow control of the shared buffer usage by vacuum and analyze (Melanie Plageman)

The VACUUM/ANALYZE option is BUFFER_USAGE_LIMIT, and the vacuumdb option is --buffer-usage-limit. 
The default value is set by server variable vacuum_buffer_usage_limit, which also controls autovacuum.
```
ref : https://www.postgresql.org/docs/release/16.0/

この機能の実装前には VACUUM に使用できるのは 256KB のリングバッファのみで、これ以上のサイズのオブジェクトの VACUUM 時には、VACUUM を実施しているプロセス自身が datafile へ write するため、VACUUM 時のパフォーマンスが storage の random write iops 等に常に影響を受ける状態でした。[^1]

この挙動が上記 vacuum_buffer_usage_limit の追加によりどのように変化するのかを以下に検証したいと思います。検証にあたり重視するのは以下の 2 点です。

- vacuum_buffer_usage_limit がオブジェクトサイズに対して十分なサイズである場合、VACUUM の datafile への write システムコールが抑制されるかどうか
- VACUUM の速度が改善されるかどうか

# Result
検証方法については後述しますが、約 2.5GB(table + index) のデータの半数に対する vacuum で比較すると、以下のような結果になりました。
| Version | Vacuum Buffer Usage Limit | Datafile への pwrite64 (calls) | Vacuum Avg Write | Vacuum 時間 | Vacuum 後の Checkpoint 時の Wrote Buffer |
|---------|----------------------------|--------------------------------|------------------|-------------|-----------------------------------------|
| 15.4    | なし (256 KB)              | 337800                         | 81 MB/s          | 32.42 s     | wrote 40 buffers                        |
| 16.4    | 256 KB                     | 332453                         | 86 MB/s          | 30.30 s     | wrote 42 buffers                        |
|        | 2 GB                       | 104868                         | 109 MB/s         | 24.06 s     | wrote 232014 buffers                    |
|        | 10 GB                      | 1                              | 178 MB/s         | 12.96 s     | wrote 295206 buffers                    |


pwrite64 の call 数と VACUUM 時間をグラフにすると以下のようになります。

![vacuum_buffer_usage_limit](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2598644/f7347ed3-e0d4-4ad5-d0ca-030c6d2db121.png)
vacuum_buffer_usage_limit を上げたケースでは、Datafileへの random write が減少し、書き込みパフォーマンスが向上しています。

※ のちの checkpoint や bgwriter の挙動により datafile への random write 自体は発生します。

# Test Method
簡易的な情報ですが、以下の環境で検証を行っています。
```C
-- machine
vCPU : 20
memory : 240 GB
storage : SSD

-- PostgreSQL
Version : PostgreSQL 16.4
Replication : Stream replication 構成(sync,remote_write)
```
テストに使用したコマンド群は以下です。(psql からの実行)

<details open><summary>10GBでの計測時サンプル</summary>

```SQL
-- テーブル設定
DROP TABLE table1;
CREATE TABLE table1 (
  id SERIAL PRIMARY KEY,
  filler VARCHAR(32)
);

ALTER TABLE table1 SET (log_autovacuum_min_duration = 0,autovacuum_enabled = false);
INSERT INTO table1 (filler) SELECT md5(random()::text) FROM generate_series(1, 10000000);
CREATE INDEX ON table1 (filler);

-- パラメータ設定
-- autovcuum の並列実行数は 1
-- shared_buffers は 80GB
-- autovacuum_vacuum_cost_delay は 0 にしておく
-- walsize/時間 起因のチェックポイントが発生しないようにする
-- vacuum_buffer_usage_limit を 10GB に設定する(shared buffer 80GB に対する max)
ALTER SYSTEM SET autovacuum_max_workers=1;
ALTER SYSTEM SET shared_buffers='80GB';
ALTER SYSTEM SET autovacuum_vacuum_cost_delay=0;
alter system set autovacuum_naptime='5';
alter system set max_wal_size='100GB';
alter system set checkpoint_timeout='12h';
alter system set vacuum_buffer_usage_limit='10GB';
\! sudo systemctl restart postgres
select 1;
SELECT name, setting, unit, short_desc
FROM pg_settings
WHERE name in ('autovacuum_max_workers','shared_buffers','autovacuum_vacuum_cost_delay','autovacuum_vacuum_cost_limit','vacuum_cost_limit','vacuum_cost_delay','vacuum_buffer_usage_limit');


-- table1 に更新を入れて checkpoint
ALTER TABLE table1 SET (log_autovacuum_min_duration = 0,autovacuum_enabled = false, toast.autovacuum_enabled = false);
UPDATE table1 SET filler = md5(random()::text) WHERE id <= (SELECT max(id) FROM table1) * 2 / 4;
checkpoint;
 
-- 一度再起動して buffer を未使用にする
\! sudo systemctl restart postgres
select 1;
SELECT
  count(*) AS shared_buffer_count,
  COUNT(relfilenode) AS in_use_count,
  count(*) - COUNT(relfilenode) AS free_buffer_count,
  COUNT(*) FILTER (WHERE isdirty = false) AS clean_buffer_count,
  COUNT(*) FILTER (WHERE isdirty = true) AS dirty_buffer_count
FROM pg_buffercache;
 
SELECT c.relname,
       CASE WHEN b.isdirty THEN 'dirty' ELSE 'clean' END as buffer_state,
       count(*) as buffers
FROM pg_buffercache b
JOIN pg_class c ON b.relfilenode = c.relfilenode
JOIN pg_database d ON b.reldatabase = d.oid
WHERE d.datname = current_database()
GROUP BY c.relname, buffer_state
ORDER BY buffers DESC LIMIT 10;

-- page cache もクリアする
\q
exit
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'

sudo su - postgres
psql

-- autovacuum する
ALTER TABLE table1 SET (autovacuum_enabled = true, toast.autovacuum_enabled = true);
 
-- strace で systemcall を確認する
sudo strace -c -p $(pgrep -f "postgres: autovacuum worker")
 
-- 終わったら shared buffer を確認してみる
SELECT
  count(*) AS shared_buffer_count,
  COUNT(relfilenode) AS in_use_count,
  count(*) - COUNT(relfilenode) AS free_buffer_count,
  COUNT(*) FILTER (WHERE isdirty = false) AS clean_buffer_count,
  COUNT(*) FILTER (WHERE isdirty = true) AS dirty_buffer_count
FROM pg_buffercache;
 
SELECT c.relname,
       CASE WHEN b.isdirty THEN 'dirty' ELSE 'clean' END as buffer_state,
       count(*) as buffers
FROM pg_buffercache b
JOIN pg_class c ON b.relfilenode = c.relfilenode
JOIN pg_database d ON b.reldatabase = d.oid
WHERE d.datname = current_database()
GROUP BY c.relname, buffer_state
ORDER BY buffers DESC LIMIT 10;
 
-- 手動 checkpoint してログを確認する
checkpoint;
```
</details>

# Conclusion
検証結果から、VACUUM 実施プロセスの random write が実際に減少していることが確認できました。

機能自体は checkpoint や bgwriter 動作時まで VACUUM 結果の disk flush を先送りにするものではありますが、以下の点から設定を導入するメリットは十分にあると考えています。

- VACUUM が長時間化した際のデメリット(aggressive 時のロックや各種コンフリクト) が VACUUM の時間短縮により軽減できる
- checkpoint や bgwriter には負荷平準化のためのパラメータが豊富なため、調整しやすい。


[^1]:VACUUM でのオブジェクト scan 時使用されるのは 256Kb の ring buffer のみで、shared buffer にはキャッシュされない。そのため 256Kb を超えるデータの VACUUM 時には ring buffer 追い出しによる datafile への random write が発生する。ただし VACUUM 時に shared buffer に既にオブジェクトが載っている場合は利用できる
https://github.com/postgres/postgres/tree/REL_15_STABLE/src/backend/storage/buffer