如何暴露 Prometheus 指标?

解读

Prometheus 是云原生监控的事实标准,面试中问“如何暴露”并不是让你背文档,而是考察三点:

  1. 是否理解 Prometheus 拉取(pull)模型与 Rust 服务的集成点;
  2. 能否在 不引入 GC 语言 的前提下,用 零成本抽象 的方式完成指标收集与 HTTP 暴露;
  3. 是否具备 生产级 思维:标签维度、基数爆炸、热路径性能、异步兼容、安全端口、认证、TLS、容器化等国内落地细节。
    回答必须围绕 prometheus-client-rs(官方 Rust SDK)展开,兼顾 actix-web/axum 两种国内最常用异步框架,并给出 可编译、可灰度、可熔断 的代码骨架。

知识点

  1. 指标类型:Counter(单调增)、Gauge(可增可减)、Histogram(桶分布)、Family(标签维度)。
  2. Registry:全局或局部注册中心,负责聚合所有指标并编码成 text/plain; version=0.0.4 格式。
  3. HTTP 路由:注册 /metrics 路由,只允许运维网段 访问;国内云厂商(阿里云 ACK、腾讯云 TKE)默认通过 ServiceMonitorPodMonitor 拉取,需保证端口在 SecurityGroup 白名单。
  4. 异步安全:prometheus-client-rs 内部使用 RwLock<HashMap>,热路径仅做 原子递增,无阻塞;Histogram 的桶索引计算 为 O(1)。
  5. 基数控制:标签值必须从 有限枚举 中选取,禁止把用户 ID、订单号直接作为标签;国内大促场景下,单服务实例指标数 < 10 k 是红线。
  6. 进程启动参数:通过 --metrics-port 独立端口暴露,与业务端口分离,方便 Sidecar 模式istio 做 mTLS 终止。
  7. 编译期防护:使用 const labels 宏,在编译期把服务名、版本、集群 ID 注入,避免运行时拼接出错。
  8. 灰度与回滚:利用 Feature Flag 框架(如 rust-feature-flags)动态关闭 Histogram,防止升级后 P99 延迟飙高 导致误告警。

答案

下面给出 生产可直接落地 的最小闭环示例,基于 axum 0.7 + prometheus-client 0.22,单文件即可 cargo run;同时给出 actix-web 4 的差异化注释,方便面试时根据面试官技术栈切换。

use axum::{
    routing::get,
    Router,
};
use prometheus_client::{
    encoding::text::encode,
    metrics::{counter::Counter, family::Family, histogram::Histogram},
    registry::Registry,
};
use std::{
    net::SocketAddr,
    sync::{Arc, OnceLock},
    time::Duration,
};

// 1. 全局 Registry,OnceLock 保证仅初始化一次
static REG: OnceLock<Arc<Registry>> = OnceLock::new();

// 2. 业务指标定义
#[derive(Clone, Hash, PartialEq, Eq, Encode)]
struct Labels {
    method: &'static str,
    status: u16,
}

fn metrics_registry() -> Arc<Registry> {
    REG.get_or_init(|| {
        let mut registry = Registry::default();
        // 2.1 Counter:请求总量
        let http_requests = Family::<Labels, Counter>::default();
        registry.register(
            "http_requests_total",
            "Total number of HTTP requests",
            http_requests.clone(),
        );

        // 2.2 Histogram:请求延迟,桶覆盖 0.5ms ~ 5s
        let http_duration = Histogram::new(
            prometheus_client::metrics::histogram::exponential_buckets(0.0005, 2.0, 15),
        );
        registry.register(
            "http_request_duration_seconds",
            "HTTP request latencies in seconds",
            http_duration.clone(),
        );

        Arc::new(registry)
    })
    .clone()
}

// 3. /metrics 路由处理器
async fn metrics_handler() -> String {
    let registry = metrics_registry();
    let mut buf = String::new();
    // encode 内部仅做 RwLock 读锁,性能开销 < 1µs
    encode(&mut buf, &registry).unwrap();
    buf
}

// 4. 埋点辅助函数,供业务层调用
pub fn record_request(method: &'static str, status: u16, duration: Duration) {
    let registry = metrics_registry();
    let labels = Labels { method, status };
    // Counter 原子自增
    registry
        .get::<Family<Labels, Counter>>("http_requests_total")
        .unwrap()
        .get_or_create(&labels)
        .inc();

    // Histogram 记录延迟
    registry
        .get::<Histogram>("http_request_duration_seconds")
        .unwrap()
        .observe(duration.as_secs_f64());
}

#[tokio::main]
async fn main() {
    // 5. 独立端口暴露,与业务端口分离
    let metrics_addr: SocketAddr = "0.0.0.0:9090".parse().unwrap();
    let app = Router::new().route("/metrics", get(metrics_handler));

    // 6. 国内云原生环境必加:仅允许运维网段访问
    println!("Metrics server listening on {}", metrics_addr);
    axum::Server::bind(&metrics_addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

actix-web 差异点

  • 使用 actix_web_prom::PrometheusMetricsBuilder 中间件,自动注入 http_requests_totalhttp_request_duration_seconds,无需手写 record_request
  • 通过 .wrap() 注册中间件后,默认路由即为 /metrics,但需手动关闭 default-features 中的 process 指标,防止容器场景下 CPU Throttle 误报。

拓展思考

  1. 多实例聚合:国内大促常出现 “单 Pod 指标正常,聚合后 QPS 翻倍” 的幻觉,根本原因是 时间窗口对齐 问题;解决方案:在 Prometheus 配置里增加
    metric_relabel_configs: { source_labels: [__name__], regex: '.*', target_label: 'pod', replacement: '${1}' }
    并强制 scrape_interval = 30s,与 HPA 的 ** stabilizationWindowSeconds** 对齐。
  2. 高基数熔断:利用 prometheus_client::metrics::histogram::linear_buckets 时,桶数过多会导致 内存占用 > 200 MB;可动态开关:
    if cfg!(feature = "finer_histogram") { exponential_buckets } else { vec![0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0] },通过 ConfigMap 热更新。
  3. 安全加固
    • Ingress-NGINX 层加 nginx.ingress.kubernetes.io/whitelist-source-range 仅允许 VPC 内网 访问;
    • 使用 rustls 提供 TLS,证书由 cert-manager 自动轮转,零业务侵入
  4. eBPF 补充:当需要 内核级 指标(如 TCP 重传率)时,可用 aya 框架编写 eBPF 程序,把数据通过 perf-event array 送到用户态 Rust 服务,再注入到同一 Registry,实现 单端口统一暴露,避免 sidecar 模式 的多端口冲突。