liuziyi

liuziyi

Oracle RAC与 ADG巡检脚本分析

Oracle RAC 与 ADG 的巡检建议按四层依次进行:集群层 → ASM 存储层 → 数据库实例层 → Data Guard 同步层。如果 RAC 主库和 RAC 备库都需要巡检,就应在主库和备库分别执行一遍。


一、集群层(Grid 用户)

# 1. 集群守护进程状态(CRS/CSS/OHASD)
crsctl check crs

# 2. 所有集群资源状态,重点看 ONLINE/OFFLINE/UNKNOWN
crsctl stat res -t
crsctl stat res -t -init

# 3. 节点成员与角色
olsnodes -n -s -t

# 4. 私网(心跳)网卡配置,确认 interconnect 绑定正确
oifcfg getif

# 5. 表决盘与 OCR
crsctl query css votedisk
ocrcheck

# 6. 数据库/服务/监听资源
srvctl status database -d <db_unique_name> -v
srvctl status service  -d <db_unique_name>
srvctl config database -d <db_unique_name>
srvctl status asm -n <节点名>
srvctl status listener
lsnrctl status

二、ASM 存储层(+ASM 实例)

asmcmd lsdg
asmcmd lsdsk --statistics
-- 磁盘组容量与冗余余量(ASM 实例执行)
SELECT name, type, state, total_mb, free_mb,
       required_mirror_free_mb, usable_file_mb, offline_disks
FROM v$asm_diskgroup_stat
ORDER BY name;

-- 磁盘挂载/成员状态,检查是否有 MISSING/FORCING/OFFLINE 盘
SELECT group_number, disk_number, name, path,
       mount_status, header_status, mode_status, state
FROM v$asm_disk
ORDER BY group_number, disk_number;

三、RAC 实例层(任一实例执行)

-- 1. 各实例状态、角色、启动时间
SELECT inst_id, instance_number, instance_name, host_name, version,
       status, database_status, active_state, startup_time, thread#
FROM gv$instance
ORDER BY inst_id;

-- 2. 集群私网互连地址
SELECT inst_id, name, ip_address, is_public, source
FROM gv$cluster_interconnects
ORDER BY inst_id;

-- 3. 服务在节点间的分布(防止单节点承载全部连接)
SELECT inst_id, name, network_name, goal, clb_goal
FROM gv$active_services
ORDER BY inst_id, name;

-- 4. 在线日志与线程对应关系(RAC 每个实例一个线程)
SELECT group#, thread#, sequence#, bytes, status, archived
FROM v$log
ORDER BY thread#, group#;

-- 5. 会话与等待概况(按实例汇总)
SELECT inst_id, COUNT(*) AS session_cnt,
       SUM(CASE WHEN status = 'ACTIVE' THEN 1 ELSE 0 END) AS active_cnt
FROM gv$session
GROUP BY inst_id
ORDER BY inst_id;

四、ADG 同步层(核心,必须在备库执行)

1. 数据库角色与保护模式

SELECT db_unique_name, database_role, protection_mode, open_mode,
       switchover_status, force_logging, flashback_on, current_scn
FROM v$database;

期望:备库 DATABASE_ROLE = PHYSICAL STANDBY、OPEN_MODE = READ ONLY WITH APPLY;PROTECTION_MODE 与规划一致。

2. 传输延迟与应用延迟

SELECT name, value, unit, time_computed, datum_time
FROM v$dataguard_stats
WHERE LOWER(name) IN ('apply lag', 'transport lag')
ORDER BY name;

注意:在主库执行该视图返回空集,不能据此判断同步正常。value 是字符串,必须连同 unit 一起读;连续两次采样 datum_time 不前进,说明没有收到新日志,此时延迟显示很小也不能视为正常。

3. 归档目标状态(11gR2 及以上)

SELECT dest_id, status, type, database_mode, recovery_mode, destination,
       error, gap_status, archived_thread#, archived_seq#,
       applied_thread#, applied_seq#
FROM v$archive_dest_status
WHERE status <> 'INACTIVE'
ORDER BY dest_id;

期望:status = VALID、error 为空、gap_status = VALID。

4. 归档日志缺口

SELECT thread#, low_sequence#, high_sequence#
FROM v$archive_gap
ORDER BY thread#;

返回空表示当前没有阻断恢复的缺口;但空结果不等于全程没有丢过日志。

5. Data Guard 进程(RAC 备库按实例看)

SELECT inst_id, name, pid, type, role, action, client_role, client_pid,
       thread#, sequence#, block#, block_count, delay_mins, dest_id
FROM gv$managed_standby
ORDER BY inst_id, name;

关键看 MRP0 是否为 APPLYING_LOG。RAC 备库若启用多实例 Redo Apply,MRP 会分布在多个实例;单实例模式则只在一个实例上有 MRP0。

6. 备库恢复/应用进度

SELECT start_time, type, item, units, sofar, total, timestamp
FROM v$recovery_progress
WHERE type = 'MEDIA RECOVERY'
ORDER BY start_time DESC, item;

非协调实例在 Multi-Instance Redo Apply 下可能无行;sofar/total 含义随 item 变化,不要统一换算百分比。

7. 备用日志组(Standby Redo Log)

SELECT group#, thread#, sequence#, bytes, used, archived, status
FROM v$standby_log
ORDER BY thread#, group#;

容量与在线日志一致,且每个线程的 SRL 组数 ≥ 该线程在线日志组数 + 1;status 不应长期为 UNASSIGNED。

8. 同步位点对比(主备分别执行)

-- 主库:各线程当前最大序列
SELECT thread#, MAX(sequence#) AS last_seq
FROM v$log
GROUP BY thread#
ORDER BY thread#;

-- 备库:各线程已应用到的最大序列
SELECT thread#, MAX(sequence#) AS applied_seq
FROM v$archived_log
WHERE applied = 'YES'
GROUP BY thread#
ORDER BY thread#;

两侧差值持续增大,说明应用跟不上或传输中断。

9. 最近 24 小时 Data Guard 告警

SELECT *
FROM (
  SELECT timestamp, facility, severity, dest_id, message_num, error_code, message
  FROM v$dataguard_status
  WHERE severity IN ('Warning', 'Error', 'Fatal')
    AND timestamp >= SYSDATE - 1
  ORDER BY timestamp DESC, message_num DESC
) WHERE ROWNUM <= 100;

10. Broker 配置巡检(若使用 Broker 管理)

dgmgrl sys/<密码>@<主库TNS> "show configuration verbose"
dgmgrl sys/<密码>@<主库TNS> "show database verbose <db_unique_name>"
dgmgrl sys/<密码>@<主库TNS> "validate database <db_unique_name>"

重点看 Configuration Status、各库 Status、Transport Lag、Apply Lag、Protection Mode,以及 validate 是否报错。


五、主机与日志层

# 资源使用
top -b -n1 | head -20
vmstat 1 5
iostat -x 1 5
free -g
df -h

# 进程检查(备库看 MRP,主库看 LNS/ARCn)
ps -ef | grep -iE "mrp|lns|arc"

# 告警日志
adrci -exec "show alert -tail 200"

六、结果判定要点

检查项 正常判定
集群资源 所有资源 ONLINE,无 UNKNOWN/INTERMEDIATE
ASM state = MOUNTED,offline_disks = 0,usable_file_mb 有合理余量
实例 所有节点 status = OPEN,database_status = ACTIVE
备库角色 PHYSICAL STANDBY,READ ONLY WITH APPLY
传输/应用延迟 接近 0,且 datum_time 持续推进
归档目标 VALID、无 error、gap_status = VALID
日志缺口 v$archive_gap 为空
MRP 进程 MRP0 处于 APPLYING_LOG
SRL 数量满足“每线程 ≥ 在线组数 + 1”,无长期 UNASSIGNED

下面是一份可直接保存执行的 Oracle RAC + ADG 一键巡检脚本(Bash + SQL*Plus)。脚本只读,不改动任何配置;默认以 root 运行时可自动切换到 grid / oracle 用户执行对应命令,也可分别以对应用户运行。

使用方式

chmod +x rac_adg_inspect.sh
./rac_adg_inspect.sh -d <db_unique_name> [-s <ORACLE_SID>] [-o <输出目录>]
# 主库、备库各跑一次;Broker 巡检需额外提供连接串:
DG_BROKER_CONNECT='sys/<密码>@<主库TNS>' ./rac_adg_inspect.sh -d <db_unique_name>

脚本内容

#!/usr/bin/env bash
# =============================================================================
# rac_adg_inspect.sh — Oracle RAC + ADG 一键巡检(只读)
# 自动探测:DB_UNIQUE_NAME / 本地 ORACLE_SID / ORACLE_HOME / 集群节点 / RAC 实例
# 适用:Oracle 11gR2 / 12c / 18c / 19c / 21c+
# 用法:./rac_adg_inspect.sh [-d <db_unique_name>] [-s <ORACLE_SID>] [-o <目录>] [-n]
#   -d  DB_UNIQUE_NAME,可选,未指定时自动探测
#   -s  本地 ORACLE_SID,可选,未指定时自动探测
#   -o  输出目录,默认 /tmp/rac_adg_inspect_<时间戳>
#   -n  不尝试 SSH 到其他节点(默认尝试)
# 环境变量:DG_BROKER_CONNECT='sys/<密码>@<主库TNS>'
# =============================================================================
set -uo pipefail

DB_UNIQUE_NAME=""
ORACLE_SID="${ORACLE_SID:-}"
ORACLE_HOME="${ORACLE_HOME:-}"
OUT_DIR=""
NO_SSH=0
GRID_USER="grid"
DB_USER="oracle"

usage() {
  cat <<EOF
用法: $0 [-d <db_unique_name>] [-s <ORACLE_SID>] [-o <输出目录>] [-n]

  -d  DB_UNIQUE_NAME,可选,自动探测
  -s  本地 ORACLE_SID,可选,自动探测
  -o  输出目录,默认 /tmp/rac_adg_inspect_<时间戳>
  -n  不尝试 SSH 到其他节点
  -h  显示帮助

可选环境变量:
  DG_BROKER_CONNECT   Data Guard Broker 连接串,如 'sys/pass@ORCL_DG'
EOF
  exit 0
}

while [ $# -gt 0 ]; do
  case "$1" in
    -d) DB_UNIQUE_NAME="${2:-}"; shift 2 ;;
    -s) ORACLE_SID="${2:-}"; shift 2 ;;
    -o) OUT_DIR="${2:-}"; shift 2 ;;
    -n) NO_SSH=1; shift ;;
    -h|--help) usage ;;
    *) echo "未知参数: $1"; usage ;;
  esac
done

TS="$(date +%Y%m%d_%H%M%S)"
OUT_DIR="${OUT_DIR:-/tmp/rac_adg_inspect_${TS}}"
mkdir -p "$OUT_DIR/sql" || exit 1
chmod 755 "$OUT_DIR" "$OUT_DIR/sql"
LOG="$OUT_DIR/inspect.log"
: > "$LOG"
chmod 644 "$LOG"

CUR_USER="$(id -un)"

log() { echo "$*" | tee -a "$LOG"; }
sec() { log ""; log "==================== $* ===================="; }

is_root()   { [ "$(id -u)" -eq 0 ]; }
have_user() { id "$1" >/dev/null 2>&1; }

is_local_node() {
  local n="$1" h
  h="$(hostname)"
  [ "$n" = "$h" ] && return 0
  [ "${n%%.*}" = "${h%%.*}" ] && return 0
  return 1
}

# 以指定用户执行命令;非 root 且用户不匹配时输出到 stderr 并返回失败
run_as() {
  local u="$1" c="$2"
  if [ "$CUR_USER" = "$u" ]; then
    bash -lc "$c"
    return
  fi
  if is_root && have_user "$u"; then
    su - "$u" -c "$c"
    return
  fi
  echo "[SKIP] 需要 ${u} 用户权限执行: ${c}" >&2
  return 1
}

run_grid()   { run_as "$GRID_USER" "$1"; }
run_oracle() { run_as "$DB_USER" "$1"; }

run_db() {
  local env="export ORACLE_SID=${ORACLE_SID};"
  [ -n "${ORACLE_HOME:-}" ] && env="${env} export ORACLE_HOME=${ORACLE_HOME};"
  run_oracle "${env} sqlplus -S -L / as sysdba @$1"
}

# 单条 SQL 查询,返回纯文本结果
db_query() {
  local f="$OUT_DIR/sql/_q_$.sql"
  printf "SET HEADING OFF FEEDBACK OFF PAGESIZE 0 LINESIZE 400 TRIMSPOOL ON\n%s\nEXIT\n" "$1" > "$f"
  chmod 644 "$f"
  run_db "$f" 2>/dev/null
  rm -f "$f"
}

# ---------------------------------------------------------------------------
# 探测函数
# ---------------------------------------------------------------------------
detect_local_sid() {
  ps -ef 2>/dev/null | grep '[o]ra_pmon_' | awk '{print $NF}' \
    | sed 's/^ora_pmon_//' | grep -v '^+ASM' | head -1
}

detect_oracle_home_by_sid() {
  local sid="$1" h=""
  if [ -n "$sid" ] && [ -r /etc/oratab ]; then
    h="$(awk -F: -v s="$sid" '$1==s {print $2; exit}' /etc/oratab)"
  fi
  [ -z "$h" ] && h="${ORACLE_HOME:-}"
  printf '%s' "$h"
}

detect_db_unique_name() {
  local list cnt sid du
  list="$(run_grid "srvctl config database" 2>/dev/null | sed 's/[[:space:]]*$//' | sed '/^$/d')"
  cnt="$(printf '%s\n' "$list" | grep -c .)"
  if [ "$cnt" -eq 1 ]; then
    printf '%s' "$list"
    return
  fi
  sid="$(detect_local_sid)"
  if [ -n "$sid" ]; then
    ORACLE_SID="$sid"
    du="$(db_query "SELECT db_unique_name FROM v\$database;" | tr -d '[:space:]')"
    [ -n "$du" ] && { printf '%s' "$du"; return; }
  fi
  printf '%s' ""
}

detect_nodes() {
  local out
  out="$(run_grid "olsnodes -n -s" 2>/dev/null | awk '{print $1}' | sed '/^$/d')"
  [ -z "$out" ] && out="$(run_grid "olsnodes" 2>/dev/null | sed '/^$/d')"
  if [ -z "$out" ]; then
    out="$(db_query "SELECT DISTINCT host_name FROM gv\$instance ORDER BY 1;" | sed '/^$/d')"
  fi
  [ -z "$out" ] && out="$(hostname -s)"
  printf '%s\n' "$out"
}

detect_instances() {
  local out
  out="$(run_grid "srvctl config database -d ${DB_UNIQUE_NAME} -a" 2>/dev/null \
        | awk -F': *' 'tolower($1) ~ /database instance/ {print $2; exit}')"
  if [ -n "$out" ]; then
    printf '%s\n' "$out" | tr ',' '\n' | tr -d '[:space:]' | sed '/^$/d'
    return
  fi
  db_query "SELECT instance_name FROM gv\$instance ORDER BY inst_id;" | sed '/^$/d'
}

detect_instance_map() {
  db_query "SELECT instance_name||'|'||host_name||'|'||thread#||'|'||status FROM gv\$instance ORDER BY inst_id;" | sed '/^$/d'
}

inst_on_node() {
  local n="$1"
  printf '%s\n' "$INST_MAP" | awk -F'|' -v n="$n" '
    { h=$2; sub(/\..*/,"",h); if (h==n || $2==n) {print $1; exit} }'
}

# ---------------------------------------------------------------------------
# 初始化探测
# ---------------------------------------------------------------------------
sec "0. 环境探测"

[ -z "$ORACLE_SID" ] && ORACLE_SID="$(detect_local_sid)"
ORACLE_HOME="$(detect_oracle_home_by_sid "$ORACLE_SID")"

if [ -z "$DB_UNIQUE_NAME" ]; then
  DB_UNIQUE_NAME="$(detect_db_unique_name)"
fi

if [ -z "$DB_UNIQUE_NAME" ]; then
  log "错误:无法自动探测 DB_UNIQUE_NAME。"
  log "候选数据库:$(run_grid 'srvctl config database' 2>/dev/null | tr '\n' ' ')"
  log "请使用 -d 指定后重试。"
  exit 1
fi

if [ -z "$ORACLE_SID" ]; then
  ORACLE_SID="$(run_grid "srvctl config database -d ${DB_UNIQUE_NAME} -a" 2>/dev/null \
    | awk -F': *' 'tolower($1) ~ /database instance/ {print $2; exit}' \
    | cut -d, -f1 | tr -d '[:space:]')"
fi

if [ -z "$ORACLE_HOME" ]; then
  ORACLE_HOME="$(run_grid "srvctl config database -d ${DB_UNIQUE_NAME} -a" 2>/dev/null \
    | awk -F': *' 'tolower($1) ~ /oracle home/ {print $2; exit}')"
fi

NODES="$(detect_nodes)"
INSTANCES="$(detect_instances)"
INST_MAP="$(detect_instance_map)"

DB_VERSION="$(db_query "SELECT version FROM v\$instance;" | head -1 | tr -d '[:space:]')"
MAJOR="$(printf '%s' "$DB_VERSION" | sed 's/^\([0-9][0-9]*\).*/\1/')"
[ -z "$MAJOR" ] && MAJOR=0

# SSH 免密检测
SSH_OK=0
if [ "$NO_SSH" -eq 0 ]; then
  SSH_OK=1
  for n in $NODES; do
    is_local_node "$n" && continue
    if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "${GRID_USER}@${n}" "true" 2>/dev/null; then
      SSH_OK=0
      break
    fi
  done
fi

NODE_CNT="$(printf '%s\n' "$NODES" | grep -c .)"
INST_CNT_CFG="$(printf '%s\n' "$INSTANCES" | grep -c .)"

log "巡检时间        : $(date '+%Y-%m-%d %H:%M:%S')"
log "执行用户        : ${CUR_USER} @ $(hostname)"
log "DB_UNIQUE_NAME  : ${DB_UNIQUE_NAME}"
log "本地 ORACLE_SID : ${ORACLE_SID:-未探测到}"
log "ORACLE_HOME     : ${ORACLE_HOME:-未探测到}"
log "数据库版本      : ${DB_VERSION:-未探测到}(主版本 ${MAJOR})"
log "集群节点(${NODE_CNT})     : $(printf '%s' "$NODES" | tr '\n' ' ')"
log "RAC 实例(${INST_CNT_CFG})     : $(printf '%s' "$INSTANCES" | tr '\n' ' ')"
log "跨节点 SSH      : $([ "$SSH_OK" -eq 1 ] && echo '可用' || echo '不可用,远程节点需本地执行')"
log ""
log "实例-节点映射:"
printf '%s\n' "$INST_MAP" | awk -F'|' '{printf "  %-12s -> %-20s thread=%s status=%s\n",$1,$2,$3,$4}' | tee -a "$LOG"

if [ -z "$ORACLE_SID" ]; then
  log "警告:未能探测到本地 ORACLE_SID,SQL 检查将跳过。"
fi

# ---------------------------------------------------------------------------
# 1. 集群层(集群级,执行一次)
# ---------------------------------------------------------------------------
sec "1. 集群层 (Grid Infrastructure)"

for c in \
  "crsctl check crs" \
  "crsctl stat res -t" \
  "crsctl stat res -t -init" \
  "olsnodes -n -s -t" \
  "oifcfg getif" \
  "crsctl query css votedisk" \
  "ocrcheck" \
  "srvctl status database -d ${DB_UNIQUE_NAME} -v" \
  "srvctl status service -d ${DB_UNIQUE_NAME}" \
  "srvctl config database -d ${DB_UNIQUE_NAME} -a"
do
  log "--- ${c}"
  run_grid "$c" 2>&1 | tee -a "$LOG"
done

log "--- asmcmd lsdg"
run_grid "asmcmd lsdg" 2>&1 | tee -a "$LOG"

log "--- asmcmd lsdsk --statistics"
run_grid "asmcmd lsdsk --statistics" 2>&1 | tee -a "$LOG"

# ---------------------------------------------------------------------------
# 2. 按节点检查
# ---------------------------------------------------------------------------
sec "2. 按节点检查"

for n in $NODES; do
  log ""
  log "########## 节点: ${n} ##########"

  log "--- srvctl status asm -n ${n}"
  run_grid "srvctl status asm -n ${n}" 2>&1 | tee -a "$LOG"

  log "--- srvctl status listener -n ${n}"
  run_grid "srvctl status listener -n ${n}" 2>&1 | tee -a "$LOG"

  log "--- 实例与关键进程"
  run_on_node() {
    local u="$1" node="$2" cmd="$3"
    if is_local_node "$node"; then
      run_as "$u" "$cmd"
    elif [ "$SSH_OK" -eq 1 ]; then
      ssh -o BatchMode=yes -o ConnectTimeout=8 "${u}@${node}" "$cmd" 2>&1
    else
      echo "[SKIP] ${node}:无 SSH 免密,请在该节点本地执行"
    fi
  }

  run_on_node "$GRID_USER" "$n" \
    "ps -ef | grep -E '[o]ra_pmon_|[o]ra_asm_pmon_' | awk '{print \$NF}'" 2>&1 | tee -a "$LOG"

  log "--- 主机资源"
  run_on_node "$GRID_USER" "$n" "top -b -n1 | head -15" 2>&1 | tee -a "$LOG"
  run_on_node "$GRID_USER" "$n" "free -g" 2>&1 | tee -a "$LOG"
  run_on_node "$GRID_USER" "$n" "df -h" 2>&1 | tee -a "$LOG"
  run_on_node "$GRID_USER" "$n" "iostat -x 1 3 2>/dev/null" 2>&1 | tee -a "$LOG"

  inst="$(inst_on_node "$n")"
  if [ -n "$inst" ]; then
    log "--- 节点 ${n} 对应实例 ${inst} 的告警日志尾部"
    run_on_node "$DB_USER" "$n" \
      "export ORACLE_SID=${inst}; adrci -exec 'show alert -tail 100'" 2>&1 | tee -a "$LOG"
  else
    log "[INFO] 节点 ${n} 未映射到运行中的实例,跳过告警日志"
  fi
done

# ---------------------------------------------------------------------------
# 3. 数据库 + ADG 巡检 SQL(gv$ 全局,执行一次)
# ---------------------------------------------------------------------------
sec "3. 数据库实例与 ADG 同步状态"

if [ "$MAJOR" -ge 11 ] 2>/dev/null; then
  GAP_EXTRA=", gap_status"
  DATUM_EXTRA=", datum_time"
else
  GAP_EXTRA=""
  DATUM_EXTRA=""
fi

cat > "$OUT_DIR/sql/db.sql" <<'SQL'
SET PAGESIZE 300 LINESIZE 320 FEEDBACK OFF TRIMSPOOL ON
SET UNDERLINE =
ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS';

PROMPT ===== 实例状态(RAC 全局) =====
SELECT inst_id, instance_number, instance_name, host_name, version,
       status, database_status, startup_time, thread#
FROM gv$instance
ORDER BY inst_id;

PROMPT ===== 集群互连 =====
SELECT inst_id, name, ip_address, is_public, source
FROM gv$cluster_interconnects
ORDER BY inst_id;

PROMPT ===== 服务分布 =====
SELECT inst_id, name, network_name, goal, clb_goal
FROM gv$active_services
ORDER BY inst_id, name;

PROMPT ===== 在线日志 =====
SELECT thread#, group#, sequence#, bytes, status, archived
FROM v$log
ORDER BY thread#, group#;

PROMPT ===== 会话概况 =====
SELECT inst_id, COUNT(*) AS session_cnt,
       SUM(CASE WHEN status='ACTIVE' THEN 1 ELSE 0 END) AS active_cnt
FROM gv$session
GROUP BY inst_id
ORDER BY inst_id;

PROMPT ===== 数据库角色与保护模式 =====
SELECT db_unique_name, database_role, protection_mode, open_mode,
       switchover_status, force_logging, flashback_on, current_scn
FROM v$database;

PROMPT ===== 传输/应用延迟(仅备库有数据) =====
SELECT name, value, unit, time_computed__DATUM_EXTRA__
FROM v$dataguard_stats
WHERE LOWER(name) IN ('apply lag','transport lag')
ORDER BY name;

PROMPT ===== 归档目标状态 =====
SELECT dest_id, status, type, database_mode, recovery_mode, destination,
       error, archived_thread#, archived_seq#,
       applied_thread#, applied_seq#__GAP_EXTRA__
FROM v$archive_dest_status
WHERE status <> 'INACTIVE'
ORDER BY dest_id;

PROMPT ===== 归档缺口 =====
SELECT thread#, low_sequence#, high_sequence#
FROM v$archive_gap
ORDER BY thread#;

PROMPT ===== Data Guard 进程(RAC 按实例) =====
SELECT inst_id, process, pid, status, client_process, client_pid,
       thread#, sequence#, block#, delay_mins
FROM gv$managed_standby
ORDER BY inst_id, process;

PROMPT ===== 备库恢复/应用进度 =====
SELECT start_time, type, item, units, sofar, total, timestamp
FROM v$recovery_progress
WHERE type = 'MEDIA RECOVERY'
ORDER BY start_time DESC, item;

PROMPT ===== 备用日志组 =====
SELECT group#, thread#, sequence#, bytes, used, archived, status
FROM v$standby_log
ORDER BY thread#, group#;

PROMPT ===== 备库已应用序列 =====
SELECT thread#, MAX(sequence#) AS applied_seq
FROM v$archived_log
WHERE applied = 'YES'
GROUP BY thread#
ORDER BY thread#;

PROMPT ===== 主库当前日志序列 =====
SELECT thread#, MAX(sequence#) AS last_seq
FROM v$log
GROUP BY thread#
ORDER BY thread#;

PROMPT ===== 最近24小时 DG 告警 =====
SELECT *
FROM (
  SELECT timestamp, facility, severity, dest_id, message_num, error_code, message
  FROM v$dataguard_status
  WHERE severity IN ('Warning','Error','Fatal')
    AND timestamp >= SYSDATE - 1
  ORDER BY timestamp DESC, message_num DESC
) WHERE ROWNUM <= 100;

PROMPT ===== ASM 磁盘组 =====
SELECT name, type, state, total_mb, free_mb,
       required_mirror_free_mb, usable_file_mb, offline_disks
FROM v$asm_diskgroup_stat
ORDER BY name;

PROMPT ===== ASM 磁盘状态 =====
SELECT group_number, disk_number, name, path,
       mount_status, header_status, mode_status, state
FROM v$asm_disk
ORDER BY group_number, disk_number;

EXIT
SQL

sed -i "s/__GAP_EXTRA__/${GAP_EXTRA}/g; s/__DATUM_EXTRA__/${DATUM_EXTRA}/g" "$OUT_DIR/sql/db.sql"
chmod 644 "$OUT_DIR/sql/db.sql"

if [ -n "$ORACLE_SID" ]; then
  run_db "$OUT_DIR/sql/db.sql" 2>&1 | tee -a "$LOG"
else
  log "[SKIP] 无 ORACLE_SID,跳过数据库 SQL 巡检"
fi

# ---------------------------------------------------------------------------
# 4. 自动判定
# ---------------------------------------------------------------------------
sec "4. 巡检结论汇总"

if [ "$MAJOR" -ge 11 ] 2>/dev/null; then
  DATUM_CHK="SELECT 'CHK_DATUM_TIME='||NVL(MAX(CASE WHEN LOWER(NAME)='apply lag' THEN DATUM_TIME END),'NA') FROM V\$DATAGUARD_STATS;"
else
  DATUM_CHK="SELECT 'CHK_DATUM_TIME=NA' FROM DUAL;"
fi

cat > "$OUT_DIR/sql/check.sql" <<'SQL'
SET HEADING OFF FEEDBACK OFF PAGESIZE 0 LINESIZE 400 TRIMSPOOL ON
SELECT 'CHK_ROLE='||DATABASE_ROLE FROM V$DATABASE;
SELECT 'CHK_PROTECTION='||PROTECTION_MODE FROM V$DATABASE;
SELECT 'CHK_OPEN='||OPEN_MODE FROM V$DATABASE;
SELECT 'CHK_FORCE_LOGGING='||FORCE_LOGGING FROM V$DATABASE;
SELECT 'CHK_ASM_OFFLINE='||NVL(SUM(offline_disks),0) FROM V$ASM_DISKGROUP_STAT;
SELECT 'CHK_ARCHIVE_GAP='||COUNT(*) FROM V$ARCHIVE_GAP;
SELECT 'CHK_MRP='||NVL(MAX(CASE WHEN PROCESS='MRP0' THEN STATUS END),'NONE') FROM GV$MANAGED_STANDBY;
SELECT 'CHK_APPLY_LAG='||NVL(MAX(CASE WHEN LOWER(NAME)='apply lag' THEN VALUE END),'NA') FROM V$DATAGUARD_STATS;
SELECT 'CHK_TRANSPORT_LAG='||NVL(MAX(CASE WHEN LOWER(NAME)='transport lag' THEN VALUE END),'NA') FROM V$DATAGUARD_STATS;
SELECT 'CHK_NODES_IN_DB='||COUNT(DISTINCT host_name) FROM GV$INSTANCE;
SELECT 'CHK_INST_CNT='||COUNT(*) FROM GV$INSTANCE;
SELECT 'CHK_INST_NOT_OPEN='||COUNT(*) FROM GV$INSTANCE WHERE STATUS<>'OPEN';
SELECT 'CHK_INST_NOT_ACTIVE='||COUNT(*) FROM GV$INSTANCE WHERE DATABASE_STATUS<>'ACTIVE';
SELECT 'CHK_SRL_SHORT='||COUNT(*) FROM (
  SELECT l.thread#
  FROM v$standby_log s, v$log l
  WHERE s.thread#=l.thread#
  GROUP BY l.thread#
  HAVING COUNT(DISTINCT s.group#) < COUNT(DISTINCT l.group#)+1
);
__DATUM_CHK__
SELECT 'CHK_CDB='||CASE WHEN (SELECT COUNT(*) FROM V$CONTAINERS)>0 THEN 'YES' ELSE 'NO' END FROM DUAL;
EXIT
SQL

sed -i "s#__DATUM_CHK__#${DATUM_CHK}#" "$OUT_DIR/sql/check.sql"
chmod 644 "$OUT_DIR/sql/check.sql"

CHKOUT="$OUT_DIR/check.out"
if [ -n "$ORACLE_SID" ]; then
  run_db "$OUT_DIR/sql/check.sql" > "$CHKOUT" 2>&1
fi
get_chk() { grep -m1 "^CHK_$1=" "$CHKOUT" 2>/dev/null | cut -d= -f2-; }

RC=0
ok()   { log "  [PASS] $*"; }
warn() { log "  [WARN] $*"; [ "$RC" -lt 1 ] && RC=1; }
bad()  { log "  [FAIL] $*"; RC=2; }

ROLE="$(get_chk ROLE)"
PROT="$(get_chk PROTECTION)"
OPEN="$(get_chk OPEN)"
FORCELOG="$(get_chk FORCE_LOGGING)"
ASMOFF="$(get_chk ASM_OFFLINE)"
GAP="$(get_chk ARCHIVE_GAP)"
MRP="$(get_chk MRP)"
ALAG="$(get_chk APPLY_LAG)"
TLAG="$(get_chk TRANSPORT_LAG)"
DBNODES="$(get_chk NODES_IN_DB)"
DBINST="$(get_chk INST_CNT)"
NOTOPEN="$(get_chk INST_NOT_OPEN)"
NOTACT="$(get_chk INST_NOT_ACTIVE)"
SRLSHORT="$(get_chk SRL_SHORT)"
DATUMTIME="$(get_chk DATUM_TIME)"
ISCDB="$(get_chk CDB)"

log "  识别角色: ${ROLE:-未知} / ${OPEN:-未知} / ${PROT:-未知}"
log "  数据库类型: $([ "$ISCDB" = "YES" ] && echo CDB || echo 'NON-CDB/未识别')"

# 节点与实例覆盖
if [ -n "$DBNODES" ] && [ "$DBNODES" -lt "$NODE_CNT" ] 2>/dev/null; then
  bad "集群节点 ${NODE_CNT} 个,但仅 ${DBNODES} 个节点运行实例"
elif [ -n "$DBNODES" ]; then
  ok "节点覆盖正常:${DBNODES}/${NODE_CNT}"
fi

if [ -n "$DBINST" ] && [ "$DBINST" -lt "$INST_CNT_CFG" ] 2>/dev/null; then
  bad "配置实例 ${INST_CNT_CFG} 个,实际运行 ${DBINST} 个"
elif [ -n "$DBINST" ]; then
  ok "实例数量正常:${DBINST}/${INST_CNT_CFG}"
fi

[ "${NOTOPEN:-0}" = "0" ] && ok "所有实例状态为 OPEN" || bad "存在非 OPEN 实例:${NOTOPEN} 个"
[ "${NOTACT:-0}" = "0" ]  && ok "所有实例 database_status 为 ACTIVE" || bad "存在非 ACTIVE 实例:${NOTACT} 个"

[ "$FORCELOG" = "YES" ] && ok "FORCE_LOGGING = YES" || warn "FORCE_LOGGING = ${FORCELOG},建议开启"

if [ -n "$ASMOFF" ] && [ "$ASMOFF" != "0" ]; then
  bad "ASM 存在离线磁盘:offline_disks=${ASMOFF}"
else
  ok "ASM 无离线磁盘"
fi

case "$ROLE" in
  PRIMARY)
    log "  [INFO] 当前为主库,跳过备库专用判定"
    ;;
  *STANDBY*)
    log "  [INFO] 当前为备库,执行同步层判定"

    if [ "${GAP:-1}" = "0" ]; then ok "无归档缺口"; else bad "存在归档缺口:${GAP} 条"; fi

    case "$MRP" in
      APPLYING_LOG)     ok "MRP0 状态 APPLYING_LOG";;
      WAIT_FOR_LOG|IDLE) warn "MRP0 状态 ${MRP},需确认是否有新日志";;
      WAIT_FOR_GAP)     bad "MRP0 状态 WAIT_FOR_GAP,应用被缺口阻断";;
      NONE)             bad "未发现 MRP0 进程,备库可能未应用日志";;
      *)                warn "MRP0 状态异常:${MRP}";;
    esac

    case "$ALAG" in
      NA|"") warn "未能读取 apply lag";;
      *00:00:00*) ok "apply lag = ${ALAG}";;
      *) warn "apply lag = ${ALAG},请持续观察";;
    esac

    case "$TLAG" in
      NA|"") warn "未能读取 transport lag";;
      *00:00:00*) ok "transport lag = ${TLAG}";;
      *) warn "transport lag = ${TLAG},请持续观察";;
    esac

    if [ -n "$DATUMTIME" ] && [ "$DATUMTIME" != "NA" ]; then
      log "  [INFO] apply lag 数据时间 DATUM_TIME = ${DATUMTIME}(需与上次采样比较是否推进)"
    fi

    if [ "${SRLSHORT:-0}" = "0" ]; then
      ok "备用日志组数量满足每线程 >= 在线组数 + 1"
    else
      warn "存在线程备用日志组数量不足:${SRLSHORT} 个线程"
    fi
    ;;
  *)
    warn "未能识别数据库角色"
    ;;
esac

# ---------------------------------------------------------------------------
# 5. Data Guard Broker(可选)
# ---------------------------------------------------------------------------
if [ -n "${DG_BROKER_CONNECT:-}" ]; then
  sec "5. Data Guard Broker"
  log "--- show configuration verbose"
  run_oracle "dgmgrl -silent '${DG_BROKER_CONNECT}' 'show configuration verbose'" 2>&1 | tee -a "$LOG"
  log "--- show database verbose ${DB_UNIQUE_NAME}"
  run_oracle "dgmgrl -silent '${DG_BROKER_CONNECT}' 'show database verbose ${DB_UNIQUE_NAME}'" 2>&1 | tee -a "$LOG"
  log "--- validate database ${DB_UNIQUE_NAME}"
  run_oracle "dgmgrl -silent '${DG_BROKER_CONNECT}' 'validate database ${DB_UNIQUE_NAME}'" 2>&1 | tee -a "$LOG"
else
  log ""
  log "[INFO] 未设置 DG_BROKER_CONNECT,跳过 Broker 巡检"
fi

# ---------------------------------------------------------------------------
# 6. 结果
# ---------------------------------------------------------------------------
sec "6. 巡检结束"
case "$RC" in
  0) log "  总体结论: 正常(PASS)";;
  1) log "  总体结论: 存在告警项,建议人工复核(WARN)";;
  2) log "  总体结论: 存在异常项,需立即处理(FAIL)";;
esac
log "  完整日志: ${LOG}"
log "  结束时间: $(date '+%Y-%m-%d %H:%M:%S')"

exit "$RC"

posted on 2026-10-27 15:15  刘子毅  阅读(7)  评论(0)    收藏  举报

导航