基准测试工具之你不知道的两三事 | 9.如何模拟乱序写入
顺序写入通常是时序数据库最容易处理的路径:同一条序列持续写入递增的时间戳。网络抖动、异步链路或边缘缓存则可能让较早的时间戳晚于新数据抵达。
IoT Benchmark 的“乱序”并不是把一批记录随机打散后直接发送。阅读它的实现会发现:工具先根据一个stepOffset生成记录,再在生成时间戳时按模式改写这个 offset。理解这条链路,才能知道OUT_OF_ORDER_RATIO、POISSON和BATCH实际改变了什么。
1. 什么才算乱序
以一条设备序列为例:数据库已经收到10:06的数据,随后又收到10:03的数据;后到数据的时间戳更早,这才是乱序。
反过来,设备在 10:06 才上传自己在 10:01 至 10:05 产生的数据,但该序列自 10:00 后没有写入任何更新数据,这批数据虽然迟到,对这一序列仍可以是顺序写入。
所以,测试前要先写清楚:哪些序列发生乱序、迟到点占多少、以及迟到点相对同序列最新点落后多少。不能只写“开启了乱序”。
2. 代码中的一条记录怎样获得时间戳
生成写入负载时,SingletonWorkDataWorkLoad先用全局递增的insertLoop为设备批次分配位置,再为 batch 内每条记录计算原始stepOffset:
stepOffset = (curLoop / DEVICE_NUMBER) * BATCH_SIZE_PER_WRITE + batchOffset随后,代码分别做两件事:
- 调用
getCurrentTimestamp(stepOffset)生成时间戳; - 调用
generateOneRow(..., stepOffset)生成测点值。
当使用等长时间戳(IS_REGULAR_FREQUENCY=true)时,时间戳可概括为:
timestamp = START_TIMESTAMP × precision + POINT_STEP × stepOffset' + POINT_STEP其中stepOffset'是可能被乱序模式改写后的 offset。若关闭等长时间戳,最后一个POINT_STEP会替换为一个随机的[0, POINT_STEP)内偏移。
这张图揭示了一个容易忽略的事实:当前实现只改时间戳,不改生成测点值所使用的原始 offset。因而它模拟的是“带有当前生成值的记录,以较早时间戳抵达”的写入压力,而不是从历史缓存中原样重放一条完整旧记录。若业务特别依赖“值与事件发生时刻”的严格对应关系,应采用真实数据集模式或自行构造数据,而不能把该生成模式直接当作历史回放。
3.POISSON:逐点决定是否后移
在POISSON模式下,每生成一条记录,代码先以OUT_OF_ORDER_RATIO为概率抽样:
if random() < OUT_OF_ORDER_RATIO: stepOffset' = stepOffset - poissonDelta() else: stepOffset' = stepOffsetpoissonDelta()由PoissonDistribution产生。LAMBDA和MAX_K用来控制该离散分布的取值;delta 的单位是step,转换成时间差后约为delta × POINT_STEP。
因此,POISSON适合表达“多数数据按原节奏到达,少数点随机变旧”的形态。这里的OUT_OF_ORDER_RATIO是每条记录被选中改写 offset 的概率,实际采样比例会随运行而波动;它不是服务端最终统计到的、精确不变的乱序事件比例。
还应注意两个边界:
| 边界 | 对测试的影响 |
|---|---|
| delta 大于当前 stepOffset | 代码不会把 offset 截断到 0,早期记录可能被推到基准开始时间之前。 |
| 多客户端并发 | 工具决定的是记录的时间戳;同一序列请求的实际抵达顺序仍会受线程调度和网络影响。 |
所以,POISSON的结果应通过抽样写入数据或服务端记录验证,而不应只凭配置值认定“已经产生了 x% 的乱序”。
4.BATCH:对 offset 做循环平移
BATCH不是“对每一条记录按概率抽一次”。它先计算:
OUT_OF_ORDER_BASE = floor(LOOP × OUT_OF_ORDER_RATIO) stepOffset' = (stepOffset + OUT_OF_ORDER_BASE) mod LOOP同时,SingletonWorkDataWorkLoad会把全局insertLoop的起点设置为同一个OUT_OF_ORDER_BASE。也就是说,这一模式通过确定性的索引平移与回绕,生成一段成批出现的时间戳重排;它不是随机地从 batch 中挑出若干点。
| 模式 | 代码层面的机制 | 更适合观察的压力形态 |
|---|---|---|
POISSON | 每条记录独立地按概率回退若干 step | 零散、持续出现的迟到点。 |
BATCH | 使用floor(LOOP × ratio)计算平移基数,再按LOOP取模 | 成段时间戳回绕带来的批式重排。 |
这里有一个很重要的细节:在BATCH模式中,OUT_OF_ORDER_RATIO是计算平移基数的输入,而不像POISSON那样直接作为逐条抽样概率。再加上取模的模数是LOOP,实际写入的时间戳序列与“乱序点恰好占 ratio”并不应简单画等号。使用BATCH时尤其要导出或抽样检查时间戳序列。
5. 两个容易让乱序失效或变复杂的开关
第一,IS_RECORD_CURRENT_REALLY_TIME=true时,SingletonWorkDataWorkLoad会直接以System.currentTimeMillis()作为记录时间戳,并不调用getCurrentTimestamp(stepOffset)。因此,基于IS_OUT_OF_ORDER的这套时间戳改写逻辑不会生效。要测试工具生成的乱序,应该保持该开关为false。
第二,IS_REGULAR_FREQUENCY=false会额外给每个时间戳加入随机的子POINT_STEP偏移。它可以用于不规则采样数据,但会把“采样不规则”与“乱序”叠加。第一次定位乱序代价时,建议使用IS_REGULAR_FREQUENCY=true,先把两类变量分开。
6. 一套更可靠的配置与验证流程
先固定数据规模、并发、批大小、写入间隔、POINT_STEP和数据库版本,仅改变乱序相关配置。
# 先做顺序写基线 IS_RECORD_CURRENT_REALLY_TIME=false IS_REGULAR_FREQUENCY=true IS_OUT_OF_ORDER=false # 再开启乱序;其余负载参数保持不变 IS_OUT_OF_ORDER=true OUT_OF_ORDER_MODE=POISSON OUT_OF_ORDER_RATIO=<从较低比例开始> LAMBDA=<按目标迟到跨度设置> MAX_K=<按目标迟到跨度设置>推荐按以下顺序进行:
- 跑顺序写基线,记录吞吐、P99、失败数和服务端资源。
- 选择
POISSON,从较低OUT_OF_ORDER_RATIO开始扫描;同时抽样确认迟到时间差是否接近预期。 - 在相同负载下切换
BATCH,检查回绕后的时间戳序列,而不是只看配置文件。 - 只有当两种模式的时间戳形态都被验证后,再比较吞吐、P99、失败、CPU、内存、磁盘和查询结果。
对每轮测试,至少保存配置快照、版本号、原始日志、抽样时间戳以及数据核验结果。乱序写入的吞吐高,不代表时间戳、数值和写入数量已经正确落库。
7. 三个常见误区
第一,把OUT_OF_ORDER_RATIO当作所有模式下的精确乱序比例。在POISSON中它是逐点抽样概率;在BATCH中它参与计算平移基数。两者的代码语义不同。
第二,把BATCH直接理解为业务补传。它实现的是 batch 式时间戳重排,不会描述设备离线时长、缓存积压量或恢复过程。
第三,开启真实当前时间后还期待乱序生效。IS_RECORD_CURRENT_REALLY_TIME=true绕过了乱序时间戳生成路径。
8. 小结
IoT Benchmark 的乱序逻辑可以概括为:先由stepOffset生成记录,再在生成时间戳时按POISSON或BATCH改写该 offset。POISSON是逐点概率抽样加随机回退;BATCH是按LOOP的确定性循环平移。
理解这层实现后,测试的重点就很清楚:固定其他负载变量,验证真实生成的时间戳形态,并把“配置参数”与“实际落到数据库的乱序序列”分开记录。这样得到的结果,才真正能说明系统面对哪一种乱序压力会付出多大代价。
系列文章
- 第一篇:数据库基准测试工具是什么
- 第二篇:从一次时序数据库写入测试开始
- 第三篇:如何模拟线上写入负载
- 第四篇:如何读懂测试结果波动
- 第五篇:什么是读写混合负载
- 第六篇:如何模拟线上读写混合负载
- 第七篇:怎样做一场公平的性能对比
- 第八篇:怎样找到并发与批大小的合理区间
