写了一个 Oracle DG 监控脚本,这下可以安心睡觉了

举报
Lucifer三思而后行 发表于 2026/09/27 09:18:03 2026/09/27
【摘要】 前言昨晚有一套 Oracle DG 数据库一直发告警邮件:ADG error:SCN not change,please check。连上数据库主机看了一下同步:SELECT process, thread#, sequence#, statusFROM v$managed_standby;PROCESS THREAD# SEQUENCE# ...

前言

昨晚有一套 Oracle DG 数据库一直发告警邮件:

ADG error:SCN not change,please check。

连上数据库主机看了一下同步:

SELECT process,
       thread#,
       sequence#,
       status
FROM v$managed_standby;

PROCESS   THREAD#   SEQUENCE#   STATUS
-------   -------   ---------   ------------
MRP0            1      169889   APPLYING_LOG
RFS             1      169889   IDLE

再看延迟:

SELECT name,
       value
FROM v$dataguard_stats
WHERE name IN ('transport lag','apply lag');

transport lag   +00 00:00:00
apply lag       +00 00:00:00

发现是 DG 同步是完全正常的。

检查发现服务器上原来有一个 DG 监控脚本,逻辑也比较简单:

查询 CURRENT_SCN
      ↓
等待 5 秒
      ↓
再次查询 CURRENT_SCN
      ↓
SCN 没变化
      ↓
发送 DG 异常邮件

核心 SQL 就这一句:

SELECT current_scn
FROM v$database;

实际检查时发现,两次查询的 SCN 确实一样:

86803034214
86803034214

也就是说:DG 本身完全正常,只是这几秒 SCN 没有变化。

所以用:SCN 5 秒没变化 = DG 异常 作为监控条件,比较容易产生误报。

重新写了一个 DG 监控脚本

为了后续不再误报,我重新写了一个 DG 检查脚本,主要检查内容:

Database Role
Open Mode
MRP 状态
RFS 状态
Transport Lag
Apply Lag
Received / Applied Sequence
最近的 DG Error/Fatal

其中真正用于判断异常的主要是:

MRP 是否正常运行
Transport Lag 是否超过阈值
Apply Lag 是否超过阈值
最近是否存在 DG Error/Fatal

Sequence 更多用于辅助判断,不直接拿 Received - Applied 作为告警条件。

因为开启 Real-Time Apply 后,MRP 可能已经在应用当前 Standby Redo Log,而 V$ARCHIVED_LOG 中显示的 Sequence 还慢一条,这是正常现象。

实际运行效果

在一套 Oracle 19c ADG 上执行:

./check_dg_status.sh

输出:

======================================================================
 Oracle Data Guard Monitor V4.3
======================================================================
Database          : ORCL
Oracle Version    : 19.0.0.0.0
Host / Check Time : ssthlmesdbdg01 / 2026-09-26 20:24:40
Role / Open Mode  : PHYSICAL STANDBY / READ ONLY WITH APPLY
Protection Mode   : MAXIMUM PERFORMANCE
Switchover Status : NOT ALLOWED

[Instances]
  Inst=1 Name=orcldg Host=ssthlmesdbdg01 Status=OPEN Thread=1

[Recovery]
  MRP Count=1 Inst=1 Status=APPLYING_LOG Thread=2 Seq=9415
  RFS Count=10
  RFS Inst=1 Status=IDLE Thread=1 Seq=9389
  RFS Inst=1 Status=IDLE Thread=2 Seq=9415

[Lag]
  Transport Lag : +00 00:00:00
  Apply Lag     : +00 00:00:00

[Archive by Thread]
  Thread 1: Received=9388 Applied=9388 Diff=0
            LastReceive=2026-09-26 19:55:52 LastApply=2026-09-26 19:55:52
  Thread 2: Received=9414 Applied=9414 Diff=0
            LastReceive=2026-09-26 20:06:27 LastApply=2026-09-26 20:06:27

[Recent DG Errors]
  NONE

Status            : HEALTHY
Health Basis      : MRP running; Transport Lag=+00 00:00:00; Apply Lag=+00 00:00:00; no recent DG Error/Fatal
======================================================================

目前已经上实际跑过的版本:

  1. Oracle 11.2.0.4 Physical Standby
  2. Oracle 19c Active Data Guard

同时支持多 Redo Thread,所以 RAC 主库的 Thread 1、Thread 2 会分开统计。

告警也做了简单控制

为了避免偶尔的网络抖动就发邮件,现在的规则是:

  • 正常 → 不发邮件
  • 第一次异常 → 记录
  • 连续第二次异常 → 发告警邮件
  • 一直异常 → 每 6 小时提醒一次
  • 恢复正常 → 清除异常状态,不发邮件

我最后放到 crontab 每 5 分钟检查一次:

*/5 * * * * $HOME/jobs/check_dg_status.sh >/dev/null 2>&1

正常情况下完全静默,真正出现 DG 异常才会收到邮件。

最后

这样更符合平时 DBA 手工检查 Data Guard 的思路,也能少掉不少误报。

完整脚本如下:

#!/bin/bash
# Oracle Data Guard Monitor V4.3
# Tested scenarios:
#   Oracle 11.2.0.4 Physical Standby / MOUNTED
#   Oracle 19c Active Data Guard / READ ONLY WITH APPLY
# Supports multi redo threads. RAC standby code path is retained but not field-tested.
#
# Alert policy:
#   Healthy: no mail
#   First abnormal check: record only
#   Second consecutive abnormal check: send alert
#   Persistent abnormal: reminder every 6 hours
#   Recovery: clear state silently, no recovery mail

source ~/.bash_profile
export NLS_LANG=AMERICAN_AMERICA.AL32UTF8

MAIL_TO="pc1107750981@163.com"
TRANSPORT_LAG_THRESHOLD=300
APPLY_LAG_THRESHOLD=300
FAIL_THRESHOLD=2
REMINDER_INTERVAL=21600
DG_ERROR_LOOKBACK_MIN=10

HOST=$(hostname)
CHECK_TIME=$(date '+%Y-%m-%d %H:%M:%S')
NOW_EPOCH=$(date +%s)

TMP_FILE="/tmp/dg_monitor_$$.out"
MAIL_FILE="/tmp/dg_monitor_mail_$$.out"
STATE_DIR="$HOME/jobs/.dg_monitor"
FAIL_FILE="$STATE_DIR/fail_count"
ALERT_FILE="$STATE_DIR/alert_sent"
LAST_MAIL_FILE="$STATE_DIR/last_mail_epoch"

mkdir -p "$STATE_DIR"
trap 'rm -f "$TMP_FILE" "$MAIL_FILE"' EXIT

read_int() {
    local file="$1"
    local default_value="$2"
    local value
    if [ ! -f "$file" ]; then
        echo "$default_value"
        return
    fi
    value=$(cat "$file" 2>/dev/null)
    case "$value" in
        ''|*[!0-9]*) echo "$default_value" ;;
        *) echo "$value" ;;
    esac
}

lag_to_seconds() {
    local lag="$1"
    local clean days tm hh mm ss
    if [ -z "$lag" ]; then
        echo -1
        return
    fi
    clean=$(echo "$lag" | sed 's/^+//' | xargs)
    days=$(echo "$clean" | awk '{print $1}')
    tm=$(echo "$clean" | awk '{print $2}')
    hh=$(echo "$tm" | cut -d: -f1)
    mm=$(echo "$tm" | cut -d: -f2)
    ss=$(echo "$tm" | cut -d: -f3 | cut -d. -f1)
    if [ -z "$days" ] || [ -z "$hh" ] || [ -z "$mm" ] || [ -z "$ss" ]; then
        echo -1
        return
    fi
    echo $((10#$days * 86400 + 10#$hh * 3600 + 10#$mm * 60 + 10#$ss))
}

sqlplus -s / as sysdba > "$TMP_FILE" 2>&1 <<EOF
whenever sqlerror exit 10
whenever oserror exit 11
set heading off feedback off pagesize 0 linesize 1000 trimspool on verify off echo off tab off

SELECT 'DB|'||name||'|'||database_role||'|'||open_mode||'|'||
       protection_mode||'|'||switchover_status
FROM v\$database;

SELECT 'VER|'||version
FROM v\$instance;

SELECT 'INST|'||inst_id||'|'||instance_name||'|'||host_name||'|'||
       status||'|'||thread#
FROM gv\$instance
ORDER BY inst_id;

SELECT 'MRP|'||inst_id||'|'||status||'|'||thread#||'|'||sequence#
FROM gv\$managed_standby
WHERE process='MRP0'
ORDER BY inst_id;

SELECT 'RFS|'||inst_id||'|'||status||'|'||thread#||'|'||sequence#
FROM gv\$managed_standby
WHERE process='RFS'
ORDER BY inst_id,thread#,sequence#;

SELECT 'TLAG|'||value
FROM v\$dataguard_stats
WHERE name='transport lag';

SELECT 'ALAG|'||value
FROM v\$dataguard_stats
WHERE name='apply lag';

SELECT 'ERR|'||TO_CHAR(timestamp,'YYYY-MM-DD HH24:MI:SS')||'|'||
       severity||'|'||NVL(TO_CHAR(error_code),'0')||'|'||
       REPLACE(REPLACE(REPLACE(message,'|','/'),CHR(10),' '),CHR(13),' ')
FROM v\$dataguard_status
WHERE severity IN ('Error','Fatal')
AND timestamp > SYSDATE - (${DG_ERROR_LOOKBACK_MIN}/1440)
ORDER BY timestamp;

SELECT 'REC|'||thread#||'|'||MAX(sequence#)||'|'||
       TO_CHAR(MAX(completion_time),'YYYY-MM-DD HH24:MI:SS')
FROM v\$archived_log
WHERE registrar='RFS'
AND resetlogs_change#=(SELECT resetlogs_change# FROM v\$database)
GROUP BY thread#
ORDER BY thread#;

SELECT 'APP|'||thread#||'|'||MAX(sequence#)||'|'||
       TO_CHAR(MAX(completion_time),'YYYY-MM-DD HH24:MI:SS')
FROM v\$archived_log
WHERE applied='YES'
AND resetlogs_change#=(SELECT resetlogs_change# FROM v\$database)
GROUP BY thread#
ORDER BY thread#;

exit
EOF

SQL_RC=$?

if [ "$SQL_RC" -ne 0 ]; then
    FAIL_COUNT=$(read_int "$FAIL_FILE" 0)
    FAIL_COUNT=$((FAIL_COUNT + 1))
    echo "$FAIL_COUNT" > "$FAIL_FILE"

    {
        echo "======================================================================"
        echo " Oracle Data Guard Monitor - CHECK FAILED"
        echo "======================================================================"
        echo "Host          : $HOST"
        echo "Check Time    : $CHECK_TIME"
        echo "SQLPlus RC    : $SQL_RC"
        echo "Failure Count : $FAIL_COUNT/$FAIL_THRESHOLD"
        echo
        cat "$TMP_FILE"
        echo "======================================================================"
    } > "$MAIL_FILE"

    cat "$MAIL_FILE"

    if [ "$FAIL_COUNT" -ge "$FAIL_THRESHOLD" ]; then
        LAST_MAIL=$(read_int "$LAST_MAIL_FILE" 0)
        if [ ! -f "$ALERT_FILE" ] || [ $((NOW_EPOCH - LAST_MAIL)) -ge "$REMINDER_INTERVAL" ]; then
            mailx -s "[DG CHECK FAILED] $HOST" "$MAIL_TO" < "$MAIL_FILE"
            if [ $? -eq 0 ]; then
                touch "$ALERT_FILE"
                echo "$NOW_EPOCH" > "$LAST_MAIL_FILE"
            fi
        fi
    fi
    exit 2
fi

DB_LINE=$(grep '^DB|' "$TMP_FILE" | head -1)
if [ -z "$DB_LINE" ]; then
    echo "[ERROR] Cannot obtain V\$DATABASE information."
    exit 2
fi

DB_NAME=$(echo "$DB_LINE" | cut -d'|' -f2)
DB_ROLE=$(echo "$DB_LINE" | cut -d'|' -f3)
OPEN_MODE=$(echo "$DB_LINE" | cut -d'|' -f4)
PROTECTION_MODE=$(echo "$DB_LINE" | cut -d'|' -f5)
SWITCHOVER_STATUS=$(echo "$DB_LINE" | cut -d'|' -f6)
ORACLE_VERSION=$(grep '^VER|' "$TMP_FILE" | head -1 | cut -d'|' -f2)

MRP_COUNT=$(grep -c '^MRP|' "$TMP_FILE")
if [ "$MRP_COUNT" -gt 0 ]; then
    MRP_LINE=$(grep '^MRP|' "$TMP_FILE" | head -1)
    MRP_INST=$(echo "$MRP_LINE" | cut -d'|' -f2)
    MRP_STATUS=$(echo "$MRP_LINE" | cut -d'|' -f3)
    MRP_THREAD=$(echo "$MRP_LINE" | cut -d'|' -f4)
    MRP_SEQ=$(echo "$MRP_LINE" | cut -d'|' -f5)
else
    MRP_INST="-"
    MRP_STATUS="NOT_RUNNING"
    MRP_THREAD="-"
    MRP_SEQ="-"
fi

RFS_COUNT=$(grep -c '^RFS|' "$TMP_FILE")
TRANSPORT_LAG=$(grep '^TLAG|' "$TMP_FILE" | head -1 | cut -d'|' -f2)
APPLY_LAG=$(grep '^ALAG|' "$TMP_FILE" | head -1 | cut -d'|' -f2)
TRANSPORT_SEC=$(lag_to_seconds "$TRANSPORT_LAG")
APPLY_SEC=$(lag_to_seconds "$APPLY_LAG")
DG_ERROR_COUNT=$(grep -c '^ERR|' "$TMP_FILE")

ABNORMAL=0
PROBLEM=""

add_problem() {
    ABNORMAL=1
    PROBLEM="${PROBLEM}
$1"
}

if [ "$DB_ROLE" != "PHYSICAL STANDBY" ]; then
    add_problem "[CRITICAL] Database role is $DB_ROLE; expected PHYSICAL STANDBY."
fi

case "$OPEN_MODE" in
    "MOUNTED"|"READ ONLY WITH APPLY") ;;
    *) add_problem "[CRITICAL] Unexpected standby open mode: $OPEN_MODE." ;;
esac

if [ "$MRP_COUNT" -eq 0 ]; then
    add_problem "[CRITICAL] MRP0 is NOT running."
fi

if [ "$TRANSPORT_SEC" -lt 0 ]; then
    add_problem "[WARNING] Cannot obtain Transport Lag."
elif [ "$TRANSPORT_SEC" -gt "$TRANSPORT_LAG_THRESHOLD" ]; then
    add_problem "[WARNING] Transport Lag $TRANSPORT_LAG exceeds ${TRANSPORT_LAG_THRESHOLD}s."
fi

if [ "$APPLY_SEC" -lt 0 ]; then
    add_problem "[WARNING] Cannot obtain Apply Lag."
elif [ "$APPLY_SEC" -gt "$APPLY_LAG_THRESHOLD" ]; then
    add_problem "[WARNING] Apply Lag $APPLY_LAG exceeds ${APPLY_LAG_THRESHOLD}s."
fi

if [ "$DG_ERROR_COUNT" -gt 0 ]; then
    add_problem "[CRITICAL] $DG_ERROR_COUNT Data Guard Error/Fatal message(s) detected in last ${DG_ERROR_LOOKBACK_MIN} minutes."
fi

print_report() {
    echo "======================================================================"
    echo " Oracle Data Guard Monitor V4.3"
    echo "======================================================================"
    echo "Database          : $DB_NAME"
    echo "Oracle Version    : $ORACLE_VERSION"
    echo "Host / Check Time : $HOST / $CHECK_TIME"
    echo "Role / Open Mode  : $DB_ROLE / $OPEN_MODE"
    echo "Protection Mode   : $PROTECTION_MODE"
    echo "Switchover Status : $SWITCHOVER_STATUS"

    echo
    echo "[Instances]"
    grep '^INST|' "$TMP_FILE" |
    while IFS='|' read -r TAG INST_ID INST_NAME INST_HOST INST_STATUS INST_THREAD
    do
        echo "  Inst=$INST_ID Name=$INST_NAME Host=$INST_HOST Status=$INST_STATUS Thread=$INST_THREAD"
    done

    echo
    echo "[Recovery]"
    echo "  MRP Count=$MRP_COUNT Inst=$MRP_INST Status=$MRP_STATUS Thread=$MRP_THREAD Seq=$MRP_SEQ"
    echo "  RFS Count=$RFS_COUNT"
    grep '^RFS|' "$TMP_FILE" |
    while IFS='|' read -r TAG R_INST R_STATUS R_THREAD R_SEQ
    do
        if [ "$R_THREAD" -gt 0 ] 2>/dev/null && [ "$R_SEQ" -gt 0 ] 2>/dev/null; then
            echo "  RFS Inst=$R_INST Status=$R_STATUS Thread=$R_THREAD Seq=$R_SEQ"
        fi
    done

    echo
    echo "[Lag]"
    echo "  Transport Lag : ${TRANSPORT_LAG:--}"
    echo "  Apply Lag     : ${APPLY_LAG:--}"

    echo
    echo "[Archive by Thread]"
    THREADS=$(
        {
            grep '^REC|' "$TMP_FILE" | cut -d'|' -f2
            grep '^APP|' "$TMP_FILE" | cut -d'|' -f2
        } | grep '^[0-9][0-9]*$' | sort -n -u
    )

    if [ -z "$THREADS" ]; then
        echo "  No archive thread information."
    else
        for THREAD in $THREADS
        do
            REC_LINE=$(grep "^REC|${THREAD}|" "$TMP_FILE" | tail -1)
            APP_LINE=$(grep "^APP|${THREAD}|" "$TMP_FILE" | tail -1)

            REC_SEQ=$(echo "$REC_LINE" | cut -d'|' -f3)
            REC_TIME=$(echo "$REC_LINE" | cut -d'|' -f4)
            APP_SEQ=$(echo "$APP_LINE" | cut -d'|' -f3)
            APP_TIME=$(echo "$APP_LINE" | cut -d'|' -f4)

            [ -n "$REC_SEQ" ] || REC_SEQ=0
            [ -n "$APP_SEQ" ] || APP_SEQ=0
            [ -n "$REC_TIME" ] || REC_TIME="-"
            [ -n "$APP_TIME" ] || APP_TIME="-"

            if echo "$REC_SEQ" | grep -q '^[0-9][0-9]*$' &&
               echo "$APP_SEQ" | grep -q '^[0-9][0-9]*$'; then
                ARCHIVE_DIFF=$((REC_SEQ - APP_SEQ))
            else
                ARCHIVE_DIFF="-"
            fi

            echo "  Thread $THREAD: Received=$REC_SEQ Applied=$APP_SEQ Diff=$ARCHIVE_DIFF"
            echo "            LastReceive=$REC_TIME LastApply=$APP_TIME"
        done
    fi

    echo
    echo "[Recent DG Errors]"
    if [ "$DG_ERROR_COUNT" -eq 0 ]; then
        echo "  NONE"
    else
        grep '^ERR|' "$TMP_FILE" | sed 's/^ERR|/  /'
    fi
}

if [ "$ABNORMAL" -eq 1 ]; then
    FAIL_COUNT=$(read_int "$FAIL_FILE" 0)
    FAIL_COUNT=$((FAIL_COUNT + 1))
    echo "$FAIL_COUNT" > "$FAIL_FILE"

    print_report
    echo
    echo "Status            : ABNORMAL"
    echo "Failure Count     : $FAIL_COUNT/$FAIL_THRESHOLD"
    echo "Problems:"
    printf "%b\n" "$PROBLEM"
    echo "======================================================================"

    if [ "$FAIL_COUNT" -ge "$FAIL_THRESHOLD" ]; then
        LAST_MAIL=$(read_int "$LAST_MAIL_FILE" 0)
        SEND_MAIL=0
        SUBJECT="[DG ALERT] $DB_NAME@$HOST"

        if [ ! -f "$ALERT_FILE" ]; then
            SEND_MAIL=1
        elif [ $((NOW_EPOCH - LAST_MAIL)) -ge "$REMINDER_INTERVAL" ]; then
            SEND_MAIL=1
            SUBJECT="[DG REMINDER] $DB_NAME@$HOST"
        fi

        if [ "$SEND_MAIL" -eq 1 ]; then
            {
                print_report
                echo
                echo "Status            : ABNORMAL"
                echo "Failure Count     : $FAIL_COUNT/$FAIL_THRESHOLD"
                echo "Problems:"
                printf "%b\n" "$PROBLEM"
                echo "======================================================================"
            } > "$MAIL_FILE"

            mailx -s "$SUBJECT" "$MAIL_TO" < "$MAIL_FILE"
            if [ $? -eq 0 ]; then
                touch "$ALERT_FILE"
                echo "$NOW_EPOCH" > "$LAST_MAIL_FILE"
            fi
        fi
    fi
    exit 1
fi

echo 0 > "$FAIL_FILE"
rm -f "$ALERT_FILE" "$LAST_MAIL_FILE"

print_report
echo
echo "Status            : HEALTHY"
echo "Health Basis      : MRP running; Transport Lag=${TRANSPORT_LAG:--}; Apply Lag=${APPLY_LAG:--}; no recent DG Error/Fatal"
echo "======================================================================"
exit 0

需要的话直接拿去改一下邮箱和阈值就能用。

更多数据库相关脚本,我都在放在: 可自行获取!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。