PostgreSQLのチェックポイント処理のチューニング

# はじめに
この記事は2ndQuandrant社の記事 [Basics of Tuning Checkpoints](http://blog.2ndquadrant.com/basics-of-tuning-checkpoints/) を翻訳した内容です。

記事の内容を確認するため、翻訳しながら読んでました。
気になるのは、後半のcheckpoint_completion_targetのチューニングの話で、次のタイムアウトでの発火までの時間で、OSにはフラッシュの時間を2分残すというところ。
2分はOS側でダーティをフラッシュしてもらうことで、チェックポイントはそんなにいい効果があるのか。。。実機で確認したほうが早いかな。

# チェックポイントのチューニングの基礎
大量の書き込みが行われるシステムのパフォーマンスでは、チェックポイントのチューニングが重要になってくる。ただ、その設定は難しくてユーザからは度々問い合わせが来ている。

# チェックポイントとは
そもそもPostgreSQLは全ての更新ログをWALというデータファイルに依存している。
WALによって耐久性が担保され、クラッシュしてもWALの内容を再生するだけでクラッシュの直前まで復旧が可能である。

データ更新とWALへの書き込みでwrite量が2倍になるようが、実際はこの方式のほうが性能が良くなる。ユーザは更新の際、WALの書き込み（フラッシュ）だけ待てばよくて、データはメモリ内だけで更新させており、フラッシュはバックグラウンドで行わせている。データはランダムな書き込みだけど、WALはシーケンシャルな書き込みであり、ちょうどよい。

ただ、この仕組みは裏を返すと復旧の際にはWALが必須というのは面倒なところがある。年間どれだけのWALが溜まるからどんだけの領域を用意しないといけないか考える必要があるし、WALを再生する量によってはリカバリ時間は結構長くなる。

与えられたWALの位置までの内容をDBが保証できる場合、ディスクにフラッシュして、リカバリの開始位置を進めることが可能。これにより、WALの再生時間はぐっと短くなる。さらに、フラッシュが終わった位置までのWALは不要になるので、WALの容量を減らすことができる。

ここらへんをバチッとやるのが、今回の話のメインである「チェックポイント」である。チェックポイント処理によって、WALで必要なディスク容量を抑えられ、且つリカバリ時間も短くなる。

極端な話、めちゃくちゃチェックポイントを打った場合、リカバリ時間はほぼ無くなりますが、非同期のデータを同期させ続けることになるので、ユーザへの深刻なインパクトはなる。

つまり、パフォーマンス、リカバリ時間、ディスク容量を考慮したいい感じのチェックポイントにチューニングするのが大事ってことです。

# チェックポイントの契機　（checkpoint_timeout、max_wal_sizeのチューニング）
チェックポイントの契機は以下のとおりです。

- CHEKPOINTコマンド
- チェックポイントが必要なコマンド（pg_start_backup、CREATE DATABASE等）
- 前回チェックポイントからの時間経過
- 前回チェックポイントからのWALの出力量

上の2つはチューニングどうこうの話ではないので、時間経過と出力量の2つの要素について考える。

```
checkpoint_timeout = 5min
max_wal_size = 1GB (before PostgreSQL 9.5 this was checkpoint_segments)
```

上記のデフォルト設定の場合、チェックポイントは5分置きに実施され、ディスク容量としてWAL領域ではだいたい1GB分ぐらいを使用するという設定になっています。

***
補足：
max_wal_sizeはトータルWAL量のソフトリミットであり、ディスク領域内の空きスペースをコントロールに使われている。また、チェックポイントの契機としても使われており、max_wal_sizeの1/3～1/2ぐらいのWALが出力されたら、チェックポイントが発火する。つまり、デフォルトだと300MB～500MBで発火する。値に少し幅があるのは、後述のcheckpoint_comletion_targetというパラメータの値も依存しているからです。
***

とりあえず言えるのは、PostgreSQLのデフォルト値は小さすぎるから**絶対変えたほうがよい**ということ。

## 設定値の決め方

で、どういう感じで設定値を決めていくかですが、以下の2stepで決めます。
1. いい感じのタイムアウト値を設定すること。
2. WAL生成量の契機で動かないように設定すること。

## checkpoint_timeout

リカバリに許容できる時間　RTO（recovery time objective）によって設定すべきという話ではあるけど、それがちょっとトリッキーです。タイムアウトのリミットはWAL生成の時間の中で有効で、直接リカバリタイムと結びついてはいない。しかもリカバリにどれくらい掛かるのかは明言できない。

- WALは複数プロセスによって作られるが、リカバリはシングルに実施される。
- ローカルのリカバリだけではなくスタンバイへのFOでも同様。
- リブート後のリカバリでシステムキャッシュがcoldのときも

色々書いているけど、一般的には30min～1hがいいと言われている。(なんと、9.6からはさらに大きくして1dayでもOKなケースもある。)ちなみに、短すぎるとfull page writeの量が多くなるので要注意です。。

よっぽど特殊な使い方とかでなければ、30minにしとけばよい！！

## max_wal_size

タイムアウト値が決めたら、次のstepではそのタイムアウト30分でどれくらいの量のWALが生成されるか推定して、その値からmax_wal_sizeを決定します。

30分でのWALの生成量を推定するには、いくつか方法がある。

- 実際のWAL位置をpg_current_xlog_insert_location()で確認して、30分でどれくらい値が変化するかを見る。
- log_checkpoint=onにして、サーバログからWALの量を見る。
- pg_stat_bgwriterでチェックポイントの数を見る。

以下、例として1つ目のpg_current_xlog_insert_location()での確認手順を示す。
***
■pgbenchを実行し、5minでのWAL量を求める。
（※ここの詳細はもとの記事を見てください）
動かしてみると、5分間の生成量：1,845,614,240bytes ＝1.8GB
となったので、タイムアウトが30minの場合は、1.8 * 6なので、だいたい10GBのWALが出力されることがわかります。
チェックポイントの契機は、max_wal_sizeの1/3～1/2なので、10GB * 3 で、max_wal_sizeは30GBを設定しておけばよいという計算になります。
※ほかの方法であっても、設定すべき値はだいたい一緒のところに落ち着きます。
***


# チェックポイントの拡散（checkpoint_completion_targetのチューニング）
チェックポイントの処理では、以下の3つの処理を行っている。
1. 共有メモリの全てのダーティなブロックを識別
2. ディスクにそれら全てをwrite
3. 変更のあった全てのファイルをディスクにfsybnc()する

これら3つの処理が全て終わったときにチェックポイントは完了となる。PG8.2までは、全てのダーティをwriteし、fsyncしできるだけ早くこれらを終わらせようとしていた。しかし、これはIOのストールを引き起こしユーザセッションに影響を与えていました。

PG83以降はチェックポイントを一定期間でゆっくり実施するような挙動に変更されました。その結果、OSがダーティなページをフラッシュする時間を与えられて、チェックポイントのfsyncのコストが下がることにつながりました。

DBとしては、OSがダーティなデータをバックグラウンドでフラッシュするために必要な時間を十分に残す必要がある。ページキャッシュからダーティなページがexpireされる時間はカーネルパラメータで30秒（デフォルト）に設定されている。

***
補足：
ここでカーネルパラメータのvm.dirty_background_bytesが重要になってくる。システムに大量のメモリがあると大量のダーティを許容してしまう。そうなるとせっかくチェックポイントを期間の中でゆっくりやらせる効果が薄くなる。
※デフォルトでは、vm.dirty_background_ratio=10が設定されている。
***

checkpoint_completion_targetは、次のチェックポイントが実行されるまでの間にどれくらいの時間を掛けて、書き出すか決めるパラメータです。タイムアウトのみがチェックポイントの契機だった場合、書き込み速度を抑えて、チェックポイントが開始されてから2.5分後に終わるようにゆっくり書きます。

次のチェックポイントが実行されるまでの2.5分をOSはディスクへのflushに使えるので、（バックグラウンドでOSが書いて、チェックポイントでfsyncするダーティを減らせるので）5分後のチェックポイントでのfsync()は安価に実施できる。

***
追記 11/15
いや、最後のfsync()までに時間を空けることで、ちょっとずつwriteしていた分をOS側でflushしてくれて、2.5分後のfsync()の負荷を減らせるということを言っているようです。
***

expireタイムアウトが30秒であることを考慮すると、システムに2.5分も残すのは過度に見える。checkpoint_completion_targetを0.85とかに増やすかもしれない。そうするとシステムの使える時間は30秒よりちょい多めの45秒になります。ただ、これは推奨できない。一時的に書き込み高騰し、ファイル数契機に発火し、5分以内にチェックポイントされた場合、OSが使える時間は30秒より少なくなる可能性があり、バックグラウンドでの書き出しが効かなくなり、fsyncが重くなる。

だから、間違ってもファイル契機でのチェックポイントは発生させてはいけない。


書き込みメインのワークロードのシステムでは、タイムアウトで長時間を設定することは少ないので、checkpoint_completion_targetの値では低すぎる。例えば、30分がタイムアウト、checkpoint_completion_target=0.5（デフォルト）だとDBは最初の15分で全てをwriteする。自身に書くのと、チェックポイントで2倍書く感じ。で、残りの15分は暇をする感じ。


***
checkpoint_completion_targetは以下のように算出できる。
checkpoint_completion_target = (checkpoint_timeout - 2min) / checkpoint_timeout
***

上記の式でcheckpoint_completion_targetを算出するとタイムアウト値が30minのときはcheckpoint_completion_target=0.93となる。
だいたいはこの式で決めても問題はないが、0.9以上の値になるとき、タイムアウトが極端にでかいシステムだと、推奨できないこともある。PG9.6だと、タイムアウトが1dayでもOKなケースもあるので、そのときは考慮の余地あり。

# サマリ

- チェックポイントはとにかく、時間で発火させるべき。
 - パフォーマンスとリカバリ時間での天秤
 - checkpoint_timeoutは15min～30minが一般的。1hでも問題なし。
- max_wal_size
 - checkpoint_timeoutで発生するWAL量の見積もりで算出する
- checkpoint_completion_target
 - OSがフラッシュするのに十分な時間を設定をすべき。
- vm.dirty_background_bytes
 - ページキャッシュに大量のダーティなページを残さないチューニングをすべき。