PostgreSQLのarchiverの挙動を調べた

# はじめに
最近、業務でスタンバイでのarchiverの挙動について話題になっていたので、自分でも調べてみました。

確認対象はPostgreSQL 10.0です。（なぜ10.1にしなかった。。。）

この記事で書いているのは

- archiverの概要
- archive_mode = alwaysでの「対象ファイル」と「実行契機」の確認

# まずは基本。マニュアルに記載されている内容の確認
- WALのarchiveについて、色々と書かれているので一度は目を通しておきましょう。
    - [archive_mode](https://www.postgresql.org/docs/10/static/runtime-config-wal.html#GUC-ARCHIVE-MODE)
        - 各モードについての話が書かれている。
            - alwaysモードのとき、アーカイブからリストアされたファイル、streamingされたファイルをすべてアーカイブする。
    - [Continuous archiving in standby](https://www.postgresql.org/docs/10/static/warm-standby.html#CONTINUOUS-ARCHIVING-IN-STANDBY)
        - 継続されたアーカイブの取得についての考え方として、以下のような内容が書かれている。
            - 左のように「アーカイブ領域を共有する」か右のように「各ローカルに持たせる」のかという話。
                ![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/125929/475048f7-f44f-fb86-d250-013b2ad304ef.png)
            - アーカイブWALを上書きをすべきではないという話。

特に注目なのは、**「In always mode, all files restored from the archive or streamed with streaming replication will be archived (again). 」** という部分です。

今回はこの処理（**リストアされたアーカイブWALをもう一度アーカイブする**）をソースで確認してみようと思います。

# archiverの概要
まずは簡単にarchiverプロセスについて紹介。

- archive_modeを有効にしている場合、PostgreSQL起動時に子プロセスとしてforkされるプロセス
    - スタンバイで有効にする場合には「always」を設定する。
- pgarch_start() -> PgArchiverMain() -> pgarch_MainLoop() -> WaitLatch()と呼ばれていき、プロセス起動後は待機している。
- archiveの実行契機は以下のいずれか
    - 時間契機(PGARCH_AUTOWAKE_INTERVAL デフォルト：60秒)
    - PostmasterからのSIGNAL(PMSIGNAL_WAKEN_ARCHIVER)を受け取る
- archiveの実行対象
    - archive_statusで.readyとなっているWAL

# 本題
ここからは、リストアされたアーカイブWALの.readyファイルがいつ作成されているのかを確認していきます。

## そもそも.readyファイルはどこで作成されているか

アーカイブ対象を決める.readyファイルの作成自体はココでやっています。

```xlogarchive.c
511 void
512 XLogArchiveNotify(const char *xlog)
513 {
...
517         /* insert an otherwise empty file called <XLOG>.ready */
518         StatusFilePath(archiveStatusPath, xlog, ".ready");
519         fd = AllocateFile(archiveStatusPath, "w");
...
537         /* Notify archiver that it's got something to do */
538         if (IsUnderPostmaster)
539                 SendPostmasterSignal(PMSIGNAL_WAKEN_ARCHIVER);
```

- XLogArchiveNotify関数でやっているのは
    - .readyファイルの作成
    - archiverプロセスにarchive処理を行うようにSIGNALを送る

つまり、この関数が呼ばれたら対象のWALの.readyが作成され、そのままarchiverプロセスにarchiveをリクエストしている。

## XLogArchiveNotify関数の呼び出し元

呼び出し元の関数を追っていくとこんな感じでした。


    XLogArchiveNotify() <--- 1. XLogArchiveNotifySeg() <--- XLogWrite() <--- 1-1. XLogFlush()
                      |                                                   |- 1-2. XLogBackgroundFlush()
                      |                                                   |- 1-3. AdvanceXLInsertBuffer()
                      |<--- 2. StartupXLOG()
                      |        |- XLogPageRead()経由で呼ぶ
                      |        |- 直接呼ぶ
                      |
                      |<--- 3. WalReceiverMain()

これだと何かわからないので、もう少し書いてみるとこんな感じです。（大体あっているはず。間違えていたらご指摘いただけると幸いです汗）

1. WALファイルがwrite/flushされていき、ファイルの終端に到達して新しいファイルに切り替わったとき
1. Startupプロセスからいくつかのパターンで呼んでいる。
    - 起動時のrestore_command実行後に取得したファイルをアーカイブしている。★ココでやっている！！
    - `ArchiveRecoveryRequested`が有効のとき(recovery.confが存在し、PITRさせた時)に呼んでいる
        - `writeTimeLineHistory()`で.historyファイルを作成し、即座に.historyファイルをアーカイブしている
        - 古いTLIのWALを、`XLogArchiveNotify(partialfname);`でpartialファイルとしてアーカイブしている。
1. walreceiverプロセスがいくつかのタイミングで呼んでいる
    - streamingのループ内で`XLogWalRcvProcessMsg()`でWALレコードを受け取って、次のセグメントファイルに到達したとき
        - `XLogWalRcvWrite()`でディスクに書くときにWALの位置が現在開いているファイル内に収まっているか確認している。
            - セグメントファイルに入っていない場合は`XLogArchiveNotify(xlogfname)`を呼んで、アーカイブして次のセグメントファイルに進む。
    - streamingのループを抜けた後、要求したTLIに到達したWALの最後を受け取ったとき
        - `XLogArchiveNotify(xlogfname)`を呼んで、最後のWALをアーカイブしている。
        - その後は、Startupプロセスから新しいTLI等が通知されるまで`WalRcvWaitForStartPosition()`で待機している。

# 該当のソースを紹介
上記の★部分が今回見たかったところですが、せっかくなので、★以外の部分も少し確認したので合わせて紹介します。

いつか、誰かがここらへんのソースを読むときの参考になれば幸いです。

## XLogArchiveNotifySeg()
見たらわかる簡単なやつや！

XLogArchiveNotify()をwrappingしているだけです。

```xlogarchive.c
542 /*
543  * Convenience routine to notify using segment number representation of filename
544  */
545 void
546 XLogArchiveNotifySeg(XLogSegNo segno)
547 {
548         char            xlog[MAXFNAMELEN];
549
550         XLogFileName(xlog, ThisTimeLineID, segno);
551         XLogArchiveNotify(xlog);
552 }
```

`XLogArchiveNotifySeg()`は、WriteRqstを受け取ったWALをwriteかfsyncしている`XlogWrite()`でのみ使用されています。

で、このWALの行っているのが以下の関数たちです。

- XLogFlush()：指定された位置までXLOGをflushしている関数
    - 色んな所から呼ばれているチェックポイントとかでも使われている。
- XLogBackgroundFlush()：位置の指定はなく、xlogのwrite、flushを行っている関数
    - walwriterプロセスから永続的に呼ばれている
- AdvanceXLInsertBuffer()：WALバッファの初期化
    - XlogBackgroudFlush()からも呼ばれている。

## StartupXLOG()

名前からもなんとなくわかるStartup processのメインループをやっている関数。

```xlog.c
6193 void
6194 StartupXLOG(void)
6195 {
...
6298         /*
6299          * Check for recovery control file, and if so set up state for offline
6300          * recovery
6301          */
6302         readRecoveryCommandFile();  // recovery.confが存在する場合ここでArchiveRecoveryRequestedがtrueになる。
...
6508         /*
6509          * Get the last valid checkpoint record.  If the latest one according
6510          * to pg_control is broken, try the next-to-last one.
6511          */
6512         checkPointLoc = ControlFile->checkPoint;
6513         RedoStartLSN = ControlFile->checkPointCopy.redo;
6514         record = ReadCheckpointRecord(xlogreader, checkPointLoc, 1, true); ★
...
7021                         /*
7022                          * main redo apply loop
7023                          */
7024                         do
7025                         {
...
7233                                 /* Exit loop if we reached inclusive recovery target */
7234                                 if (recoveryStopsAfter(xlogreader))
7235                                 {
7236                                         reachedStopPoint = true;
7237                                         break;
7238                                 }
7239
7240                                 /* Else, try to fetch the next WAL record */
7241                                 record = ReadRecord(xlogreader, InvalidXLogRecPtr, LOG, false);
7242                         } while (record != NULL);
7243
7244                         /*
7245                          * end of main redo apply loop
7246                          */
...
7591         if (ArchiveRecoveryRequested)
7592         {
...
7633                 if (EndOfLog % XLOG_SEG_SIZE != 0 && XLogArchivingActive())
7634                 {
7635                         char            origfname[MAXFNAMELEN];
7636                         XLogSegNo       endLogSegNo;
7637
7638                         XLByteToPrevSeg(EndOfLog, endLogSegNo);
7639                         XLogFileName(origfname, EndOfLogTLI, endLogSegNo);
7640
7641                         if (!XLogArchiveIsReadyOrDone(origfname))
7642                         {
...
7658                                 XLogArchiveNotify(partialfname);★
```

今回見たかった、archiveからrestoreされたWALが再度archiveされているのは、`6514 record = ReadCheckpointRecord(xlogreader, checkPointLoc, 1, true);`でした。

何をやっているかというと、チェックポイントレコードを探すときにrestore_commandが呼んでおり、その後、取得したWALに対してXLogArchiveNotify()を実行しているというものでした。

```
StartupXLOG()
  -> ReadCheckpointRecord()
    -> ReadRecord()
      -> XLogReadRecord()
        -> ReadPageInternal()
          -> XLogPageRead()
            -> WaitForWALToBecomeAvailable()
              -> XLogFileReadAnyTLI()
                -> XLogFileRead()
                  -> KeepFileRestoredFromArchive()
                    -> XLogArchiveNotify()
```

なるほど、これでrestoreしたWALがもう一度archiveされているわけですね～。

### ＜余談＞
本題からは完全にそれますが（ ・∀・）へ～　ってなったのは、ココ。

```
6302         readRecoveryCommandFile();
```

- recovery.confでPITRの設定等はこの関数内でparseされている。
    - で、この関数内でArchiveRecoveryRequestedのフラグが有効になることで、以降のpartialファイルやhistoryファイルの作成が行われる。
- もちろんrecovery.confがなければ、ArchiveRecoveryRequestedはfalseのまま返却される。


ちなみにStartupプロセスのメインループを抜けるかどうかの判定はココにある。

```
7233                                 /* Exit loop if we reached inclusive recovery target */
7234                                 if (recoveryStopsAfter(xlogreader))
7235                                 {
7236                                         reachedStopPoint = true;
7237                                         break;
7238                                 }
```

- readRecoveryCommandFile()で読み込んだrecovery.confで設定した内容に到達したかどうかでループを抜けている。
- 抜けるのはどっちか。
    - recovery_target_*が設定あり：その目的のポイントまでのリカバリが完了したときに抜ける
    - recovery_target_*の設定なし：リカバリできるとこまでリカバリして、一貫性をもった状態になったときに抜ける


## WalReceiverMain()

walreceiverのソースですね。

```walreceiver.c
186 /* Main entry point for walreceiver process */
187 void
188 WalReceiverMain(void)
189 {
...
597                 /*
598                  * End of WAL reached on the requested timeline. Close the last
599                  * segment, and await for new orders from the startup process.
600                  */
601                 if (recvFile >= 0)
602                 {
...
616                         XLogFileName(xlogfname, recvFileTLI, recvSegNo);
617                         if (XLogArchiveMode != ARCHIVE_MODE_ALWAYS)
618                                 XLogArchiveForceDone(xlogfname);
619                         else
620                                 XLogArchiveNotify(xlogfname);★
621                 }
```

- 該当の処理に入るのはstreamingのループを抜けた後（要求したTLIに到達したWALの最後を受け取ったとき）
    - archive_mode = always のときは.readyファイルを作成する。
    - always 以外のときはXLogArchiveForceDone()で、強制的に.doneを作成してアーカイブを完了ステータスにする。
        - つまり、アーカイブしていないという話。

# さいごに
なんかダラダラ書いてしまいましたが、とりあえずマニュアルで書いてあった再archiveしている処理を確認できたので良しとします。

アーカイブ周りって、なんとなくわかるけど実際どうなっているのかちゃんと読んだことがなかったので、良い機会になりました。