
做Oracle运维的人几乎没有不被Oracle Scheduler调度器坑过的。凌晨两点的告警短信、卡在RUNNING状态死活不动的作业、悄无声息“失联”的定时任务这些场景我相信每个DBA都经历过。Oracle Scheduler是数据库内置的作业调度引擎处理定时执行PL/SQL块、存储过程、外部脚本这些常规场景非常方便但一旦出问题定位起来往往比应用本身还要费劲——因为它涉及到作业状态、调度链路、资源窗口、依赖关系等多层因素。这篇内容我从实际操作出发把Oracle Scheduler任务故障诊断的完整思路梳理一遍适合正在排查调度问题的朋友也适合刚接手数据库运维、想系统掌握调度器排查技巧的人。全文以真实场景为主线配合可以直接抄的SQL和排查方法尽量做到你看完能上手用。1. 诊断前先搞懂Scheduler的工作原理1.1 核心组件Job、Program、Schedule与Chain把Scheduler当成一个工厂来理解会容易很多。Job就是生产指令告诉工厂要做什么活Program是工序说明书定义了这个活具体怎么干Schedule是排班表规定什么时候开工Chain则是流水线的联动装置用来编排多个Job之间的先后依赖关系。在Oracle 10g之前DBA用的还是DBMS_JOB那个年代写作业就是直接指定一个存储过程和执行间隔比较粗糙。从10g开始Oracle推出了DBMS_SCHEDULER核心的变化就是把这几个概念拆开了。为什么要拆最直接的好处是复用多个Job可以共用一个Program同一个Job可以挂不同的Schedule调度逻辑和业务逻辑分离之后维护成本明显下降。做故障诊断的时候这个拆分的结构对我们很有用。拿到一个出问题的Job你先得搞清楚它到底用的是内联定义还是引用定义。内联定义就是在创建Job时直接写入job_type和job_action引用定义则是通过program_name关联到Program对象。如果Job是引用Program的排查时必须同时检查Program是否存在、是否被禁用、参数是否有变化否则很容易漏掉根因。1.2 状态机一个作业从创建到消亡的完整生命周期Scheduler作业的状态变化是整个诊断体系的地基。我见过不少同事排查问题上来就查告警日志结果绕了一大圈其实从作业状态里就能直接看出问题方向。一个常规作业会经历这些状态创建之后初始是CREATED满足调度条件后进入SCHEDULED到点开始执行就变成RUNNING跑完正常结束是SUCCEEDED失败则是FAILED被手动停止是STOPPED连续失败达到阈值还会自动进入BROKEN状态。如果是Chain中的环节还有CHAIN_STALLED这种等待状态。这里有个关键点BROKEN状态不代表作业本身代码有错而是指调度器主动“罢工”了。作业在连续失败次数超过max_failures之后调度器就会把它标记为BROKEN不再继续调度。很多新手看到BROKEN就慌了以为是数据库出了问题其实只要定位到失败原因、修复之后手动enable一下就能恢复。状态机思维的价值在哪里排查时你先看当前状态快速缩小问题范围SCHEDULED卡住要查调度器进程RUNNING卡住要查会话和锁BROKEN要查历史日志。不同的状态对应完全不同的排查路径这一步判断对了后面能省至少一半时间。2. 核心诊断工具系统表和视图才是第一现场2.1 必背的第一张视图DBA_SCHEDULER_JOBS排查Oracle Scheduler问题第一站永远是DBA_SCHEDULER_JOBS。这张视图记录了每个作业的元数据和当前生命周期状态一条SQL就能摸清基本信息。SELECT job_name, job_type, job_action, enabled, state, run_count, max_runs, failure_count, max_failures, retry_count, last_start_date, last_run_duration, next_run_date FROM dba_scheduler_jobs WHERE job_name YOUR_JOB_NAME;几个字段需要重点理解。STATE字段决定排查方向前面已经说过。RUN_COUNT和FAILURE_COUNT是历史累积值分别反映作业成功执行次数和失败次数。MAX_FAILURES是作业能容忍的最大连续失败数默认值跟版本有关系。RETRY_COUNT表示当前作业已重试的次数如果设置了retry_count参数作业失败后调度器会自动重新执行。有一个容易被忽略的字段是JOB_ACTION。很多作业改过代码之后忘了同步实际执行的仍是旧的存储过程这种故障在状态上完全看不出异常——作业每次都成功但逻辑已经不对了。所以排查问题如果找不到头绪把job_action拉出来和业务方确认一下往往有惊喜。再补一条实用技巧直接查dba_scheduler_jobs里enabled为FALSE但业务方说“应该跑”的作业。现实中大量的“作业没跑”问题其实是有人手动禁用后忘了恢复。2.2 历史执行记录DBA_SCHEDULER_JOB_RUN_DETAILS是真相所在大部分诊断结论最终都要落到执行历史记录上。DBA_SCHEDULER_JOB_RUN_DETAILS保存了每个作业每次执行的明细包括开始时间、结束时间、运行时长、状态、错误信息等。SELECT log_id, job_name, status, error#, req_start_date, actual_start_date, run_duration, additional_info FROM dba_scheduler_job_run_details WHERE job_name YOUR_JOB_NAME ORDER BY log_id DESC FETCH FIRST 20 ROWS ONLY;重点看三个字段。STATUS字段直接告诉执行结果最常见是SUCCEEDED、FAILED、STOPPED。ERROR#是数据库错误号如果作业执行过程抛了ORA错误会记录在这里。ADDITIONAL_INFO非常关键它会包含具体的错误堆栈、失败原因和错误发生时的环境信息。实际工作中我遇到过好几次ERROR#为0但作业状态却是FAILED的情况。这种时候错误信息全在ADDITIONAL_INFO里可能写着ORA-06512之类的位置信息也可能记录了用户自定义异常的消息文本。所以诊断时不能只看ERROR#一定要展开ADDITIONAL_INFO。注意这个视图默认会清理历史数据只保留最近30天左右的记录。如果作业是更早之前失败的可能查不到。这时候可以用DBMS_SCHEDULER.PURGE_LOG手动控制清理策略我在后面第5章会专门讲。2.3 正在执行的作业DBA_SCHEDULER_RUNNING_JOBS与v$session联动如果作业卡在RUNNING单看元数据就不够了得找到它对应的数据库会话。数据字典视图DBA_SCHEDULER_RUNNING_JOBS记录了当前正在运行的作业信息包括作业名、会话ID和从属进程名。SELECT rj.job_name, rj.session_id, s.serial#, s.status, s.sql_id, s.event, s.program, s.module, s.last_call_et FROM dba_scheduler_running_jobs rj LEFT JOIN v$session s ON rj.session_id s.sid WHERE rj.job_name YOUR_JOB_NAME;拿到会话信息后能不能顺着SQL_ID去v$sql里找到具体执行的SQL语句然后通过v$session_wait、v$active_session_history分析等待事件。这一步就是把Oracle Scheduler的问题转换成常规的数据库会话问题来处理了之后的排查思路跟普通慢SQL、锁等待完全一样。补充一个容易踩的坑dba_scheduler_running_jobs里的SESSION_ID对应v$session的SID字段但如果你开了RAC不要忘记再加上INST_ID条件。多节点环境下作业可能在另一个节点上跑本地节点查不到会话信息这是分布式环境下最常见的误判。2.4 窗口与资源计划一个容易忽略的隐藏关卡Oracle Scheduler有一个很多初级DBA不清楚的机制作业可以放在窗口Window里调度窗口打开时会自动激活对应的资源计划Resource Plan。这个设计的初衷是好的让数据库能在规定时间窗内优先保证某些作业的资源但如果配置不当反而会成为故障源头。比如作业确实到点触发了但分配的CPU资源极少或者被其他高优先级作业抢占跑得非常慢看起来就像卡住了一样。这时候从作业状态看是RUNNING从历史日志看还没失败但运行时长异常偏长。排查这类问题要查DBA_SCHEDULER_WINDOWS和DBA_SCHEDULER_WINDOW_DETAILS确认作业关联的窗口是否在预期时间打开、窗口绑定的资源计划是否合理、有没有被更高优先级的窗口抢占。另外别忘了DBA_SCHEDULER_WINDOW_GROUPS如果作业时通过窗口组调度的组内窗口的先后顺序也会影响实际执行。3. 实战演练一个作业卡在RUNNING的真实排查过程3.1 场景描述与第一眼定位某天早上收到告警生产库的核心数据同步作业ETL_SYNC从凌晨1点开始执行到现在1小时过去还是RUNNING状态。正常情况下这个作业跑10分钟就能结束。登录数据库先看作业状态SELECT job_name, state, enabled, run_count, failure_count, last_start_date, last_run_duration FROM dba_scheduler_jobs WHERE job_name ETL_SYNC;返回结果里STATE是RUNNINGLAST_START_DATE对应当天凌晨1点LAST_RUN_DURATION没有更新——因为作业还没跑完。第一反应是作业确实还在运行中不是调度器没触发。下一步就是用上一节的方法找到作业对应的会话。3.2 定位会话并分析等待事件执行关联查询后拿到了session_id为142v$session里status是ACTIVEevent是“enq: TX - row lock contention”LAST_CALL_ET已经超过3000秒。这个等待事件信息量很大翻译成大白话就是作业正在等一把行锁而且已经等了快1个小时。顺着这个线索查锁等待的阻塞源SELECT blocking_session, blocking_session_serial#, wait_class, seconds_in_wait FROM v$session WHERE sid 142;发现阻塞会话SID是98于是继续看SID 98是什么来头结果是一个应用连接长时间占用着某张表的某一行事务一直未提交。再查数据库的锁详细信息确认了业务表的某一行被应用会话锁住而ETL_SYNC作业恰好要更新同一行两边就杠上了。3.3 处理与复盘这种情况处理方案很明确先跟业务确认SID 98对应的应用操作是否可以终止得到确认后直接杀掉阻塞会话ALTER SYSTEM KILL SESSION 98, 12345 IMMEDIATE;阻塞解除后ETL_SYNC作业自动恢复执行很快就跑完了。事后复盘时需要在文档里记录两件事第一为什么应用会长时间未提交事务——这属于业务侧的异常行为需要应用团队排查第二这个作业为什么会被单行锁卡死——可以跟开发确认作业内部是否有不合理的锁顺序能否改成更细粒度的处理逻辑。这个案例能说明一个问题Oracle Scheduler作业卡住的根因往往不在于调度器本身而在于它拉起的会话在数据库层面遇到了阻塞。诊断的关键路径就是“作业状态→会话→等待事件→阻塞源”一环扣一环每一步在对应的视图里都能找到答案。3.4 作业卡住的备选处理手段如果阻塞源一时半会儿无法处理作业又不能一直挂在那儿占着资源可以考虑用STOP_JOB强制停止。BEGIN DBMS_SCHEDULER.STOP_JOB(job_name ETL_SYNC, force TRUE); END;注意STOP_JOB的force参数FALSE表示正常停止调度器会等待作业当前的调用完成后才停TRUE表示立即终止作业会话。实际操作中如果作业卡在锁等待上正常停止可能等很久一般建议先尝试FALSE等不到结果再上TRUE。但一定记得强制停止之后要把作业置回可调度状态而且对正在跑的事务要评估回滚的影响。4. 高频故障模式与排查速查表4.1 作业完全不执行先查“有没有被调度”“该跑没跑”是第二大类高频问题。作业配置正常、历史跑得好好的某天突然没执行。排查思路按顺序展开。先查DBA_SCHEDULER_JOBS的STATE和ENABLED。如果ENABLED是FALSE查一下最近有没有人手动禁用过。如果没有再看NEXT_RUN_DATE。如果NEXT_RUN_DATE是空的说明作业的调度条件被改坏了检查之前设置的时间间隔或日期表达式。还有一个常被忽略的情况作业虽然启用了但它的Schedule对象被禁用或者被删了这会让作业失去触发源。RAC环境下还要多查一层。调度器默认有JOB_QUEUE_PROCESSES参数控制作业队列进程数检查这个参数是否为0。在多节点环境下如果作业被指定了服务名或节点而目标节点刚好不在线也会导致作业一直等待。这种状态下作业的STATE通常会显示SCHEDULED但永远到不了RUNNING。最后查提醒确认作业的Owner是否有正确的权限。Oracle 12c以后权限管理更严格如果作业用到了外部脚本或凭证Credential权限问题会更隐蔽。4.2 作业频繁失败看历史日志定位错误作业能跑、但经常失败这种情况反而好定位因为DBA_SCHEDULER_JOB_RUN_DETAILS里记录的信息足够多。把最近20条记录拉出来按时间排一下看失败是否有规律。如果是固定时间点失败比如每天第一次跑失败第二次成功很可能是依赖的数据还没准备好属于调度时间设计问题。如果是随机失败那就重点看ADDITIONAL_INFO里的错误代码。常见的错误我已经整理在手边错误代码含义排查方向ORA-00942表或视图不存在对象权限、对象被删除、同义词失效ORA-01031权限不足作业Owner是否缺少必要权限ORA-04063PL/SQL对象失效编译错误、依赖对象被修改ORA-06512PL/SQL执行位置信息结合堆栈往上找根因ORA-27486权限不足Scheduler层是否缺少CREATE JOB、EXECUTE权限ORA-27300外部作业相关系统错误操作系统层问题ORA-20000用户自定义异常打开告警中业务逻辑信息记住一个原则ADDITIONAL_INFO里的内容就是第一手证据强烈建议排查时先把这里的完整内容复制保存尤其是ORA-06512给出的堆栈位置能直接指出PL/SQL代码的出错行号。重试机制也要理解。如果作业设置了retry_count失败后调度器会自动重试。很多人看历史记录发现同一个作业连续多条FAILED就是没配置重试——本来设计上就是这么跑的。要判断作业是否健康不能只看单次失败要看连续失败后有没有自动成功以及MAX_FAILURES阈值有没有设得合理。4.3 作业执行到一半中断查阻断事件跟RUNNING卡住不同中断更常见于数据库异常或者外部因素干预。比如数据库实例重启、会话被kill、磁盘空间满、归档日志目录不可写等等。通过JOB_RUN_DETAILS查询时状态显示STOPPED就说明作业是被外部停止的。原因可能来自手动STOP_JOB调用也可能来自数据库层面的会话终止。结合数据库告警日志和监听日志可以进一步确认。一个特有的场景是Oracle升级或打补丁后部分作业执行到一半报错中断。这种情况往往是因为内部包版本升级导致行为变化排查时优先确认作业的PL/SQL代码做了什么操作以及依赖的内置包是否有行为变更。从经验看升级后大量作业异常多数是权限批量失效或者同义词没刷新导致的。如果是外部脚本作业system类型还要检查脚本路径是否正确、脚本是否有执行权限、操作系统环境变量是否一致。Oracle Scheduler执行外部作业时用的是作业创建时指定的Credential如果Credential对应的系统用户权限变了或者密码改了也会造成作业启动后立即退出。4.4 调度链Chain挂起的排查思路Chain是Oracle Scheduler的高级功能适合处理复杂的依赖调度场景。问题也更复杂Chain中的作业不是独立调度而是由一个链控制器统一管理任何一环出问题后续环节都会停在等待状态。Chain挂起时作业状态通常是CHAIN_STALLED。排查路径是先定位Chain停在了哪个步骤然后检查该步骤对应的事件或条件。Chain中的步骤有几种如果是Program步骤查它自身的历史执行记录如果是WaitEvent步骤查它依赖的事件是不是一直没有被触发。实操中常见的Chain问题有两种。第一种是定义事件步骤时用错了事件类型导致等待条件永远不成立。第二种是Chain步骤里的Program执行成功了但步骤没有按照预期转向下一个节点。这两种情况都要打开DBA_SCHEDULER_CHAIN_RULES和DBA_SCHEDULER_CHAIN_STEPS两张视图逐条核对每个规则的条件和动作同时把Chain的执行历史也拿出来对照。4.5 快速排查速查表综合多年的踩坑经验整理出下面这个速查表遇到Scheduler故障可以先对着查。作业状态可能原因首选排查视图RUNNING但卡死锁等待、资源不足、外部依赖阻塞DBA_SCHEDULER_RUNNING_JOBS v$sessionSCHEDULED但一直不跑队列进程异常、服务名不匹配、窗口未打开DBA_SCHEDULER_JOBS JOB_QUEUE_PROCESSESFAILED代码错误、权限不足、对象失效DBA_SCHEDULER_JOB_RUN_DETAILSBROKEN连续失败次数超阈值DBA_SCHEDULER_JOBS 历史详情CHAIN_STALLED链步骤事件未满足DBA_SCHEDULER_CHAIN_STEPS CHAIN_RULES无状态/不存在作业被删除、同义词失效DBA_OBJECTS确认对象是否存在这张表是我做故障诊断时挂在手边的底稿每次遇到问题先对号入座能节约不少判断时间。5. 日常运维中让故障少一半的几条经验5.1 日志保留与清理别等需要时才后悔DBA_SCHEDULER_JOB_RUN_DETAILS默认只保留最近30天数据但生产环境往往需要更长的历史周期来做趋势分析。我习惯在数据库初始化或巡检时就把日志保留策略调好。BEGIN DBMS_SCHEDULER.SET_SCHEDULER_ATTRIBUTE( attribute LOG_HISTORY, value 90 ); END;同时建议把不用的历史记录定期清理避免占用SYSAUX空间BEGIN DBMS_SCHEDULER.PURGE_LOG( logs_between SYSDATE - 90, logs_until NULL ); END;这里有一个容易踩的坑LOG_HISTORY属性值设置太大会导致SYSAUX表空间膨胀太短又会让历史数据很快消失。90天是一个经验上比较合理的折中值。另外清理操作也会产生redo建议在维护窗口执行不要在大白天业务高峰期跑。5.2 定期巡检脚本把故障拦在前面与其等问题爆发不如每周自动跑一遍巡检。下面这条SQL查出所有异常状态和即将达到阈值的作业SELECT job_name, state, enabled, run_count, failure_count, max_failures, next_run_date FROM dba_scheduler_jobs WHERE enabled TRUE AND (state IN (BROKEN, CHAIN_STALLED) OR (max_failures 0 AND failure_count max_failures - 1)) ORDER BY state;再配合另一个视图查最近24小时内失败的作业SELECT job_name, status, error#, actual_start_date, run_duration FROM dba_scheduler_job_run_details WHERE status FAILED AND actual_start_date SYSDATE - 1 ORDER BY actual_start_date DESC;把这两条SQL放到Zabbix或者任何监控平台上每周汇总一次大部分调度问题都能在可控范围内提前发现。5.3 配置变更管理小改动大事故调度器相关配置改动虽然不像应用发布那么频繁但影响面很大。我经历过一次教训开发人员为了测试直接改了生产环境的作业调度频率忘了改回来结果作业在业务高峰期连续触发把数据库负载打满了。现在我的做法是所有Scheduler相关变更统一走变更流程——创建作业、修改调度、调整窗口都要有对应的脚本评审记录。修改之前先确认作业的任务类型和负载特征修改之后立刻观察接下来几个周期内是否出现了资源抢占或者锁竞争。另外作业参数和Program对象建议纳入版本管理。调度逻辑也是代码应该和业务代码一样被严肃对待。5.4 个人经验合理设置失败阈值与重试参数最后分享一点实际体会。很多团队创建Scheduler作业时很随意失败重试次数、最大失败阈值都保持默认值这给后续运维埋了很大的雷。我一般建议对核心作业设置retry_count为2到3次、max_failures为5次左右。重试次数太少了遇到瞬时故障很容易直接失败太多了如果作业本身有bug就会反复执行占用大量资源。MAX_FAILURES设置太小作业一两次失败就进入BROKEN状态需要人工介入设置太大则失去了自动熔断的保护作用。起一个作业前多花两分钟想清楚这个问题“这个作业连续失败多少次我应该接到告警”答案写进max_failures里这就是最基础的自我保护。还有个小技巧对于关键作业建议在作业开头加一段日志记录把开始执行的参数记录下来这样排查问题时能直接确认作业当时拿到的输入是什么不需要靠猜。