4. Shell脚本驱动Oracle运维

当Oracle运维从“手工敲命令”走向“批量、定时、可追溯”时,Shell脚本是最贴近数据库服务器的自动化载体。它不需要额外中间件,能直接调用sqlplus、RMAN、Data Pump、crontab等原生工具,把零散命令封装成可重复执行、可判断结果、可记录日志的运维单元。本节围绕一条完整的脚本链路展开:命令如何封装、sqlplus如何规范调用、日志如何记录、返回值如何判断、任务如何接入调度。

一、常用命令封装:把“一次性操作”变成“可复用函数”

Oracle运维中高频出现的动作包括:查询实例状态、检查表空间使用率、收集统计信息、导出schema、清理归档日志、检查告警日志关键字等。如果每次都手写完整命令,既容易出错,也无法统一日志和错误处理。合理的做法是在脚本头部定义环境变量和公共函数,再按任务调用。

#!/bin/bash
export ORACLE_SID=ORCL
export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1
export PATH=$ORACLE_HOME/bin:$PATH
export NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'

LOG_DIR=/u01/scripts/log
LOG_FILE=$LOG_DIR/check_tablespace_$(date +%Y%m%d).log

log(){
  echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"
}

run_sql(){
  local sql="$1"
  sqlplus -S "/ as sysdba" <<EOF
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF
SET LINESIZE 200 TRIMSPOOL ON
$sql
EXIT
EOF
}

这里的关键点有三个:第一,ORACLE_SID、ORACLE_HOME、PATH必须在脚本内显式导出,不能依赖登录shell的隐式环境,否则crontab执行时极易找不到sqlplus;第二,NLS_DATE_FORMAT统一日期格式,避免日志和查询结果因会话参数不同而难以比对;第三,run_sql函数把sqlplus调用参数固定下来,后续所有查询只传SQL正文,减少重复代码。

二、sqlplus调用规范:静默、可控、可解析

脚本中调用sqlplus,核心目标是“输出可被Shell解析”。因此要避免交互式提示、避免页眉页脚、避免多余空行。常用规范如下:

  • 使用 -S 静默模式,抑制sqlplus版本和登录提示。
  • 使用 SET PAGESIZE 0、FEEDBACK OFF、HEADING OFF,让结果只保留数据行。
  • 使用 SET TRIMSPOOL ON 去掉行尾空格,便于后续字符串比较。
  • 使用 WHENEVER SQLERROR EXIT SQL.SQLCODE,让SQL报错时sqlplus以非零码退出,Shell才能捕获。
  • 连接方式优先使用操作系统认证 / as sysdba 或钱包,避免明文密码出现在脚本和进程列表中。

一个带错误退出的查询封装如下:

check_ts_usage(){
  sqlplus -S "/ as sysdba" <<'EOF'
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF
SET LINESIZE 200 TRIMSPOOL ON
WHENEVER SQLERROR EXIT SQL.SQLCODE
SELECT d.tablespace_name || '|' ||
       ROUND((d.bytes - NVL(f.bytes,0)) / d.bytes * 100, 2)
FROM   dba_data_files d,
       (SELECT tablespace_name, SUM(bytes) bytes
        FROM   dba_free_space GROUP BY tablespace_name) f
WHERE  d.tablespace_name = f.tablespace_name(+)
ORDER  BY 2 DESC;
EXIT
EOF
}

注意这里使用 <<'EOF' 而非 <<EOF,单引号会阻止Shell对SQL中的 $、反引号做变量展开,避免SQL正文被Shell误解析。输出用 | 分隔,是为了后续用 awk -F'|' 稳定取值。

三、日志记录与返回值判断:让脚本“会说话、会报警”

日志不是简单追加一行文本,而要包含时间、脚本名、任务名、结果和关键数据。建议统一格式:[时间] [级别] [脚本] 消息。级别至少区分INFO、WARN、ERROR。返回值判断则依赖两个层面:sqlplus的退出码和SQL查询结果本身。

THRESHOLD=85
check_ts_usage | while IFS='|' read -r ts_name used_pct; do
  [ -z "$ts_name" ] && continue
  if [ "$(echo "$used_pct > $THRESHOLD" | bc)" -eq 1 ]; then
    log "WARN  表空间 $ts_name 使用率 ${used_pct}% 超过阈值 ${THRESHOLD}%"
  else
    log "INFO  表空间 $ts_name 使用率 ${used_pct}% 正常"
  fi
done
rc=${PIPESTATUS[0]}
if [ "$rc" -ne 0 ]; then
  log "ERROR sqlplus执行失败,退出码 $rc"
  exit 2
fi

这里有两个容易踩的坑:一是管道中while在子shell执行,循环内变量无法回传父shell,因此判断sqlplus退出码要用PIPESTATUS[0];二是浮点比较不能直接用-gt,需借助bc或awk。若脚本用于监控,还应在ERROR时触发告警,例如调用邮件或企业微信机器人接口。

Shell脚本驱动Oracle运维:执行链路与判断分支 crontab / 调度器 定时触发脚本 环境变量导出 ORACLE_SID / ORACLE_HOME PATH / NLS_DATE_FORMAT 命令封装函数 run_sql / check_ts_usage log 统一日志函数 sqlplus -S 静默执行SQL WHENEVER SQLERROR 输出解析与阈值判断 awk -F'|' 提取 tablespace_name / used_pct bc 浮点比较:used_pct > 85 ? INFO 正常 写入日志,退出码 0 WARN / ERROR 触发告警,退出码非0 未超阈值 超阈值 退出码判断:PIPESTATUS[0] rc != 0 → 记录 ERROR,exit 2;rc = 0 → 继续 sqlplus 退出码 统一日志文件:/u01/scripts/log/check_tablespace_YYYYMMDD.log 格式:[时间] [级别] [脚本] 消息 —— 支持事后审计、趋势分析与告警回溯

四、任务调度集成:crontab中的Oracle脚本要点

把脚本接入crontab后,最容易出现三类问题:环境缺失、并发重入、日志丢失。对应处理方式如下:

  • 环境缺失:crontab不加载用户profile,必须在脚本内显式source环境或直接export。建议在脚本开头写 source /home/oracle/.bash_profile 并再次确认which sqlplus可用。
  • 并发重入:若任务执行时间可能超过调度间隔,需加锁。常用 flock 或基于目录的锁文件,例如 flock -n /tmp/check_ts.lock -c "/u01/scripts/check_tablespace.sh",避免同一任务叠加执行。
  • 日志丢失:crontab默认把stdout/stderr发邮件,若未配置邮件会丢弃。应在脚本内重定向,或crontab行尾追加 >> /u01/scripts/log/cron.log 2>&1。

一个可参考的crontab条目如下,表示每天7点检查表空间,每15分钟检查一次告警日志关键字:

0 7 * * * /u01/scripts/check_tablespace.sh >> /u01/scripts/log/cron.log 2>&1
*/15 * * * * /u01/scripts/check_alertlog.sh >> /u01/scripts/log/cron.log 2>&1

五、一个可直接套用的脚本骨架

把上述要点合并,可得到如下骨架。它体现了“环境—日志—封装—执行—判断—退出”的完整闭环,实际使用时只需替换SQL和阈值。

#!/bin/bash
set -o pipefail
export ORACLE_SID=ORCL
export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1
export PATH=$ORACLE_HOME/bin:$PATH
export NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'

LOG_DIR=/u01/scripts/log
LOG_FILE=$LOG_DIR/check_tablespace_$(date +%Y%m%d).log
THRESHOLD=85

log(){ echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"; }

check_ts_usage(){
  sqlplus -S "/ as sysdba" <<'EOF'
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF
SET LINESIZE 200 TRIMSPOOL ON
WHENEVER SQLERROR EXIT SQL.SQLCODE
SELECT d.tablespace_name || '|' ||
       ROUND((d.bytes - NVL(f.bytes,0)) / d.bytes * 100, 2)
FROM   dba_data_files d,
       (SELECT tablespace_name, SUM(bytes) bytes
        FROM   dba_free_space GROUP BY tablespace_name) f
WHERE  d.tablespace_name = f.tablespace_name(+)
ORDER  BY 2 DESC;
EXIT
EOF
}

log "INFO  开始检查表空间使用率,阈值 ${THRESHOLD}%"
check_ts_usage | while IFS='|' read -r ts_name used_pct; do
  [ -z "$ts_name" ] && continue
  if [ "$(echo "$used_pct > $THRESHOLD" | bc)" -eq 1 ]; then
    log "WARN  表空间 $ts_name 使用率 ${used_pct}% 超过阈值"
  else
    log "INFO  表空间 $ts_name 使用率 ${used_pct}% 正常"
  fi
done
rc=${PIPESTATUS[0]}
if [ "$rc" -ne 0 ]; then
  log "ERROR sqlplus执行失败,退出码 $rc"
  exit 2
fi
log "INFO  检查完成"
exit 0
实践提示:脚本上线前,先用 bash -n script.sh 做语法检查,再用 bash -x script.sh 跟踪一次执行路径;确认无误后,再放入crontab并观察前两次调度日志。对于涉及数据变更的脚本,建议先在测试库执行,并保留回滚SQL或备份。
安全注意:不要在脚本中硬编码数据库账号密码。优先使用操作系统认证、Oracle Wallet或外部密码文件;若必须使用密码,应限制脚本权限为700,并避免在ps -ef中暴露明文。

总结来看,Shell脚本驱动Oracle运维的价值不在于“能跑”,而在于“可重复、可判断、可追溯、可调度”。命令封装解决复用问题,sqlplus规范解决解析问题,日志与返回值解决可信问题,crontab集成解决自动化问题。四者合在一起,才构成一个真正可用于生产环境的运维脚本。