注册
一边写订单,一边跑报表:DM9 TP+AP 混合负载本地实测【#达梦数据库 #达梦同行者征文】
专栏/技术分享/ 文章详情 /

一边写订单,一边跑报表:DM9 TP+AP 混合负载本地实测【#达梦数据库 #达梦同行者征文】

李游Leo 2026/09/04 146 3 0
摘要

981d357f2cfb40a684e9e8f6cc02e9bb.png

文中的 TPS、延迟和 SQL 耗时均来我的次实际测试

1. 测试背景:订单事务与报表查询的资源竞争

做订单系统时,有个场景挺常见的。

一边是用户不断下单,系统要查商品、扣库存、写订单;另一边是运营盯着报表,隔一会儿就要看小时销售额、热销商品和区域业绩。两边都着急,而且往往读写的是同一批数据。

我想验证的核心问题是:报表开始运行后,订单事务会不会明显变慢?

这次没有从大段概念讲起,而是在 DM9 上构造一套订单数据,分别测试订单任务、报表任务以及两者同时运行的情况。TPS 下降多少、报表耗时增加多少,都以程序日志和三轮测试结果为准。

我主要想弄清四件事:单跑基线是多少;混跑之后差多少;这个变化是不是连续三轮都能看到;最后,加个索引到底是真优化,还是看起来像优化。说白了,能用数字回答的,就尽量不靠感觉。

02tpapbusinessconflict.png

两边访问的是同一套订单数据,CPU、内存和磁盘自然也得一起分。

2. TP与AP负载的业务特征

先用订单业务解释一下。

TP 做的是一笔一笔的小事务。比如创建订单:选两件商品,查价格,扣库存,写一条订单,再写两条明细,没问题就提交,有异常就回滚。每一笔不算复杂,但是量大、频率高。所以我更关心 TPS、平均延迟、P95 和失败数。

AP 则是在已经产生的订单里找答案。比如某个小时卖了多少钱,哪些商品卖得最好,各地区订单量和客单价怎么样。单条查询次数没那么多,但它会扫更多数据,还要做连接、分组和排序。我看的主要是每条查询耗时。

这里有个测试前提:TP 和 AP 必须碰同一套表。如果订单写 A 库,报表查 B 库,两边井水不犯河水,那就测不出混合负载的影响。所以这次两类任务都直接访问 ORDERS 和 ORDER_ITEM。这样结果可能没那么漂亮,但更接近我真正想看的问题。

3. 测试环境与软件版本

性能数据必须结合运行环境理解,下面是本次测试使用的软硬件配置。

我拿到的是 DM9 x86 CentOS 7 安装包,但这次实际跑测试的环境是 Ubuntu 24.04 容器。核心程序能启动,数据库能建,JDBC 也能正常连接。不过这只能算一次隔离环境里的本地验证,不能据此说它就是正式兼容方案。真要上生产,安装包还是要和目标操作系统对应。

项目 本次实测环境
DM9 版本 DM Database Server 64 V9;03151060506-20260417-322930-20218;DB Version 0x7000D
JDBC 驱动 DmJdbcDriver11 9.1.0.26 - Production
操作系统 Ubuntu 24.04.3 LTS,Linux 6.18.35
CPU 9 vCPU,Intel Xeon Platinum 8573C,KVM
内存 15 GiB,无 Swap
磁盘 32 GiB 虚拟磁盘,系统标记 rotational=1
部署方式 单实例、容器化 KVM 本地环境
压测工具 Java 17.0.20,自编 JDBC 并发程序
数据库关键参数 端口 5236;页大小 16K;BUFFER 2048 MB;WORKER_THREADS 16;TASK_THREADS 16;其余保持初始化值

这里还有两个不太标准的地方,我也不藏着。

第一,容器里不能正常切换 UID 和修改文件属主,所以这次进程是用 root 跑的。生产环境肯定不建议这样做,按安装手册创建 dmdba 专用用户才是正常做法。
第二,不同命令会话之间是隔离的,我只能把数据库服务和压测程序放到同一个执行会话里。所以下面的数据,只代表我这次这台机器和这套运行方式。

03dm9testenvironment.png

机器不算大,所以这里更适合看变化趋势,不适合拿去做产品横向排名。

4. 数据模型、数据规模与初始化方法

模型我没有做得很大,一共四张表:APP_USER、PRODUCT、ORDERS 和 ORDER_ITEM。用户表里放地区,商品表里放价格和库存,订单表保存时间、状态和总金额,明细表记录每件商品。

这几个字段都是后面真要用的,不是为了把表做得像业务系统。ORDER_TIME 决定报表查多长时间,STATUS 用来筛选已支付订单,TOTAL_AMOUNT 和 REGION 分别用来算销售额和区域汇总。下面贴的是实际执行过的核心建表 SQL,外键也确实建了。

CREATE TABLE ORDERS ( ID BIGINT PRIMARY KEY, USER_ID BIGINT NOT NULL, ORDER_TIME TIMESTAMP NOT NULL, STATUS VARCHAR(16) NOT NULL, TOTAL_AMOUNT DECIMAL(14,2) NOT NULL, REGION VARCHAR(16) NOT NULL, CONSTRAINT FK_ORDER_USER FOREIGN KEY(USER_ID) REFERENCES APP_USER(ID) ); CREATE TABLE ORDER_ITEM ( ID BIGINT PRIMARY KEY, ORDER_ID BIGINT NOT NULL, PRODUCT_ID BIGINT NOT NULL, QUANTITY INT NOT NULL, UNIT_PRICE DECIMAL(12,2) NOT NULL, LINE_AMOUNT DECIMAL(14,2) NOT NULL, CONSTRAINT FK_ITEM_ORDER FOREIGN KEY(ORDER_ID) REFERENCES ORDERS(ID), CONSTRAINT FK_ITEM_PRODUCT FOREIGN KEY(PRODUCT_ID) REFERENCES PRODUCT(ID) ); CREATE INDEX IDX_ITEM_ORDER ON ORDER_ITEM(ORDER_ID);

04orderdatamodel.png

四张表既支撑短事务写入,也为三类分析查询提供来源。

造数据时我固定了随机种子 20260903,不然每次重跑,数据分布都变,前后对比就不太好解释。

最后生成了 5 万用户、1 万商品、30 万订单和 90 万订单明细。基础订单覆盖 180 天,最晚到 2026-09-01 23:59:59,每笔订单固定 3 条明细。PAID 状态占得比较多,是故意的。要是已支付订单只占很小一部分,报表很容易因为过滤掉大量数据而显得特别快,那就没什么意思了。

主键使用递增长整型,主要是方便测试后按 ID 找到新增数据。ORDER_ITEM 还单独给 ORDER_ID 建了索引,因为删订单之前,要先把对应明细删掉。如果这里每次都扫全表,轮次之间的清理反而会成为另一个负载。

初始化时,用户和商品每 2000 行提交一次,订单每 500 笔连同 1500 条明细提交一次。整套数据写入并收集统计信息,用了 28.908 秒。写完以后我没有马上压测,而是逐表跑 COUNT(*)。结果正好是 50000、10000、300000、900000。数量对上以后,才开始下一步。

05testdatapreparation.png

压测前逐表核对数据量,避免初始化偏差影响后续结果。

5. TP负载设计:订单写入与库存更新

TP 这部分,我按一次真实下单来设计。

每个工作线程使用一个 JDBC 连接,并关闭自动提交。一次事务随机选两个不同商品,先按商品 ID 从小到大排序,再查价格、扣库存、写订单和明细,最后提交。如果中间有一步失败,就整笔回滚。

为什么先排序?因为多个线程同时更新两个商品时,如果锁定顺序一会儿先 A 后 B,一会儿先 B 后 A,更容易互相等。统一顺序不能保证完全没有等待,但至少能减少这种人为制造的交叉锁序。下面这段就是实际逻辑,省略的只是参数绑定和计数器。

c.setAutoCommit(false); try { BigDecimal p1 = queryPrice(priceQ, product1); BigDecimal p2 = queryPrice(priceQ, product2); if (updateStock(stockU, product1) != 1 || updateStock(stockU, product2) != 1) throw new SQLException(); insertOrder(orderI, orderId, userId, p1.add(p2)); insertItem(itemI, itemId1, orderId, product1, p1); insertItem(itemI, itemId2, orderId, product2, p2); c.commit(); } catch (Exception e) { c.rollback(); }

TP 一共开 8 个线程,也就是 8 个并发连接。线程先用 CountDownLatch 等齐,然后一起开始。每组先预热 10 秒,清掉预热数据,再正式跑 60 秒。整个流程重复 3 次,最后取中位数。

我没有再套一个连接池框架。线程启动时建好连接,正式计时期间一直复用。因为这次要比较的是 TP、AP 和混跑,不是比较连接池性能。每轮结束后,新增订单会删除,库存恢复成 100 万,避免越往后表越大。

TP_ONLY 三轮 TPS 分别是 5215.530、5185.830、4515.208。第三轮明显低一些,所以不能只拿第一轮最好看的数字。我最终用中位数:TPS 5185.830,平均延迟 1.541 ms,P95 3.880 ms,三轮业务失败数都是 0。

06tpworkloadrunning.png

第三轮确实掉了一截,所以这里不挑最好成绩,只看中位数。

6. AP负载设计:关联、聚合与排序查询

AP 我开了 2 个线程。每个线程按 Q1、Q2、Q3 的顺序循环执行。计时从提交 SQL 开始,一直到 JDBC 把结果集读完,不是只记数据库返回第一行的时间。

三条 SQL 的时间窗口不一样,这是我故意保留的。真实报表一般也不会整整齐齐地只查一个范围。

-- Q1:2026-08-01 起,按小时统计订单量与销售额 SELECT TO_CHAR(ORDER_TIME,'YYYY-MM-DD HH24') HOUR_BUCKET, COUNT(*) ORDER_COUNT, SUM(TOTAL_AMOUNT) SALES FROM ORDERS WHERE STATUS='PAID' AND ORDER_TIME>=? GROUP BY TO_CHAR(ORDER_TIME,'YYYY-MM-DD HH24') ORDER BY HOUR_BUCKET; -- Q2:2026-08-25 起,统计销量最高的 20 个商品 SELECT * FROM ( SELECT I.PRODUCT_ID, SUM(I.QUANTITY) QTY, SUM(I.LINE_AMOUNT) REVENUE FROM ORDER_ITEM I JOIN ORDERS O ON O.ID=I.ORDER_ID WHERE O.STATUS='PAID' AND O.ORDER_TIME>=? GROUP BY I.PRODUCT_ID ORDER BY QTY DESC ) WHERE ROWNUM<=20; -- Q3:2026-06-01 起,按地区统计订单与客单价 SELECT REGION, COUNT(*) ORDER_COUNT, SUM(TOTAL_AMOUNT) SALES, AVG(TOTAL_AMOUNT) AVG_TICKET FROM ORDERS WHERE STATUS='PAID' AND ORDER_TIME>=? GROUP BY REGION ORDER BY SALES DESC;

Q1 查一个月的小时趋势;Q2 虽然只查 2026-08-25 之后的数据,但要连接订单和明细,再按商品聚合、排序并取前 20;Q3 查的时间最长,不过最终只有 8 个地区分组。实际跑下来,最慢的是 Q2,这也不意外,连接、聚合、排序基本都占了。

AP_ONLY 三轮总体平均耗时是 55.251、64.165、52.521 ms,中位数 55.251 ms。拆开看,Q1 是 38.231 ms,Q2 是 85.075 ms,Q3 是 42.465 ms。

这里也能看到,查询范围长不等于一定最慢。Q3 查得更久,但分组少;Q2 时间范围短一些,却要穿过明细表。因此后面看执行计划时,我没有只盯着总代价,还会看它是不是回表、聚合前经过多少数据。

07apqueryrunning.png

Q2 查的天数不算最长,但连接、聚合、排序一个不少,最后它最慢。

7. 基准结果:TP、AP与混合负载对比

三组测试使用同一个数据库实例和同一份基础数据,以减少环境差异对结果的干扰。

为了让三组结果能放到一起比,我尽量不动其他条件:同一个 DM9 实例,同一份 30 万订单基线,同样的线程代码;每组预热 10 秒,正式运行 60 秒,连续跑 3 次,最后取中位数。有 TP 的场景,每轮结束后都会把新增数据清掉并恢复库存。

CPU 是每秒读取一次 /proc/stat。每轮先取采样峰值,三轮再取中位数。所以表里的 CPU 不是 60 秒平均值,这个口径要先说清楚。

参数 TP_ONLY AP_ONLY MIXED / 优化后
TP 线程 8 0 8
AP 线程 0 2 2
预热 / 正式运行 10 s / 60 s 10 s / 60 s 10 s / 60 s
重复次数 3 3 3
汇总方法 中位数 中位数 中位数

把 TP 和 AP 同时运行以后,程序没有出现业务错误,订单持续提交,三条报表也在循环执行。CPU 峰值中位数从 68.791% 升至 83.462%,说明资源竞争已经出现,具体影响仍需结合 TPS 和查询耗时判断。

08mixedworkloadmonitoring.png

表面上两边都在正常跑,真正的影响要看 TPS 和延迟。

测试场景 TP TPS TP平均延迟 TP P95延迟 AP平均耗时 CPU峰值中位数 失败数
TP单独运行 5185.830 1.541 ms 3.880 ms 68.791% 0
AP单独运行 55.251 ms 45.333% 0
TP+AP混合运行 3872.585 2.065 ms 4.893 ms 127.605 ms 83.462% 0
覆盖索引后混合运行 3803.020 2.102 ms 5.027 ms 90.696 ms 85.028% 0

先看 TP。TPS 从 5185.830 掉到 3872.585,少了 25.32%。平均延迟从 1.541 ms 增加到 2.065 ms,P95 从 3.880 ms 增加到 4.893 ms。换成绝对值,就是每秒少完成 1313.245 笔事务,P95 多了 1.013 ms。

这不是某一轮突然抖了一下。MIXED 三轮 TPS 分别是 4166.097、3872.585、3696.472,全部低于 TP_ONLY 三轮里最低的 4515.208。

AP 受到的影响更明显。总体平均耗时从 55.251 ms 涨到 127.605 ms,增加 130.96%。MIXED 三轮是 127.605、124.801、149.762 ms,也全部高于 AP_ONLY 最慢一轮的 64.165 ms。Q2 混跑后到了 209.975 ms,还是三条 SQL 里最慢的。

CPU 峰值中位数同时多了 14.671 个百分点,这和吞吐下降、查询变慢是对得上的。不过我这次没有采集 DM9 等待事件,也没有单独记录磁盘延迟,所以不能拍脑袋说“一定就是 CPU”或者“一定就是 I/O”。能确定的是,两类负载放在一起以后,双方都受到了影响。

中途还踩了一个挺真实的坑。

第一次跑到 MIXED 第三轮以后,业务事务本身没失败,出错的反而是清理程序。它想在一个事务里删掉 253670 笔新增订单和 507340 条明细,DM9 返回了 “Out of undo segment space”。

这不是下单报错,而是我写的测试清理逻辑太粗暴。我把它改成每 10000 个订单 ID 提交一批,再跑一次,5.979 秒就清完了。为了不让这次异常影响后面的数据,原第三轮没有放进汇总,而是在基线恢复以后重新跑了一轮。这个坑也提醒我:压测脚本不只是“把压力打上去”,数据恢复逻辑同样要认真写。

09threescenariosresults.png

三轮方向都一样,所以这次的下降不是只靠一轮数据得出的。

8. 索引优化与执行计划变化

看到混跑变慢以后,我最开始的想法也很直接:Q1 经常按状态和订单时间筛选,那就先给这两列加联合索引。没有先动内存参数,也没有一上来就改并发数。

CREATE INDEX IDX_ORDERS_AP ON ORDERS(STATUS, ORDER_TIME); CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'ORDERS'); CALL DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'ORDER_ITEM');

普通索引的结果和我原先的预期不一致。加完以后,TPS 是 3886.169,只比原混跑多 0.35%;P95 变成 5.196 ms;AP 平均耗时不但没降,反而从 127.605 ms 涨到了 213.111 ms,慢了 67.01%。

我又看了一遍执行计划。没加 AP 索引时,Q1 计划代价是 40,主要走 CSCN2 后过滤;加普通索引后,代价变成 38,也确实出现了 SSEK2 IDX_ORDERS_AP,但后面还跟着 BLKUP2。

问题就在这里。PAID 占比不低,Q1 除了状态和时间,还要读取 TOTAL_AMOUNT 并按小时分组。索引命中以后,大量记录还是得回表。普通索引三轮 AP 平均耗时分别是 216.956、213.111、211.491 ms,三轮都比原 MIXED 慢,不像是偶发抖动。

所以,加了索引不等于完成优化。这个失败结果我觉得反而挺有价值,就保留下来了。

第二次我把 Q1、Q3 需要的列一起放进索引,做成覆盖索引:

CREATE INDEX IDX_ORDERS_AP_COVER ON ORDERS(STATUS, ORDER_TIME, ID, TOTAL_AMOUNT, REGION);

重新收集 ORDERS、ORDER_ITEM 的统计信息后,Q1 计划代价降到了 5,计划里只剩 SSEK2 IDX_ORDERS_AP_COVER,BLKUP2 不见了。

这次复测终于有变化。AP 平均耗时从 127.605 ms 降到 90.696 ms,下降 28.92%;Q1 从 89.946 降到 40.543 ms;Q3 从 82.938 降到 46.822 ms;Q2 也从 209.975 降到 184.523 ms。三轮 AP 平均耗时分别是 81.783、90.696、103.971 ms,每一轮都比原 MIXED 对应范围低。

覆盖索引也带来了额外成本。TP TPS 变成 3803.020,比原 MIXED 又低了 1.80%;P95 增加 2.74%,CPU 峰值中位数升到 85.028%。原因也不难理解:报表少回表了,但每写一笔订单,数据库还要维护这条更宽的索引。

所以我更愿意把它叫作一次取舍:牺牲一点 TP 吞吐,换来更明显的 AP 改善。至于 BUFFER、线程数这些参数,我这次没有实际调过,就不把它们包装成优化成果了。

10optimizationcomparison.png

覆盖索引对报表有效,但写订单也多了一份索引维护成本。

9. 测试结论与适用范围

综合三组基准数据和两轮索引调整,可以得到以下结论。

在 9 vCPU、15 GiB 内存和 30 万基础订单的环境里,DM9 能同时跑 8 个订单线程和 2 个报表线程,纳入统计的业务失败数是 0。但“能一起跑”不等于“互不影响”。混跑后,TP TPS 下降 25.32%,AP 平均耗时增加 130.96%。

第一次加普通联合索引没有解决问题,反而让 AP 更慢;换成覆盖索引后,AP 平均耗时下降 28.92%,代价是 TP 吞吐又损失大约 1.80%。这个结果不算完美,但我觉得比写一句“加索引后性能大幅提升”更真实。

当然,这组数字不能拿去代表 DM9 的生产性能上限。机器是虚拟环境,测试只有 10 个业务连接,每轮也只跑了 60 秒。下载包对应 CentOS 7,实际运行环境却是 Ubuntu 24.04;能启动并完成测试,只能当作本地实验记录,不能当成正式兼容承诺。

如果数据变成数亿行、连接变成几百个,锁竞争、日志、缓存和执行计划很可能都会变。真要做上线前预演,我还会补下面几项:

  • 使用官方支持的操作系统、专用数据库用户和独立数据盘;
  • 将正式运行延长到数小时,同时记录 CPU、内存、I/O、等待与日志刷盘;
  • 按真实峰值逐级增加 TP、AP 并发,而不是只测一个线程组合;
  • 分别验证冷缓存、热缓存、不同数据倾斜和更长时间窗口;
  • 对索引收益与写放大、空间占用、备份恢复和故障切换一起验收。
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服