137 lines
5.1 KiB
Python
137 lines
5.1 KiB
Python
# -*- coding: utf-8 -*-
|
||||
|
|
"""采集 / 总线 NFR 压测(Issue #87,对应 PRD 第 9 章)。
|
|||
|
|
|
|||
|
|
验证目标(PRD 5.1 / 5.2 / 9 章):
|
|||
|
|
- 采集 P99 延迟 ≤ **1.8s**(600 点位 1Hz 基线);
|
|||
|
|
- 丢失率 ≤ **0.02%**;
|
|||
|
|
- 可用性 ≥ **99.8%**;
|
|||
|
|
- 批量写入幂等去重(不丢不重)。
|
|||
|
|
|
|||
|
|
用合成点位字典 + 模拟驱动做规模化压测,输出 SLA 达标结论。
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
# 引导加载内核模块
|
|||
|
|
import tests.perf._bootstrap # noqa: F401
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import tempfile
|
|||
|
|
import time
|
|||
|
|
import unittest
|
|||
|
|
|
|||
|
|
from collector.engine import CollectorEngine
|
|||
|
|
from collector.metrics import HealthMetrics
|
|||
|
|
from collector.spool import SpoolStore
|
|||
|
|
from drivers import SimulatorDriver
|
|||
|
|
from point_dict.loader import Point, PointDict
|
|||
|
|
|
|||
|
|
from data_bus import BatchWriter, MemorySink
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _make_point_dict(n_points: int = 600) -> PointDict:
|
|||
|
|
"""合成 600 点位(PRD 5.1 基线:600 点位 1Hz),分布到 12 台设备。"""
|
|||
|
|
points = []
|
|||
|
|
n_devices = max(1, n_points // 50) # 每台设备约 50 点位
|
|||
|
|
for i in range(n_points):
|
|||
|
|
dev = i % n_devices
|
|||
|
|
points.append(Point(
|
|||
|
|
device_id=f"CLF-{dev+1:02d}",
|
|||
|
|
point_id=f"CLF-{dev+1:02d}.P{i:04d}",
|
|||
|
|
name=f"测点{i}",
|
|||
|
|
unit="℃",
|
|||
|
|
data_type="float",
|
|||
|
|
sample_rate=1000,
|
|||
|
|
quality_code=True,
|
|||
|
|
row_number=i + 2,
|
|||
|
|
))
|
|||
|
|
return PointDict(points)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class CollectionLatencyNFRTest(unittest.TestCase):
|
|||
|
|
"""采集 P99 延迟 / 可用性压测。"""
|
|||
|
|
|
|||
|
|
def setUp(self) -> None:
|
|||
|
|
self._tmp = tempfile.mkdtemp(prefix="iaop_perf_")
|
|||
|
|
self.point_dict = _make_point_dict(600)
|
|||
|
|
self.spool = SpoolStore(spool_dir=self._tmp, cache_limit_bytes=64 * 1024 * 1024)
|
|||
|
|
self.metrics = HealthMetrics()
|
|||
|
|
self.engine = CollectorEngine(
|
|||
|
|
point_dict=self.point_dict,
|
|||
|
|
driver_slots=[("simulator", SimulatorDriver(), [])],
|
|||
|
|
spool=self.spool, metrics=self.metrics,
|
|||
|
|
interval_ms=1000, max_pending=500_000,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_p99_latency_under_threshold(self) -> None:
|
|||
|
|
"""压测:30 轮采集(600 点位/轮),P99 ≤ 1.8s。
|
|||
|
|
|
|||
|
|
规模说明:PRD 5.1 基线为「600 点位 1Hz」。本用例用 30 轮(30 秒等价)
|
|||
|
|
压测单轮批处理 P99,足以验证调度引擎 + 模拟驱动 + spool 落盘链路
|
|||
|
|
在目标点位规模下的时延达标性;CI 中保持可重复执行的耗时(< 30s)。
|
|||
|
|
"""
|
|||
|
|
rounds = 30
|
|||
|
|
for _ in range(rounds):
|
|||
|
|
self.engine.collect_once()
|
|||
|
|
|
|||
|
|
p99 = self.metrics.p99_latency()
|
|||
|
|
self.assertLessEqual(p99, 1.8,
|
|||
|
|
f"采集 P99 {p99:.4f}s 超过 1.8s 阈值(PRD 5.1)")
|
|||
|
|
# 可用性 ≥ 99.8%
|
|||
|
|
self.assertGreaterEqual(self.metrics.availability, 0.998,
|
|||
|
|
f"可用性 {self.metrics.availability:.4%} < 99.8%")
|
|||
|
|
# 丢失率 ≤ 0.02%
|
|||
|
|
self.assertLessEqual(self.metrics.loss_rate, 0.0002,
|
|||
|
|
f"丢失率 {self.metrics.loss_rate:.4%} > 0.02%")
|
|||
|
|
self.assertTrue(self.metrics.meets_sla())
|
|||
|
|
print(f"\n[采集压测] {rounds} 轮 × {len(self.point_dict)} 点位,"
|
|||
|
|
f"P99={p99*1000:.1f}ms,可用性={self.metrics.availability:.4%},"
|
|||
|
|
f"丢失率={self.metrics.loss_rate:.6%}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class BusLosslessnessNFRTest(unittest.TestCase):
|
|||
|
|
"""总线批量写入「不丢不重」压测。"""
|
|||
|
|
|
|||
|
|
def setUp(self) -> None:
|
|||
|
|
self._tmp = tempfile.mkdtemp(prefix="iaop_bus_")
|
|||
|
|
self.point_dict = _make_point_dict(600)
|
|||
|
|
self.spool = SpoolStore(spool_dir=self._tmp, cache_limit_bytes=64 * 1024 * 1024)
|
|||
|
|
self.metrics = HealthMetrics()
|
|||
|
|
self.engine = CollectorEngine(
|
|||
|
|
point_dict=self.point_dict,
|
|||
|
|
driver_slots=[("simulator", SimulatorDriver(), [])],
|
|||
|
|
spool=self.spool, metrics=self.metrics,
|
|||
|
|
interval_ms=1000, max_pending=500_000,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_no_loss_no_dup_at_scale(self) -> None:
|
|||
|
|
"""压测:10 轮 × 600 点位 = 6000 样本,落库不丢不重。"""
|
|||
|
|
rounds = 10
|
|||
|
|
expected_total = rounds * len(self.point_dict)
|
|||
|
|
for _ in range(rounds):
|
|||
|
|
self.engine.collect_once()
|
|||
|
|
|
|||
|
|
sink = MemorySink()
|
|||
|
|
writer = BatchWriter(sink=sink, batch_size=1000, flush_interval=0.0)
|
|||
|
|
accepted = 0
|
|||
|
|
for sample in self.spool.pending_records():
|
|||
|
|
if writer.push(sample):
|
|||
|
|
accepted += 1
|
|||
|
|
self.spool.ack(sample)
|
|||
|
|
writer.flush()
|
|||
|
|
|
|||
|
|
# 不丢:落库数 = 采集数
|
|||
|
|
self.assertEqual(len(sink.rows), expected_total)
|
|||
|
|
self.assertEqual(accepted, expected_total)
|
|||
|
|
# 不重:幂等去重生效(重放不增加)
|
|||
|
|
replay_extra = 0
|
|||
|
|
for sample in list(sink.rows):
|
|||
|
|
if writer.push(sample):
|
|||
|
|
replay_extra += 1
|
|||
|
|
self.assertEqual(replay_extra, 0)
|
|||
|
|
print(f"\n[总线压测] {expected_total} 样本落库,"
|
|||
|
|
f"丢失 0,重复 0(不丢不重达标)")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
unittest.main()
|