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时触发告警,例如调用邮件或企业微信机器人接口。
四、任务调度集成: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或备份。
ps -ef中暴露明文。
总结来看,Shell脚本驱动Oracle运维的价值不在于“能跑”,而在于“可重复、可判断、可追溯、可调度”。命令封装解决复用问题,sqlplus规范解决解析问题,日志与返回值解决可信问题,crontab集成解决自动化问题。四者合在一起,才构成一个真正可用于生产环境的运维脚本。