运维管理19 分钟阅读
写了一个 Oracle DG 监控脚本,这下可以安心睡觉了
2026年9月26日阅读—点赞—收藏—
OracleDG
在知识库中专注阅读,并随时返回相关工具与课程
昨晚有一套 Oracle DG 数据库一直发告警邮件:
1ADG error:SCN not change,please check。连上数据库主机看了一下同步:
1SELECT process,2 thread#,3 sequence#,4 status5FROM v$managed_standby;67PROCESS THREAD# SEQUENCE# STATUS8------- ------- --------- ------------9MRP0 1 169889 APPLYING_LOG10RFS 1 169889 IDLE再看延迟:
1SELECT name,2 value3FROM v$dataguard_stats4WHERE name IN ('transport lag','apply lag');56transport lag +00 00:00:007apply lag +00 00:00:00发现是 DG 同步是完全正常的。
检查发现服务器上原来有一个 DG 监控脚本,逻辑也比较简单:
1查询 CURRENT_SCN2 ↓3等待 5 秒4 ↓5再次查询 CURRENT_SCN6 ↓7SCN 没变化8 ↓9发送 DG 异常邮件核心 SQL 就这一句:
1SELECT current_scn2FROM v$database;实际检查时发现,两次查询的 SCN 确实一样:
186803034214286803034214也就是说:DG 本身完全正常,只是这几秒 SCN 没有变化。
所以用:SCN 5 秒没变化 = DG 异常 作为监控条件,比较容易产生误报。
为了后续不再误报,我重新写了一个 DG 检查脚本,主要检查内容:
1Database Role2Open Mode3MRP 状态4RFS 状态5Transport Lag6Apply Lag7Received / Applied Sequence8最近的 DG Error/Fatal其中真正用于判断异常的主要是:
1MRP 是否正常运行2Transport Lag 是否超过阈值3Apply Lag 是否超过阈值4最近是否存在 DG Error/FatalSequence 更多用于辅助判断,不直接拿 Received - Applied 作为告警条件。
因为开启 Real-Time Apply 后,MRP 可能已经在应用当前 Standby Redo Log,而 V$ARCHIVED_LOG 中显示的 Sequence 还慢一条,这是正常现象。
在一套 Oracle 19c ADG 上执行:
1./check_dg_status.sh输出:
1======================================================================2 Oracle Data Guard Monitor V4.33======================================================================4Database : ORCL5Oracle Version : 19.0.0.0.06Host / Check Time : ssthlmesdbdg01 / 2026-09-26 20:24:407Role / Open Mode : PHYSICAL STANDBY / READ ONLY WITH APPLY8Protection Mode : MAXIMUM PERFORMANCE9Switchover Status : NOT ALLOWED1011[Instances]12 Inst=1 Name=orcldg Host=ssthlmesdbdg01 Status=OPEN Thread=11314[Recovery]15 MRP Count=1 Inst=1 Status=APPLYING_LOG Thread=2 Seq=941516 RFS Count=1017 RFS Inst=1 Status=IDLE Thread=1 Seq=938918 RFS Inst=1 Status=IDLE Thread=2 Seq=94151920[Lag]21 Transport Lag : +00 00:00:0022 Apply Lag : +00 00:00:002324[Archive by Thread]25 Thread 1: Received=9388 Applied=9388 Diff=026 LastReceive=2026-09-26 19:55:52 LastApply=2026-09-26 19:55:5227 Thread 2: Received=9414 Applied=9414 Diff=028 LastReceive=2026-09-26 20:06:27 LastApply=2026-09-26 20:06:272930[Recent DG Errors]31 NONE3233Status : HEALTHY34Health Basis : MRP running; Transport Lag=+00 00:00:00; Apply Lag=+00 00:00:00; no recent DG Error/Fatal35======================================================================目前已经上实际跑过的版本:
同时支持多 Redo Thread,所以 RAC 主库的 Thread 1、Thread 2 会分开统计。
为了避免偶尔的网络抖动就发邮件,现在的规则是:
我最后放到 crontab 每 5 分钟检查一次:
1*/5 * * * * $HOME/jobs/check_dg_status.sh >/dev/null 2>&1正常情况下完全静默,真正出现 DG 异常才会收到邮件。
这样更符合平时 DBA 手工检查 Data Guard 的思路,也能少掉不少误报。
完整脚本如下:
1#!/bin/bash2# Oracle Data Guard Monitor V4.33# Tested scenarios:4# Oracle 11.2.0.4 Physical Standby / MOUNTED5# Oracle 19c Active Data Guard / READ ONLY WITH APPLY6# Supports multi redo threads. RAC standby code path is retained but not field-tested.7#8# Alert policy:9# Healthy: no mail10# First abnormal check: record only11# Second consecutive abnormal check: send alert12# Persistent abnormal: reminder every 6 hours13# Recovery: clear state silently, no recovery mail1415source ~/.bash_profile16export NLS_LANG=AMERICAN_AMERICA.AL32UTF81718MAIL_TO="pc1107750981@163.com"19TRANSPORT_LAG_THRESHOLD=30020APPLY_LAG_THRESHOLD=30021FAIL_THRESHOLD=222REMINDER_INTERVAL=2160023DG_ERROR_LOOKBACK_MIN=102425HOST=$(hostname)26CHECK_TIME=$(date '+%Y-%m-%d %H:%M:%S')27NOW_EPOCH=$(date +%s)2829TMP_FILE="/tmp/dg_monitor_$$.out"30MAIL_FILE="/tmp/dg_monitor_mail_$$.out"31STATE_DIR="$HOME/jobs/.dg_monitor"32FAIL_FILE="$STATE_DIR/fail_count"33ALERT_FILE="$STATE_DIR/alert_sent"34LAST_MAIL_FILE="$STATE_DIR/last_mail_epoch"3536mkdir -p "$STATE_DIR"37trap 'rm -f "$TMP_FILE" "$MAIL_FILE"' EXIT3839read_int() {40 local file="$1"41 local default_value="$2"42 local value43 if [ ! -f "$file" ]; then44 echo "$default_value"45 return46 fi47 value=$(cat "$file" 2>/dev/null)48 case "$value" in49 ''|*[!0-9]*) echo "$default_value" ;;50 *) echo "$value" ;;51 esac52}5354lag_to_seconds() {55 local lag="$1"56 local clean days tm hh mm ss57 if [ -z "$lag" ]; then58 echo -159 return60 fi61 clean=$(echo "$lag" | sed 's/^+//' | xargs)62 days=$(echo "$clean" | awk '{print $1}')63 tm=$(echo "$clean" | awk '{print $2}')64 hh=$(echo "$tm" | cut -d: -f1)65 mm=$(echo "$tm" | cut -d: -f2)66 ss=$(echo "$tm" | cut -d: -f3 | cut -d. -f1)67 if [ -z "$days" ] || [ -z "$hh" ] || [ -z "$mm" ] || [ -z "$ss" ]; then68 echo -169 return70 fi71 echo $((10#$days * 86400 + 10#$hh * 3600 + 10#$mm * 60 + 10#$ss))72}7374sqlplus -s / as sysdba > "$TMP_FILE" 2>&1 <<EOF75whenever sqlerror exit 1076whenever oserror exit 1177set heading off feedback off pagesize 0 linesize 1000 trimspool on verify off echo off tab off7879SELECT 'DB|'||name||'|'||database_role||'|'||open_mode||'|'||80 protection_mode||'|'||switchover_status81FROM v\$database;8283SELECT 'VER|'||version84FROM v\$instance;8586SELECT 'INST|'||inst_id||'|'||instance_name||'|'||host_name||'|'||87 status||'|'||thread#88FROM gv\$instance89ORDER BY inst_id;9091SELECT 'MRP|'||inst_id||'|'||status||'|'||thread#||'|'||sequence#92FROM gv\$managed_standby93WHERE process='MRP0'94ORDER BY inst_id;9596SELECT 'RFS|'||inst_id||'|'||status||'|'||thread#||'|'||sequence#97FROM gv\$managed_standby98WHERE process='RFS'99ORDER BY inst_id,thread#,sequence#;100101SELECT 'TLAG|'||value102FROM v\$dataguard_stats103WHERE name='transport lag';104105SELECT 'ALAG|'||value106FROM v\$dataguard_stats107WHERE name='apply lag';108109SELECT 'ERR|'||TO_CHAR(timestamp,'YYYY-MM-DD HH24:MI:SS')||'|'||110 severity||'|'||NVL(TO_CHAR(error_code),'0')||'|'||111 REPLACE(REPLACE(REPLACE(message,'|','/'),CHR(10),' '),CHR(13),' ')112FROM v\$dataguard_status113WHERE severity IN ('Error','Fatal')114AND timestamp > SYSDATE - (${DG_ERROR_LOOKBACK_MIN}/1440)115ORDER BY timestamp;116117SELECT 'REC|'||thread#||'|'||MAX(sequence#)||'|'||118 TO_CHAR(MAX(completion_time),'YYYY-MM-DD HH24:MI:SS')119FROM v\$archived_log120WHERE registrar='RFS'121AND resetlogs_change#=(SELECT resetlogs_change# FROM v\$database)122GROUP BY thread#123ORDER BY thread#;124125SELECT 'APP|'||thread#||'|'||MAX(sequence#)||'|'||126 TO_CHAR(MAX(completion_time),'YYYY-MM-DD HH24:MI:SS')127FROM v\$archived_log128WHERE applied='YES'129AND resetlogs_change#=(SELECT resetlogs_change# FROM v\$database)130GROUP BY thread#131ORDER BY thread#;132133exit134EOF135136SQL_RC=$?137138if [ "$SQL_RC" -ne 0 ]; then139 FAIL_COUNT=$(read_int "$FAIL_FILE" 0)140 FAIL_COUNT=$((FAIL_COUNT + 1))141 echo "$FAIL_COUNT" > "$FAIL_FILE"142143 {144 echo "======================================================================"145 echo " Oracle Data Guard Monitor - CHECK FAILED"146 echo "======================================================================"147 echo "Host : $HOST"148 echo "Check Time : $CHECK_TIME"149 echo "SQLPlus RC : $SQL_RC"150 echo "Failure Count : $FAIL_COUNT/$FAIL_THRESHOLD"151 echo152 cat "$TMP_FILE"153 echo "======================================================================"154 } > "$MAIL_FILE"155156 cat "$MAIL_FILE"157158 if [ "$FAIL_COUNT" -ge "$FAIL_THRESHOLD" ]; then159 LAST_MAIL=$(read_int "$LAST_MAIL_FILE" 0)160 if [ ! -f "$ALERT_FILE" ] || [ $((NOW_EPOCH - LAST_MAIL)) -ge "$REMINDER_INTERVAL" ]; then161 mailx -s "[DG CHECK FAILED] $HOST" "$MAIL_TO" < "$MAIL_FILE"162 if [ $? -eq 0 ]; then163 touch "$ALERT_FILE"164 echo "$NOW_EPOCH" > "$LAST_MAIL_FILE"165 fi166 fi167 fi168 exit 2169fi170171DB_LINE=$(grep '^DB|' "$TMP_FILE" | head -1)172if [ -z "$DB_LINE" ]; then173 echo "[ERROR] Cannot obtain V\$DATABASE information."174 exit 2175fi176177DB_NAME=$(echo "$DB_LINE" | cut -d'|' -f2)178DB_ROLE=$(echo "$DB_LINE" | cut -d'|' -f3)179OPEN_MODE=$(echo "$DB_LINE" | cut -d'|' -f4)180PROTECTION_MODE=$(echo "$DB_LINE" | cut -d'|' -f5)181SWITCHOVER_STATUS=$(echo "$DB_LINE" | cut -d'|' -f6)182ORACLE_VERSION=$(grep '^VER|' "$TMP_FILE" | head -1 | cut -d'|' -f2)183184MRP_COUNT=$(grep -c '^MRP|' "$TMP_FILE")185if [ "$MRP_COUNT" -gt 0 ]; then186 MRP_LINE=$(grep '^MRP|' "$TMP_FILE" | head -1)187 MRP_INST=$(echo "$MRP_LINE" | cut -d'|' -f2)188 MRP_STATUS=$(echo "$MRP_LINE" | cut -d'|' -f3)189 MRP_THREAD=$(echo "$MRP_LINE" | cut -d'|' -f4)190 MRP_SEQ=$(echo "$MRP_LINE" | cut -d'|' -f5)191else192 MRP_INST="-"193 MRP_STATUS="NOT_RUNNING"194 MRP_THREAD="-"195 MRP_SEQ="-"196fi197198RFS_COUNT=$(grep -c '^RFS|' "$TMP_FILE")199TRANSPORT_LAG=$(grep '^TLAG|' "$TMP_FILE" | head -1 | cut -d'|' -f2)200APPLY_LAG=$(grep '^ALAG|' "$TMP_FILE" | head -1 | cut -d'|' -f2)201TRANSPORT_SEC=$(lag_to_seconds "$TRANSPORT_LAG")202APPLY_SEC=$(lag_to_seconds "$APPLY_LAG")203DG_ERROR_COUNT=$(grep -c '^ERR|' "$TMP_FILE")204205ABNORMAL=0206PROBLEM=""207208add_problem() {209 ABNORMAL=1210 PROBLEM="${PROBLEM}211$1"212}213214if [ "$DB_ROLE" != "PHYSICAL STANDBY" ]; then215 add_problem "[CRITICAL] Database role is $DB_ROLE; expected PHYSICAL STANDBY."216fi217218case "$OPEN_MODE" in219 "MOUNTED"|"READ ONLY WITH APPLY") ;;220 *) add_problem "[CRITICAL] Unexpected standby open mode: $OPEN_MODE." ;;221esac222223if [ "$MRP_COUNT" -eq 0 ]; then224 add_problem "[CRITICAL] MRP0 is NOT running."225fi226227if [ "$TRANSPORT_SEC" -lt 0 ]; then228 add_problem "[WARNING] Cannot obtain Transport Lag."229elif [ "$TRANSPORT_SEC" -gt "$TRANSPORT_LAG_THRESHOLD" ]; then230 add_problem "[WARNING] Transport Lag $TRANSPORT_LAG exceeds ${TRANSPORT_LAG_THRESHOLD}s."231fi232233if [ "$APPLY_SEC" -lt 0 ]; then234 add_problem "[WARNING] Cannot obtain Apply Lag."235elif [ "$APPLY_SEC" -gt "$APPLY_LAG_THRESHOLD" ]; then236 add_problem "[WARNING] Apply Lag $APPLY_LAG exceeds ${APPLY_LAG_THRESHOLD}s."237fi238239if [ "$DG_ERROR_COUNT" -gt 0 ]; then240 add_problem "[CRITICAL] $DG_ERROR_COUNT Data Guard Error/Fatal message(s) detected in last ${DG_ERROR_LOOKBACK_MIN} minutes."241fi242243print_report() {244 echo "======================================================================"245 echo " Oracle Data Guard Monitor V4.3"246 echo "======================================================================"247 echo "Database : $DB_NAME"248 echo "Oracle Version : $ORACLE_VERSION"249 echo "Host / Check Time : $HOST / $CHECK_TIME"250 echo "Role / Open Mode : $DB_ROLE / $OPEN_MODE"251 echo "Protection Mode : $PROTECTION_MODE"252 echo "Switchover Status : $SWITCHOVER_STATUS"253254 echo255 echo "[Instances]"256 grep '^INST|' "$TMP_FILE" |257 while IFS='|' read -r TAG INST_ID INST_NAME INST_HOST INST_STATUS INST_THREAD258 do259 echo " Inst=$INST_ID Name=$INST_NAME Host=$INST_HOST Status=$INST_STATUS Thread=$INST_THREAD"260 done261262 echo263 echo "[Recovery]"264 echo " MRP Count=$MRP_COUNT Inst=$MRP_INST Status=$MRP_STATUS Thread=$MRP_THREAD Seq=$MRP_SEQ"265 echo " RFS Count=$RFS_COUNT"266 grep '^RFS|' "$TMP_FILE" |267 while IFS='|' read -r TAG R_INST R_STATUS R_THREAD R_SEQ268 do269 if [ "$R_THREAD" -gt 0 ] 2>/dev/null && [ "$R_SEQ" -gt 0 ] 2>/dev/null; then270 echo " RFS Inst=$R_INST Status=$R_STATUS Thread=$R_THREAD Seq=$R_SEQ"271 fi272 done273274 echo275 echo "[Lag]"276 echo " Transport Lag : ${TRANSPORT_LAG:--}"277 echo " Apply Lag : ${APPLY_LAG:--}"278279 echo280 echo "[Archive by Thread]"281 THREADS=$(282 {283 grep '^REC|' "$TMP_FILE" | cut -d'|' -f2284 grep '^APP|' "$TMP_FILE" | cut -d'|' -f2285 } | grep '^[0-9][0-9]*$' | sort -n -u286 )287288 if [ -z "$THREADS" ]; then289 echo " No archive thread information."290 else291 for THREAD in $THREADS292 do293 REC_LINE=$(grep "^REC|${THREAD}|" "$TMP_FILE" | tail -1)294 APP_LINE=$(grep "^APP|${THREAD}|" "$TMP_FILE" | tail -1)295296 REC_SEQ=$(echo "$REC_LINE" | cut -d'|' -f3)297 REC_TIME=$(echo "$REC_LINE" | cut -d'|' -f4)298 APP_SEQ=$(echo "$APP_LINE" | cut -d'|' -f3)299 APP_TIME=$(echo "$APP_LINE" | cut -d'|' -f4)300301 [ -n "$REC_SEQ" ] || REC_SEQ=0302 [ -n "$APP_SEQ" ] || APP_SEQ=0303 [ -n "$REC_TIME" ] || REC_TIME="-"304 [ -n "$APP_TIME" ] || APP_TIME="-"305306 if echo "$REC_SEQ" | grep -q '^[0-9][0-9]*$' &&307 echo "$APP_SEQ" | grep -q '^[0-9][0-9]*$'; then308 ARCHIVE_DIFF=$((REC_SEQ - APP_SEQ))309 else310 ARCHIVE_DIFF="-"311 fi312313 echo " Thread $THREAD: Received=$REC_SEQ Applied=$APP_SEQ Diff=$ARCHIVE_DIFF"314 echo " LastReceive=$REC_TIME LastApply=$APP_TIME"315 done316 fi317318 echo319 echo "[Recent DG Errors]"320 if [ "$DG_ERROR_COUNT" -eq 0 ]; then321 echo " NONE"322 else323 grep '^ERR|' "$TMP_FILE" | sed 's/^ERR|/ /'324 fi325}326327if [ "$ABNORMAL" -eq 1 ]; then328 FAIL_COUNT=$(read_int "$FAIL_FILE" 0)329 FAIL_COUNT=$((FAIL_COUNT + 1))330 echo "$FAIL_COUNT" > "$FAIL_FILE"331332 print_report333 echo334 echo "Status : ABNORMAL"335 echo "Failure Count : $FAIL_COUNT/$FAIL_THRESHOLD"336 echo "Problems:"337 printf "%b\n" "$PROBLEM"338 echo "======================================================================"339340 if [ "$FAIL_COUNT" -ge "$FAIL_THRESHOLD" ]; then341 LAST_MAIL=$(read_int "$LAST_MAIL_FILE" 0)342 SEND_MAIL=0343 SUBJECT="[DG ALERT] $DB_NAME@$HOST"344345 if [ ! -f "$ALERT_FILE" ]; then346 SEND_MAIL=1347 elif [ $((NOW_EPOCH - LAST_MAIL)) -ge "$REMINDER_INTERVAL" ]; then348 SEND_MAIL=1349 SUBJECT="[DG REMINDER] $DB_NAME@$HOST"350 fi351352 if [ "$SEND_MAIL" -eq 1 ]; then353 {354 print_report355 echo356 echo "Status : ABNORMAL"357 echo "Failure Count : $FAIL_COUNT/$FAIL_THRESHOLD"358 echo "Problems:"359 printf "%b\n" "$PROBLEM"360 echo "======================================================================"361 } > "$MAIL_FILE"362363 mailx -s "$SUBJECT" "$MAIL_TO" < "$MAIL_FILE"364 if [ $? -eq 0 ]; then365 touch "$ALERT_FILE"366 echo "$NOW_EPOCH" > "$LAST_MAIL_FILE"367 fi368 fi369 fi370 exit 1371fi372373echo 0 > "$FAIL_FILE"374rm -f "$ALERT_FILE" "$LAST_MAIL_FILE"375376print_report377echo378echo "Status : HEALTHY"379echo "Health Basis : MRP running; Transport Lag=${TRANSPORT_LAG:--}; Apply Lag=${APPLY_LAG:--}; no recent DG Error/Fatal"380echo "======================================================================"381exit 0需要的话直接拿去改一下邮箱和阈值就能用。
更多数据库相关脚本,我都在放在:https://ora100.com/script-library 可自行获取!
