PostgreSQL + repmgr fencing処理の実装

## はじめに
PostgreSQLのHA管理ツールであるrepmgrでfencing処理を実装する方法について紹介します。

## SplitBrainとfencingの必要性
repmgrでHA構成を組む場合に、SplitBrainが発生するリスクを検討して、対処しておく必要があります。

SplitBrainとは、Primary(Active)-StandbyのHA構成などで、Primary-Standby間のネットワーク断などの影響により、Standbyが昇格し、両ノードが更新可能なPrimaryとなり、ノード間でデータの不整合が発生する状態を指します。
また、fencing処理とは、SplitBrain 発生時に早急に旧Primaryを停止させ、データ不整合を回避する為の処理です。

### 2ノード構成の場合
まず、単純な2ノード構成を想定します。

| role | DC |
|---|---|
| Primary | AZ1 |
| Standby | AZ2 |

AZ1-AZ2間にネットワーク断が発生したとします。
Primaryは正常に稼働を続けますが、StandbyからPrimaryに疎通は出来ないため、repmgrdはStandbyを昇格させます。その結果、AZ1、AZ2のPostgreSQLが共にPrimaryとなり、SplitBrainが発生してしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3716670/ab093e67-42b3-dff2-2508-0291b65a3acd.png)

### 3ノード構成の場合
次に3ノード構成を想定します。
| role | DC |
|---|---|
| Primary | AZ1 |
| Standby | AZ2 |
| Witness | AZ3 |

repmgrにWitnessという監視ノードの配置が可能です。
Witnessを配置して、primary_visibility_consensusをtrueにすることによって、(Witnessを含む)クラスタを構成する過半数ノードの合意を得なければ、Failoverしないように構成する事が出来ます。
この設定により、2ノード構成の例で説明したような、単純なAZ1-AZ2間のネットワーク断によるSplitBrainを防ぐ事が出来ます。
```
primary_visibility_consensus=true
```

では、AZ1がネットワーク的に隔離され、AZ2/AZ3と疎通出来ないケースではどうでしょうか？
この場合、AZ2のStandbyに加えて、AZ3のWitnessもPrimaryと疎通が出来ないと主張します。その結果、クラスタ内の2/3の合意が得られるので、FailoverしてSplitBrainが発生してしまいます。
この際に、AZ1のPrimaryと同じDCにアプリケーションが配置されていた場合、AZ1の古いPrimaryは更新され続けてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3716670/329604ca-dea7-3f91-93e8-a56d788c94e3.png)

こういったPrimaryがネットワーク的に隔離されたケースを想定して、fencing処理の実装が必要となります。

## Environment
primary 1台 + standby 1台 + witnessの構成を想定しています。

| role | host | IP |
|---|---|---|
primary | postgres-node1| 192.0.2.1 |
standby | postgres-node2| 192.0.2.2 |
witness | witness-node  | 192.0.2.3 |

#	Version
|#| Version |
|---|---|
OS|Rocky Linux 8.10
DB|PostgreSQL 15.4
HA|repmgr 5.3.3

# How？
repmgrには子ノードの切断、接続をモニタリングする機能があります。
一定間隔でPrimaryのpg_stat_replicationを監視して、子ノードが切断された場合にrepmgrdにchild_node_disconnectイベントを生成させる事が出来ます。
また、一定数の子ノードがPrimaryから切断され、一定期間が経過した場合に、child_nodes_disconnect_commandに指定された任意のスクリプトを実行させる事が出来ます。
この機能を利用して、Primaryがネットワークから孤立した場合のfencingを実装する事が出来ます。

具体的な例を挙げて以下で説明します。


# repmgr configuration
具体的なrepmgr.confの設定例を以下に示します。
この設定で、Primaryがネットワークから隔離され、Primary視点で子ノード(Standby + Witness)が切り離された状態になった場合、3-4秒(child_nodes_check_interval + child_nodes_disconnect_timeout)経過後、child_nodes_disconnect_commandに指定された fencing.sh を実行する、といったことが実現出来ます。


|name| setting |
|---|---|
| child_nodes_check_interval | 1 |
| child_nodes_disconnect_timeout | 3 |
| child_nodes_connected_min_count | 1 |
| child_nodes_disconnect_min_count | - |
| child_nodes_connected_include_witness | true |
| child_nodes_disconnect_command | /home/postgres/fencing.sh |

各パラメータの意味は以下の通りです。

- child_nodes_check_interval はrepmgrに登録されたノードリストとpg_stat_replicationの比較チェックを行う間隔を指定します。(秒)
- child_nodes_disconnect_timeout は接続されている子ノードの数が不十分と判断されてから、child_nodes_disconnect_command が実行されるまでの時間を指定します。(秒)
- child_nodes_connected_include_witness は子ノードの定義にWitnessを含むかどうかを定義します。child_nodes_connected_min_count と child_nodes_disconnect_min_count にWitnessを含むかどうかに影響します。
- child_nodes_connected_min_count はPrimaryに接続されている子ノードの数が、このパラメータで指定された数を下回ると、child_nodes_disconnect_command に指定されたスクリプトがrepmgrdによってトリガーされます。
- child_nodes_disconnect_min_count は今回は設定していませんが、child_nodes_connected_min_count と排他的なパラメータになっています。
child_nodes_connected_min_count が接続されているべき子ノードの数を指定するのに対して、child_nodes_disconnect_min_count は切断された子ノードの数の閾値となります。

パラメータ説明の詳細は以下の文書をご確認ください。
https://www.repmgr.org/docs/5.3/repmgrd-primary-child-disconnection.html

# Test
擬似的にPrimaryのネットワークが孤立した状態を発生させます。
iptablesで、PrimaryとStandby、Witness間の通信を遮断します。
192.0.2.2 .... Standby
192.0.2.3 .... Witness

```
iptables -A OUTPUT -d 192.0.2.2/32 -o eth0 -j REJECT
iptables -A INPUT  -d 192.0.2.2/32 -i eth0 -j REJECT
iptables -A OUTPUT -d 192.0.2.3/32 -o eth0 -j REJECT
iptables -A INPUT  -d 192.0.2.3/32 -i eth0 -j REJECT
```

fencing.shは実際にpostgresは停止させずにechoするだけにします。
```
cat /home/postgres/fencing.sh
#!/bin/bash
date >> /tmp/fencing.log
echo "systemctl stop postgres" >> /tmp/fencing.log
```

iptables実行後、repmgr cluster eventを確認します。
```
repmgr cluster event
 Node ID | Name         | Event                          | OK | Timestamp           | Details
---------+--------------+--------------------------------+----+---------------------+----------------------------------------------------------------------------------------
 1       | node1        | child_nodes_disconnect_command | t  | 2024-09-10 19:52:07 | "child_nodes_disconnect_command" successfully executed
 1       | node1        | child_node_disconnect          | t  | 2024-09-10 19:52:04 | standby node "node2" (ID: 2) has disconnected
 1       | node1        | child_node_disconnect          | t  | 2024-09-10 19:52:02 | witness node "witness10001" (ID: 10001) has disconnected
```

Witness、Standbyの順にchild_node_disconnectイベントが発生して、Primaryから切り離されたことを確認出来ます。
また、最後の子ノード(今回だとStandby)が切り離されてから、3秒後に、child_nodes_disconnect_commandイベントが発生して、コマンド実行が成功したことを確認できます。

次にfencing.logを確認します。
child_nodes_disconnect_commandが発生した際に、fencing.shが実行されたことが確認出来ます。
```
cat /tmp/fencing.log
Tue Sep 10 19:52:07 JST 2024
systemctl stop postgres
```

# まとめ
repmgrの子ノードの監視機能を使って、fencingの実装を行う方法を紹介しました。
引き続き、repmgrやPostgreSQLに関しての記事を作成していきたいと思いますので、よろしくお願いいたします。