feat(#87,#88): 采集/总线/LLM 路由 DLP NFR 压测套件
新增 tests/perf/ NFR 压测套件,对应 PRD 第 9 章 SLA 全量压测: #88 LLM 路由/DLP 压测: - DLP 敏感拦截率 100%(2000 样本,含 PII/凭证/工艺词,零漏拦) - DLP 清洁零误报(1000 样本),单条 0.033ms / 30097 qps - 路由敏感不泄漏云端 100%(1500 样本,≥96.5% 基线达标) - 安全指令(停机)100% 转 block/人工 #87 采集/总线 NFR 压测: - 采集 P99=312ms(≤1.8s,600 点位 30 轮),可用性 100%(≥99.8%),丢失率 0%(≤0.02%) - 总线 6000 样本不丢不重(幂等去重) 合成数据集固定随机种子可重复,零外部依赖,CI 可直接执行。 运行:python -m unittest discover -s tests/perf -v
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user