- 点位字典 CSV schema/加载/自动校验(缺失字段/量纲/重复点号,PRD 5.1) - 协议可插拔只读驱动:OPC UA(S7-1200 适配)/S7/Modbus/称重/能源/模拟 - 周期采集引擎:只读+背压保护+健康度指标(丢失率/P99/可用性) - Kafka 流式上行 + 本地 spool 断点续传(丢失率≤0.02% 保障) - 模板配置外置(gateway.yaml + 点位字典 CSV),换行业零改码 - 18 个单元测试全绿;端到端运行 SLA 达标
96 lines
3.8 KiB
Python
96 lines
3.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""采集健康度统计 —— 丢失率 / P99 延迟 / 可用性(PRD 9 章 NFR 落点)。
|
|
|
|
指标口径(对齐 PRD 5.1 验收):
|
|
- 丢失率:未读到(含超时/失败)样本数 / 应采集样本总数,目标 ≤ 0.02%;
|
|
- P99 延迟:单轮采集批处理耗时(入队到读取完成)的 P99,目标 ≤ 1.8s;
|
|
- 可用性:成功轮次 / 总轮次(连续两轮成功间不间断视为可用),目标 ≥ 99.8%。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
from typing import List, Optional
|
|
|
|
|
|
class HealthMetrics:
|
|
"""线程安全的采集健康度统计器。"""
|
|
|
|
def __init__(self, window_size: int = 4096):
|
|
self._lock = threading.Lock()
|
|
# 轮次耗时(秒),用于 P99 分位计算
|
|
self._round_latencies: List[float] = []
|
|
self._window_size = max(64, window_size)
|
|
# 样本计数
|
|
self.total_samples = 0 # 应采集样本总数
|
|
self.lost_samples = 0 # 未读到样本数
|
|
self.failed_rounds = 0 # 失败轮次(驱动异常/超时)
|
|
self.total_rounds = 0 # 总轮次数
|
|
|
|
def record_round(self, latency_sec: float, expected: int, got: int, failed: bool = False) -> None:
|
|
"""记录一轮采集结果。
|
|
|
|
Args:
|
|
latency_sec: 本轮批处理耗时(秒);
|
|
expected: 本轮应采集样本数;
|
|
got: 本轮实际读到样本数;
|
|
failed: 本轮是否整体失败(驱动异常等)。
|
|
"""
|
|
with self._lock:
|
|
self.total_rounds += 1
|
|
self.total_samples += expected
|
|
self.lost_samples += max(0, expected - got)
|
|
if failed:
|
|
self.failed_rounds += 1
|
|
self._round_latencies.append(latency_sec)
|
|
if len(self._round_latencies) > self._window_size:
|
|
# 只保留最近窗口,避免无限增长
|
|
self._round_latencies = self._round_latencies[-self._window_size:]
|
|
|
|
# ------------------------------------------------------------------
|
|
@property
|
|
def loss_rate(self) -> float:
|
|
"""丢失率(0~1 区间的小数,如 0.0002 表示 0.02%)。"""
|
|
with self._lock:
|
|
if self.total_samples == 0:
|
|
return 0.0
|
|
return self.lost_samples / self.total_samples
|
|
|
|
@property
|
|
def availability(self) -> float:
|
|
"""可用性(0~1 区间小数,如 0.998 表示 99.8%)。"""
|
|
with self._lock:
|
|
if self.total_rounds == 0:
|
|
return 1.0
|
|
return 1.0 - self.failed_rounds / self.total_rounds
|
|
|
|
def p99_latency(self) -> float:
|
|
"""最近窗口内采集批处理耗时的 P99(秒)。"""
|
|
with self._lock:
|
|
if not self._round_latencies:
|
|
return 0.0
|
|
ordered = sorted(self._round_latencies)
|
|
idx = max(0, min(len(ordered) - 1, int(len(ordered) * 0.99)))
|
|
return ordered[idx]
|
|
|
|
def snapshot(self) -> dict:
|
|
"""一次性导出全部健康度指标(供驾驶舱/日志上报)。"""
|
|
return {
|
|
"total_rounds": self.total_rounds,
|
|
"total_samples": self.total_samples,
|
|
"lost_samples": self.lost_samples,
|
|
"loss_rate": round(self.loss_rate, 6),
|
|
"p99_latency_sec": round(self.p99_latency(), 4),
|
|
"availability": round(self.availability, 6),
|
|
"failed_rounds": self.failed_rounds,
|
|
"window_size": self._window_size,
|
|
}
|
|
|
|
def meets_sla(self) -> bool:
|
|
"""是否满足 PRD 验收基线(P99≤1.8s / 丢失率≤0.02% / 可用性≥99.8%)。"""
|
|
return (
|
|
self.p99_latency() <= 1.8
|
|
and self.loss_rate <= 0.0002
|
|
and self.availability >= 0.998
|
|
)
|