健康接口每秒能回多少次,不是采购容量

采购操作要获取连接、锁定业务状态、写入申请与订单,还可能等待外部确认。对一个只返回 ready 的 HTTP 接口压测,得到的数值主要反映 Web 入口;即使吞吐很高,也不能推导“同时批准一批申请仍满足唯一订单约束”。容量问题要求负载、系统状态与业务终态同时固定。

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

基线工程在 examples/javaee-enterprise/README.md:JDK 21、Jakarta EE 11 Platform、Open Liberty 26.0.0.5、PostgreSQL 16.15、pgJDBC 42.7.7。JAVAEE_DEMO_MODE=true 的实验接口仅能在 loopback 隔离运行。尚无压测报告、生产流量比例、连接池瓶颈或 p99 数字;以下微型采样只是说明怎样得到原始时延,不是吞吐/性能验收。

现有采购链路比健康检查复杂:FixedPriceCatalog 把两支纸与三支笔算成 2 × 3.50 + 3 × 2.25 = 13.75,JdbcRequestStore.find() 需要读取申请和明细,建单可能执行 INSERT ... ON CONFLICT ... RETURNING id 与版本条件更新。一次重试若发现已经 ORDERED,会走按租户查订单的分支;它与新建订单走过的 SQL 不完全一样。因此压测输入必须注明新建和重试各占多少,不能随机混在一起再说“每个采购请求执行两条 SQL”。

相同延迟数字可以有不同病因

固定到达率时,要记录发送端计划发起多少请求、实际完成多少、未发起多少以及超时数;按固定并发找饱和点时,客户端会随服务变慢而自动降速,不能把两种试验的吞吐曲线直接比较。p95/p99 是明确观察窗内的完成请求分布;若只统计成功请求而漏掉超时与排队未发起者,就会人为“改善”尾延迟。请求混合至少分别记录读、创建、提交、审批、建单、重试,且每个写请求用独立测试租户/申请,免得唯一键冲突压倒正常流量。

线程池与连接池不是两个可独立无限增加的旋钮。连接池借用等待可能从 HTTP 排队转移到数据库,事务持续时间又会延长锁持有;池调大还可能把 PostgreSQL 的活动会话和 I/O 推到极限。对两种瓶颈做可复现对照,应每次只改变一项配置,例如连接池上限或慢 SQL 数据规模,并保存 DB 锁等待、池等待、GC、CPU、磁盘和客户端资源。需同时确认压测后租户数据未串、订单唯一且金额正确;HTTP 200 占比不够。

第一种实验是排队:在固定数据库和请求混合下,逐步增加请求发起率。如果吞吐不增反降而响应队列/连接借用等待增长,先区分瓶颈在容器池还是在数据库。第二种实验是行竞争:让多个可控请求争用同一个已审批申请,观察条件更新零行、订单唯一约束与锁等待;它测的是冲突行为,不应用“最快 200 响应”代表业务容量。当前 deploy/server.xml 没有针对这两组实验冻结池参数或负载发生器,更没有两组对照的原始输出,以下是待实现的实验设计而非性能结论。

请求率与并发也不是一个数:固定并发 40,若请求平均耗时翻倍,客户端每秒发出的请求数会下降;固定每秒 40 个尝试,不管服务是否变慢,都可能形成等待队列。要说明“最大可持续吞吐”,需指定允许的超时率、资源上限、观察窗和恢复终态。p95/p99 在短窗口里可能因一次 GC、迁移或外部慢请求波动;比较版本时不仅记录分位数,还要保存完整原始样本、请求数、失败样本计入方式及置信程度,不应从个位数成功请求中估计容量。

从可运行的小采样开始

按工程 README 部署独立 javaee_lab 库和 WAR,在启动服务器前将 JAVAEE_DEMO_MODE=true 加入服务器环境,确保 localhost 9085 可用;仅在客户端 export 无法打开教学接口。下列命令只采样 20 次串行健康请求的总耗时和状态码;没有预热、并发、负载比例和资源指标,不能计算此应用的业务吞吐/p99。数字必须从当次 stdout 得到,不预填结果。

1
2
3
4
5
6
7
8
9
10
cd examples/javaee-enterprise
export JAVAEE_PORT=9085 JAVAEE_DEMO_MODE=true JAVAEE_LAB_USER=javaee_lab
printf 'Local lab database password: '; read -rs JAVAEE_LAB_PASSWORD; printf '\n'
export JAVAEE_LAB_PASSWORD
bash scenarios/00-health.sh
for request in {1..20}; do
curl -sS --max-time 10 -o /dev/null -w '%{http_code},%{time_total}\n' \
"http://127.0.0.1:${JAVAEE_PORT}/procurement/api/health" || break
done
bash scenarios/09-lab-procurement.sh

09-lab-procurement.sh 不制造并发;它通过实际 SQL 结果为后续负载提供一个业务正确性哨兵。失败样本可复现脚本内的“审批前建单”请求:它期望 HTTP 409 而非 200,且完成后检查申请能继续审批、最终只有一张订单。另一次受管事务失败可用:

为了避免把状态码 409 或客户端的连接失败偷偷过滤掉,可以增加一次小批量本机请求的逐行原始样本。下例只请求只读健康端点,不修改业务表;curl 写出 状态码,秒,失败时 curl 通常会返回非零并由 shell 提醒。并发数 4、总请求数 40 是演示命令参数,服务端 127.0.0.1、客户端在同一机器,不能直接外推生产吞吐:

1
2
3
4
5
set -o pipefail
seq 1 40 | xargs -P 4 -I '{}' sh -c '
curl -sS --max-time 10 -o /dev/null -w "%{http_code},%{time_total}\n" \
"http://127.0.0.1:${JAVAEE_PORT}/procurement/api/health"
' | sort -t, -k2,2n

xargs 可能对有输出的子进程只返回一个聚合退出码,还可能因调用端资源限制丢失请求。实际记录应当明确写出尝试 40 次、产生的样本行数是否也是 40,每行是否为 200;缺样本与 000/非 200 都不能从分位数计算时默默剔除。测试业务流量时,还需为每次 POST /lab/requests 保存生成的租户和申请 ID,然后按业务键而非计时窗口内的请求条数汇总最终订单。

数据库的只读快照可以帮助区分“容器连接池里排队”与“数据库当前正在等锁”。在实验同时另开一个终端执行;如账号权限不足,只看到自己的会话,也不能推断数据库整体没有等待:

1
2
PGPASSWORD="$JAVAEE_LAB_PASSWORD" psql -h 127.0.0.1 -U javaee_lab -d javaee_lab \
-v ON_ERROR_STOP=1 -c "SELECT state,wait_event_type,wait_event,count(*) AS sessions FROM pg_stat_activity WHERE datname='javaee_lab' GROUP BY state,wait_event_type,wait_event ORDER BY sessions DESC"

该快照不会捕捉所有毫秒级等待。若需要检查租户状态查询使用哪个索引,可以对只读 SELECT 用 EXPLAIN (ANALYZE, BUFFERS),但它会真实执行 SELECT,结果依赖数据规模、缓存与统计信息;别在共享生产库上为了教学测试执行可能很重的扫描:

1
2
PGPASSWORD="$JAVAEE_LAB_PASSWORD" psql -h 127.0.0.1 -U javaee_lab -d javaee_lab \
-v ON_ERROR_STOP=1 -c "EXPLAIN (ANALYZE, BUFFERS) SELECT id,status FROM purchase_request WHERE tenant_id='lab-capacity-inspection' AND status='APPROVED' ORDER BY id LIMIT 20"

即使看到索引扫描,也只能说明本次查询条件和数据下的计划,不能直接判定采购下单路径一定没有锁竞争。idx_purchase_request_tenant_status 真实存在于当前初始化 SQL;如查询计划或表结构与之不符,要先核对当前库的 schema 与数据统计,避免拿别的库的 EXPLAIN 为这次负载背书。

1
2
# 要求同一隔离实例仍启用 JAVAEE_DEMO_MODE=true;预期脚本自身退出 0
bash scenarios/18-lab-rollback.sh

该场景应输出注入 HTTP 500、失败租户已提交申请数为 0;这不是模拟容量耗尽。若脚本退出非零,应调查原因,不得把它一律计为后端容量错误。若负载期间制造连接池等待或 DB 故障,需要分别保存客户端超时、容器队列、数据库会话和恢复后行集合;相关对照目前 NOT_RUN。

恢复不只是等 /health 再变成 200。对于限流或资源耗尽期间提交的申请,要按测试生成的业务键重新读取:多少请求真正写入、多少建单请求已生成订单、超时客户端是否还能用同一申请取得原订单 ID、失败事务是否留下半份明细。现成 09-lab-procurement.sh 在故障后可以为一个新租户做哨兵,18-lab-rollback.sh 可以证明某类提交前异常的零行终态;两者没有遍历负载中的所有业务 ID。因此即使这两个脚本都退出零,也不能把“压测后所有业务不变量成立”写进发布结论。

容量实验需要保存两个独立的失败清单。技术失败包括连接失败、客户端取消、HTTP 5xx 和数据库错误;业务拒绝包括提前建单的 409、找不到演示租户时的 404。对不在预设请求混合中的状态码,先追到其输入和数据库终态,再决定是正常负例还是系统故障。负载发生器本身的 CPU、文件描述符和网络端口耗尽,同样可能让成功率下降;没有客户端资源曲线,不能归因到 Java 服务端。

如果需要从本机小批量样本练习统计口径,可以先把原始行保存到终端可管理的文件,再筛出真实响应 200 的时长;这仍然只适合演示计算过程。比如将上一段 seq 管道的输出保存到临时文件,并检查每一行是否包含状态和秒数:

1
2
3
4
5
6
7
8
set -e -o pipefail
samples_file=$(mktemp)
seq 1 40 | xargs -P 4 -I '{}' sh -c '
curl -sS --max-time 10 -o /dev/null -w "%{http_code},%{time_total}\n" \
"http://127.0.0.1:${JAVAEE_PORT}/procurement/api/health"
' > "$samples_file"
wc -l "$samples_file"
awk -F, '$1 != 200 { print "non-200:", $0 }' "$samples_file"

每次实验把样本文件位置与客户端、服务端版本记在同一实验记录;临时文件可能在系统清理后消失,需要长期归档的原始数据须由运行者移到有权限控制的位置。即使 wc -l 显示 40,仍应检查每行的状态码与 curl 子进程退出情况;xargs 聚合退出码不能替代每个请求的结果。真实业务压力需要实现按业务 ID 归档的负载驱动,现有健康探针无法输出申请和订单终态。

订单容量还受序列化热点影响。purchase_order.request_id 的唯一键在同一申请上竞争时可能使后到事务等待前一个事务的结果,而 purchase_request 的状态/版本条件更新可能随后返回零行。只看 SQL 平均耗时会把“批量不同申请”的写入吞吐与“同一申请重试”的锁等待混在一起。为对照第二种瓶颈,应固定申请状态 APPROVED、固定并发开始屏障、记录每次请求的开始/结束、状态码与订单 ID,再用一个新连接查询 purchase_order 和 purchase_request.version。当前脚本只做顺序重试,这套屏障及多连接驱动尚未实现;不应把一次业务脚本的 409 当作锁竞争实测。

恢复时若仍见连接池等待,应先观察队列是否持续排空、数据库连接是否归还,再逐步恢复输入速率;立刻用相同峰值重压可能在旧请求未结束时再次堆积。不要为了得到好看的 p99 丢弃取消请求:对已经发出但客户端超时的建单,重新按申请 ID 查订单,如果存在就记录为“客户端结果未知但业务已完成”;若无订单且事务已终止,再按业务重试政策处理。如果后端重启使采集端样本归零,分段报告重启前后观察窗,不能把两段时延直接拼成同一稳定分布。

业务正确性与容量对照还要使用同一种数据库初始状态。如果第一轮压测后堆积了大量已下单申请,第二轮同一 SQL 的缓存命中、索引大小和重复请求比例都可能改变;仅交换应用配置而复用被第一轮写过的库,会混淆原因。最小做法是在可丢弃库用同一迁移脚本和相同数量的测试申请重置基线,再分别保存加载过程、统计信息与索引定义。业务 ID 必须由本轮生成并隔离,绝不能为重置压测数据直接清空共用采购库。

合格的容量记录要列机器与容器配额、数据规模与索引、池大小、JVM 参数、预热、测试时长、并发或请求率、输入分布、原始每请求状态/时间、SQL 与锁等待、GC/CPU、错误和数据库终态。先固定请求率对照成功/失败曲线,再逐步寻找饱和点;每次改动仅一项。p99 必须带样本数、观察窗和超时口径,不能把 20 次健康请求估成精确 p99。执行记录字段在 容量实验合同。

压测结束后把同一业务键的“发起次数、HTTP 成功次数、最终订单数”并列存档,特别标出超时后读回已有订单的情况;这些计数不能相互替代。此项核对未在当前 40 次健康请求示例中实现。

提交压测报告前,先核对原始样本的持续时间是否覆盖完整的预热与稳态窗口;服务端日志和数据库快照要标同一时间基准,避免把停止发压之后的回落误算成峰值性能。稳态窗口不足或数据量变动时,给出不可比较的结论比公布一个无口径的 p99 更可靠。

两道练习

练习一: 100 个请求里有 5 个在客户端超时;报告只把 95 个成功的响应时间计算 p95,并说调大连接池改善了尾延迟。还缺什么?

解: 至少缺超时阈值和次数、未能发起的请求数、窗口内计划与实际请求率、池等待与 DB 活动连接数。成功样本的 p95 不代表全部业务请求;连接池调大也可能仅把等待从池迁到锁或 I/O。必须在相同负载和数据基线上比较,同时读回订单终态。

练习二: 采购脚本在负载结束后显示 ORDERED|13.75|1,能否据此确认压测期间所有申请正确且没有重复订单?

解: 不能。这是脚本随机测试租户的一条顺序业务路径。对负载生成的所有申请按租户核对状态、金额及每个 request_id 的订单计数;将超时请求按业务键读回,区分“客户端没有收到答复”和“数据库没有提交”,再比较发送数、完成数与业务行集合。

边界与官方资料

20 次 HTTP 采样以及现成的顺序业务脚本只提供实验入口,不是已运行的性能证据。本章没有报告吞吐、p95/p99 改善、压力下事务语义或恢复性能;归档采购/回滚的单次运行也不具备性能代表性。