上周帮朋友看一份19c的AWR报告,加载出来之后我盯着屏幕看了好几秒:Top 5等待事件里,第二个位置赫然写着Failed Logon Delay。这个名字在等待事件里真不算常见,大多数时候它只是偶尔露个脸,占零点几秒,没想到这次成了第二名。干DBA这些年,Failed Logon Delay 排进前五都不多见,排到第二,说明系统的失败登录已经不是零星误操作,而是有东西正在反复撞数据库的认证大门。
我在群里把这个截图发出去,第一反应是“是不是有人拿着旧密码疯狂重试”,第二反应是“如果直接把FAILED_LOGON_DELAY调成 0,报告是不是就好看了”。但这两件事,恰恰都是坑。这个事件背后的原理、定位方法和处理方式,值得好好捋一遍。下文就按我实际排障的思路展开,不绕弯子,直接说怎么从AWR里看到它、怎么定位、怎么处理。
1. AWR里的“隐形人”为什么会突然刷到第二位
1.1 这个事件到底在等什么
先解决一个基础问题:Failed Logon Delay 到底是什么等待事件?
简单说,当客户端向数据库提交登录认证请求后,如果用户名、口令校验失败,数据库并不会立刻把“ORA-01017: invalid username/password; logon denied”返回给客户端,而是先在内部“睡”一小段时间,睡完再返回错误。这个睡眠动作,在等待事件里就是Failed Logon Delay。
睡眠时长由初始化参数FAILED_LOGON_DELAY控制,默认值是 1 秒,取值范围是 0 到 3600 秒,单位是秒。
为什么设计成等待事件,而不是直接闷头 sleep 一下?因为做成等待事件,AWR、ASH、GV$SESSION 都能观测到,DBA 才能知道“数据库花了多少时间在处理失败登录”。如果没有这个事件,失败登录虽然也在拖慢数据库,但你根本没法量化,问题会隐藏得更深。
这里有个容易混淆的点:这种睡眠不是正常的业务等待,而是安全机制主动制造出来的延迟。一个客户端输错密码,数据库在这里白等 1 秒;一万个客户端输错密码,数据库就在这里白等一万秒,DB Time 自然被顶高。
1.2 Top 2 意味着 DB Time 被 sleep 填掉了一大块
AWR 报告里的 Top 等待事件,是按“总等待时间”排序的。Failed Logon Delay能排到第二,意味着它累计等待的时间非常可观。
做个粗略计算:1 秒等待 × 1 万次失败登录,就是 10000 秒,约 2.78 小时;5 万次失败,就是 13.9 小时;要是到了 18 万次,就是整整 50 小时。在动辄看 60 分钟、120 分钟 AWR 快照的场景里,一个等待事件累计几十小时,排名自然靠前。
所以看到这个事件排到 Top 2,第一反应应该是:失败登录的次数已经多到不正常的量级。
正常OLTP系统,即便有连接池反复建立会话,通常也是成功的登录占大多数。失败登录如果到了几万次,必然存在某个固定的“错误源”,要么是应用配置里的密码不对,要么是外部攻击在扫账户。它不会是自己跑出来的。
1.3 与 FAILED_LOGIN_ATTEMPTS 别搞混
很多刚接触这个事件的人,会把FAILED_LOGON_DELAY和用户 PROFILE 里的FAILED_LOGIN_ATTEMPTS混为一谈,这两个完全是不同维度的东西。
| 项目 | FAILED_LOGON_DELAY | FAILED_LOGIN_ATTEMPTS |
|---|---|---|
| 类别 | 初始化参数 | 用户PROFILE限制 |
| 作用 | 登录失败后延迟多少秒再返回错误 | 连续失败多少次后锁定账号 |
| 默认 | 1秒(0-3600秒) | 无限制/由PROFILE决定 |
| 效果 | 每次失败都要等 | 达到阈值后报 ORA-28000 |
实际环境里两者往往会同时出现。攻击者反复试密码,一方面每个失败都触发 1 秒延迟,把 DB Time 顶高;另一方面失败次数达到 PROFILE 阈值,又会把账号锁掉,造成业务账户连接失败。DBA 在排查时,不要只盯着“账号被锁”这个表象,要继续往前查失败来源。
2. 底层原理:数据库为什么故意给你“慢半拍”
2.1 这是反暴力破解的“门锁阻尼器”
把数据库登录比作门锁就很好理解:一个人站在门口反复试钥匙,如果门锁每次都立刻告诉他对不对,他就能以极快的速度穷举所有钥匙。数据库也一样,如果每次认证失败都立刻返回错误,攻击者就能一秒试几百次密码,普通口令根本撑不住。
FAILED_LOGON_DELAY就是给门锁加了一个阻尼器:每次密码错误,必须等待 1 秒才能再试。这样暴力破解速度被硬生生压到每秒最多 1 次,攻击成本成倍上升。
有意思的是,这个等待不仅对“密码错误”生效,对“用户名不存在”同样生效。目的很明确:防止攻击者通过响应时间差异来判断某个用户是否存在。如果“用户不存在”响应快、“密码错误”响应慢,那就等于把账户字典免费送给了攻击者。统一延迟,才能把这个信息抹平。
2.2 为什么 CPU 不高,DB Time 却暴涨
这类故障最迷惑人的地方在于:从系统层面看,CPU 不高,I/O 也不高,数据库好像很闲;但 AWR 报告里 DB Time 就是很高,大量时间花在Failed Logon Delay上。
原因是,认证失败后的睡眠是前台会话的等待事件,会计入 DB Time。DB Time 的构成就是“前台会话 CPU 时间 + 前台会话非空闲等待时间”。sleep 虽然不是真正的计算资源消耗,但它是非空闲等待,照样往 DB Time 里记。
这就像高速公路上一百辆车堵在收费口,高速本身的承载能力没问题,但所有车都在“等待”,总等待时间一样会爆炸。这个等待事件把数据库的认证环节放大成了一个显眼的瓶颈,但瓶颈的根源不在数据库的 CPU 或 I/O,而是在认证请求本身。
2.3 19c 下相关参数的配置与注意点
在 19c 环境里,查看参数很简单:
SHOW PARAMETER FAILED_LOGON_DELAY;默认输出基本是 1。如果确认要调整,可以执行:
ALTER SYSTEM SET FAILED_LOGON_DELAY = 0 SCOPE = BOTH;这句会把全局延迟关闭,重启也保持。恢复默认则执行:
ALTER SYSTEM SET FAILED_LOGON_DELAY = 1 SCOPE = BOTH;但这里有几个坑:
第一,在 CDB/PDB 架构下,一定要先确认当前容器。你以为在改 CDB 根,实际可能只影响某个 PDB,或者反过来。稳妥做法是先ALTER SESSION SET CONTAINER =到目标容器,再执行ALTER SYSTEM。
第二,如果是 RAC,所有节点均需要确认参数一致。单个节点改,另一个节点还是旧值,排障时会被误导。
第三,这个参数单位是整数秒,没有 0.5 秒这种中间档。要么保持 1,要么直接为 0,不存在“稍微调低一点”的空间。
3. 定位“凶手”:从等待事件反查失败登录现场
3.1 先抓现场:谁正卡在 Failed Logon Delay 里
因为每次失败登录只等待 1 秒,抓现场要快,最好连续采样。首先可以直接查当前会话:
SELECT inst_id, sid, serial#, username, machine, program, module, event, seconds_in_wait, wait_time FROM gv$session WHERE event = 'Failed Logon Delay' AND type != 'BACKGROUND';这条 SQL 能看到正在等待的会话,包括用户名、客户端机器名、程序名。如果结果为空,不代表没有,只是 1 秒窗口太短,没赶上。
我习惯在 Linux 上跑一个小循环,连续抓 60 次,每 1 秒抓一次:
for i in $(seq 1 60); do sqlplus -s / as sysdba <<'SQL' set pagesize 0 feedback off timing off select to_char(sysdate,'HH24:MI:SS')||' | '||inst_id||' | '||sid||' | '||username||' | '||machine||' | '||program||' | '||event from gv$session where event='Failed Logon Delay' and type != 'BACKGROUND'; exit SQL sleep 1 done这个脚本在生产上跑 1 分钟压力不大,但能得到一个快照集,能看出失败登录是否有固定周期、是否来自同一台应用节点。如果看到的全是同一个中间件服务器、同一个程序名,基本可以锁定方向。
3.2 从审计记录按来源聚合
抓现场只能抓到“现在”,要统计“过去几小时谁失败了”,最好的路径是审计日志。如果环境已经开启了登录审计,可以直接查统一审计视图:
SELECT DB_USERNAME, USERHOST, CLIENT_PROGRAM_NAME, COUNT(*) AS FAIL_COUNT FROM UNIFIED_AUDIT_TRAIL WHERE ACTION_NAME = 'LOGON' AND RETURN_CODE != 0 AND EVENT_TIMESTAMP >= SYSDATE - 1 GROUP BY DB_USERNAME, USERHOST, CLIENT_PROGRAM_NAME ORDER BY FAIL_COUNT DESC;这个查询会把过去一天登录失败次数按数据库用户、客户端主机、程序名聚合,结果会非常直观:通常有一个 DB_USERNAME 占据绝大多数,来源主机就一两台。
如果环境之前没开审计,那过去的数据已经找不回来了。这时候可以先补一个最简单的登录失败审计策略,等下一次故障复现时采集:
AUDIT LOGON WHENEVER NOT SUCCESSFUL;注意这个动作本身会增加少量审计负担,生产环境要评估后执行。但相对于问题排查的收益,这点开销是值得的。
3.3 AWR和ASH为什么常常帮不上忙
有人会问,AWR 里都排第二了,直接查DBA_HIST_ACTIVE_SESS_HISTORY不就能定位吗?
实际经验是:经常定位不到。
Failed Logon Delay的等待时间正好是 1 秒,而 ASH 的采样周期也是 1 秒左右。一个会话只在数据库里闪了一下就结束了,采样线程大概率没采到它。我从很多次实操里看到的情况是,ASH 中这个事件的历史样本稀疏得可怜,甚至一片空白。
AWR 的优势是“累计统计”,它把 1 小时内的等待时间全部加起来了,所以能排到 Top;但 AWR 只告诉你“发生了什么”,不告诉你“谁做的”。要定位到具体主机、具体用户、具体程序,靠的是审计和现场采样,不能只依赖 AWR。
3.4 从失败频率特征快速判断是应用Bug还是攻击
排查时不要盲目试,先看频率分布。这是我总结的一个判断表格:
| 失败特征 | 倾向结论 |
|---|---|
| 固定时间间隔出现,数量稳定 | 应用调度/连接池重试 |
| 单个应用账号高频失败 | 配置中的密码不正确或已过期 |
| 单个IP尝试大量不同用户名 | 暴力破解/账户扫描 |
| 集中在凌晨或低峰期 | 批处理任务或定时攻击 |
| 失败后伴随账号锁定报警 | 已触发 PROFILE 锁定机制 |
这个表不绝对,但能帮你在第一时间确定方向。如果看到“固定间隔 + 单个账号”,八成是应用配置问题;如果看到“单 IP + 多用户名”,赶紧先封 IP 再谈其他。
4. 优化与治理:不要一上来就把 FAILED_LOGON_DELAY 设成 0
4.1 为什么直接关掉是自欺欺人
面对 Top 2 的等待事件,最手痒的想法就是ALTER SYSTEM SET FAILED_LOGON_DELAY = 0,然后 AWR 立马清爽。
但这是典型的“把仪表盘遮住当故障不存在”。
关掉延迟后,等待事件确实没了,DB Time 也会瞬间降下来。但背后的失败登录请求还在,每一次登录失败依然要消耗数据库认证的 CPU 和内存。如果攻击者正在扫密码,关掉延迟等于把数据库的安全防线主动拆掉,暴力破解速度直接飙升。
等待事件只是报警器,报警器拆掉不等于火灭掉了。DBA 当然可以让报告漂亮,但不能替业务承担后续的安全事故责任。
4.2 什么情况下可以临时调整,调完怎么恢复
我并不是说FAILED_LOGON_DELAY永远不能碰。如果已经定位到根因,并且应用修复需要时间,为了避免失败登录把整个库的 DB Time 全线占满,可以在安全团队认可的前提下临时把参数降到 0,等应用侧修复后再改回来:
-- 临时关闭(重启失效) ALTER SYSTEM SET FAILED_LOGON_DELAY = 0 SCOPE = MEMORY; -- 应用恢复后,重启保持生效 ALTER SYSTEM SET FAILED_LOGON_DELAY = 1 SCOPE = BOTH;SCOPE = MEMORY表示只改当前运行实例,重启后恢复原来的值。这样临时止血不会留下永久隐患。但记住,止血之后必须继续找根因,否则故障会反复出现。
4.3 从根上消除失败登录压力
处理这类问题的正确顺序是:先找到失败来源,再解释来源为什么存在,最后做对应整改。
如果是应用密码不一致,比如配置中心已经轮换密码但部分应用节点没同步,那就把所有节点的连接配置统一刷新,并限制中间件连接池的启动重试频率。
如果是连接池配置问题,比如连接池测试连接时使用了错误凭据,导致每隔一段时间就用旧密码探测一次,那要修正配置并让连接池缓存过期。
如果是外部暴力破解,那就不是DBA单方面能解决的,需要网络安全团队配合做来源IP封禁,同时数据库侧启用账号锁定策略:
ALTER PROFILE app_user LIMIT FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 10;这个 PROFILE 表示同一个账号连续失败 5 次后锁定 10 天,能有效挡住大部分随机尝试。注意服务账号要单独评估,别锁了真正要用的系统账号。
4.4 一份可以抄走的预防清单
经历了这次故障后,我给自己的巡检单里加了下面几项:
- 每周检查 AWR 的 Top 5 等待事件,只要出现 Failed Logon Delay,立即立项排查。
- 生产环境开启登录失败审计,日志保留周期至少 7 天。
- 所有数据库账号纳入统一凭据管理,密码轮换时先发配置中心,再重启应用连接池。
- 中间件连接池配置“重试退避 + 熔断”,不要每 5 秒、10 秒密集重试。
- 关键服务账号单独设置 PROFILE,平衡安全策略与业务连续性。
- 外部来源访问数据库,统一经过防火墙白名单,非必要不直接暴露数据库端口。
5. 一次Failed Logon Delay排第二的实战复盘
5.1 现象是第一眼的“错觉”
某天上午,朋友发来一份 19c RAC 两节点的 AWR,我打开后先看Top 5 Timed Events,第二名的等待时间高达 17 小时。再往下看,DB CPU只有几小时,逻辑读和物理读都不高。
直观感受是:数据库负载明明很轻,为什么 DB Time 这么高?
再看事件名,Failed Logon Delay,我心里已经有数了:这不是数据库性能问题,是有人在反复认证失败。剩下的工作就是找出那个人。
5.2 顺着审计一路挖到应用配置
我先在 RAC 两个节点上分别跑了一分钟采样脚本,抓到若干个正在等待该事件的会话。它们的共同特征是:MACHINE指向一台中间件服务器,PROGRAM是 JDBC Thin Client,USERNAME始终是同一个服务账号。
接着查统一审计,运行了 3.2 小节里的聚合 SQL,结果非常明确:过去两天,这个服务账号的登录失败次数占全部失败次数的 94%,来源主机就是那台中间件服务器,并且失败行为呈固定周期,大约每 15 秒一批,批次里会连续出现几十次失败。
应用团队复查后发现,该服务账号的密码已经在配置中心做过轮换,但有两台应用节点一直保留旧配置,连接池里的密码没刷新。于是每次连接池定时重建连接时,都会拿旧密码去数据库认证,失败后就等 1 秒,再继续下一批。几十个连接 × 1 秒等待,每 15 秒循环一次,两个节点产生大量 DB Time。
5.3 修复与验证
应用团队刷新了这两台节点的连接池配置,并清掉了旧的缓存连接,重启连接池后,新的连接全部使用新密码,认证失败量很快降到 0。
下一份 AWR 报告里,Failed Logon Delay直接从 Top 5 消失,DB Time 恢复正常水平。
整个过程中我没有改FAILED_LOGON_DELAY的值,因为根因不在这个参数上。就算当时设为 0,也只是让报告好看,应用节点一样会在每个调度周期用错误密码去撞认证接口,等到密码轮换周期变长、失败次数累积到锁定阈值,最终还是会爆发账号锁定故障。
5.4 复盘后留下的几条经验
这次排障给我最大的提醒是:等待事件排名不会说谎,但解读排名的人容易想歪。
Failed Logon Delay排到 Top 2,看起来像“数据库认证环节被拖垮”,实际上更可能是“上游某个应用的凭据悄悄变了”。DBA 的职责不是默认数据库配置有问题,而是顺着等待事件找到背后那个“错误请求”。
另外,一定要重视审计。如果没有登录失败审计,这次排查就只能靠抓现场碰运气,效率低很多。开启审计、定期保留日志,这类功夫在平时,关键时刻能救命。
我在实际处理里碰到Failed Logon Delay排到 Top 2,第一反应一定是去翻审计和来源,而不是急着调参数。数据库每一个等待事件都是在替上游的错误背锅,把“背锅的锅”删掉,锅本身还在。希望这篇思路能让你下次看到它时,少走弯路。