欢迎来到我的博客,代码的世界里,每一行都是一个故事


在这里插入图片描述

🎏:你只管努力,剩下的交给时间

🏠 :小破站

摘要:本文记录了在 CentOS 7.6 / 4核 / 8GB 内存的 Docker 单节点环境中,对 KWDB 3.1.0 进行的一次读写性能测试。没有使用任何第三方 benchmark 工具,全程用 Python + Shell 脚本模拟批量写入,用 KWDB 自带的查询计时测量查询响应。测试内容包括单条写入延迟、不同批次大小的写入吞吐量、四种典型查询的响应时间,以及连续30轮写入的稳定性。文章也记录了测试过程中遇到的时间戳格式校验问题,以真实数据说话。


为什么不用 benchmark 工具

做 KWDB 的性能测试时,首先碰到一个选择:用 sysbenchFIO 还是 KWDB 内置的 workload 工具?

考虑了一下,决定都不用。benchmark 工具的跑分数字太"漂亮",参数调一调就能高出一大截,对实际工程参考意义有限。我更想知道的是:在接近真实业务场景的情况下,KWDB 能跑到什么水平。

所以这次的方式是:Python 脚本生成传感器测试数据,不同批次大小循环写入,每种重复5次取平均,然后用 SQL 客户端直接测查询延迟。这是 KWDB 在正常使用条件下的表现,不经过任何调优。

测试环境:

  • 操作系统:CentOS 7.6,内核 3.10
  • CPU:4核,内存:8GB
  • KWDB:3.1.0,Docker 单节点,无副本
  • 数据目录:宿主机 /data/kwdb 挂载,宿主机系统盘(未单独挂SSD)

建立测试数据库

重新建一套独立的测试数据库,不复用其他场景的库。

CREATE TS DATABASE perf_ts;
CREATE DATABASE perf_mgmt;

两个库分别耗时:

  • CREATE TS DATABASE perf_ts6.044ms
  • CREATE DATABASE perf_mgmt5.134ms

然后在 perf_ts 里建时序表 perf_metrics,6个指标列,2个 TAG 列:

USE perf_ts;
CREATE TABLE perf_metrics (
  k_timestamp   TIMESTAMPTZ NOT NULL,
  temp_c        FLOAT,
  pressure_bar  FLOAT,
  vibration_g   FLOAT,
  current_a     FLOAT,
  voltage_v     FLOAT,
  power_kw      FLOAT
) TAGS (
  device_id   INT NOT NULL,
  line_id     INT NOT NULL
) PRIMARY TAGS(device_id)
ACTIVETIME 90 DAY;

时序表建表耗时 26.690ms,比关系表(6.874ms)慢了约4倍。ACTIVETIME 90 DAY 是 KWDB 时序表的特有语法,指定热存储时间窗口,超出窗口的数据会被自动转储——这也是 KWDB 区别于普通关系数据库的地方之一。

建库建表截图
时序表建表截图
关系表建表截图


单条写入延迟:均值约 1.17ms

先测最基础的数字:不带批量,直接在 SQL 客户端里单条 INSERT 三次:

INSERT INTO perf_metrics(k_timestamp,device_id,line_id,temp_c,pressure_bar,vibration_g,current_a,voltage_v,power_kw)
VALUES ('2026-05-01 00:00:01+08',1001,1,72.5,1.82,2.31,5.5,220.3,18.5);

INSERT INTO perf_metrics(k_timestamp,device_id,line_id,temp_c,pressure_bar,vibration_g,current_a,voltage_v,power_kw)
VALUES ('2026-05-01 00:00:02+08',1002,1,68.4,1.75,2.10,4.8,219.8,16.2);

INSERT INTO perf_metrics(k_timestamp,device_id,line_id,temp_c,pressure_bar,vibration_g,current_a,voltage_v,power_kw)
VALUES ('2026-05-01 00:00:03+08',1003,2,75.0,2.10,3.12,6.2,221.0,22.8);

三次实测结果:

INSERT 1  Time: 1.252041ms
INSERT 1  Time: 1.156214ms
INSERT 1  Time: 1.089314ms

三次均值约 1.165ms,计算方式:(1.252 + 1.156 + 1.089) / 3 = 1.165ms。这是 KWDB SQL 客户端在容器内直接执行的延迟,包含了 SQL 解析、写入时序引擎、WAL 落盘的全部链路。

单条写入截图


批次大小对比:吞吐量差距 929 倍

单条 1.17ms 听起来快,但工业 IoT 场景下多台设备同时上报,怎么组织写入请求对吞吐量的影响非常大。

用 Python 脚本测了 4 种批次大小,每种重复 5 次取平均。关键代码逻辑:

for batch in [1, 10, 100, 1000]:
    times = []
    for _ in range(REPEAT):
        rows = [make_row(idx + i) for i in range(batch)]
        # 每次 subprocess 调用 docker exec 执行一次 INSERT
        times.append(run_insert(rows))
    avg = sum(times) / len(times)

结果(每种批次大小都重复5次,取均值):

批次大小 每次请求均耗时 吞吐量 单行耗时
1行/次 239.22ms 4 行/秒 239,222μs
10行/次 239.34ms 42 行/秒 23,934μs
100行/次 236.86ms 422 行/秒 2,369μs
1000行/次 269.05ms 3,717 行/秒 269μs

这组数据揭示的规律: 1行和1000行的单次调用耗时相差不多,都在 240~270ms 左右。这段时间不是 KWDB 写入导致的,而是每次 docker exec 启动子进程 + 建立网络连接的固定开销(约 235ms),跟写多少行无关。

真正体现差距的是吞吐量:同样一次调用的固定成本,携带 1000 行数据,吞吐量从 4行/秒 提升到 3717行/秒,差距 929倍。这是"每次连接成本固定"的底层逻辑决定的,在实际 IoT 接入层做数据聚合批量写入,比逐条写入效率上有质的差别。

批量对比截图


写入约 10 万行数据(以及一次真实的踩坑)

脚本分批写入,每批 1000 行,计划写 100 批共 10 万行。结果遇到了一个实际问题。

脚本里时间戳的生成方式是按秒数递增:第0秒是 00:00:00,第86400秒就变成了 24:00:00——小时数超过了 23。KWDB 对时间戳格式有严格校验,这些批次被完整拒绝,没有静默写入或截断处理。

最终入库的有效数据:91,555 行(86个完整批次 × 1000行,加上前面批次对比测试写入的 5,555 行)。

这个行为值得记录一下:KWDB 选择了"拒绝整批"而不是"忽略无效行继续写"。对于时序数据来说,这是合理的做法——时间戳错乱会导致时间范围查询结果不可信。后来稳定性测试额外写入了 15,000 行,测试结束时总量达 106,558 行


查询性能:四种典型场景

说明: KWDB 不需要手动开启计时,每条 SQL 执行后自动打印 Time: xxx ms,以下所有耗时均来自截图中的实测数据。


COUNT 总行数

SELECT COUNT(*) AS total_rows FROM perf_metrics;
-- 结果:106558(1 row)
-- Time: 3.991612ms

3.99ms 完成 106,558 行的全表计数。


Q1:时间范围过滤聚合

SELECT COUNT(*), AVG(temp_c), MAX(temp_c), MIN(temp_c)
FROM perf_metrics
WHERE k_timestamp > '2026-03-01 22:00:00+08';
count | avg                | max | min
------+--------------------+-----+-----
12354 | 74.4880200744698   |  89 |  60
(1 row)
Time: 7.089083ms

从 106K 行里按时间范围筛出 12,354 行并同时计算 AVG/MAX/MIN,耗时 7.089ms。时序表的 k_timestamp 作为主键,时间范围过滤直接走主键定位,不是全扫。


Q2:全表 GROUP BY 聚合

SELECT device_id, COUNT(*) AS data_points, AVG(temp_c), MAX(pressure_bar)
FROM perf_metrics
GROUP BY device_id
ORDER BY device_id;
device_id | data_points | avg_temp          | max_press
----------+-------------+-------------------+----------
     1001 |       18311 | 72.49822511058926 |       1.9
     1002 |       18311 | 73.49822511058926 |       1.9
     1003 |       18311 | 74.49822511058926 |       1.9
     1004 |       18311 | 75.49822511058926 |       1.9
     1005 |       18311 | 76.49822511058926 |       1.9
(5 rows)
Time: 4.842201ms

5台设备,各 18,311 条,全表聚合耗时 4.842ms

18311 × 5 = 91,555,正好是稳定性测试开始前的总行数,证明数据分布完全均匀,没有倾斜。


Q3:多条件复合过滤

SELECT k_timestamp, device_id, temp_c, vibration_g
FROM perf_metrics
WHERE k_timestamp BETWEEN '2026-03-01 10:00:00+08' AND '2026-03-01 12:00:00+08'
  AND temp_c > 86
ORDER BY temp_c DESC LIMIT 10;
k_timestamp                | device_id | temp_c | vibration_g
---------------------------+-----------+--------+-------------
2026-03-01 02:04:59+00:00  |      1005 |     89 |         2.5
2026-03-01 02:04:29+00:00  |      1005 |     89 |         2.5
...(共10行,均为 device 1005)
Time: 5.824193ms

2小时时间窗口 + temp_c > 86 双条件过滤,5.824ms。结果全部来自 device 1005,因为测试数据生成时 device_id 越大,温度基线越高(1001基线72°, 1005基线76°),超过86°的自然都在1005。


Q4:跨模 JOIN 聚合

SELECT d.device_name, COUNT(*) AS total_points,
       ROUND(AVG(m.temp_c), 2) AS avg_temp,
       MAX(m.temp_c) AS peak_temp,
       ROUND(AVG(m.power_kw), 2) AS avg_power
FROM perf_ts.perf_metrics m
JOIN perf_mgmt.perf_device d ON m.device_id = d.device_id
GROUP BY d.device_name
ORDER BY avg_temp DESC;
device_name  | total_points | avg_temp | peak_temp | avg_power
-------------+--------------+----------+-----------+----------
传感器-1005   |        18311 |     76.5 |        89 |     18.77
传感器-1004   |        18311 |     75.5 |        88 |     18.71
传感器-1003   |        18311 |     74.5 |        87 |     18.65
传感器-1002   |        18311 |     73.5 |        86 |     18.59
传感器-1001   |        18311 |     72.5 |        85 |     18.53
(5 rows)
Time: 41.811912ms

跨两个数据库(时序引擎的 perf_ts.perf_metrics + 关系引擎的 perf_mgmt.perf_device),JOIN 后全量聚合,41.812ms

与 Q2 的纯时序聚合(4.84ms)相比,跨模 JOIN 慢了约 8.6 倍。这个额外开销来自跨引擎的数据交换,对于这次查询(10万行时序 JOIN 5行关系表)来说 41ms 仍在实用范围内。

查询测试截图
跨模查询截图


稳定性测试:30轮连续写入

稳定性测试的关注点不是某一次的峰值,而是连续运行期间延迟是否会逐渐漂移。30轮,每轮写 500 行,每轮间隔 5 秒,总时间约4分钟。

30轮耗时范围:235ms ~ 309ms,第1轮 309ms 是最大值,从第二轮起维持在 235~306ms 区间,没有随时间增长的趋势

从时间戳也能看出来:16:20:12 开始,16:24:14 结束,每轮约 9 秒(5秒等待 + 4分钟写入),节奏均匀。对于通过 shell 脚本 + docker exec 方式调用的测试,这个抖动是正常水平。

稳定性测试截图


资源消耗

测试后查了一下容器资源占用(测试完毕、无额外负载状态下):

NAME   CPU%    MEM USAGE / LIMIT     MEM%
kwdb   1.65%   634.6MiB / 7.64GiB   8.11%

CPU 1.65%,内存 634.6MiB(约8.11%)

这是静止状态的数字(测试已结束,没有并发查询),不代表峰值。但单节点跑完约10万行写入测试之后,内存只用了 634MB,对于一台 8GB 内存的机器来说,还有大量余量,基本不影响同台宿主机上的其他服务。

资源占用截图


数据连续性验证

最后跑了一次按小时分桶的连续性检查,看数据有没有缺行或重复:

SELECT DATE_TRUNC('hour', k_timestamp) AS hour_bucket, COUNT(*) AS rows_in_hour
FROM perf_metrics
GROUP BY hour_bucket ORDER BY hour_bucket LIMIT 10;
hour_bucket              | rows_in_hour
-------------------------+-------------
2026-02-28 16:00:00+00:00 |         3600
2026-02-28 17:00:00+00:00 |         3600
2026-02-28 18:00:00+00:00 |         3600
...(后续各小时均为3600)
(10 rows)
Time: 51.552382ms

每个小时桶正好 3600 行(5台设备轮流写入,每秒1行,一小时写3600秒 = 3600行),分布完全均匀,没有缺失。这条查询耗时 51.55ms,因为 DATE_TRUNC 要对每一行时间戳做函数运算,比直接聚合慢,这是预期内的行为。

最终验证截图


测试结果汇总

测试项目 实测结果
单条写入延迟(SQL直连) 均值约 1.165ms(三次实测均值)
批次写入吞吐(1行/次) 4 行/秒
批次写入吞吐(1000行/次) 3,717 行/秒(提升 929×)
COUNT 全表 106,558 行 3.991ms
时间范围过滤+聚合 7.089ms(过滤出12,354行)
全量 GROUP BY 聚合(10万行) 4.842ms
多条件复合过滤 5.824ms
跨模 JOIN 全量聚合 41.812ms
稳定性(30轮每轮500行) 235~309ms,无漂移趋势
资源占用(测试后空闲) 634.6MiB 内存,1.65% CPU

几点实际感受

关于批次写入: 批次大小从1增到1000,吞吐量差距接近1000倍,但单次调用耗时几乎不变。原因是外部调用链路(进程启动 + 网络连接)是固定开销,KWDB 的实际写入时间只是其中一部分。实际应用中客户端必须做连接复用和批量攒发,单条写入的模式在高频场景下会把大部分时间耗在连接上而不是 DB 操作上。

关于聚合性能: 4.842ms 完成 10 万行 GROUP BY 是这次测试最直观的数字。这背后是时序列式存储的基本特性——同一列的数据在存储上相邻,聚合时不需要读取整行,只扫目标列。这种存储结构特别适合传感器类数据:写入频繁、每次查询只关心少数几个指标字段的聚合。

关于时间戳校验: KWDB 拒绝了小时数超出 0~23 范围的时间戳,不做降级处理。导致原定10万行最终只入库 91,555 行。这对我们来说是一次踩坑,但从数据库的角度来说,强校验比静默写入更安全——时序数据的时间线一旦错乱,后续所有基于时间的查询结果都会受影响。

关于跨模查询: Q4 的 41ms 比纯时序聚合慢8.6倍,但考虑到这是跨两套存储引擎的 JOIN,41ms 在单节点无调优的条件下是可以接受的查询延迟。如果业务上需要频繁做这类关联查询,做好查询计划分析和必要的缓存策略会更有帮助。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐