Oracle 里的概要文件(profile)这个对象,平时存在感很低,出问题时存在感极高。我印象最深的一次,是某套业务系统早上八点整批量报错,日志里刷满了 ORA-28000 账户已被锁定,值班的 A 同学第一反应是数据库密码被人改了,查了半小时才发现是三个月前有人把 DEFAULT 概要文件的口令限制参数一起改掉了,导致全实例所有用户的失败登录次数阈值从 10 次变成了 3 次,几个跑批程序的旧连接重试三次就被锁死。这件事之后我养成了一个习惯:只要动概要文件,先想清楚影响面,再动手。概要文件本质上就是一份用户级的"资源配额 + 密码策略"档案,它管两件事——一个用户能消耗多少数据库资源(会话数、CPU 时间、连接时长、逻辑读、私有 SGA 等),以及这个用户的密码怎么管(有效期、复用、失败锁定、复杂度校验)。它解决的是多用户共用一套数据库时"一个人把资源吃光、一个弱密码拖垮整个安全基线"的问题。这篇内容适合已经能建用户、能写基本 SQL 的 DBA 和开发,也适合刚接手一套现成库、看到一堆 ORA-28xxx 报错不知道从哪下手的朋友。我按自己踩坑的顺序,把参数含义、生效条件、实操脚本和排查套路一次性讲透。
1. 概要文件到底是什么:从一次账号被锁说起
1.1 概要文件在用户体系里的位置
理解概要文件,最好把它和用户、权限放在一张图里看。Oracle 里一个用户(user/schema)身上挂着三样东西:身份(密码、认证方式)、权限(系统权限、对象权限、角色)、以及配额(概要文件)。前两样决定了"你能干什么",概要文件决定的是"你干这件事的时候能用多少、密码要符合什么规矩"。它不像表空间配额那样直接卡住 INSERT,而是在会话建立和语句执行的过程中由后台持续计量,超了就直接把会话杀掉或者拒绝后续操作。这个设计思路很像公司给员工发的门禁卡:权限决定你能进哪些房间,配额决定你一天能进几次、每次待多久、刷卡失败几次卡就被冻结。
概要文件的绑定关系非常干脆——一个用户只能绑一个概要文件,多对一,不能叠加。想给不同业务不同策略,就得建多个概要文件再分别 ALTER USER 指定。这点和角色完全不同,角色可以叠加、可以嵌套,概要文件不行。所以实际项目里常见的做法是按"业务类型 + 连接方式"分类建概要文件,比如 OLTP 在线交易一套、批处理一套、只读报表一套、外部接口一套,而不是一个用户建一个,那样后期维护会疯掉。我见过一个库里有七十多个概要文件,名字全是 PROFILE_01 到 PROFILE_70,没人说得清哪个是干什么的,参数也各不相同,最后只能重建一套标准化的再逐个迁移。
需要特别强调的是 DEFAULT 这个概要文件的地位。它是实例创建时自带的、不可删除的默认档,所有新建用户如果没有显式指定 PROFILE 子句,自动继承 DEFAULT。这意味着你对 DEFAULT 的任何一次修改,都会瞬间作用到所有"还在用默认档"的用户身上。很多事故的根源就在这里:本意是想管住某个测试账号,结果手一抖改的是 DEFAULT。
1.2 DEFAULT 概要文件的特殊地位与继承逻辑
DEFAULT 概要文件的特殊性体现在三个方面。第一,它不能被 DROP,DROP PROFILE DEFAULT会直接报 ORA-00933 或权限类错误,因为系统必须有兜底档。第二,删除任何自定义概要文件时,如果没有加 CASCADE,只要还有用户绑在上面就会报 ORA-02382(profile 存在依赖用户);加了 CASCADE,那些用户的概要文件会被自动重置回 DEFAULT,参数随之变成默认值,这个"静默回退"是最容易被忽略的坑,因为删除当时不报错,业务下次登录才发现策略变了。第三,DEFAULT 的口令参数一旦设置,新密码策略对所有默认用户立即生效,但已经连接着的会话不会被踢掉,它们会在下一次认证或者下一次资源计量刷新时才感知到变化。
这里有个非常容易混淆的点:概要文件本身没有"继承"机制。不存在"我的自定义档没写的参数自动继承 DEFAULT"这种设计。实际上,CREATE PROFILE ... LIMIT里没写的参数,会取建库时定义的默认值(大多数是 UNLIMITED,口令类参数在较新版本里有些会给默认值,比如密码验证函数会挂上系统自带的校验函数),而不是实时读取 DEFAULT 的当前值。也就是说,如果今天你把 DEFAULT 的 FAILED_LOGIN_ATTEMPTS 改成 5,你那个三个月前建的自定义概要文件里的这个参数不会跟着变成 5,它还是当初创建时的取值。这个行为我建议所有人在心里默念三遍,因为它直接决定了"我改 DEFAULT 为什么这个用户没反应"这类问题的答案。
提示:动 DEFAULT 概要文件之前,先跑一句
SELECT username FROM dba_users WHERE profile='DEFAULT';看清楚这一改会波及多少个账号,里面有系统账号、有第三方监控账号、有早就没人管的僵尸账号。这一步花三十秒,能省掉后面三小时的排障。
从影响范围来讲,概要文件的改动可以分成三个圈层。最内层是单个用户,ALTER USER 改绑,只影响他自己;中间层是一个自定义概要文件上的所有用户,改参数时影响这一批;最外层是 DEFAULT,改它等于改全库。所以我的操作顺序永远是:先建新档、验证参数、灰度绑一个非关键账号跑几天、确认没问题再批量迁移、最后才考虑要不要动 DEFAULT。这个顺序看着啰嗦,但在生产环境上,它的性价比高得离谱。
2. 资源限制参数:把CPU、会话、连接时间关进笼子
2.1 会话级与调用级资源参数逐个拆解
资源限制参数分两个粒度:SESSION 级和 CALL 级。SESSION 级是从登录到断开的整个生命周期内累计,CALL 级是单次解析执行(一次 parse+execute+fetch)内累计。理解这个区别很重要,因为 CALL 级限制是防止单条烂 SQL 长时间占着 CPU,SESSION 级限制是防止某个应用整天挂着不放。两类参数的单位和含义差别很大,我用一张表来对齐:
| 参数 | 粒度 | 单位 | 典型取值思路 |
|---|---|---|---|
| SESSIONS_PER_USER | 会话 | 个数 | 按应用连接池上限的 1.2 倍给 |
| CPU_PER_SESSION | 会话 | 1/100 秒 | 累计 CPU,按业务单次跑批峰值估 |
| CPU_PER_CALL | 调用 | 1/100 秒 | 防单条 SQL 打满,通常给 60~300 秒 |
| CONNECT_TIME | 会话 | 分钟 | 连接总存活时长,批处理给足 |
| IDLE_TIME | 会话 | 分钟 | 空闲多久被踢,注意连接池心跳 |
| LOGICAL_READS_PER_SESSION | 会话 | 数据块数 | 累计逻辑读,防全表扫雪崩 |
| LOGICAL_READS_PER_CALL | 调用 | 数据块数 | 单条语句读块上限 |
| PRIVATE_SGA | 会话 | K / M | 仅共享服务器模式下有意义 |
| COMPOSITE_LIMIT | 会话 | service units | 多资源加权综合上限 |
CPU 相关的两个参数单位是百分之一秒,这个细节特别容易搞错。你看到CPU_PER_CALL 100000,别以为是十万秒,实际是 1000 秒,也就是 16 分多钟。我见过有人按"秒"来填,填了个 300,结果等于 3 秒,业务上一条稍微复杂点的查询直接报 ORA-02392 快照太旧类似的资源超限错误,排查了半天才发现单位错了。所以填这类参数之前,先在心里做一次除法,把 1/100 秒换算成你熟悉的秒数再判断合不合理。
IDLE_TIME 和 CONNECT_TIME 都是分钟,但它们的杀伤力完全不一样。IDLE_TIME 超了,会话是"被动的、非正常断开",客户端下次用这条连接会收到 ORA-02396 或者连接已断开的错误,应用如果没有重连机制就会直接崩;CONNECT_TIME 超了也是类似效果。所以我给生产环境的建议是:IDLE_TIME 不要设置得比应用连接池的 keepalive 间隔更短,比如连接池每 5 分钟做一次心跳检测,你 IDLE_TIME 设 3 分钟,那就是自己给自己找事。批处理类应用的 CONNECT_TIME 必须给足,因为一个大跑批挂四五个小时很正常,你给 120 分钟,跑到一半连接被杀,前面的活全白干。
SESSIONS_PER_USER 看着最简单,其实也有讲究。它不是"同时活跃会话数",而是"这个用户当前存在的会话总数",包括那些已经空闲但还没被回收的连接。连接池动不动就预热几十条连接,你按"实际并发"去估就会踩坑。我的经验公式是:连接池最大连接数 × 1.3,再往上留点余量给运维手工登录排查用。如果应用有多个节点,每个节点都开池子,那就要乘节点数。这个数字算错了的后果是高峰期随机报 ORA-00018 maximum number of sessions exceeded 或者更隐蔽的"偶发登录失败"。
2.2 COMPOSITE_LIMIT 的加权算法与参数取值推演
COMPOSITE_LIMIT 是整个资源参数里最"玄"的一个,它把 CPU_PER_SESSION、CONNECT_TIME、LOGICAL_READS_PER_SESSION、PRIVATE_SGA 四项折算成统一的 service units 再求和,只要总和超了就直接拒绝。理论上这是个很优雅的设计——一条总闸门管住四条支流。但实际用起来,我建议大多数场景直接设 UNLIMITED,原因很简单:Oracle 并没有把权重换算公式完整公开,你没法像算预算那样精确推导"我给 50000 units 到底相当于多少 CPU 秒加多少逻辑读"。
我做过一些粗糙的实测,在测试库上用单个会话反复压测,观察V$SESSMETRIC和V$SESSTAT的变化来反推比例关系。结论只能说是"数量级参考",不同版本、不同负载形态下偏差不小。所以我的实操建议是:先不用 COMPOSITE_LIMIT,而是把四项单独设好。这四项各自都会独立生效,等于四道闸门已经拦住了大部分风险。等你跑了几个月,从 AWR 或者会话历史里摸清楚正常业务的资源画像,再决定要不要上综合限额。真要上,也别拍脑袋,按"正常峰值 × 2"给,留足缓冲。
至于 PRIVATE_SGA,它只在共享服务器(shared server)模式下才起作用,因为专用服务器模式下每个会话的私有内存本来就不受这个参数管。现在绝大多数系统都跑专用服务器,所以这个参数设了基本等于没设。不过我还是建议显式写成 UNLIMITED 而不是留空,因为留空时取的是创建时的默认值,将来万一有人切换到共享服务器模式,这些历史遗留的奇怪数值会突然开始生效,那种问题排查起来非常难受。
2.3 资源限制生效的前置条件:RESOURCE_LIMIT 与资源计划
这是新手最容易翻车的地方:参数建好了、用户也绑了、跑测试却一点效果都没有,以为是自己写错了。其实概要文件的资源限制(注意是资源限制,口令限制不受此影响)默认是不生效的,因为RESOURCE_LIMIT这个初始化参数默认是 FALSE。你必须显式打开它:
-- 查看当前开关状态 SHOW PARAMETER resource_limit; -- 打开,动态生效,不需要重启 ALTER SYSTEM SET resource_limit = TRUE SCOPE = BOTH;为什么默认关着?因为开启后每次语句执行都要做一些额外的资源计量,会有轻微性能开销;而且很多系统本来就靠应用层限流,不需要数据库再管一层。但对于有多租户、有外部接口账号、有临时排查账号的环境,我强烈建议打开。开销实测下来在正常 OLTP 负载下几乎测不出来,换来的是资源不被单个会话拖垮的确定性。
第二个前置条件是资源管理器(Resource Manager)。如果你启用了某个 resource plan,那么 CPU 相关的限制(CPU_PER_SESSION、CPU_PER_CALL、COMPOSITE_LIMIT 里的 CPU 部分)就交给计划去管了,概要文件里的这些参数会被忽略,但 SESSIONS_PER_USER、IDLE_TIME、CONNECT_TIME 这几项仍然由概要文件控制。这个分工很多人不清楚,导致"我明明在 profile 里限制了 CPU 怎么没起作用"的疑问。查一下当前有没有启用计划:
SELECT plan, is_top_plan FROM v$rsrc_plan;如果有输出且 IS_TOP_PLAN 为 TRUE,说明资源计划正在生效。这时候你调整 profile 里的 CPU 参数是白费劲,应该去改对应的资源计划。
3. 口令限制参数:密码策略的落地方式
3.1 生命周期、复用、锁定与宽限期四件套
口令限制参数是概要文件里真正天天被感知的部分,因为几乎所有 ORA-28xxx 报错都跟它们有关。核心就六个参数,我把它们的相互作用关系理一遍,这比单独背定义有用得多。
PASSWORD_LIFE_TIME 是密码有效期(天),从最后一次改密开始算。超了之后进入 PASSWORD_GRACE_TIME 宽限期,这段时间内还能登录,但每次登录都会收到 ORA-28002 警告——注意是警告不是错误,很多应用把它当错误处理就去改代码了,其实是应用日志解析太粗暴。宽限期过了就变成 ORA-28001,密码彻底过期,必须改密才能连。
PASSWORD_REUSE_TIME 和 PASSWORD_REUSE_MAX 是一对连体参数,管密码复用。前者是"隔多少天才能重用旧密码",后者是"要改多少次之后才能重用旧密码"。这里有个必须记住的规则:这两个参数要么都设成具体数值,要么都设成 UNLIMITED,如果一个设了值另一个是 UNLIMITED,结果不是"放宽"而是"密码永远无法重用",因为两个条件需要同时满足,UNLIMITED 那个条件在逻辑上永远成立不了。这个行为我专门验证过,确实如此,网上一堆人说"设一个就够"是错的。
FAILED_LOGIN_ATTEMPTS 和 PASSWORD_LOCK_TIME 是锁定组合。连续输错 N 次,账号被锁,锁 PASSWORD_LOCK_TIME 天。这里有两个细节:一是"连续"的判定——只要中间成功登录一次,计数就清零;二是 PASSWORD_LOCK_TIME 如果设成 UNLIMITED,那锁上之后只能 DBA 手工解锁,对于无人值守的系统来说这是灾难,因为一旦被撞库锁死,业务就彻底断了。我的建议是给具体天数,比如 1 天或者 1/24 天(一小时),让系统能自愈。
INACTIVE_ACCOUNT_TIME 是较新版本引入的,含义是"多少天没登录就自动锁定账号"。这个参数对付僵尸账号特别有效,很多安全审计要求的"长期未使用账号必须禁用"可以直接用它落地。但要注意它和 FAILED_LOGIN_ATTEMPTS 的锁定是两种不同的锁定状态,在 DBA_USERS 的 ACCOUNT_STATUS 里显示的文字不一样,解锁方式都是 ALTER USER ... ACCOUNT UNLOCK。
| 参数 | 单位 | UNLIMITED 的含义 | 常见坑 |
|---|---|---|---|
| PASSWORD_LIFE_TIME | 天 | 永不过期 | 设太短导致频繁改密,应用存了硬编码密码就崩 |
| PASSWORD_GRACE_TIME | 天 | 宽限期无限 | 等于密码事实上永不过期 |
| PASSWORD_REUSE_TIME | 天 | 见上文连体规则 | 与 REUSE_MAX 不匹配导致无法改密 |
| PASSWORD_REUSE_MAX | 次 | 见上文连体规则 | 同上 |
| FAILED_LOGIN_ATTEMPTS | 次 | 永不锁定 | 给撞库留了口子 |
| PASSWORD_LOCK_TIME | 天 | 锁定不解 | 需要人工介入,无人值守系统慎用 |
| INACTIVE_ACCOUNT_TIME | 天 | 不检查 | 较新版本才有 |
3.2 密码复杂度校验函数 PASSWORD_VERIFY_FUNCTION 的写法与注册
前面那些参数管的是"密码多久换、能不能复用",管不了"密码是不是 123456"。要卡复杂度,必须挂一个校验函数。这个函数的签名是固定的三参数:用户名、新密码、旧密码,返回 BOOLEAN。函数体里通过抛出异常(最好是 RAISE_APPLICATION_ERROR 配合自定义错误号)来拒绝不合规的密码,正常通过就 RETURN TRUE。
下面这个版本是我在多个项目里用过的精简版,长度、字符类型、常见弱词、与用户名相同这几项都卡了:
CREATE OR REPLACE FUNCTION verify_password_fn( p_userid IN VARCHAR2, p_password IN VARCHAR2, p_old_password IN VARCHAR2 ) RETURN BOOLEAN IS v_len NUMBER := NVL(LENGTH(p_password), 0); v_upper NUMBER := 0; v_lower NUMBER := 0; v_digit NUMBER := 0; v_spec NUMBER := 0; v_ch VARCHAR2(1); BEGIN IF v_len < 10 THEN RAISE_APPLICATION_ERROR(-20001, '密码长度不得少于10位'); END IF; IF UPPER(p_password) = UPPER(p_userid) THEN RAISE_APPLICATION_ERROR(-20002, '密码不得与用户名相同'); END IF; IF p_old_password IS NOT NULL AND UPPER(p_password) = UPPER(p_old_password) THEN RAISE_APPLICATION_ERROR(-20003, '新密码不得与旧密码相同'); END IF; IF INSTR(LOWER(p_password), 'password') > 0 OR INSTR(LOWER(p_password), 'admin') > 0 OR INSTR(LOWER(p_password), '123456') > 0 THEN RAISE_APPLICATION_ERROR(-20004, '密码包含常见弱口令片段'); END IF; FOR i IN 1 .. v_len LOOP v_ch := SUBSTR(p_password, i, 1); IF v_ch BETWEEN 'A' AND 'Z' THEN v_upper := v_upper + 1; ELSIF v_ch BETWEEN 'a' AND 'z' THEN v_lower := v_lower + 1; ELSIF v_ch BETWEEN '0' AND '9' THEN v_digit := v_digit + 1; ELSE v_spec := v_spec + 1; END IF; END LOOP; IF v_upper = 0 OR v_lower = 0 OR v_digit = 0 OR v_spec < 2 THEN RAISE_APPLICATION_ERROR(-20005, '密码须同时包含大写字母、小写字母、数字及至少2个特殊字符'); END IF; RETURN TRUE; END verify_password_fn; /几个实操要点。第一,函数必须在 SYS 下创建或者创建后授权到位,因为数据库在做密码校验时是以内部身份调用的,执行权限不够会直接报 ORA-28003 或者更含糊的"密码验证失败"。稳妥做法是建完立刻GRANT EXECUTE ON verify_password_fn TO PUBLIC;。第二,函数里不要做任何 DML,也不要查业务表,密码校验发生在认证阶段,这时候查表极可能因为递归 SQL 或者权限问题直接挂掉,我见过有人在校验函数里 SELECT 一张黑名单表,结果那张表所在表空间满了,全库用户都改不了密码。第三,改完函数记得重新编译,并且用一个测试账号实际改一次密码验证,别只看编译通过就完事。
还有一个"坑中之坑":如果你在概要文件里设置了任何口令限制参数,但 PASSWORD_VERIFY_FUNCTION 留空,较新版本的 Oracle 会自动挂上系统自带的默认校验函数。这个默认函数的要求未必符合你的预期,可能比你想的严格也可能更宽松。所以我在建档时永远显式写清楚这一项,要自定义就写函数名,不要就写 NULL,绝不留空。
4. 实操全过程:从建profile到验证生效
4.1 创建、修改、分配与查询的完整脚本
假设现在要给一套在线交易应用建一套策略:会话数限制住防止连接池泄漏、CPU 单语句不能跑太久、空闲连接一小时内回收、密码 90 天换一次、错 5 次锁 1 小时。完整脚本如下:
CREATE PROFILE app_oltp_profile LIMIT SESSIONS_PER_USER 20 CPU_PER_SESSION UNLIMITED CPU_PER_CALL 30000 CONNECT_TIME 720 IDLE_TIME 60 LOGICAL_READS_PER_SESSION UNLIMITED LOGICAL_READS_PER_CALL 200000 PRIVATE_SGA UNLIMITED COMPOSITE_LIMIT UNLIMITED FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LIFE_TIME 90 PASSWORD_REUSE_TIME 180 PASSWORD_REUSE_MAX 5 PASSWORD_LOCK_TIME 1/24 PASSWORD_GRACE_TIME 7 INACTIVE_ACCOUNT_TIME 90 PASSWORD_VERIFY_FUNCTION verify_password_fn;几个取值我解释一下为什么这么定。CPU_PER_CALL 给 30000(即 300 秒),是因为在线交易单条语句正常在毫秒级,给到 5 分钟已经是极大的冗余,超过这个时间的语句基本可以判定是 Bug 或者缺索引,杀掉不冤。LOGICAL_READS_PER_CALL 给 200000 块,按 8K 块大小算大约 1.5GB 的逻辑读,一个正常的点查或小范围扫描远远到不了,而一个全表扫会瞬间超限,正好起到"拦住坏 SQL"的作用。CONNECT_TIME 给 720 分钟是给批量对账留的余量,IDLE_TIME 给 60 分钟则是配合连接池的保活周期,一般连接池 30 分钟探活一次,60 分钟就能保证活跃连接不被误杀。
分配很简单:
ALTER USER app_user PROFILE app_oltp_profile;这里有个必须知道的限制:ALTER USER 改 profile 只影响之后的会话,当前已经连着的会话仍然用旧的概要文件,直到它重新连接。所以改绑之后你要么等业务自然重连,要么在维护窗口做一次连接池重启。指望"改完立刻生效"去验证,大概率会被误导。
查询概要文件内容用 DBA_PROFILES,这个视图按"行"展开,一行一个参数,比看建表语句直观:
SELECT resource_name, limit FROM dba_profiles WHERE profile = 'APP_OLTP_PROFILE' ORDER BY resource_type, resource_name;想快速对比自定义档和 DEFAULT 的差异,用自连接最省事:
SELECT a.resource_name, a.limit AS custom_limit, d.limit AS default_limit FROM dba_profiles a JOIN dba_profiles d ON a.resource_name = d.resource_name AND d.profile = 'DEFAULT' WHERE a.profile = 'APP_OLTP_PROFILE' AND a.limit <> d.limit;这条查询我几乎每次接手新库都会跑一遍,能一眼看出这个档到底被改过什么,比逐行看省时间。想批量把一批用户迁到新档,也可以让数据库帮你生成语句:
SELECT 'ALTER USER ' || username || ' PROFILE app_oltp_profile;' FROM dba_users WHERE profile = 'DEFAULT' AND username NOT IN ('SYS','SYSTEM','DBSNMP','OUTLN','APPQOSSYS') ORDER BY username;生成出来先看一遍,确认没有系统账号混进去,再复制执行。我习惯把生成结果先存成一个 .sql 文件,加上注释和执行时间戳,将来出问题能追溯。
4.2 验证与回滚:怎么确认真的生效了
参数改完不验证等于没做。验证分三层。第一层查视图,确认参数值确实写进去了;第二层查用户绑定关系,确认目标账号真的挂在新档上;第三层做行为验证,真正去触发一次限制。
行为验证口令部分最简单:拿一个测试账号,故意输错密码 5 次,然后看账号状态:
SELECT username, account_status, expiry_date FROM dba_users WHERE username = 'TEST_USER';正常情况下 STATUS 会变成 LOCKED(TIMED),意思是被锁但有时间限制,到点自动解开。如果显示 LOCKED 没有 TIMED 后缀,说明 PASSWORD_LOCK_TIME 是 UNLIMITED,得手工解锁。验证完记得ALTER USER test_user ACCOUNT UNLOCK;,别把测试账号留着锁住忘了。
验证资源限制需要点技巧,因为很多限制在测试环境不容易触发。验证 SESSIONS_PER_USER 最直接,用同一个账号开若干个 SQL*Plus 窗口一路连,连到第 N+1 个的时候会报 ORA-02391 exceeded simultaneous SESSIONS_PER_USER limit。验证 IDLE_TIME 就开一个会话挂在那儿什么都不做,等到超过阈值之后随便发一条 SQL,会收到 ORA-02396 提示会话因为空闲时间过长已被终止。验证 CPU_PER_CALL,可以跑一条笛卡尔积的烂 SQL,比如对一张大表做无条件自连接,跑够时间就能看到 ORA-02392。这些验证一定要在测试环境做,而且做完把证据(报错截图、时间点)记下来,将来验收或者审计都用得上。
回滚方案要提前准备好。最轻量的是把用户绑回原档:ALTER USER app_user PROFILE DEFAULT;。如果整个自定义档都要撤,先确认没用户绑着,再DROP PROFILE app_oltp_profile;。想连用户迁移一起回滚就加 CASCADE,但要清楚 CASCADE 意味着那些用户全被打回 DEFAULT,参数会变成默认值——如果默认值里有你没料到的限制,回滚本身会引发新问题。所以我个人习惯是:删档之前先把绑定的用户列表导出来存一份,实在不行还能照着恢复。
监控层面,日常巡检可以挂两条查询。一条看有没有用户被锁:
SELECT username, account_status, lock_date FROM dba_users WHERE account_status LIKE '%LOCKED%' OR account_status LIKE '%EXPIRED%';另一条看密码快过期的账号,提前通知业务方改密,比等他们报错强:
SELECT username, expiry_date, TRUNC(expiry_date - SYSDATE) AS days_left FROM dba_users WHERE expiry_date IS NOT NULL AND expiry_date - SYSDATE < 15 ORDER BY expiry_date;4.3 profile 的导出、迁移与批量管理
概要文件这东西,跨环境迁移特别容易漏。表结构、索引、权限都能用数据泵带过去,但概要文件绑在用户上,数据泵导用户的时候其实会带上,只是常常因为目标环境没有同名 profile 而静默回退到 DEFAULT,然后你发现测试环境一切正常、生产环境参数完全不一样。
稳妥做法是单独把 profile 的 DDL 抽出来,用 DBMS_METADATA:
SET LONG 200000 SET PAGESIZE 0 SELECT DBMS_METADATA.GET_DDL('PROFILE', 'APP_OLTP_PROFILE') FROM dual;拿到 DDL 之后注意两件事:一是 DDL 里不含校验函数的创建语句,函数得单独迁;二是如果目标库已经有同名 profile,直接执行会报错,得改成先 DROP 再 CREATE,或者手工拼成 ALTER PROFILE。我一般的做法是把导出的 DDL 和校验函数脚本放在同一个部署包里,加一个前置脚本检查目标库是否已存在同名 profile,存在就先备份现有定义再覆盖。
批量管理可以再进一步,把所有 profile 的定义导出成一张对照表,方便做基线比对。思路很简单,把 DBA_PROFILES 的结果按 profile 做行转列,或者干脆导成 CSV 存进版本库。我们现在的做法是每次变更后都导一份快照放 Git 里,两个版本一 diff 就知道谁改了什么。这个习惯帮我抓到过一次"某个概要文件被临时改成 UNLIMITED 忘了改回来"的问题,如果没有基线,这种改动几乎不可能被发现。
5. 常见问题与排查实录
5.1 五类高频报错的原因与处理
概要文件相关的报错,类型其实很集中,我整理成速查表:
| 报错 | 触发原因 | 处理方式 |
|---|---|---|
| ORA-28000 | 连续输错达阈值,账号被锁 | 确认为本人操作后ALTER USER xx ACCOUNT UNLOCK |
| ORA-28001 | 密码超过有效期加宽限期 | 改密,改完立刻恢复 |
| ORA-28002 | 密码进入宽限期 | 只是提醒,尽快改密;应用侧别当错误处理 |
| ORA-28003 | 密码未通过校验函数 | 看函数里抛的自定义错误文案,按提示改 |
| ORA-02391 | 超出 SESSIONS_PER_USER | 查连接池配置或是否有泄漏连接 |
| ORA-02396 | 空闲超时被终止 | 检查 IDLE_TIME 与连接池保活间隔 |
| ORA-02392 | CPU 或逻辑读超限 | 找对应 SQL 优化,别急着放大参数 |
| ORA-01017 | 用户名密码错误(有时与锁定混淆) | 先看账号状态再看密码 |
ORA-28000 是最常见的,也是最容易被误判的。要注意它有两种成因:一种是失败次数达到 FAILED_LOGIN_ATTEMPTS,另一种是撞上了 INACTIVE_ACCOUNT_TIME 的长期未登录锁定。两者在 DBA_USERS 里显示的 ACCOUNT_STATUS 文案不同,前者是 LOCKED(TIMED) 或 LOCKED,后者是 LOCKED(TIMED) 加一个不同的描述,具体文案随版本略有差异。排查时先看 LOCK_DATE 和账号最近一次登录时间(可以从审计或者 V$SESSION 历史里找),能快速区分。如果是长期未登录导致的,那说明这个账号本身可能就是废弃的,解锁前先确认业务是否还需要它。
ORA-28002 这个"警告式报错"值得单独说。它在登录成功的同时返回,SQL*Plus 里能看到一句提示,但如果应用用的是某些驱动,在异常处理上可能会把它当致命错误抛出来,导致明明能连却报登录失败。遇到这种"密码没错但应用连不上"的情况,先看应用日志里有没有 28002,如果有,就是应用把警告当错误了,要么改应用的错误过滤逻辑,要么干脆把 PASSWORD_GRACE_TIME 调大留出更长的缓冲窗口。
5.2 改了不生效?按这个顺序查
"我明明改了参数,怎么没效果"这类问题,按下面的顺序查,基本五分钟内能定位:
第一步,确认改的是不是生效的那个档。查用户实际绑的 profile:SELECT username, profile FROM dba_users WHERE username='XXX';。很多情况下用户绑的根本不是你以为的那个档,尤其是接手别人的库。
第二步,确认改的是不是"当前值"。DBA_PROFILES 里看到的是档的定义,但用户的密码过期时间是从上次改密日算的,你改 PASSWORD_LIFE_TIME 不会重置已有的过期时间。想立刻生效得执行ALTER USER xxx IDENTIFIED BY "临时密码";或者ALTER USER xxx PASSWORD EXPIRE;强制走一轮。
第三步,资源类限制查 RESOURCE_LIMIT 开关和资源计划。前面说过,没开开关的话资源参数完全不生效,这是最高频的原因。
第四步,确认会话是不是老的。改绑 profile 和修改口令参数,对已经建立的会话不一定立即生效,尤其是资源累计值不会清零。等新会话建立后再验证。
第五步,多租户环境要确认容器。较新版本里概要文件有本地和公共之分,你在 PDB 里建的是本地档,只在当前 PDB 可见;如果在根容器里建公共档,命名和可见性规则又不一样。跨容器排查时,务必带上CON_ID一起看,或者直接切到对应容器里查。我遇到过在根容器改了半天发现业务连的是某个 PDB,白忙一场。
5.3 一些踩坑心得与生产环境建议
第一条心得:永远不要在生产上用 DEFAULT 做试验田。想试参数就新建一个档,绑一个测试用户跑,跑通再推广。DEFAULT 的改动影响面是全库,而且回滚时你未必记得住原来的每个参数值。我现在的习惯是每次动 DEFAULT 之前先SELECT * FROM dba_profiles WHERE profile='DEFAULT';导出一份存文件,命名带上时间戳,回滚有据可依。
第二条心得:口令策略的调整要和应用方对齐。PASSWORD_LIFE_TIME 从 UNLIMITED 改成 90 天这种变更,在很多老系统上是致命的,因为应用可能把数据库密码写死在配置文件里,密码一过期整个服务就起不来。上线前一定要问清楚:密码存在哪、谁负责改、改密流程要多久。如果应用根本没能力定期改密,那就老老实实给一个很长的有效期,同时用强校验函数把密码强度拉起来,用强度换时间,这比硬上一个 90 天然后每周救火要务实得多。
第三条心得:概要文件的变更要进变更管理。它不像建表那样看得见摸得着,改了很容易被忘掉,半年后出问题没人想得起来是谁改的。我们现在的做法是所有 profile 变更都走同一个脚本模板,模板头部强制填变更单号、影响用户列表、回滚语句,执行完把 DBA_PROFILES 的输出追加到变更记录里。这套流程一开始大家嫌烦,出过两次事之后没人抱怨了。
第四条心得:校验函数要考虑可维护性。别把规则写死在 PL/SQL 里就完事,业务方过半年说"特殊字符要求从 2 个改成 3 个",你得改函数、重编译、还得担心影响正在改密的用户。我的做法是把阈值抽成一张小配置表或者用常量集中定义在函数头部,改的时候只动一个数字,风险可控。当然,函数里查表这件事前面说过要谨慎,配置表要么放固定表空间,要么在函数里做异常兜底,读不到就用硬编码默认值,保证认证路径永远不断。
第五条心得:定期做一次"影子演练"。挑一个非核心业务账号,故意让它密码过期、故意输错密码锁掉,然后走一遍完整的发现、通知、处理流程,看看监控能不能报出来、值班的人知不知道怎么解。这个演练成本很低,但能暴露出一堆流程问题,比如监控只报了锁没报过期、比如解锁脚本权限没给到值班同学。我在两个团队推过这个做法,每次都能查出点东西。
最后分享一个我自己常用的小技巧:想快速看清一个用户身上到底有哪些限制在起作用,不用一个个视图翻,直接拼一条查询把用户、profile、关键参数一次拉出来:
SELECT u.username, u.profile, u.account_status, p.resource_name, p.limit FROM dba_users u JOIN dba_profiles p ON p.profile = u.profile WHERE u.username = 'APP_USER' AND p.resource_name IN ( 'SESSIONS_PER_USER','IDLE_TIME','CONNECT_TIME', 'FAILED_LOGIN_ATTEMPTS','PASSWORD_LIFE_TIME', 'PASSWORD_GRACE_TIME','PASSWORD_LOCK_TIME', 'PASSWORD_REUSE_TIME','PASSWORD_REUSE_MAX') ORDER BY p.resource_name;这条语句在我处理"某个账号行为异常"类问题时出场率极高,一眼就能看出这个账号是被哪条策略卡住的,比翻文档快得多。概要文件这套机制本身不复杂,复杂的是它跟用户、资源计划、多租户容器、应用连接池之间的相互作用,把这些关系理顺了,那些 ORA-28xxx 和 ORA-023xx 就不再是玄学,而是一条条能对得上的规则。