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 解析时间单独记指标:
超过 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 模块配置里禁用代理:
案例 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),彻底避免监控盲区。