第一次遇到 ORA-02391 这个报错,是很多年前在客户现场排查一套财务系统的时候。当时开发那边反馈业务突然大面积报错,登录用户集体掉线,我拉了一下v$session,发现某个业务账号的会话数已经冲到两百多,直接把实例的 SESSIONS 参数顶满了,整个库几乎处于半瘫痪状态。事后排查发现是应用代码里存在连接泄漏,每次请求都新建连接但没走释放逻辑。那次之后,我就在 Profile 里给这个账号设置了 SESSIONS_PER_USER 的上限,类似问题再没出现过。
Oracle 数据库里,SESSIONS_PER_USER 是一个非常实用但容易被忽视的并发控制参数。它属于用户配置文件(Profile)里的资源限制项,可以精确限制"单个用户最多同时建立多少个数据库会话"。这篇文章就从它的原理、配置、验证方法、踩坑经验以及和实例级参数的关系,完整梳理一遍。主要内容面向 DBA、数据库运维同学,也建议做应用开发的朋友了解下,毕竟连接池设计时,数据库侧还有这么一道约束。
1. 为什么需要限制用户的并发连接数
1.1 不设限的典型事故场景
数据库实例层面有 PROCESSES 和 SESSIONS 两个参数兜底,很多 DBA 觉得有了这两个参数就万事大吉。但实际上,它们解决的是"数据库整体扛不扛得住"的问题,解决不了"某个用户把资源吃光,殃及池鱼"的问题。一个很典型的现象:实例的 SESSIONS 参数设的是 1000,正常情况下整个库 600 个会话左右,一切平稳。但如果某个业务账号因为代码 bug 或人为误操作,一下子把连接数从 50 打到 500,那其他所有业务的会话都会被挤压,最终表现为整个库的连接数爆掉,谁也登录不上去。
类似的事故场景我见过不少,包括但不限于以下几种:
- 应用代码有连接泄漏。Java 程序里每次请求都
getConnection()但忘记close(),跑上半天连接数就一路涨。 - 报表任务或批量任务用同一个账号,一次性打开几十个并发会话跑数据,占用大量临时表空间和 PGA。
- 运维或开发人员排查问题时用同一个业务账号反复登录,用完不退出,连接越积越多。
- 连接池的
maximumPoolSize配置得过大,而且应用是多节点部署,每个节点都按最大池大小创建连接,合起来远超数据库预期。
如果账号设置了 SESSIONS_PER_USER,上面这些场景最多只会影响这一个账号,不会拖垮整个实例。这就是用户级并发控制的价值所在——故障隔离,或者说,至少能限制故障半径。
1.2 哪些场景建议必须配置 Profile 配额
通过这些年做数据库运维和架构评审的经验,下面这几类场景,我基本上都会坚持建议客户配置 Profile 配额,而不是只依赖实例级参数:
| 场景 | 原因 |
|---|---|
| 多个应用共享同一个 Oracle 账号 | 单个应用的连接行为不受控,一个出问题会拖垮另一个 |
| 第三方厂商提供的黑盒应用 | DBA 无法修改应用侧连接配置,只能从数据库侧加限制 |
| 高密度生产环境,多个业务系统共用一个实例 | 需要通过配额为不同业务账号分配资源边界 |
| 等保、企业内控或合规审计要求 | 安全基线里明确要求限制账号并发会话数 |
| 数据库账号直接暴露给运维脚本和手工登录 | 防止有人开了会话忘记退出,长期占用空闲连接 |
在这些场景下,SESSIONS_PER_USER 就是个很轻量的流量阀门,能从账号维度做资源隔离。
2. SESSIONS_PER_USER 的工作机制
2.1 参数含义:限定用户级并发会话数
Profile 在 Oracle 里是一组资源限制的集合,默认情况下每个用户都会绑定一个名为 DEFAULT 的 Profile。SESSIONS_PER_USER 是这个集合里的一个资源限制项,含义是"该用户最多可以同时建立的数据库会话(session)数"。
举个例子,如果给某个账号设置了SESSIONS_PER_USER 5,那么这个账号在同一时间最多只能建立 5 个会话。第 6 个会话发起时,数据库会直接拒绝,报ORA-02391: exceeded simultaneous SESSIONS_PER_USER limit。
通过数据字典可以很方便地查看当前所有 Profile 的资源限制值:
SELECT profile, resource_name, limit FROM dba_profiles WHERE resource_type = 'KERNEL' ORDER BY profile, resource_name;注意RESOURCE_TYPE = 'KERNEL'这个条件。Profile 里的参数分两类,一类是内核资源限制(KERNEL),比如 SESSIONS_PER_USER、CPU_PER_SESSION、IDLE_TIME;另一类是口令管理(PASSWORD),比如 PASSWORD_LIFE_TIME、FAILED_LOGIN_ATTEMPTS。SESSIONS_PER_USER 属于前者。
2.2 总开关:RESOURCE_LIMIT 参数
这里必须强调一个硬约束:Profile 里的所有内核资源限制参数,包括 SESSIONS_PER_USER,都受数据库参数RESOURCE_LIMIT控制。如果RESOURCE_LIMIT = FALSE,哪怕你创建了 Profile、绑定给了用户,限制也不会生效。
-- 查看当前状态 SHOW PARAMETER resource_limit; -- 动态开启,立即生效,无需重启 ALTER SYSTEM SET resource_limit = TRUE;RESOURCE_LIMIT默认值是 FALSE,这是 Oracle 为了兼容旧版本行为保留的默认设置。网上很多教程只教你怎么建 Profile、绑用户,不提这个开关,结果有人配置完一测试,发现压根没限制,还以为自己操作错了。
还有一个容易忽略的细节:RESOURCE_LIMIT只影响 KERNEL 类资源限制,不影响 PASSWORD 类的口令管理参数。也就是说,就算RESOURCE_LIMIT = FALSE,你设了FAILED_LOGIN_ATTEMPTS密码尝试次数限制,该锁账号还是会锁。这一点在做排查时很有用,能帮我们快速区分问题方向。
如果确定要在生产环境启用,建议同时写入静态参数文件:
ALTER SYSTEM SET resource_limit = TRUE SCOPE = BOTH;SCOPE=BOTH 表示同时修改当前实例和 spfile,避免下次重启后配置丢失。
2.3 什么样的连接会计入 SESSIONS_PER_USER
搞清统计口径才能准确估算配额。SESSIONS_PER_USER 统计的是指定用户的所有会话,统计范围很宽:
- SQL*Plus、SQL Developer 等客户端的命令行连接
- 应用通过 JDBC、ODBC 建立的连接
- 共享服务器(Shared Server)模式下分配给该用户的所有会话
- 指向本地用户的数据库链路(dblink)会话
- 通过监听器建立的专用服务器进程对应会话
有一点需要特别提一下:以 SYSDBA/SYSOPER 等管理员权限登录的会话,本质上走的是管理员通道,通常不会受普通用户的 Profile 资源限制约束。我见过有人在测试环境用 SYSTEM 账号测试 SESSIONS_PER_USER,结果怎么都触发不了报错,研究半天才发现方向错了。
另外,如果应用使用的是代理认证(Proxy Authentication),用户连接时实际是以代理用户身份建立的会话,那会话归属的计数就要看最终的后端用户名,而不是连接时的外部用户。这种场景比较少见,但如果遇到了,记得从v$session里的USERNAME字段去判断当前会话到底记在谁头上。
3. 从创建 Profile 到生效的完整配置流程
3.1 第一步:确认总开关状态
在动手配置前,先确认RESOURCE_LIMIT已经打开:
SHOW PARAMETER resource_limit;如果当前是 FALSE,执行:
ALTER SYSTEM SET resource_limit = TRUE SCOPE = BOTH;这一步建议在变更窗口内操作。虽然它本身是动态参数,不会导致实例重启,但在高并发的生产库上,开启资源限制后,某些原本不受约束的用户可能立刻触发限制,引发应用连接报错。所以最好先和业务侧沟通,尤其在应用连接数峰值已经很高的场景下,别贸然开。
3.2 第二步:创建自定义 Profile
假设我们要给财务系统账号 FINAPP 做一个限制,并发会话数上限 5,空闲会话超过 30 分钟断开,单次连接时间不超过 8 小时。可以这样创建:
CREATE PROFILE fin_profile LIMIT SESSIONS_PER_USER 5 IDLE_TIME 30 CONNECT_TIME 480;我这里只设置了三个参数,没写的资源项会继承 DEFAULT Profile 的默认值。比如CPU_PER_SESSION没写的话,默认是 UNLIMITED;PASSWORD_LIFE_TIME没写的话,默认继承 DEFAULT 里的设置。
这个继承机制在运维时很容易踩坑:你建了一个新 Profile,只设了 SESSIONS_PER_USER,以为密码策略也自动继承 DEFAULT 了。但如果你在 DEFAULT Profile 里改了密码有效期,而自定义 Profile 里明确设了PASSWORD_LIFE_TIME 180,那绑到该 Profile 的用户就会用 180 这个值,而不是 DEFAULT 的新值。所以建 Profile 时最好把口令策略也一并明确写出来。
3.3 第三步:把 Profile 绑定给用户
ALTER USER finapp PROFILE fin_profile;绑定之后,可以验证一下用户的 Profile 是否切换成功:
SELECT username, profile FROM dba_users WHERE username = 'FINAPP';正常情况下,查询结果里PROFILE字段应该是FIN_PROFILE。注意 Oracle 默认会把 Profile 名称存储为大写,除非你用引号创建了小写或混合大小写的名称。这一点在后面查询和修改时会带来麻烦,建议创建时统一用大写字母。
3.4 第四步:触发测试,验证限制真的生效
开 5 个终端窗口,分别用 FINAPP 登录:
sqlplus finapp/finapp_password@orcl如果前 5 个会话都登录成功,第 6 个窗口继续执行同样的命令,就会看到:
ERROR: ORA-02391: exceeded simultaneous SESSIONS_PER_USER limit这个报错本身就是最直接的验证结果。如果第 6 个连接也成功,说明配置没生效,优先检查RESOURCE_LIMIT是否为 TRUE,以及dba_profiles里这个 Profile 的 SESSIONS_PER_USER 值是否为 5。
3.5 修改配额与清理 Profile
业务扩容后需要调整并发上限,直接改 Profile 就行,不需要动用户:
ALTER PROFILE fin_profile LIMIT SESSIONS_PER_USER 20;修改是立即生效的,已经超过旧限制但小于新限制的会话不会被中断,新会话按新上限执行。反过来,如果调低限制,已经存在的会话也不会被踢掉,但新会话会按新上限判断。这点很重要,我在第 5 节还会详细说。
如果某个自定义 Profile 不再需要了,删除时要注意,它一旦被用户引用,必须加 CASCADE:
DROP PROFILE fin_profile CASCADE;加 CASCADE 之后,原来绑定该 Profile 的用户会自动落到 DEFAULT Profile。这个动作是隐式的,删之前一定要确认用户本来使用的是否就是 DEFAULT 策略,避免密码策略等配置被意外切换。
4. 实际压测验证:并发超限会发生什么
4.1 准备测试账号和带到环境的注意点
在测试环境或者专门的验证库上做压测,建议准备一个独立的账号,别直接用生产账号试。我在测试时通常会这样准备:
-- 创建测试用户并赋予最小权限 CREATE USER conntest IDENTIFIED BY conntest_pwd; GRANT CREATE SESSION TO conntest; -- 创建测试 Profile,限制 3 个并发会话 CREATE PROFILE conntest_profile LIMIT SESSIONS_PER_USER 3; -- 绑定 ALTER USER conntest PROFILE conntest_profile;如果你只想在现有账号上测试,记得测试完把 SESSIONS_PER_USER 调回原值或改回 DEFAULT Profile,避免影响业务。
4.2 压测步骤和结果
在服务器上同时打开多个终端,或者在脚本里循环执行:
for i in $(seq 1 5) do sqlplus -S conntest/conntest_pwd@orcl <<EOF SELECT 'session_$i connected' AS info FROM dual; sleep 60; EOF done &这个脚本会尝试同时建立 5 个会话。前 3 个会成功,并且执行SELECT后进入 sleep 状态,第 4、5 个会失败并返回 ORA-02391。
如果不想等 sleep,也可以用下面这种方式快速验证:开 3 个 SQL*Plus 窗口手动挂着,再开第 4 个窗口执行。
-- 第 4 个窗口执行 SELECT COUNT(*) FROM v$session WHERE username = 'CONNTEST';查询结果大概率是 3,说明当前会话数已经顶满。此时新登录会直接报错。
4.3 超限时应用的感知是什么
从应用侧看,数据库超限报错表现为典型的连接建立失败。Java 应用使用 JDBC 时,堆栈里会出现类似的异常:
java.sql.SQLException: ORA-02391: exceeded simultaneous SESSIONS_PER_USER limit这里有个容易被忽略的问题:如果应用本身有连接池,而且池子里minimumIdle和maximumPoolSize设置得不够理智,那么在连接数被限住的瞬间,应用会反复尝试建立新连接,数据库反复返回 ORA-02391,应用日志里会刷出大量同样的错误堆栈。数据库端看到的则是大量连接尝试记录。这种情况下,仅仅调高 SESSIONS_PER_USER 并不能根治问题,还得同时排查连接池参数和应用侧是否存在连接泄漏。
4.4 关键细节:超限只拒绝新连接,不断老连接
这一点我特意拿出来强调。SESSIONS_PER_USER 超限后,动作只有"拒绝新的会话建立",对已经存在的会话完全不影响。哪怕第 6 个连接一直尝试、一直失败,前面 5 个会话依然能正常执行 SQL、正常提交事务。
这个特性和实例级参数超限时"所有会话都受限"的表现不同。比如PROCESSES参数满了,整个实例的新连接都会被拒,老连接也不能幸免于一些需要新进程的操作。而 SESSIONS_PER_USER 更像一把只卡在门口的门禁,房间里的人不受打扰。
所以在做容量评估时,要给临界情况留出缓冲:如果业务峰值并发会到 10,你设 10 看起来刚好,但一旦业务临时冲一下到 11,新请求就会全部失败,而且因为老连接不释放,失败状态可能持续很久。实际生产环境中,设到正常峰值的 1.2 到 1.5 倍会更稳妥,给流量毛刺留一点余量。
5. 踩过的坑与常见误区
5.1 DEFAULT Profile 的"默认值"其实是 UNLIMITED
DEFAULT Profile 里 SESSIONS_PER_USER 的默认值是 UNLIMITED,这也是为什么大多数人从来没遇到过 ORA-02391 的原因——系统默认压根不做用户级限制。Oracle 把所有新用户自动放到 DEFAULT Profile,所以如果不主动建 Profile,这个参数就是一种存在但从未生效的状态。
在做安全基线排查时,建议批量查一下当前有哪些用户仍然挂在 DEFAULT Profile 下:
SELECT username, profile FROM dba_users WHERE profile = 'DEFAULT' ORDER BY username;很多次渗透测试和安全审计时,这条 SQL 都是第一轮必查项。结果通常能看到不少高权限账号还挂在 DEFAULT 上,且 DEFAULT 的 SESSIONS_PER_USER 是 UNLIMITED,这就是高风险点。
5.2 改了 Profile 却不生效的几种原因
这类问题在我帮助用户排查时遇到频率非常高,根因通常集中在下面几个地方:
RESOURCE_LIMIT还是 FALSE。这是第一大坑,优先确认。- 用户绑定的 Profile 不是你以为的那个。比如有人改了
fin_profile,但用户实际绑的是default。 - 应用连接时用的是服务账号的代理用户,计数归属到别的账号。
- 数据库里存在同名大小写不同的 Profile。如果用引号创建过小写名称,后面写大写名称查到的是另一个 Profile。
- 修改 Profile 后没有重新建立连接。已经存在的连接在会话建立时就按当时的限制判断,改 Profile 不会自动刷新已有连接的配额状态。
其中最后一点特别容易误导人:你调低了 SESSIONS_PER_USER,但之前那些已经超过新限制的老连接全都还挂着,这时候想知道新限制是否生效,必须新起一个会话去测,而不是看老会话有没有被断开。
5.3 连接池场景下的配额计算要算总账
连接池是 SESSIONS_PER_USER 最容易翻车的场景。很多团队只在一台应用服务器上做了测试,配了maximumPoolSize=20,然后 SESSIONS_PER_USER 设了 25,看着没问题。可实际上生产环境是 4 个应用节点,每个节点都是这个池大小,加起来就是 80,25 的配额瞬间被打满,应用启动时连接池初始化都会失败。
正确的做法是,把所有使用该数据库账号的节点池大小加总,再乘以一个冗余系数。比如 4 个节点,每个池最大 20,合计 80,那 SESSIONS_PER_USER 至少设置 96(80 × 1.2)左右,再预留 DBA 手工连接和监控账号的空间。如果同一个账号还要跑夜间批处理,批处理的并发连接数也要单独计入。
5.4 会话数不等于连接数:两种特殊连接形态
严格来说,在专用服务器(Dedicated Server)模式下,一个会话对应一个连接,两者基本可以画等号。但在共享服务器(Shared Server)模式下,用户到数据库的物理连接是复用的,会话数超过了物理连接数。SESSIONS_PER_USER 限制的是会话数,而不是物理连接数量。这表示在共享服务器模式下,一个应用可以建立少数几个物理连接,却产生很多个并行会话,同样会触发限额报警。
另外,如果应用使用了 DRCP(Database Resident Connection Pool),会话被池化后,统计上也会体现为多个会话归属于同一个用户。这种情况下,单纯看数据库侧的连接数很容易跟应用侧对不上。排查问题时要先确认数据库运行在专用服务器模式还是共享服务器模式,否则方向容易跑偏。
5.5 排查脚本:快速定位当前账号的会话占用
当收到 ORA-02391 告警时,用下面这几条 SQL 可以快速定位:
-- 查看该用户当前所有会话 SELECT sid, serial#, username, machine, program, status, logon_time FROM v$session WHERE username = 'FINAPP' ORDER BY logon_time; -- 按机器和程序统计会话分布,找到"连接大户" SELECT machine, program, COUNT(*) AS session_cnt FROM v$session WHERE username = 'FINAPP' GROUP BY machine, program ORDER BY session_cnt DESC;通过第二条 SQL,通常能一眼看出是哪台应用服务器、哪个程序占了大量会话。结合logon_time,还能判断这些会话是不是从某一个时间点开始集中建立,从而反推应用发布或代码变更的时间线。
6. 与其他并发控制手段的配合使用
6.1 三级限制的层次差异
数据库里的并发限制其实是多层次的,SESSIONS_PER_USER 不是唯一的工具,它和实例级参数各有分工。
| 限制层次 | 参数 / 对象 | 作用范围 | 超限报错 |
|---|---|---|---|
| 操作系统进程数 | PROCESSES | 整个实例 | ORA-00020: maximum number of processes exceeded |
| 实例会话数 | SESSIONS | 整个实例 | ORA-00018: maximum number of sessions exceeded |
| 用户并发会话数 | PROFILE.SESSIONS_PER_USER | 单个用户 | ORA-02391: exceeded simultaneous SESSIONS_PER_USER limit |
从运维角度看,PROCESSES 和 SESSIONS 是最后一道防线,保护的是数据库这个容器本身。SESSIONS_PER_USER 则是更精细的"分闸",在单个用户的层面做隔离。实际生产环境中两者不能互相替代:你把 SESSIONS 设得再大,也不妨碍某个业务账号靠连接泄漏把整个库拖垮;你把每个账号的 SESSIONS_PER_USER 都设得很小,但如果用户数量很多,实例级 SESSIONS 仍然可能被整体打满。所以两个维度都要管。
6.2 其他相关的 Profile 资源参数
SESSIONS_PER_USER 通常不是单独使用的,我会习惯把它和下面几个 Profile 参数搭配:
| 参数 | 作用 | 搭配理由 |
|---|---|---|
| IDLE_TIME | 空闲会话最大分钟数 | 自动清理挂机不用的会话,释放配额 |
| CONNECT_TIME | 单次连接最大分钟数 | 限制超长连接,防止泄漏连接长期占用 |
| CPU_PER_SESSION | 会话累计 CPU 时间上限 | 防止单条失控 SQL 消耗过多 CPU |
| LOGICAL_READS_PER_SESSION | 会话累计逻辑读上限 | 限制大查询读取量,保护 I/O 和缓存 |
比如同时设置SESSIONS_PER_USER 20和IDLE_TIME 30,即使应用有少量连接忘记释放,空闲超过 30 分钟后也会被数据库侧断开,避免配额一直被无效连接占着。
6.3 生产环境推荐的配套方案
在我维护过的生产库里,我一般建议按下面的思路做配置:
- 建立专门的业务 Profile 模板,统一管理 SESSIONS_PER_USER、IDLE_TIME、CONNECT_TIME、口令策略,而不是让各个账号各自为政。
- 业务账号的 SESSIONS_PER_USER 按"应用节点数 × 单节点池大小 × 1.2~1.5"设置,同时额外叠加 DBA 日常维护连接数余量。
- 定期巡检,每周跑一次 SQL,统计每个 Profile 下用户的实时会话数与配额比值,提前发现接近配额的账号。
- 把 ORA-02391 纳入数据库告警项,一旦出现,立即检查是流量暴涨还是连接泄漏,而不是等问题发酵。
6.4 临时扩容操作
业务大促、月底批量跑数这类短期高并发场景,临时调高配额是常规操作。这个操作不需要重启,不需要断连接,只需要一条 SQL:
ALTER PROFILE fin_profile LIMIT SESSIONS_PER_USER 50;活动结束后再改回原值,整个过程对业务透明,非常方便。我在做双十一大促支持时,经常提前几天把核心账号的配额临时调高,活动结束后再收紧。这里有一个小提醒:改高配额是一瞬间的事,但改低配额时,如果当时在线会话数大于新配额,新会话会受限,而老会话不会断开,业务侧要留意这种现象,别误判是系统故障。
关于 SESSIONS_PER_USER,我能分享的实操经验基本就是这些了。最后说点个人体会:这个参数看起来很小,但在生产环境里,它是我遇到过的性价比最高的并发保护手段之一。它不像 PROCESSES 那样需要动实例级配置,也不像应用侧改代码那样依赖开发排期,一条 ALTER PROFILE 就能从数据库侧卡住失控连接的蔓延。如果你手头也管着一批 Oracle 实例,建议花半天时间做一轮账号摸底,看看哪些账号还挂在 DEFAULT Profile 下,然后按业务并发峰值给它们上一个合理的配额。等到真的因为连接泄漏而收到告警时,你会庆幸当初多写了这一行配置。