请求越来越多,数据库却只收到少数连接

采购高峰时有三十个 HTTP 请求同时进入用例,如果只给数据库开很少的连接名额,一部分请求要等待,有些可能超时。继续增加 HTTP 线程不会凭空增加数据库处理能力;把池上限调大则可能耗尽 PostgreSQL 接受连接的预算。这个问题关乎队列、持有时间和失败恢复,不是“服务器开了 jdbc-4.3 功能”就能推断的性能结论。本篇在 Open Liberty 26.0.0.5、Java 21、PostgreSQL 16.15 的版本范围内提出可测条件,不把任何未采集的延迟或吞吐说成实测。

本篇源码、迁移、脚本与测试可从固定版本完整归档取得;版本 1b08ada,SHA-256 见源码清单。

借用、使用与归还是三段不同时间

真实配置 examples/javaee-enterprise/deploy/server.xml 有一个 jndiName="jdbc/Procurement" 数据源及驱动库,绑定本机 127.0.0.1 的教学实例,没有显式设置 connectionManager 的 maxPoolSize 或 connectionTimeout。因此当前仓库没有声明“小池为 2、等待 1 秒”的数值;即使运行时存在默认值,也应按实际发行版配置和指标核对。DatabaseCheckResource.check() 从 DataSource 取得连接并执行只读查询,JdbcRequestStore 的方法通过 try-with-resources 归还句柄;这些源码说明应用在什么时候请求/释放资源,不能统计池内真实并发借出数。JDBC Connection.close() 对受管池一般表示结束此次借用,不代表关闭了 PostgreSQL 的物理会话。

配置参数的生效范围首先要说清楚:一个实验服务器可能只有一个 jdbc/Procurement,两个 Liberty 实例却各有自己的 DataSource 池;数据库总连接数看的是所有实例与运维会话之和,而不是单份 server.xml 的条目数。connectionTimeout 若设定,是等待池连接的产品参数,不等于 HTTP curl --max-time,也不等于 JDBC Statement.setQueryTimeout 或 PostgreSQL 的语句执行超时。客户端先超时返回,后台操作仍可能继续持有连接;反过来池先拒绝借用,SQL 根本没开始执行。必须在服务器日志与独立指标上找到故障阶段,不能只通过 curl 的退出码给数据库贴“慢查询”标签。

要做小池实验,只在临时服务器配置副本里把 DataSource 关联到专门的 connectionManager。Open Liberty 26.0.0.5 的配置文档有 connectionManagerRef、maxPoolSize 和 connectionTimeout;例如把名为 labSmallPool 的 manager 配成上限 2、等待 5 秒,并让原来 jdbc/Procurement 的 <dataSource> 通过引用使用它。数字只是待验证的实验输入,仓库中的真实 server.xml 没有这段配置,示意片段也不能单独代替含驱动、JNDI 名和口令引用的完整文件。复制后仍应从启动日志和有效配置核对是不是同一实例读取了新文件;若仍在跑旧实例,所有“上限 2”推断都无效。不能为模拟等待顺手缩小生产池。

把一个请求的时间拆成等待借用、持有连接执行 SQL、以及无连接的业务时间。池借用时间增加可能来自 SQL 变慢、事务把连接占得太久、连接没归还,也可能是数据库主动断连后的重建;单看 HTTP 总延迟区分不了。受管 @Transactional 作用于用例,数据库事务边界也可能比单条 SQL 更长,不能简单以 DAO 方法执行时长当作全部持有时间。申请插入包含申请与明细的语句,状态转换依赖旧版本,订单操作还分读申请、插订单、更新状态;要观察每一步是否共用物理连接,应记录服务器事务 ID、池指标和 PostgreSQL 会话,而不是用两个 Java Connection 引用是否相等下结论。

在池等待队列中,线程只是排队的一方,真正限制并发的还包括数据库锁与 CPU。审批更新的 WHERE version=? 即使只影响一行,并发请求仍可能在同一申请行上相互等待;建单的唯一约束在并发冲突时也需要数据库协调。若这时把池从 2 扩为 20,可能让更多事务同时争用同一热点行,吞吐并不会按比例上升。实验应固定申请 ID 集合、同租户或跨租户负载比例、每次请求的操作类型,至少分别观察无锁争用的只读 db-check 和对同一申请的写操作;但写操作还需要符合状态机,不能为了压测往共享业务库伪造乱序审批。当前并无这组负载脚本,不填写吞吐比值或 p95 数字。

一个预算计算可以用于设计实验,而非宣称性能:假设两个相同应用实例,每个池上限 4,则单看这两个池至多可申请约 8 个数据库连接名额;还须加管理员/迁移/监控等其它会话,并留给 PostgreSQL 所需的保留空间。这个“2 × 4”是计划设置,不是现有 server.xml 的值。max_connections 是数据库侧上限,并不表示把连接数配到上限就能得到最好吞吐;当查询或锁等待成为瓶颈,扩大池只会增加同时等待的工作量。先测可接受的排队时间和超时错误,再改变池或缩短持有时间,不能只测平均延迟。

预算表的每一行应写清应用实例数、该实例最多创建的池数、各池的上限,以及同一数据库的非应用连接。迁移脚本使用的 psql、备份工具、监控与数据库管理员会话通常不从采购服务器的连接池借用,也可能占据数据库名额;PostgreSQL 的保留连接与运维策略还要求余量。不要把“DataSource 对象只有一个”误认为“它最多只有一个数据库连接”,也不要把数据库 max_connections 抄进每一个实例,误把每个池都配置为数据库总预算。上线前还要验证数据库自身可用内存与长事务代价,而非只让 TCP 建连成功。

资源耗尽与数据库重启需要不同的故障判据

配置副本、正常探针、故障恢复以及尚未建好的小池屏障测试各自的命令和空白证据栏见连接池故障实验卡。

设计耗尽试验时,在独立数据库和隔离 Liberty 配置副本里设置小池上限,给长时间占用连接的受控测试入口加同步屏障,至少使并发调用数大于池上限。记录请求开始/借用/执行/释放时刻、借出及等待计数、错误类型和 SQL/锁等待,设置明确的时间上限以免无限挂起;释放阻塞后再发健康、数据源探针及真实只读请求,核对能否恢复。现有 scenarios/05-datasource.sh 只发起一次请求,既不能建立并发交错,也不能让池耗尽;原始 connected 响应只说明正常借用成功过一次。当前没有耗尽入口、连接池计数或等待时间记录,实验 NOT_RUN。

现有正常路径至少有一条可复跑命令:在专用数据库和 loopback 监听的服务器就绪后,从工程根目录运行 JAVAEE_PORT=9085 bash scenarios/05-datasource.sh;预期脚本退出 0、返回 GET /procurement/api/db-check: connected,单独 curl -sS -i --max-time 10 http://127.0.0.1:9085/procurement/api/db-check 应返回 HTTP 200 与相同正文。已归档基础证据的响应头与正文分别保存在 datasource.headers.txt、datasource.body.txt,但没有当时的池配置快照与连续借还计数。这些命令验证服务端能成功获取一次数据库连接,不能衡量等待队列。若为便于对照连续运行十次,请给每次记录起止时间与实际服务器版本;十次顺序成功依然不等于在小池中承受十一条真正重叠的请求。

可复跑的依赖故障与“池被借光”必须分开。只有在可以自行停启的独立 PostgreSQL 实例上,先记录服务端日志窗口和一次上述正常请求;由操作者按自己的本机数据库管理命令停止这个专用实例,再在同一已部署 Liberty 实例运行同一 JAVAEE_PORT=9085 bash scenarios/05-datasource.sh,保存脚本非零退出、客户端响应及服务端 SQLState;恢复专用数据库后重跑脚本并确认最终再次 connected。停启数据库的命令没有写死,因为工程 README 没有安装或控制 PostgreSQL 实例的管理脚本,不能虚构一个可安全复用的 systemctl 命令。请求的失败时机、HTTP 状态和重试次数必须留给实际记录,不预填固定输出;这整套故障与恢复仍为 NOT_RUN。即使该实验成功,也只测试断连恢复,不测试等待池连接的超时。

数据库重启是另一种注入:隔离 PostgreSQL 停止前后保存会话/连接指标与服务端异常链,在有界超时内反复请求 /api/db-check,记录失败时客户端 HTTP 状态、服务器 SQLState、连接是否被丢弃,以及数据库恢复后第一次成功借用的时间。恢复数据库后,还要独立核对业务记录,没有重复订单或未明状态;不要凭一次健康接口的 ready 宣告依赖恢复。驱动、服务器的验证连接及清理策略由冻结配置决定,不能预先写死返回码、重连次数或具体超时文案。该数据库故障及恢复测试也为 NOT_RUN,必须确保停的是专用实例而非同事共用数据库。

已存在的 writing-plans/javaee-enterprise/verification/20261004T061900Z-pg16-foundation/ 只提供一次受管 DataSource 的 200/connected;顺序采购记录和单次受管插入后回滚分别在 20261004T062900Z-pg16-business/、20261004T063400Z-pg16-jta-rollback/。这些输出支持“数据库曾经可访问”“一笔顺序采购到达终态”“一次故障后申请行没有提交”,不包含小池同时借用数、排队分布或数据库重启。第 12 篇讨论异常关闭路径的源码;即便 JDBC 关闭语句写得正确,部署配置和负载未测时,也不能推出生产无泄漏。

做对照时一次只改一个因素。若先缩短 SQL 占用时间再提高池上限,两组响应时长无法归因;若测试期间数据库背景会话数量变化,业务池的实际名额也变了。负载记录至少区分“进入 HTTP”“等待借用”“SQL 执行”“事务结束”和“请求返回”五个时间点,把超过借用等待预算的请求与连接取得后 SQL 超时的请求分别归类。一次 getConnection() 失败不必然意味着数据库宕机;服务器实例关停、连接池耗尽、凭证错误和物理网络故障都需要不同的异常链与恢复动作。

另一种错误证据是只看数据库当前会话数推断“应用泄漏”。池的正常工作之一就是缓存可重复使用的物理连接,因此即使 JdbcRequestStore 的所有 try-with-resources 都执行完,数据库侧仍可能看到对应空闲会话。相反,pg_stat_activity 中当前会话不多,也不能保证等待借用的应用线程数低。需要同时读取相同时间窗的池借出/空闲/等待计数、数据库会话状态与请求 ID;只有请求结束后借出数长期不回落,且后续借用持续超时,再结合异常时的关闭链,才能讨论是否发生连接未归还。未记录同一时间窗,就不能把来源不同的监控截图拼成事故证据。

故障注入也要留出停止条件。小池压力实验若导致业务请求持续积压,先停止新流量并释放测试屏障,再按预先设置的等待上限观察队列是否消退;不能在等待线程仍持有数据库事务时强制杀整个数据库来“让超时更明显”。数据库断连恢复实验则应确认服务器仍指向同一个专用实例和库名,避免故障时配置漂移到另一个已启动的数据库而误报自动恢复。每轮结束后复查测试租户的申请与订单行数;只有读接口成功却留下错误的业务行,也不能叫成功恢复。为保护共享环境,上述压力与停启操作绝不在默认 javaee_lab 的共用实例上直接执行,必须由操作者单独准备可控数据库。

日志在这类实验中容易混入连接串与账号:保留服务器日志、SQLState、请求关联 ID、时间和池指标足够定位;对口令、完整 JDBC URL 中可能含有的敏感参数以及实际业务租户值要做脱敏。一次正常请求、一次故障请求与恢复后的请求要各自有独立记录,不能把 connected 的最后一条响应与前一轮池参数拼接成单次运行的证据。只有同一配置、同一 WAR 和同一个请求时间线能建立因果关系,才讨论是否通过了小池等待或数据库重启的故障判据。

连接池恢复后的验证还应同时检查业务与资源:从池里重新借到连接、执行 SELECT current_database() 返回 connected,只是依赖重新可达;此时若之前有提交结果未知的采购请求,客户端重试仍应通过申请 ID 查询订单,不应再新建一张。若断连前持有的事务已经回滚,另一连接的行集也应与业务状态一致。现有探针不检查订单或租户,因此要在专用环境把恢复后的资源查询与独立的业务终态查询并列存档;数据库重启实验缺少这些记录时,最多写“数据源重新连接成功”,不得升格为“采购业务完全恢复”。

两道练习与答案

练习一:两个实例各配置上限为 4 的池,数据库 max_connections=8。八条业务连接都能稳定建立吗?如何设合理预算?

答案:不能保证。数据库还可能被管理连接、迁移任务、监控及其它实例占用,且受数据库自身的保留连接规则约束。先列出全部实例、池和外部会话的同时上限,预留必要余量,再按隔离实例的真实 SQL 与排队测量调整。题中的 4 和 8 是假设,不是现有服务器配置或实测容量。

练习二:把池上限降为 1,连续十次运行只读 /api/db-check 都返回 connected,能否证明不会有借用超时或连接泄漏?

答案:不能,顺序请求都能成功不代表同时请求不会排队。应以屏障强制两次以上请求真正重叠,记录等待与借出数、超时和随后释放能否恢复;还应设置有界最长持有时间及回收观测。现有脚本没有并发测试,结论仍 NOT_RUN。

边界与资料

池参数与数据库预算是特定产品和部署实例的观察,不是 Jakarta EE 11 对连接数或等待时间的统一保证。入口:Open Liberty 26.0.0.5 connectionManager 配置、dataSource 配置、Java SE 21 DataSource、Connection、PostgreSQL 16 连接配置。核验需保存每次运行的源码/WAR 摘要、池参数、数据库版本、请求时间线与脱敏指标;没有它们就不填写 PASS 或容量数字。