跳转至

Blackbox Exporter 探针配置实战:多协议探测 + SLO 设计 + K8s ServiceMonitor 集成

上周有个项目的教训我到现在还记得:node_exporter 显示一台机器的 CPU、内存、磁盘全绿,Prometheus 也抓到了它的 exporter,一切正常。结果用户群里突然炸了——"你们系统打不开了"。

问题出在 nginx 前面挂了一层 CDN,CDN 到源站的证书链不完整,客户端 TLS 握手失败,但源站自己探测自己是通的。这就是典型的白盒探针盲区:node_exporter 站在主机内部看世界,它只能告诉你"我这台机器活着",不能告诉你"用户能不能连上你的服务"。

这就是 blackbox_exporter 存在的意义。它是 Prometheus 生态里唯一以"外部视角"主动探测目标可用性的组件。装完就能用不难,难的是从"能用"到"生产可用"——模块怎么选、超时怎么配、SLO 怎么算、告警怎么不刷屏、K8s 里怎么不出岔子。这篇文章按我这几年踩出来的路写一遍。

1. 白盒探针的盲区:为什么必须上黑盒

先讲清楚白盒(内部指标)和黑盒(外部探测)的分工:

维度 白盒(node_exporter / kube-state-metrics / app 自定义指标) 黑盒(blackbox_exporter)
视角 主机/进程内部 网络外部,模拟真实客户端
能看到的故障 CPU、内存、磁盘、进程崩溃、指标异常 DNS 解析慢、TLS 握手失败、CDN 缓存失效、路由黑洞、防火墙丢包
探测方式 被动暴露 /metrics 主动发起 HTTP/TCP/DNS/Ping 探测
数据语义 "我这台机器活着" "用户能不能访问到你"
典型盲区 CDN、网关、DNS、防火墙问题对它不可见 内部资源消耗、代码 bug 不可见

真实场景里,至少 40% 的"服务不可用"是白盒看不见的——DNS 记录过期、CDN 节点故障、防火墙策略误删、运营商 BGP 抖动。这些都是 node_exporter 报不出来的。

我坚持一条原则:关键业务链路(登录、下单、支付)必须至少有一个跨机房、跨运营商视角的黑盒探针,本地探针再准也不够。

2. 模块体系:8 种协议的完整对照

blackbox_exporter 的模块(module)定义了"怎么探测"。官方文档列了 10 多种,生产用得最多的是下面这 8 种:

模块 协议 探测内容 典型用途 常见超时
http_2xx HTTP(S) GET 状态码 ∈ [200,399] 99% 的 Web 服务探测 5s
http_4xx HTTP(S) GET 状态码 ∈ [400,499] 探测故意 401/404 的鉴权 API 5s
http_post HTTP POST 自定义 body + 状态码 探测登录、下单等写接口 5s
tcp_connect TCP 三次握手 端口是否 accept MySQL/Redis/自定义 TCP 服务 3s
udp_connect UDP 发起 UDP 探测 DNS/NTP/SNMP(注意丢包误报) 3s
dns_lookup DNS over TCP/UDP A/AAAA/MX 记录 检查 DNS 记录正确性 3s
grpc gRPC health check HealthCheck RPC gRPC 微服务 5s
icmp ICMP Ping 往返延迟 + TTL 网络层通断(部分防火墙丢 ICMP) 3s

选模块的思路:先看协议(HTTP/TCP/UDP/gRPC/DNS),再看探测目标类型(Web 页面 / 内部 RPC / 数据库 / DNS 记录),最后考虑是否需要自定义请求(比如带 Cookie、带 Header 的登录探测)。

3. 完整 blackbox.yaml 生产模板

下面这份 60 行模板我用了 3 年,从电商到内部平台都在跑。注释里写清楚了每个字段为什么这么配:

# /etc/blackbox_exporter/blackbox.yaml
modules:
  # 通用 Web 服务探测,生产主力
  http_2xx:
    prober: http
    timeout: 5s
    http:
      valid_status_codes: [200, 201, 204, 301, 302]
      preferred_ip_protocol: ip4
      method: GET
      headers:
        User-Agent: ["jiunotes-probe/1.0"]
      tls_config:
        insecure_skip_verify: false  # 生产务必 false
      follow_redirects: true
      no_follow_redirects: false

  # 内部健康检查端点,允许 204 空响应
  http_internal:
    prober: http
    timeout: 3s
    http:
      valid_status_codes: [200, 204]
      method: GET
      headers:
        X-Internal-Token: ["${INTERNAL_TOKEN}"]
      fail_if_body_matches_regexp: ["error", "panic"]
      fail_if_not_regex_match: ["OK"]

  # MySQL/Redis 端口连通性
  tcp_connect:
    prober: tcp
    timeout: 3s
    tcp:
      preferred_ip_protocol: ip4
      query_response:
        - send: "PING\r\n"
          expect: "+PONG\r\n"
          timeout: 2s  # Redis 特有:能顺便验证服务存活

  # DNS 记录校验:验证 A 记录指向正确 IP
  dns_lookup:
    prober: dns
    timeout: 3s
    dns:
      transport: tcp
      prefer_ipv6: false
      domain_name: example.com
      query_name: api.example.com
      query_type: A
      valid_answer_codes: ["NOERROR"]
      valid_answers: ["203.0.113.10", "203.0.113.11"]

  # gRPC 微服务健康检查
  grpc:
    prober: grpc
    timeout: 5s
    grpc:
      preferred_ip_protocol: ip4
      tls: true
      tls_config:
        insecure_skip_verify: false
      use_tls: true
      server_name: api.internal.example.com

  # ICMP:仅在网络监控场景用,应用层探测不推荐
  icmp:
    prober: icmp
    timeout: 3s
    icmp:
      preferred_ip_protocol: ip4
      timeout: 3s
      packet_loss: 0.3  # 30% 以下丢包算成功

valid_answers 那个字段是隐藏福利——不光验证 DNS 能不能解析,还能验证"是不是解析到了我们期望的 IP"。CDN 切换、DNS 污染这类问题一秒就能发现。

4. 探测端点 /probe 详解

blackbox_exporter 只有一个 HTTP 端点:/probe。所有探测都通过 query 参数控制。

# 最基础的用法
curl -G --data-urlencode "module=http_2xx" \
     --data-urlencode "target=https://api.example.com" \
     http://blackbox:9115/probe

三个关键参数:

  • target:要探测的目标。HTTP 探测可以是 URL(含 scheme),TCP/UDP 探测是 host:port。
  • module:模块名,缺省用 http_2xx。
  • ?target=xxx&module=yyy 之外的参数一律忽略,不合法 target 会返回 400。

URL 编码陷阱:target 里有 ? & = 时必须 URL 编码,不然会被解析错。curl 的 -G --data-urlencode 是标准解法:

# 错误:& 被解析成第二个参数
curl "http://blackbox:9115/probe?target=https://a.com/path?a=1&b=2&module=http_2xx"

# 正确:--data-urlencode 自动编码
curl -G --data-urlencode "target=https://a.com/path?a=1&b=2" \
     --data-urlencode "module=http_2xx" \
     http://blackbox:9115/probe

另一个坑:Prometheus 的 scrape config 里 target 也要小心。用 ServiceMonitor 时 params.target 会被自动编码,但手写 static_configs 时要自己注意:

scrape_configs:
  - job_name: "blackbox-http"
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - https://api.example.com
          - "192.168.1.100:6379"  # TCP 探测写 host:port
    relabel_configs:
      # 关键三步:把 __address__ 替换为真正要探测的 target
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115

三段 relabel 的顺序不能乱,__address__ → __param_target 那步是"把要探测的目标搬进参数",最后那步"把抓取地址指回 blackbox 自己"。

5. SLO 三指标设计与 PromQL 实现

黑盒探测的原始指标只有三个,但足以算出 SLO 三指标:

  • probe_success(0/1):本次探测是否成功
  • probe_duration_seconds:本次探测耗时
  • probe_http_status_code / probe_dns_lookup_time_seconds 等辅助指标

5.1 三个 PromQL 公式

# 指标 1:可用性(Availability)
# 语义:过去 1h 的成功率
avg_over_time(probe_success{target="api.example.com"}[1h])

# 指标 2:延迟 P99(Latency)
# 语义:过去 5m 内 99% 的探测在多少秒内完成
histogram_quantile(0.99, sum by (le) (rate(probe_duration_seconds_bucket[5m])))

# 指标 3:错误率(Error Rate)
# 语义:过去 10m 的失败次数占总探测次数比例
sum(rate(probe_success[10m])) * -1

前两个是核心 SLO,第三个是兜底告警。生产里我一般这样定目标:可用性 ≥ 99.9%(1 天允许 43 秒不可用)、P99 延迟 ≤ 2s(用户可感知阈值)、错误率告警阈值 0.1%。内部系统可以放宽到 99.5%,登录/支付接口必须 99.99%。

5.2 三条 recording rule

把这三个计算下沉成 recording rule,让告警规则和 Grafana 都直接引用,不用重复写复杂 PromQL:

groups:
  - name: blackbox-slo
    interval: 30s
    rules:
      - record: slo:blackbox:availability:1h
        expr: avg_over_time(probe_success[1h])

      - record: slo:blackbox:latency:p99:5m
        expr: |
          histogram_quantile(0.99,
            sum by (le, target) (rate(probe_duration_seconds_bucket[5m])))

      - record: slo:blackbox:error_rate:10m
        expr: |
          1 - (sum(rate(probe_success[10m]))
               / sum(rate(probe_success[10m]) + rate(probe_success_offset[10m])))

第三条 rule 处理了 probe_success 被 relabel 成两条时序的情况——blackbox 每次探测会同时把 result(0/1)和 success(1)暴露成两个时间序列,直接 rate(probe_success) 会重复计,必须配合 probe_success_offset 抵消。这是很多人写错的地方。

6. K8s ServiceMonitor 集成

在 K8s 里部署 blackbox_exporter 用 Helm 装最快,但 ServiceMonitor 一定要自己写,不能靠默认模板。

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: blackbox-http-probes
  namespace: monitoring
  labels:
    release: prometheus
spec:
  selector:
    matchLabels:
      app: blackbox-exporter
  endpoints:
    - port: http-metrics
      path: /probe
      interval: 30s
      params:
        module: [http_2xx]
      targetLabels: [target]
  # 关键:把要探测的目标注入到 ServiceMonitor
  namespaceSelector:
    any: true
---
# 用 Service 承载要探测的目标列表(推荐做法)
apiVersion: v1
kind: Service
metadata:
  name: external-probe-targets
  namespace: monitoring
  labels:
    probe.type: http
spec:
  ports:
    - port: 80
      name: http
  clusterIP: None  # headless service

三个必踩的坑:

坑 1:ClusterIP 跨命名空间访问不通。 如果你 blackbox_exporter 在 monitoring 命名空间,要探测的目标 Service 在 default,直接写 Service.default.svc:80 有时会被 CoreDNS 解析成"当前命名空间"的地址。必须写全限定名:Service.default.svc.cluster.local:80。

坑 2:NetworkPolicy 拦住探针。 目标 Service 所在命名空间如果配了 NetworkPolicy,blackbox_exporter 的 Pod 会被拒。解法两条:给 blackbox 的 Deployment 加特定 label(比如 probe: blackbox),在目标 Namespace 的 NetworkPolicy 里加一条 ingress allow;或者干脆用 NodePort + 节点 IP 绕过 Service 层。

坑 3:Pod 探针和 blackbox 探针冲突。 有些团队把 K8s 的 livenessProbe 和 blackbox 的 http_2xx 都打 /healthz,结果 blackbox 频繁探测导致 /healthz 后端慢查询被放大,反过来触发 liveness 失败重启 Pod。解法:liveness 打轻量的 /livez(只检查进程活着),blackbox 打更重的 /readyz(检查依赖)。

7. 5 个真实翻车案例

案例 1:DNS 解析慢导致 2s 超时告警误报

我们生产环境有个探测模块 timeout 设的 2s,某天开始报"api.example.com 探针超时",实际服务正常。排查发现是 CoreDNS 那个节点负载高,单次解析耗时 3-4 秒。

解法:timeout: 5s 起步,加 dns: { transport: tcp }(UDP 有时会被丢包导致超时),并把 DNS 解析时间单独记指标:

max by (target) (rate(probe_dns_lookup_time_seconds[1m]))

超过 1s 就单独告警,不要跟"服务超时"混在一起。

案例 2:TLS 证书链不完整

CDN 换了证书,只部署了叶子证书没带上 intermediate CA。客户端 curl 加了 --cacert 能通,blackbox 探测直接返回 0(握手失败)。

排查:openssl s_client -connect host:443 -showcerts 看证书链。blackbox 里 insecure_skip_verify: true 只能应急用,生产必须 false——否则 TLS 问题被吞掉,等用户报错才发现。

案例 3:出口代理绕过防火墙规则

我们机房出口走 squid 代理,某些服务配置了 no_proxy。blackbox_exporter 在容器里跑,没继承 no_proxy 环境变量,探测请求走了代理,被代理缓存的旧响应返回 200——实际后端已经挂了。

解法:容器环境里显式注入 no_proxy,或在 http 模块配置里禁用代理:

http:
  proxy_url: ""  # 显式清空

案例 4:K8s ClusterIP 从跨命名空间访问不通

target 写 api-service:8080,blackbox 在 monitoring ns,api-service 在 prod ns,探测一直 0。

根因:DNS 解析时用了 shortname,CoreDNS 优先解析到"同命名空间"的记录,monitoring 下没有叫 api-service 的 Service,走了搜索域兜底有时失败。

解法:一律写全 FQDN:api-service.prod.svc.cluster.local:8080。这一条我已经在团队 wiki 写死了。

案例 5:UDP 模块被防火墙丢包但 ping 通

某内部服务用 UDP 端口 514(syslog),blackbox 的 udp_connect 报失败,但 ping 目标 IP 完全正常。

UDP 是"发完就走"的协议,防火墙默认丢 UDP 报文,udp_connect 探测其实没有握手,只能探测"是否被立即 ICMP unreachable"。这个模块天生不适合生产探测,改用 tcp_connect 探测 TCP 端口,或者上 Syslog 专用 exporter。

结论:UDP 探测只在网络监控(比如 NTP 服务)场景用,业务服务一律不要用。

8. 告警规则清单

8 条规则覆盖黑盒探测的所有关键场景:

groups:
  - name: blackbox-alerts
    rules:
      # 1. 探针失败:核心服务不可用
      - alert: BlackboxProbeDown
        expr: probe_success == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "黑盒探针失败:{{ $labels.target }}"
          description: "{{ $labels.target }} 使用 {{ $labels.module }} 模块探测失败,请检查服务状态。"

      # 2. 连续失败:区分抖动和真故障
      - alert: BlackboxProbeUnstable
        expr: |
          changes(probe_success[10m]) > 4
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "探针不稳定:{{ $labels.target }}"

      # 3. 可用性跌破 99.9%(1h 窗口)
      - alert: BlackboxAvailabilityBelowSLO
        expr: slo:blackbox:availability:1h < 0.999
        for: 15m
        labels:
          severity: critical
        annotations:
          summary: "可用性 SLO 失守:{{ $labels.target }}"

      # 4. P99 延迟超过 2s
      - alert: BlackboxLatencyP99High
        expr: slo:blackbox:latency:p99:5m > 2
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "P99 延迟 {{ $value }}s > 2s:{{ $labels.target }}"

      # 5. DNS 解析慢(独立告警,别混在超时里)
      - alert: BlackboxDnsLookupSlow
        expr: max(probe_dns_lookup_time_seconds) by (target) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "DNS 解析慢:{{ $labels.target }}"

      # 6. HTTP 状态码异常(非 2xx)
      - alert: BlackboxHttpStatusNon2xx
        expr: probe_http_status_code == 0 or (probe_http_status_code >= 500)
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "HTTP 状态码异常:{{ $labels.target }} => {{ $value }}"

      # 7. TLS 证书即将过期
      - alert: BlackboxTlsCertExpiring
        expr: probe_httpTLSNotAfter - time() < 7 * 24 * 3600
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "TLS 证书 7 天内过期:{{ $labels.target }}"

      # 8. 错误率超过 0.1%
      - alert: BlackboxErrorRateHigh
        expr: slo:blackbox:error_rate:10m > 0.001
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "错误率 {{ $value }} > 0.1%:{{ $labels.target }}"

for 时窗是黑盒告警的灵魂——probe_success == 0 单独看太敏感(一次抖动就报),配 for: 2m 意味着连续 2 分钟失败才报警,抖动场景自然过滤。

9. Grafana 面板

三块 widget 布局:

顶行:KPI 概览(3 个大数字) - 可用性 %(slo:blackbox:availability:1h) - P99 延迟(slo:blackbox:latency:p99:5m) - 当前失败数(sum(probe_success == 0))

中行:单目标时间序列 - Availability + P99 双 Y 轴 - 用 target 变量筛选,$__rate_interval 自适应窗口

下行:状态码分布 + DNS 延迟 - 状态码堆叠图(sum by (probe_http_status_code) (rate(probe_success[5m]))) - DNS lookup 时延时间序列

Panel JSON 核心片段:

{
  "targets": [
    {
      "expr": "slo:blackbox:availability:1h{target=~\"$target\"}",
      "legendFormat": "Availability 1h"
    },
    {
      "expr": "slo:blackbox:latency:p99:5m{target=~\"$target\"}",
      "legendFormat": "P99 Latency",
      "yaxis": 2
    }
  ],
  "fieldConfig": {
    "defaults": {
      "custom": { "drawStyle": "line", "spanNulls": true },
      "unit": "percentunit",
      "thresholds": {
        "steps": [
          {"color": "red",   "value": 0.95},
          {"color": "yellow","value": 0.99},
          {"color": "green", "value": 0.999}
        ]
      }
    }
  }
}

10. FAQ

Q1:Blackbox Exporter 和 node_exporter 怎么选? 两者不冲突,一定要都用。node_exporter 是"我这台机器内部还好吗",blackbox_exporter 是"外面能连到我吗"。node_exporter 挂了不代表用户访问不了(可能只是 exporter 进程死了),blackbox_exporter 挂了也不代表主机有故障(可能是探针所在机房断网了)。生产环境我通常这样分工:node_exporter 全量部署,blackbox_exporter 部署 2-3 个跨机房实例,互相校验避免单点误报。

Q2:http_2xx 和 http_4xx 到底怎么选? 99% 场景用 http_2xx。http_4xx 只有在"故意设计成返回 4xx 的接口"才用——比如你的鉴权 API 期望未带 token 时返回 401,那 http_4xx 就是正确的探测模块。混用的话,比如一个健康检查端点设计成返回 200,你用 http_4xx 探测就会永远失败。判断依据:先看接口设计文档,再决定模块。

Q3:K8s 内部怎么部署 blackbox_exporter? 推荐 Helm 部署到 monitoring namespace,通过 ServiceMonitor 让 Prometheus 抓取。三个关键点:① target 一律用 FQDN 全限定名(api.prod.svc.cluster.local:8080)② 目标命名空间有 NetworkPolicy 时,给 blackbox Deployment 加 label 并在目标策略里放行 ③ 跨集群探测走 Ingress 域名,不要试图用 ClusterIP 跨集群访问。

Q4:SLO 三指标阈值怎么定? 通用值:可用性 99.9%(一天允许 43 秒不可用)、P99 延迟 2s、错误率 0.1%。这几个数字不是拍脑袋来的——99.9% 对应"每天 43 秒"的常见可用性合约,2s 是 Google SRE 里"用户可感知"的经验阈值。具体场景要调:内部管理系统可以放宽到 99.5%,用户端登录接口必须 99.99%,静态资源 CDN 可以放宽到 99.95%。

Q5:blackbox_exporter 自己挂了怎么办? 黑盒探针本身也是个服务,也要监控。元监控策略:blackbox_exporter 部署成 StatefulSet 多副本 + 跨机房,配合 K8s livenessProbe 自保。Prometheus 侧配一条 up{job="blackbox-exporter"} == 0 的告警,探针挂了立即知道。我一般还会在异地部署一个"探针的探针"(用另一个 blackbox_exporter 探测第一个 blackbox_exporter),彻底避免监控盲区。