工业网关承担设备接入、协议转换、数据处理、北向上报和远程运维等多类任务。一旦网关变成黑盒,现场只能回答“好像有问题”,却无法回答“问题在哪个环节、影响多少设备、持续多久、是否恢复”。
Metrics 的价值是把系统状态量化:请求量、错误率、延迟分布、连接数、队列长度、设备在线率、CPU 和内存压力都可以被持续采集、告警和趋势分析。本文从工程实战角度梳理工业网关的 Prometheus 指标暴露方案,覆盖指标类型、代码实现、端点配置、标签设计、PromQL 查询、RED / USE 模型、边缘约束和上线验收。
一、指标先回答哪些问题
在设计指标前,先明确工业网关最需要回答的问题:
| 视角 | 关键问题 | 典型指标 |
|---|---|---|
| 服务质量 | 请求是否变慢、是否失败 | 请求速率、错误率、延迟分布 |
| 设备接入 | 设备是否在线、采集是否正常 | 在线设备数、采集成功率、采集耗时 |
| 资源压力 | 网关是否快被压垮 | CPU、内存、磁盘、网络、句柄 |
| 异步任务 | 任务是否积压、重试是否失控 | 队列长度、执行时长、重试次数 |
| 北向链路 | 上报是否可用、断网是否恢复 | 上报速率、失败数、发送耗时、缓冲量 |
| 版本演进 | 新版本是否更差 | 按版本标签对比关键指标 |
不要为了“看起来可观测”而添加大量无人消费的指标。每个指标都应有明确的使用场景:进仪表盘、进告警规则,或用于故障定位。
二、四种基础指标类型
Prometheus 客户端库中常见四种类型,选择错误会让后续查询和聚合变得困难。
| 类型 | 语义 | 适用场景 | 查询要点 |
|---|---|---|---|
| Counter | 从进程启动开始单调递增的累计值 | 请求数、错误数、任务完成数、发送字节数 | 用rate()或increase()计算速率 |
| Gauge | 可上升也可下降的瞬时值 | 在线设备数、活跃连接数、队列长度、内存占用 | 直接取值,或用avg_over_time()、max_over_time() |
| Histogram | 将观测值放入累计桶,同时暴露_sum和_count | 请求耗时、采集耗时、报文大小 | 用histogram_quantile()在服务端计算分位数 |
| Summary | 客户端预计算分位数 | 单进程内的耗时分布 | 分位数不能跨实例正确聚合,分布式场景慎用 |
一个容易混淆的点是 Histogram 与 Summary:
- Histogram 的桶在暴露时已经确定,Prometheus 可以先按实例聚合
_bucket,再用histogram_quantile()计算 P95、P99; - Summary 的分位数在客户端计算完成,Prometheus 无法把多个实例的 P99 再平均成全局 P99;
- 因此网关多实例、多站点场景优先选择 Histogram。
三、指标命名与标签设计
1. 命名规范
建议统一使用小写和下划线,并带基础单位:
| 类型 | 命名示例 | 说明 |
|---|---|---|
| Counter | http_requests_total | Counter 以_total结尾,便于识别 |
| Histogram | device_collect_duration_seconds | 自动生成_bucket、_sum、_count |
| Gauge | device_online_count | 明确这是设备数量 |
| 字节数 | northbound_sent_bytes_total | 使用 bytes,不用 KB / MB |
| 温度 | cpu_temperature_celsius | 带物理单位 |
同一含义不要在不同模块中混用latency、duration、cost。团队可以先约定:耗时统一用duration_seconds,速率统一交给 PromQL 计算,指标名本身不带rate。
2. 标签只放低基数字段
标签决定时间线数量。假设 500 个网关、2000 个设备、10000 个请求 ID:
| 字段 | 是否适合作标签 | 原因 |
|---|---|---|
method | 适合 | GET / POST 等有限取值 |
route | 适合 | 使用路由模板,如/devices/{id} |
status_class | 适合 | 2xx、4xx、5xx等有限分类 |
protocol | 适合 | Modbus、OPC UA、MQTT 等有限枚举 |
result | 适合 | success、timeout、protocol_error 等受控枚举 |
site_id | 视规模适合 | 站点数量受控时可用 |
device_id | 通常不适合 | 设备多、扩容后时间线爆炸 |
request_id | 不适合 | 几乎每个请求都不同 |
user_id | 不适合 | 高基数且涉及隐私 |
| 原始 URL / path | 不适合 | 路径中可能带 ID,取值不可控 |
正确做法是:
- Metrics 保留聚合维度,例如
method、route、result; - 具体的
request_id、device_id、错误详情写入日志或 Trace; - 通过 Exemplar、Trace ID 或统一事件 ID 在异常时下钻。
如果确实需要按设备看状态,可以只对关键状态建聚合指标,例如device_online_count{site_id, protocol};不要为每个设备的高频指标都创建完整时间线。
四、Python 网关中的基础实现
下面以 FastAPI +prometheus-client为例。代码重点是:使用路由模板作为route标签,不把原始 URL 放入标签。
importtimefromcollections.abcimportAwaitable,CallablefromfastapiimportFastAPI,Request,Responsefromprometheus_clientimportCounter,Gauge,Histogram,make_asgi_app app=FastAPI()http_requests_total=Counter("http_requests_total","Total HTTP requests",["method","route","status_class"],)http_request_duration_seconds=Histogram("http_request_duration_seconds","HTTP request duration in seconds",["method","route"],buckets=[0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10],)device_collect_duration_seconds=Histogram("device_collect_duration_seconds","Device data collection duration in seconds",["protocol","result"],buckets=[0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10],)device_online_count=Gauge("device_online_count","Number of online devices",["site_id","protocol"],)defstatus_class(status_code:int)->str:ifstatus_code<200orstatus_code>=600:return"unknown"returnf"{status_code//100}xx"@app.middleware("http")asyncdefinstrument_http_requests(request:Request,call_next:Callable[[Request],Awaitable[Response]],)->Response:started_at=time.perf_counter()response=awaitcall_next(request)route=request.scope.get("route")route_name=getattr(route,"path","unmatched")http_requests_total.labels(method=request.method,route=route_name,status_class=status_class(response.status_code),).inc()http_request_duration_seconds.labels(method=request.method,route=route_name,).observe(time.perf_counter()-started_at)returnresponse metrics_app=make_asgi_app()app.mount("/prometheus",metrics_app)这里刻意将挂载点设为/prometheus。make_asgi_app()内部会在/metrics路径输出指标,因此最终抓取地址是:
http://gateway:8080/prometheus/metrics如果写成app.mount("/metrics", make_asgi_app()),实际抓取路径通常会变成/metrics/metrics,这是现场最常见的配置错误之一。
Gauge 示例:在线设备数
Gauge 适合表达“当前状态”,例如:
device_online_count.labels(site_id="site-01",protocol="modbus_tcp",).inc()device_online_count.labels(site_id="site-01",protocol="modbus_tcp",).dec()如果设备数量会周期性重算,也可以不使用inc()/dec(),而是在每轮发现后直接set():
device_online_count.labels(site_id="site-01",protocol="modbus_tcp",).set(len(online_devices))Histogram 示例:设备采集耗时
asyncdefcollect_device(protocol:str,device_id:str)->None:started_at=time.perf_counter()result="success"try:awaitread_device(device_id)exceptTimeoutError:result="timeout"raiseexceptException:result="error"raisefinally:device_collect_duration_seconds.labels(protocol=protocol,result=result,).observe(time.perf_counter()-started_at)device_id不进入指标标签,但应写入日志和 Trace,便于异常下钻。
五、端点暴露与 Prometheus 抓取配置
假设网关服务监听8080,指标端点为/prometheus/metrics,Prometheus 配置如下:
global:scrape_interval:15sevaluation_interval:15sscrape_configs:-job_name:edge-gatewaymetrics_path:/prometheus/metricsstatic_configs:-targets:-gateway-1:8080-gateway-2:8080-gateway-3:8080labels:env:prodregion:cn-east如果网关在内网,Prometheus 无法直接访问,可以考虑:
| 方案 | 适用情况 | 注意点 |
|---|---|---|
| 本地 Prometheus 抓取 | 边缘节点资源充足 | 注意磁盘保留周期和告警转发 |
| Prometheus Agent 模式 + Remote Write | 多站点集中观测 | 需要断网缓冲、重试和上限控制 |
| OTel Collector / Alloy 中转 | Logs、Traces、Metrics 统一采集 | 统一处理重试、限速和身份标签 |
| Pushgateway | 短生命周期批任务 | 不适合长期高频状态指标 |
工业现场网络中断不可避免。指标链路要明确:
- 本地最多缓存多久;
- 远端恢复后如何补传;
- 缓冲写满时先丢弃什么;
- 断网期间告警如何降级;
- 恢复后如何标记数据缺口。
六、默认进程与运行时指标
Pythonprometheus-client会自动暴露一批进程和运行时指标,常见如下:
| 指标 | 类型 | 用途 |
|---|---|---|
process_cpu_seconds_total | Counter | 进程 CPU 累计耗时 |
process_resident_memory_bytes | Gauge | 常驻内存 |
process_virtual_memory_bytes | Gauge | 虚拟内存 |
process_open_fds | Gauge | 打开的文件描述符 |
process_start_time_seconds | Gauge | 进程启动时间 |
python_gc_objects_collected_total | Counter | GC 回收对象数 |
这些指标可以补足进程视角,但还不能替代系统视角。建议同时采集:
- CPU 使用率与负载;
- 内存可用量;
- 磁盘容量、读写耗时;
- 网络收发量和错误包;
- TCP 连接状态;
- 进程句柄和线程数;
- 容器或 systemd 服务状态。
七、常用 PromQL 查询
1. 请求速率
rate()只适用于 Counter:
sum(rate(http_requests_total[5m]))按路由查看吞吐量:
sum by (route) ( rate(http_requests_total[5m]) )2. HTTP 错误率
示例使用status_class="5xx":
sum by (route) ( rate(http_requests_total{status_class="5xx"}[5m]) ) / sum by (route) ( rate(http_requests_total[5m]) )如果希望输出百分比,再乘以100。分母可能为 0 时,告警规则中应显式处理,避免无流量接口产生误判。
3. P99 延迟
Histogram 必须先聚合_bucket,再计算分位数,并保留le标签:
histogram_quantile( 0.99, sum by (le, route) ( rate(http_request_duration_seconds_bucket[5m]) ) )不要把各实例的 P99 直接平均。那不是全局 P99,而是“分位数的平均值”。
4. Top N 路由
先聚合,再取 Top N:
topk( 10, sum by (route) ( rate(http_request_duration_seconds_count[5m]) ) )如果先topk()再聚合,可能把本应进入全局 Top N 的时间线提前过滤掉。
5. 设备在线率
假设同时暴露device_online_count和device_registered_count:
sum by (site_id, protocol) (device_online_count) / sum by (site_id, protocol) (device_registered_count)6. 采集成功率
示例中device_collect_duration_seconds_count按result分桶:
sum by (protocol) ( rate(device_collect_duration_seconds_count{result="success"}[5m]) ) / sum by (protocol) ( rate(device_collect_duration_seconds_count[5m]) )八、RED 与 USE:建立覆盖面
RED 适合服务接口和北向链路,USE 适合资源对象。两者结合可以避免只监控接口而忽略网关自身压力。
1. RED
| 元素 | 含义 | 示例 |
|---|---|---|
| Rate | 吞吐速率 | HTTP QPS、设备采集次数 / 秒、北向上报次数 / 秒 |
| Errors | 错误 | 5xx、协议超时、校验失败、上报失败 |
| Duration | 耗时分布 | HTTP 耗时、采集耗时、规则执行耗时、上报耗时 |
2. USE
| 元素 | 含义 | 示例 |
|---|---|---|
| Utilization | 使用率 | CPU、内存、磁盘空间、带宽 |
| Saturation | 饱和度 | 队列长度、等待任务数、连接池等待、磁盘 IO 等待 |
| Errors | 错误 | 网络错误、读写失败、连接失败、句柄耗尽 |
一个工业网关的最小指标集合可以是:
| 类别 | 指标 |
|---|---|
| HTTP | http_requests_total、http_request_duration_seconds |
| 设备采集 | device_collect_duration_seconds、device_protocol_errors_total |
| 设备状态 | device_online_count、device_registered_count |
| 任务队列 | task_queue_length、task_duration_seconds、task_failures_total |
| 北向上报 | northbound_messages_total、northbound_duration_seconds、northbound_buffer_size |
| 资源 | CPU、内存、磁盘、网络、FD、进程重启次数 |
九、告警不是阈值堆叠
指标暴露后,常见做法是把所有阈值都配一遍,结果告警过多,现场逐渐忽略。更好的方式是先定义症状,再定位原因。
| 告警 | 建议规则方向 | 说明 |
|---|---|---|
| 网关不可达 | up == 0持续 2 分钟 | 区分单实例和网络分区 |
| HTTP 错误率高 | 5xx 占比持续超过阈值 | 结合流量下限 |
| 延迟劣化 | P95 或 P99 持续升高 | 关注趋势,不只看瞬时值 |
| 设备采集失败 | 采集失败率或超时率突增 | 按协议和站点分组 |
| 队列积压 | 队列长度持续增长 | 配合消费速率下降判断 |
| 北向断链 | 上报成功率下降或缓冲增长 | 区分本地故障与远端不可用 |
| 资源压力 | 内存、磁盘、FD 接近上限 | 磁盘满会影响业务和日志 |
| 进程重启 | 启动时间变化频繁 | 需要结合崩溃日志 |
告警内容应带上网关、站点、版本和协议等身份信息,并链接到对应仪表盘或排障手册。
十、部署与验收清单
1. 应用侧
- 指标端点可访问,路径与 Prometheus 配置一致;
- 输出包含
HELP和TYPE; - Counter 使用
_total命名; - 耗时使用秒,字节使用 bytes;
- Histogram 桶覆盖正常值、慢请求和超时场景;
- HTTP 标签使用路由模板,不使用原始 URL;
device_id、request_id等高基数信息不进标签;- 指标注册只执行一次,进程重启后历史值允许归零;
- 指标采集失败不影响业务主流程;
- 关键业务路径均有 Rate、Errors、Duration 指标。
2. 采集侧
up == 1,抓取时长在合理范围;- 网关、站点、环境、版本身份完整;
- 时间线数量有预算和告警;
- 断网时本地缓存策略明确;
- 恢复后数据能按时间对齐;
- Remote Write 有重试、限速和上限;
- Prometheus 磁盘保留周期经过容量测算。
3. 查询与运维侧
- HTTP QPS、错误率、P95 / P99 有仪表盘;
- 设备采集成功率和耗时可按站点、协议过滤;
- 队列积压和北向缓冲可观测;
- 常见告警有处置手册;
- 指标异常可以跳转日志或 Trace;
- 升级前后可按版本对比;
- 定期清理不再使用的指标和仪表盘。
十一、常见坑与应对
| 坑 | 现场表现 | 应对 |
|---|---|---|
| 抓取路径写错 | Prometheusup == 0,浏览器访问 404 | 明确挂载点与内部/metrics的组合关系 |
| 原始 path 作标签 | 时间线持续膨胀,内存和存储升高 | 使用路由模板,未匹配请求归入unmatched |
| 用户 / 请求 / 设备 ID 作标签 | Prometheus 指标爆炸 | 高基数信息放日志和 Trace |
| Counter 用 avg 查询 | 数值没有业务含义 | 使用rate()或increase() |
| Summary 跨实例聚合 | 全局分位数不准确 | 多实例优先使用 Histogram |
| Histogram 先算分位数再聚合 | 聚合维度丢失或计算错误 | 先sum by (le, ...),再histogram_quantile() |
| 先 topk 再聚合 | Top N 结果不准确 | 先聚合,再topk() |
| 桶设置不贴近业务 | 全落在一两个桶,分位数无意义 | 用真实耗时分布校准桶 |
| 无本地缓冲 | 断网后数据缺口不可恢复 | Agent 模式落盘、限速、重试 |
| 指标无身份标签 | 无法定位站点、实例和版本 | 统一 job、instance、site、version 模型 |
TL;DR
工业网关的 Metrics 暴露先从语义正确的四类指标开始:Counter 看累计事件,Gauge 看当前状态,Histogram 看可聚合分布,Summary 慎用于多实例分位数。标签必须低基数,HTTP 使用路由模板,设备、请求和用户 ID 留在日志与 Trace 中。端点配置要确认真实抓取路径;PromQL 中 Counter 用rate(),Histogram 先聚合_bucket再计算分位数,Top N 先聚合再筛选。工程落地上用 RED 覆盖服务链路,用 USE 覆盖资源,用设备采集、任务队列、北向上报和资源指标补齐工业场景,并配套断网缓冲、容量规划、身份标签、仪表盘、告警和验收清单。
如果在工业边缘协议运行时中评估可观测能力,可以参考 Zenova EdgeOS 的 Prometheus 指标暴露、远程诊断和运行监测实践。