Chuyển đến nội dung chính

レッスン 11: Patroni コールバック

コールバック スクリプト (on_start、on_stop、on_role_change) を作成し、通知用のカスタム スクリプトを作成し、監視システムと統合します。

🔒 DevSecOps — レッスン 11 レッスン 11: Patroni コールバック__HTMLTAG_53___

Patroni と PostgreSQL の高可用性etcd

パート 3: クラスター管理

xdev.asia_

目標

このレッスンを終えると、次のことができるようになります:

  • Patroni コールバックとは何か、いつトリガーされるかを理解する__HTMLTAG_71__
  • ライフサイクル イベント用のカスタム スクリプトを実装する
  • 自動化されたコールバックを構成するタスク
  • ロール変更の処理 (プライマリ ↔ レプリカ)
  • 通知と監視フックのセットアップ
  • コールバックの失敗のトラブルシューティング

1。 Callbacks Overview

1.1.コールバックとは何ですか?

コールバック = カスタム スクリプトは、クラスタのライフサイクル イベント で Patroni によって実行されます。

使用ケース_:

  • 🔔 通知: フェイルオーバーが発生したときにチームに警告するra
  • 🔧 Automation: DNS、ロードバランサー構成を更新
  • 📊 Monitoring: メトリクスをモニタリングにプッシュしますシステム
  • 🚦 トラフィック管理: アプリケーショントラフィックをリダイレクト
  • 🔐 セキュリティ: 認証情報のローテーション、更新ファイアウォール ルール
  • 📝 ロギング: カスタム監査ログ

1.2。 Available callbacks

Patroni provides callback events:

_コールバック__トリガー使用Case_
on_startPostgreSQL の開始前開始前チェック、マウントボリューム_
on_stopPostgreSQL が停止する前にクリーンアップ、通知アプリケーション_
on_restartPostgreSQL の再起動前ログの再起動イベント
_on_reload_PostgreSQL 構成のリロード後構成を確認変更
on_role_change役割の変更 (プライマリ ↔ レプリカ)ほとんど重要_ - DNS、LB を更新
_pre_promote_レプリカが昇格される前 primary_Final checks before promotion_
post_promote_レプリカがプライマリに昇格した後_更新監視を送信アラート_

1.3。コールバック実行フロー

Example: Failover scenario

Old Primary crashes ↓ Patroni detects failure (after TTL expires) ↓ Patroni selects best replica (node2) ↓ pre_promote callback runs on node2 ↓ PostgreSQL promoted to primary (pg_promote) ↓ post_promote callback runs on node2 ↓ on_role_change callback runs on node2 (role=master) ↓ Other replicas detect new leader ↓ on_role_change callback runs on replicas (role=replica) ↓ Failover complete

1.4。コールバック環境変数

Patroni は、環境変数 をスクリプトに渡します:

__ _HTMLTAG_266______HTMLTA G_278______ HTMLTAG_290___
変数_説明例___HTMLTAG_23 4___
_PATRONI_ROLE_後の現在の役割変更_master, replica__HTMLTAG_251 ___
PATRONI_SCOPEクラスターname_postgres_
PATRONI_NAMENode name_node1
PATRONI_CLUSTER_NAMEクラスター名(エイリアス)postgres
PATRONI_VERSIONPatroniバージョン_3.2.0

on_role_change:

___ HTMLTAG_318___
_変数_値
PATRONI_NEW_ROLE新規役割: マスター または レプリカ_
PATRONI_OLD_ROLE前の役割

2. Patroni

2.1 でコールバックを構成します。基本構成

patroni.yml 内:

scope: postgres
name: node1

postgresql: callbacks: on_start: /var/lib/postgresql/callbacks/on_start.sh on_stop: /var/lib/postgresql/callbacks/on_stop.sh on_restart: /var/lib/postgresql/callbacks/on_restart.sh on_reload: /var/lib/postgresql/callbacks/on_reload.sh on_role_change: /var/lib/postgresql/callbacks/on_role_change.sh

キーポイント:

  • パスは必須です絶対_
  • __HTM であることLTAG_366___スクリプトは実行可能 (chmod +x)
  • 所有である必要がありますby postgres user
  • 完了する必要があります すぐに (<30 秒)
  • ゼロ以外の終了コード= コールバックが失敗しました (ログに記録されますが、操作はブロックされません)

2.2。コールバック ディレクトリ

# On all 3 nodes
sudo mkdir -p /var/lib/postgresql/callbacks
sudo chown postgres:postgres /var/lib/postgresql/callbacks
sudo chmod 750 /var/lib/postgresql/callbacks

3 を作成します。コールバック スクリプト

3.1 を実装します。 on_start コールバック

ユースケース: 開始前検証、マウントチェック

Script: /var/lib/post gresql/callbacks/on_start.sh

#!/bin/bash

on_start.sh - Runs before PostgreSQL starts

set -e

LOG_FILE="/var/log/patroni/callbacks.log" TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

Logging function

log() { echo "[$TIMESTAMP] [ON_START] $1" | tee -a "$LOG_FILE" }

log "Starting PostgreSQL on $PATRONI_NAME" log "Role: $PATRONI_ROLE" log "Cluster: $PATRONI_SCOPE"

Check disk space

DISK_USAGE=$(df -h /var/lib/postgresql | awk 'NR==2 {print $5}' | sed 's/%//') if [ "$DISK_USAGE" -gt 90 ]; then log "ERROR: Disk usage is ${DISK_USAGE}% - critically high!" exit 1 fi log "Disk usage: ${DISK_USAGE}%"

Check if data directory is mounted

if ! mountpoint -q /var/lib/postgresql/18/data; then log "WARNING: Data directory is not a mount point" fi

Check network connectivity to etcd

for ETCD_HOST in 10.0.1.11 10.0.1.12 10.0.1.13; do if ! nc -zw3 "$ETCD_HOST" 2379 2>/dev/null; then log "ERROR: Cannot reach etcd at $ETCD_HOST:2379" exit 1 fi done log "etcd connectivity verified"

log "Pre-start checks passed" exit 0

スクリプトの作成_:

# On all nodes
sudo tee /var/lib/postgresql/callbacks/on_start.sh > /dev/null << 'EOF'
#!/bin/bash
set -e
LOG_FILE="/var/log/patroni/callbacks.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
log() { echo "[$TIMESTAMP] [ON_START] $1" | tee -a "$LOG_FILE"; }

log "Starting PostgreSQL on $PATRONI_NAME (Role: $PATRONI_ROLE)"

Disk space check

DISK_USAGE=$(df -h /var/lib/postgresql | awk 'NR==2 {print $5}' | sed 's/%//') if [ "$DISK_USAGE" -gt 90 ]; then log "ERROR: Disk usage ${DISK_USAGE}% too high" exit 1 fi log "Disk usage: ${DISK_USAGE}%"

log "Pre-start checks passed" exit 0 EOF

sudo chmod +x /var/lib/postgresql/callbacks/on_start.sh sudo chown postgres:postgres /var/lib/postgresql/callbacks/on_start.sh

3.2。 on_stop コールバック

ユースケース: 正常なシャットダウン通知

Script: /var/lib/ postgresql/callbacks/on_stop.sh

#!/bin/bash

on_stop.sh - Runs before PostgreSQL stops

set -e

LOG_FILE="/var/log/patroni/callbacks.log" TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

log() { echo "[$TIMESTAMP] [ON_STOP] $1" | tee -a "$LOG_FILE" }

log "Stopping PostgreSQL on $PATRONI_NAME" log "Role: $PATRONI_ROLE"

Notify monitoring system

if command -v curl >/dev/null 2>&1; then curl -s -X POST http://monitoring.example.com/api/events
-H "Content-Type: application/json"
-d "{ "event": "postgresql_stop", "node": "$PATRONI_NAME", "role": "$PATRONI_ROLE", "timestamp": "$TIMESTAMP" }" || log "WARNING: Failed to notify monitoring" fi

log "PostgreSQL stop initiated" exit 0

スクリプトの作成:

sudo tee /var/lib/postgresql/callbacks/on_stop.sh > /dev/null << 'EOF'
#!/bin/bash
set -e
LOG_FILE="/var/log/patroni/callbacks.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
log() { echo "[$TIMESTAMP] [ON_STOP] $1" | tee -a "$LOG_FILE"; }

log "Stopping PostgreSQL on $PATRONI_NAME (Role: $PATRONI_ROLE)" exit 0 EOF

sudo chmod +x /var/lib/postgresql/callbacks/on_stop.sh sudo chown postgres:postgres /var/lib/postgresql/callbacks/on_stop.sh

3.3。 on_role_change コールバック (最も重要!)

ユースケース: DNS、ロードバランサーの更新、送信通知。

Script: /var/lib/post gresql/callbacks/on_role_change.sh

#!/bin/bash

on_role_change.sh - Runs when role changes (master ↔ replica)

set -e

LOG_FILE="/var/log/patroni/callbacks.log" TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

log() { echo "[$TIMESTAMP] [ROLE_CHANGE] $1" | tee -a "$LOG_FILE" }

log "==========================================" log "Role change detected on $PATRONI_NAME" log "Cluster: $PATRONI_SCOPE" log "Old role: ${PATRONI_OLD_ROLE:-unknown}" log "New role: $PATRONI_ROLE" log "=========================================="

Function: Update DNS

update_dns() { local NEW_PRIMARY_IP="$1"

log "Updating DNS record for primary.postgres.local -&gt; $NEW_PRIMARY_IP"

# Example using nsupdate (BIND DNS)
# nsupdate -k /etc/dns/Kpostgres.+157+12345.key &lt;&lt; EOF
# server dns-server.local
# zone postgres.local
# update delete primary.postgres.local A
# update add primary.postgres.local 60 A $NEW_PRIMARY_IP
# send
# EOF

# Or using API (e.g., Route53, Cloudflare)
# aws route53 change-resource-record-sets --hosted-zone-id Z1234 ...

log "DNS update completed"

}

Function: Update HAProxy

update_haproxy() { local NEW_PRIMARY_IP="$1"

log "Notifying HAProxy about new primary: $NEW_PRIMARY_IP"

# Use HAProxy stats socket
# echo "set server postgres/primary addr $NEW_PRIMARY_IP" | \
#     socat stdio /var/run/haproxy.sock

log "HAProxy updated"

}

Function: Send Slack notification

send_notification() { local MESSAGE="$1" local WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

log "Sending notification: $MESSAGE"

curl -s -X POST "$WEBHOOK_URL" \
    -H "Content-Type: application/json" \
    -d "{
        \"text\": \"🔄 PostgreSQL Role Change\",
        \"attachments\": [{
            \"color\": \"warning\",
            \"fields\": [
                {\"title\": \"Node\", \"value\": \"$PATRONI_NAME\", \"short\": true},
                {\"title\": \"Cluster\", \"value\": \"$PATRONI_SCOPE\", \"short\": true},
                {\"title\": \"Old Role\", \"value\": \"${PATRONI_OLD_ROLE:-N/A}\", \"short\": true},
                {\"title\": \"New Role\", \"value\": \"$PATRONI_ROLE\", \"short\": true},
                {\"title\": \"Time\", \"value\": \"$TIMESTAMP\", \"short\": false}
            ]
        }]
    }" || log "WARNING: Notification failed"

}

Main logic

case "$PATRONI_ROLE" in master) log "This node is now PRIMARY"

    # Get this node's IP
    NODE_IP=$(hostname -I | awk '{print $1}')
    log "Node IP: $NODE_IP"
    
    # Update DNS to point to new primary
    update_dns "$NODE_IP"
    
    # Update load balancer
    update_haproxy "$NODE_IP"
    
    # Send notification
    send_notification "Node $PATRONI_NAME promoted to PRIMARY"
    
    # Set marker file for applications
    touch /tmp/postgres_is_primary
    rm -f /tmp/postgres_is_replica
    
    log "Primary promotion tasks completed"
    ;;
    
replica)
    log "This node is now REPLICA"
    
    # Remove primary marker
    rm -f /tmp/postgres_is_primary
    touch /tmp/postgres_is_replica
    
    # Send notification if demoted from primary
    if [ "${PATRONI_OLD_ROLE}" = "master" ]; then
        send_notification "Node $PATRONI_NAME demoted to REPLICA"
    fi
    
    log "Replica role tasks completed"
    ;;
    
*)
    log "Unknown role: $PATRONI_ROLE"
    exit 1
    ;;

esac

log "Role change handling completed successfully" exit 0

作成本番環境対応スクリプト:

sudo tee /var/lib/postgresql/callbacks/on_role_change.sh > /dev/null << 'EOF'
#!/bin/bash
set -e

LOG_FILE="/var/log/patroni/callbacks.log" TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') log() { echo "[$TIMESTAMP] [ROLE_CHANGE] $1" | tee -a "$LOG_FILE"; }

log "==========================================" log "Role change: $PATRONI_NAME" log "Old role: ${PATRONI_OLD_ROLE:-unknown}" log "New role: $PATRONI_ROLE" log "=========================================="

case "$PATRONI_ROLE" in master) log "This node is now PRIMARY" NODE_IP=$(hostname -I | awk '{print $1}') log "Node IP: $NODE_IP"

    # TODO: Update DNS, load balancer, etc.
    # update_dns "$NODE_IP"
    
    touch /tmp/postgres_is_primary
    rm -f /tmp/postgres_is_replica
    ;;
    
replica)
    log "This node is now REPLICA"
    rm -f /tmp/postgres_is_primary
    touch /tmp/postgres_is_replica
    ;;
    
*)
    log "Unknown role: $PATRONI_ROLE"
    exit 1
    ;;

esac

log "Role change completed" exit 0 EOF

sudo chmod +x /var/lib/postgresql/callbacks/on_role_change.sh sudo chown postgres:postgres /var/lib/postgresql/callbacks/on_role_change.sh

3.4。 on_restart コールバック

ユースケース: ログを再起動し、計画されたメンテナンスについて通知します。

sudo tee /var/lib/postgresql/callbacks/on_restart.sh > /dev/null << 'EOF'
#!/bin/bash
set -e
LOG_FILE="/var/log/patroni/callbacks.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
log() { echo "[$TIMESTAMP] [ON_RESTART] $1" | tee -a "$LOG_FILE"; }

log "Restarting PostgreSQL on $PATRONI_NAME (Role: $PATRONI_ROLE)" exit 0 EOF

sudo chmod +x /var/lib/postgresql/callbacks/on_restart.sh sudo chown postgres:postgres /var/lib/postgresql/callbacks/on_restart.sh

3.5。 on_reload コールバック

使用例: 構成の変更が適用されたことを確認します。_

sudo tee /var/lib/postgresql/callbacks/on_reload.sh > /dev/null << 'EOF'
#!/bin/bash
set -e
LOG_FILE="/var/log/patroni/callbacks.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
log() { echo "[$TIMESTAMP] [ON_RELOAD] $1" | tee -a "$LOG_FILE"; }

log "Configuration reloaded on $PATRONI_NAME"

Verify critical settings

MAX_CONN=$(sudo -u postgres psql -t -c "SHOW max_connections;") log "max_connections = $MAX_CONN"

exit 0 EOF

sudo chmod +x /var/lib/postgresql/callbacks/on_reload.sh sudo chown postgres:postgres /var/lib/postgresql/callbacks/on_reload.sh

3.6。ログ ディレクトリ

# On all nodes
sudo mkdir -p /var/log/patroni
sudo chown postgres:postgres /var/log/patroni
sudo chmod 750 /var/log/patroni

4 を作成します。 Patroni 構成

4.1 を更新します。 patroni.yml にコールバックを追加

3 つのノードすべてで、 /etc/patroni/patroni.yml:

scope: postgres
namespace: /service/
name: node1  # node2, node3 for other nodes

restapi: listen: 0.0.0.0:8008 connect_address: 10.0.1.11:8008 # Change per node

etcd3: hosts: 10.0.1.11:2379,10.0.1.12:2379,10.0.1.13:2379

bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 synchronous_mode: true synchronous_mode_strict: false

postgresql:
  parameters:
    max_connections: 100
    shared_buffers: 256MB
    wal_level: replica
    max_wal_senders: 10
    max_replication_slots: 10

postgresql: listen: 0.0.0.0:5432 connect_address: 10.0.1.11:5432 # Change per node data_dir: /var/lib/postgresql/18/data bin_dir: /usr/lib/postgresql/18/bin

authentication: replication: username: replicator password: replicator_password superuser: username: postgres password: postgres_password

parameters: unix_socket_directories: '/var/run/postgresql'

✅ Add callbacks section

callbacks: on_start: /var/lib/postgresql/callbacks/on_start.sh on_stop: /var/lib/postgresql/callbacks/on_stop.sh on_restart: /var/lib/postgresql/callbacks/on_restart.sh on_reload: /var/lib/postgresql/callbacks/on_reload.sh on_role_change: /var/lib/postgresql/callbacks/on_role_change.sh

tags: nofailover: false noloadbalance: false clonefrom: false nosync: false

4.2 を編集します。 Patroni 設定

# On all 3 nodes
sudo systemctl reload patroni

Verify callbacks configured

patronictl show-config postgres

5 をリロードします。コールバックをテスト

5.1。 _restart

# Restart a node
patronictl restart postgres node2

Check logs

sudo tail -f /var/log/patroni/callbacks.log

Expected output:

[2024-11-25 10:30:15] [ON_RESTART] Restarting PostgreSQL on node2

5.2 でテストします。 on_reload

# Reload configuration
patronictl reload postgres node2

Check logs

sudo tail /var/log/patroni/callbacks.log

Expected:

[2024-11-25 10:32:45] [ON_RELOAD] Configuration reloaded on node2

5.3 でテストします。 on_role_change のテスト (フェイルオーバー)

⚠️ 重要: 非運用環境でテストしてください!

# 1. Check current primary
patronictl list postgres

node1 is Leader

2. Stop primary

sudo systemctl stop patroni # On node1

3. Watch logs on node2 (will become new primary)

sudo tail -f /var/log/patroni/callbacks.log

Expected output:

[2024-11-25 10:35:10] [ROLE_CHANGE] ==========================================

[2024-11-25 10:35:10] [ROLE_CHANGE] Role change: node2

[2024-11-25 10:35:10] [ROLE_CHANGE] Old role: replica

[2024-11-25 10:35:10] [ROLE_CHANGE] New role: master

[2024-11-25 10:35:10] [ROLE_CHANGE] This node is now PRIMARY

[2024-11-25 10:35:10] [ROLE_CHANGE] Node IP: 10.0.1.12

[2024-11-25 10:35:10] [ROLE_CHANGE] Role change completed

4. Verify marker file

ls -la /tmp/postgres_is_*

-rw-r--r-- 1 postgres postgres 0 Nov 25 10:35 /tmp/postgres_is_primary

5. Restart node1 (will rejoin as replica)

sudo systemctl start patroni # On node1

6. Check node1 logs

sudo tail /var/log/patroni/callbacks.log

[2024-11-25 10:36:30] [ROLE_CHANGE] Old role: master

[2024-11-25 10:36:30] [ROLE_CHANGE] New role: replica

[2024-11-25 10:36:30] [ROLE_CHANGE] This node is now REPLICA

6。高度なコールバックの例_

6.1。 nsupdate を使用した DNS 更新

前提条件: DDNS が有効になっている BIND DNS サーバー。

#!/bin/bash

Update DNS via nsupdate

update_dns() { local NEW_PRIMARY_IP="$1" local DNS_KEY="/etc/dns/Kpostgres.+157+12345.key" local DNS_SERVER="dns.example.com" local ZONE="postgres.local" local RECORD="primary.postgres.local"

log "Updating DNS: $RECORD -&gt; $NEW_PRIMARY_IP"

nsupdate -k "$DNS_KEY" &lt;&lt; EOF

server $DNS_SERVER zone $ZONE update delete $RECORD A update add $RECORD 60 A $NEW_PRIMARY_IP send EOF

if [ $? -eq 0 ]; then
    log "DNS updated successfully"
else
    log "ERROR: DNS update failed"
    return 1
fi

}

In on_role_change.sh

if [ "$PATRONI_ROLE" = "master" ]; then NODE_IP=$(hostname -I | awk '{print $1}') update_dns "$NODE_IP" fi

6.2。 HAProxy バックエンドの更新

統計ソケット経由:

update_haproxy() {
local NEW_PRIMARY_IP="$1"
local HAPROXY_SOCKET="/var/run/haproxy.sock"

log "Updating HAProxy: primary backend -&gt; $NEW_PRIMARY_IP"

echo "set server postgres-primary/node addr $NEW_PRIMARY_IP port 5432" | \
    socat stdio "$HAPROXY_SOCKET"

echo "set server postgres-primary/node state ready" | \
    socat stdio "$HAPROXY_SOCKET"

log "HAProxy backend updated"

}

6.3。領事サービス登録

register_in_consul() {
local ROLE="$1"
local NODE_IP="$2"

log "Registering in Consul: $PATRONI_NAME as $ROLE"

curl -s -X PUT "http://consul.local:8500/v1/agent/service/register" \
    -H "Content-Type: application/json" \
    -d "{
        \"Name\": \"postgres-$ROLE\",
        \"ID\": \"postgres-$PATRONI_NAME\",
        \"Address\": \"$NODE_IP\",
        \"Port\": 5432,
        \"Tags\": [\"$ROLE\", \"patroni\"],
        \"Check\": {
            \"TCP\": \"$NODE_IP:5432\",
            \"Interval\": \"10s\",
            \"Timeout\": \"2s\"
        }
    }"

log "Consul registration completed"

}

Usage

NODE_IP=$(hostname -I | awk '{print $1}') register_in_consul "$PATRONI_ROLE" "$NODE_IP"

6.4。電子メール通知_

send_email_alert() {
local SUBJECT="$1"
local BODY="$2"
local RECIPIENT="[email protected]"

log "Sending email alert: $SUBJECT"

echo "$BODY" | mail -s "$SUBJECT" "$RECIPIENT"

log "Email sent to $RECIPIENT"

}

In on_role_change.sh

if [ "$PATRONI_ROLE" = "master" ]; then send_email_alert
"[ALERT] PostgreSQL Failover: $PATRONI_NAME promoted to PRIMARY"
"Cluster: $PATRONI_SCOPE Node: $PATRONI_NAME Old Role: ${PATRONI_OLD_ROLE} New Role: $PATRONI_ROLE Time: $TIMESTAMP

Action required: Verify cluster health" fi

6.5。 Slack/Teams Webhook

詳細な Slack 通知:

send_slack_alert() {
local WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
local COLOR="$1"  # good, warning, danger
local TITLE="$2"
local MESSAGE="$3"

curl -s -X POST "$WEBHOOK_URL" \
    -H "Content-Type: application/json" \
    -d "{
        \"username\": \"Patroni Monitor\",
        \"icon_emoji\": \": database:\",
        \"attachments\": [{
            \"color\": \"$COLOR\",
            \"title\": \"$TITLE\",
            \"text\": \"$MESSAGE\",
            \"fields\": [
                {\"title\": \"Cluster\", \"value\": \"$PATRONI_SCOPE\", \"short\": true},
                {\"title\": \"Node\", \"value\": \"$PATRONI_NAME\", \"short\": true},
                {\"title\": \"Old Role\", \"value\": \"${PATRONI_OLD_ROLE:-N/A}\", \"short\": true},
                {\"title\": \"New Role\", \"value\": \"$PATRONI_ROLE\", \"short\": true},
                {\"title\": \"Timestamp\", \"value\": \"$TIMESTAMP\", \"short\": false}
            ],
            \"footer\": \"PostgreSQL HA\",
            \"footer_icon\": \"https://www.postgresql.org/media/img/about/press/elephant.png\"
        }]
    }"

}

Usage

if [ "$PATRONI_ROLE" = "master" ]; then send_slack_alert "warning"
"🚨 Failover Event"
"Node $PATRONI_NAME has been promoted to PRIMARY" fi

6.6。メトリクスはモニタリングにプッシュ_

Prometheus Pushgateway にプッシュ:_

push_metrics() {
local PUSHGATEWAY="http://pushgateway.local:9091"
local JOB="patroni_callbacks"

log "Pushing metrics to Prometheus"

cat &lt;&lt; EOF | curl -s --data-binary @- "$PUSHGATEWAY/metrics/job/$JOB/instance/$PATRONI_NAME"

TYPE patroni_role_change counter

HELP patroni_role_change Number of role changes

patroni_role_change{cluster="$PATRONI_SCOPE",node="$PATRONI_NAME",new_role="$PATRONI_ROLE"} 1

TYPE patroni_role_change_timestamp gauge

HELP patroni_role_change_timestamp Timestamp of last role change

patroni_role_change_timestamp{cluster="$PATRONI_SCOPE",node="$PATRONI_NAME"} $(date +%s) EOF

log "Metrics pushed"

}

7。コールバックのベスト プラクティス_

✅ DO

  1. コールバックを高速に保つ
    • 10 ~ 30 秒以内に完了
    • Longタスク → バックグラウンド ジョブ
  2. 適切なログを使用
    • すべてのアクションをログに記録
    • タイムスタンプを含める_
    • 回転ログ
  3. エラーを適切に処理
    • 使用set -e 慎重に
    • エラーをキャッチ、ログに記録、続行
    • ゼロ以外の終了 = 警告、失敗ではない
  4. テスト徹底的に
    • ステージングでテスト
    • すべてのシナリオをシミュレーション_
    • べき等性を検証_
  5. Makeスクリプト冪等
    • 安全に複数回実行可能
    • 変更前に確認
  6. 絶対値を使用パス
    • PATH に依存しない
    • フルパスを指定
  7. 安全資格情報_
    • パスワードをハードコードしない_
    • 環境変数またはシークレットマネージャーを使用_
  8. コールバックを監視する実行
    • 失敗時のアラート
    • 実行時間の追跡_

❌しないでください_

  1. 長時間ブロックしないでください
    • Patroni はコールバックを待ちます_
    • 遅延が長い → 遅くなるフェイルオーバー_
  2. フェイルオーバー中にネットワークに依存しない_
    • ネットワークが分割される可能性があります
    • フォールバックありメカニズム_
  3. コールバックを不必要に失敗させない
    • 通知が失敗しても0を終了
    • エラーをログに記録するが続行
  4. 実行しないコールバック内のデータベース クエリ_
    • PostgreSQL の準備ができていない可能性があります
    • デッドロックが発生する可能性があります
  5. PostgreSQL を変更しないでください設定
    • Patroni に設定を管理させる_
    • Patroni のパラメータを使用
  6. インタラクティブを使用しないコマンド_
    • ユーザー入力なし
    • 無人で実行する必要があります_

8。コールバックの問題のトラブルシューティング

8.1。コールバックが実行されていません_

Check:

# 1. Verify script exists
ls -la /var/lib/postgresql/callbacks/on_role_change.sh

2. Check executable permissions

Should be: -rwxr-xr-x postgres postgres

sudo chmod +x /var/lib/postgresql/callbacks/on_role_change.sh

3. Check ownership

sudo chown postgres:postgres /var/lib/postgresql/callbacks/on_role_change.sh

4. Verify path in patroni.yml

grep -A5 "callbacks:" /etc/patroni/patroni.yml

5. Check Patroni logs

sudo journalctl -u patroni -n 100 | grep -i callback

8.2。コールバック失敗_

ログの確認:

# Patroni logs
sudo journalctl -u patroni | grep "callback.*failed"

Callback logs

sudo tail -f /var/log/patroni/callbacks.log

Test script manually

sudo -u postgres /var/lib/postgresql/callbacks/on_role_change.sh

共通問題:

  • 構文エラー:bash -n script.shを実行します。 check
  • 依存関係がありません: 必要なツール (curl、nc など) をインストールします
  • 許可が拒否されました: ファイル/ディレクトリを確認してください権限_
  • タイムアウト: スクリプトに時間がかかりすぎます

8.3。フェールオーバーの遅延を引き起こすコールバック

コールバックの実行時間を測定:

# Add timing to script
START_TIME=$(date +%s)

... your callback logic ...

END_TIME=$(date +%s) DURATION=$((END_TIME - START_TIME)) log "Callback completed in ${DURATION} seconds"

If DURATION > 30, investigate and optimize

9。本番環境コールバック テンプレート

本番環境に対応した完全なテンプレート:

#!/bin/bash

Patroni callback template

File: /var/lib/postgresql/callbacks/on_role_change.sh

set -euo pipefail # Exit on error, undefined vars, pipe failures

Configuration

readonly LOG_FILE="/var/log/patroni/callbacks.log" readonly LOCK_FILE="/tmp/callback_role_change.lock" readonly TIMEOUT=30 readonly SLACK_WEBHOOK="${SLACK_WEBHOOK_URL:-}"

Logging function

log() { local LEVEL="$1" shift local MESSAGE="$*" local TIMESTAMP TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') echo "[$TIMESTAMP] [$LEVEL] [ROLE_CHANGE] $MESSAGE" | tee -a "$LOG_FILE" }

Error handler

error_exit() { log "ERROR" "$1" cleanup exit 1 }

Cleanup function

cleanup() { rm -f "$LOCK_FILE" }

Ensure only one instance runs

if ! mkdir "$LOCK_FILE" 2>/dev/null; then log "WARN" "Another callback instance is running, exiting" exit 0 fi

trap cleanup EXIT

Set timeout

timeout "$TIMEOUT" bash << 'SCRIPT' || error_exit "Callback timed out after ${TIMEOUT}s"

log "INFO" "==========================================" log "INFO" "Role change detected" log "INFO" "Cluster: ${PATRONI_SCOPE:-unknown}" log "INFO" "Node: ${PATRONI_NAME:-unknown}" log "INFO" "Old role: ${PATRONI_OLD_ROLE:-unknown}" log "INFO" "New role: ${PATRONI_ROLE:-unknown}" log "INFO" "=========================================="

Main logic

case "${PATRONI_ROLE:-}" in master) log "INFO" "Handling promotion to PRIMARY"

    # Get node IP
    NODE_IP=$(hostname -I | awk '{print $1}')
    log "INFO" "Node IP: $NODE_IP"
    
    # Update DNS (implement your logic)
    # update_dns "$NODE_IP" || log "WARN" "DNS update failed"
    
    # Update load balancer (implement your logic)
    # update_load_balancer "$NODE_IP" || log "WARN" "LB update failed"
    
    # Send notification
    if [ -n "$SLACK_WEBHOOK" ]; then
        curl -s -X POST "$SLACK_WEBHOOK" \
            -H "Content-Type: application/json" \
            -d "{\"text\": \"🚨 Failover: $PATRONI_NAME promoted to PRIMARY\"}" \
            || log "WARN" "Slack notification failed"
    fi
    
    # Set marker files
    touch /tmp/postgres_is_primary
    rm -f /tmp/postgres_is_replica
    
    log "INFO" "PRIMARY promotion tasks completed"
    ;;
    
replica)
    log "INFO" "Handling demotion to REPLICA"
    
    # Remove primary marker
    rm -f /tmp/postgres_is_primary
    touch /tmp/postgres_is_replica
    
    # Notify if demoted from primary
    if [ "${PATRONI_OLD_ROLE:-}" = "master" ]; then
        log "WARN" "Node demoted from PRIMARY to REPLICA"
        # Send alert
    fi
    
    log "INFO" "REPLICA tasks completed"
    ;;
    
*)
    error_exit "Unknown role: ${PATRONI_ROLE:-unknown}"
    ;;

esac

log "INFO" "Callback completed successfully" exit 0

SCRIPT

10。ラボ演習

ラボ 1: 基本的なコールバックのセットアップ

タスク:

  1. コールバック ディレクトリとスクリプト_
  2. patroni.yml にコールバックを追加
  3. Patroni をリロード_
  4. patronictl でテストするrestart

ラボ 2: フェールオーバー コールバックのテスト

タスク:

  1. コールバックの監視ログ: tail -f /var/log/patroni/callbacks.log
  2. プライマリの停止: sudo systemctl stop patroni_
  3. Verify on_role_change新しいプライマリで実行
  4. マーカー ファイルを確認します: /tmp/postgres_is_*
  5. 古いプライマリを再起動します。レプリカとして再参加することを確認

ラボ 3: Slack 通知の実装

タスク:

  1. Slack Webhook を取得するURL_
  2. on_role_change.sh に通知を追加
  3. フェイルオーバーをトリガーしてテスト
  4. Slack で受信したメッセージを確認_

ラボ 4: コールバックを測定するパフォーマンス_

タスク:

  1. すべてのコールバックにタイミングを追加_
  2. さまざまなイベント(再起動、再読み込み、フェイルオーバー)
  3. コールバック実行時間の分析
  4. 遅いコールバックの最適化

11。概要

重要なポイント

✅ コールバック = ライフサイクル時のカスタム自動化events

✅ on_role_change = フェイルオーバー自動化のための最も重要なコールバック

✅ コールバックを維持するfast(<30 秒) クイック フェイルオーバー用

✅ すべてをログに記録 デバッグ

✅ 本番前に徹底的にテスト 本番前

✅ エラーを適切に処理_ - しないブロック操作__HTMLTAG_814__HTMLTAG_815___一般的な使用例

コールバック共通アクション_
on_start_プリフライトチェック、マウント確認_
on_stopクリーンアップ、通知_
on_role_changeDNS、LB を更新、送信アラート_
再起動時ログのメンテナンスイベント_
_on_reload構成変更の確認

現在のアーキテクチャ___HTMLTAG_872__CODEBLOCK_27___

レッスン 12 の準備____HTMLTAG_874__HTMLTAG_875___レッスン 12 ではPatroni REST について説明しますAPI:

  • ヘルスチェックエンドポイント
  • クラスターステータスクエリ_
  • API経由の構成管理
  • 負荷との統合バランサー
  • モニタリングとメトリクス