沿发现目标、抓取、存储、查询、规则逐层验证。up=1 只说明那次抓取成功;没有时间序列、抓取失败、应用失败必须分别判断。
先确认 target 还存在
入口就绪与发现结果分开确认
curl -fsS http://127.0.0.1:9090/-/ready
本页每条请求显式写出地址,按实际入口统一替换。代理认证/TLS 参数与生产访问方式一致。
curl -fsS http://127.0.0.1:9090/api/v1/targets |
jq '.data.activeTargets[] | {labels,scrapeUrl,health,lastError,lastScrape}'
从 lastError 区分超时、证书、连接拒绝和格式解析问题;lastScrape 也要足够新。
curl -fsS 'http://127.0.0.1:9090/api/v1/targets?state=dropped' |
jq '.data.droppedTargets[] | .discoveredLabels'
此列表受保留数量限制;没有记录时还需核对服务发现与配置,不等于一定未被丢弃。
curl -fsS --get http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=up' | jq .
先看顶层 status,再看 data.result。按实际 job/instance 缩小选择器,不能把空数组当成数值 0。
| 结果 | 含义 | 检查 |
| up = 0 | 目标存在但抓取失败 | lastError, timeout, DNS, TLS, auth, format |
| up = 1 | 抓取成功,业务可能仍异常 | 业务指标、探针与用户路径 |
| up 查询为空 | 可能未发现、被 relabel 丢弃、时间范围/标签不匹配 | discovered/dropped targets、实际标签和选择器 |
从 Prometheus 所在网络请求 scrapeUrl,检查实际 /metrics 响应;exporter 监听成功不代表 Prometheus 有相同的 DNS、凭据和 CA。配置中 job 名经 relabel 后的实际标签才是查询依据。ready 成功也不代表所有 target 健康。
step 改求值间距,不改抓取间隔
单点求值与区间求值是两个入口
curl -fsS --get http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=rate(http_requests_total[5m])' | jq .
http_requests_total 是示例指标,先确认实际存在且类型为 counter;单位是每秒增量,不是窗口内请求总数。
curl -fsS --get http://127.0.0.1:9090/api/v1/query_range \
--data-urlencode 'query=rate(http_requests_total[5m])' \
--data-urlencode 'start=2026-09-10T00:00:00Z' \
--data-urlencode 'end=2026-09-10T01:00:00Z' \
--data-urlencode 'step=30s' | jq .
这里与上一条是替代查询方式。先缩短到有问题的时间段,检查 resultType、时间戳与 values;错误返回还需读 warnings/infos。
替换为有数据的时间段。--data-urlencode 避免 []、花括号等被 curl 或 shell 错解。range query 在每个 step 时刻重新求值,rate 的 5m 是每次查看的样本窗口;减小 step 不会凭空增加原始采样。先 rate 再 sum 保留各 counter 的重置检测;窗口过短可能不足两个样本。
正在呈现知识画面
一个标签如何把系列数成倍放大对全部出现的独立组合,系列数是标签取值数的乘积。增加用户 ID 维度可把有限的 route×method×status 扩大多个数量级。
{"id":"ops-prom-cardinality","title":"一个标签如何把系列数成倍放大","summary":"对全部出现的独立组合,系列数是标签取值数的乘积。增加用户 ID 维度可把有限的 route×method×status 扩大多个数量级。","height":600,"html":"<main><p class=\"quiet\" data-i18n=\"scope\"></p><label class=\"range\" for=\"routes\"><span data-i18n=\"routes\"></span><output id=\"routesValue\"></output><input type=\"range\" id=\"routes\" min=\"1\" max=\"20\" value=\"5\"></label><label class=\"range\" for=\"users\"><span data-i18n=\"users\"></span><output id=\"usersValue\"></output><input type=\"range\" id=\"users\" min=\"1\" max=\"1000\" value=\"100\"></label><div class=\"checks\"><label><input id=\"include\" type=\"checkbox\" ><span data-i18n=\"include\"></span></label></div><div class=\"row code\" id=\"formula\"></div><section class=\"row\"><h3 data-i18n=\"series\"></h3><output class=\"large\" id=\"series\"></output><div class=\"bar\"><i id=\"fill\"></i></div><p class=\"quiet\">0 — 200,000</p></section><div class=\"explain\"><div class=\"stable \"><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"hint\"></p><p id=\"message\" aria-live=\"polite\"></p></div></div></main>\n","css":"main {\n display: grid;\n gap: 18px;\n min-width: 0;\n}\np {\n overflow-wrap: anywhere;\n}\n.controls {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n}\n.presets {\n display: flex;\n flex-wrap: wrap;\n}\n.presets button {\n flex: 1 1 85px;\n font-size: 15px;\n}\n.cards {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 10px;\n}\n.card {\n border: 1px solid var(--rule);\n border-radius: 10px;\n padding: 14px;\n min-width: 0;\n background: var(--surface);\n}\n.card h3 {\n font-size: 15px;\n color: var(--muted);\n margin-bottom: 8px;\n}\n.card strong {\n font-size: 23px;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.code {\n font-family: var(--viz-font-mono);\n font-size: 15px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.stable {\n display: grid;\n min-width: 0;\n}\n.stable > * {\n grid-area: 1/1;\n margin: 0;\n}\n.stable > .reserve {\n visibility: hidden;\n pointer-events: none;\n}\n.explain {\n border-left: 3px solid var(--accent);\n padding: 12px 14px;\n background: var(--accent-soft);\n}\n.arrow {\n text-align: center;\n color: var(--accent);\n font-size: 25px;\n}\n.row {\n padding: 12px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n min-width: 0;\n}\n.row.on {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n.row.off {\n color: var(--muted);\n}\n.bar {\n height: 18px;\n background: var(--surface);\n border: 1px solid var(--rule);\n border-radius: 5px;\n overflow: hidden;\n margin-top: 8px;\n}\n.bar > i {\n display: block;\n height: 100%;\n background: var(--accent);\n width: 0;\n transition: width 0.22s;\n}\n.checks {\n display: grid;\n gap: 8px;\n}\n.checks label {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 9px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n font-size: 16px;\n}\n.checks input {\n width: 18px;\n height: 18px;\n accent-color: var(--accent);\n flex-shrink: 0;\n}\nlabel.range {\n display: block;\n font-size: 16px;\n}\nlabel.range output {\n float: right;\n font-variant-numeric: tabular-nums;\n}\n.pair {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n}\n.flow {\n display: grid;\n gap: 6px;\n}\n.quiet {\n font-size: 14px;\n color: var(--muted);\n}\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n}\nbutton {\n overflow-wrap: anywhere;\n}\n.large {\n font-size: 32px;\n color: var(--accent);\n font-weight: 500;\n}\n.layers {\n display: grid;\n gap: 12px;\n}\n.layer {\n border-left: 3px solid var(--rule);\n padding-left: 12px;\n}\n.layer.on {\n border-color: var(--accent);\n}\n@media (max-width: 520px) {\n .cards,\n .pair {\n grid-template-columns: 1fr;\n }\n .card {\n padding: 12px;\n }\n .card strong {\n font-size: 22px;\n }\n}\n#formula {\n min-height: 5.3em;\n}\n","js":"const $ = (s) => document.querySelector(s);\nconst $$ = (s) => [...document.querySelectorAll(s)];\nconst t = (k) => viz.t(k);\nconst put = (id, v) => ($(\"#\" + id).textContent = v);\nconst say = (k) => put(\"message\", t(k));\nconst state = (v) => ($(\"main\").dataset.state = JSON.stringify(v));\nconst press = (key, v) =>\n $$(\"[data-\" + key + \"]\").forEach((b) =>\n b.setAttribute(\"aria-pressed\", String(b.dataset[key] === v)),\n );\nfunction draw() {\n const routes = +$(\"#routes\").value,\n users = +$(\"#users\").value,\n include = $(\"#include\").checked,\n series = routes * 2 * 5 * (include ? users : 1);\n put(\"routesValue\", routes);\n put(\"usersValue\", users);\n put(\n \"formula\",\n routes + \" routes × 2 methods × 5 status\" + (include ? \" × \" + users + \" users\" : \" × 1\"),\n );\n put(\"series\", viz.number(series, 0));\n $(\"#fill\").style.width = (series / 200000) * 100 + \"%\";\n state({ routes, users, include, series });\n}\n$$(\"input\").forEach((e) => {\n e.oninput = draw;\n e.onchange = draw;\n});\nsay(\"hint\");\ndraw();\n","audio":false,"strings":{"hint":"同样的请求量,按原始 URL/用户 ID 拆标签会产生更多独立序列。记录规则只产生汇总结果,不删除这些原始序列。","include":"加入 user_id 标签","routes":"route 取值数","scope":"一个普通指标,完整组合的估算上界;不含 histogram bucket、多实例、稀疏组合或历史 churn。","series":"时间序列数量","users":"user_id 取值数"}}
图中计算的是完整标签组合全部出现时的系列数量,不是每个业务都达到的实际值。user_id、request_id、原始 URL 等无界标签会产生持续 churn;用稳定 route 模板和有限枚举,详情留日志。recording rule 缓存常用表达式结果,但仍写入新系列,不自动减少已抓取的原始基数。
文件校验和运行状态都要确认
发布前:确认正在使用的文件并校验
systemctl cat prometheus.service
以下路径替换为 --config.file 和规则文件;保留修改前副本,校验工具与服务器使用相容版本。
promtool check config /etc/prometheus/prometheus.yml
校验成功后才发布;它不会替你完成真实抓取与远端认证验证。
promtool check rules /etc/prometheus/rules/*.yml
这是针对规则的独立检查,不要求每次重复完整配置检查中的同一文件。通配符必须匹配真实文件。
promtool test rules /etc/prometheus/tests/alerts.test.yml
这是测试描述文件,不是普通规则文件;应定义输入序列、预期告警/表达式结果,覆盖刚低于阈值、满足 for、恢复和缺数据。
发布后:信号、加载结果与时间戳
sudo systemctl kill --signal=HUP --kill-whom=main prometheus.service
发送成功只说明信号已送达;记录操作时间,继续验证加载结果。
journalctl -u prometheus.service --since "5 minutes ago" --no-pager
curl -fsS http://127.0.0.1:9090/api/v1/status/config | jq -r '.data.yaml'
curl -fsS --get http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query={__name__=~"prometheus_config_last_reload_successful|prometheus_config_last_reload_success_timestamp_seconds"}' | jq .
需要实例抓取自身指标,且等待一次新抓取。成功值应为 1,时间戳应对应本次操作;旧的 1 不能证明新配置成功。失败时恢复旧文件、重新校验和重载,再检查 targets 与规则状态。
按实际路径、unit 和权限调整。HUP 发给已确认的主进程;POST /-/reload 仅在启用 web.enable-lifecycle 时可用,不要为了一次重载随意公开控制接口。检查 success=1 与最近成功时间;文件检查不能发现所有权限、DNS、证书和远端问题。规则应使用 promtool test rules 的输入序列覆盖阈值、for 与恢复行为。
先定位基数、保留与磁盘,再做删除
定位空间来源
curl -fsS http://127.0.0.1:9090/api/v1/status/flags | jq '.data'
先按 storage.tsdb.path 修正下面路径;容器路径还要映射到真正的数据卷。
curl -fsS http://127.0.0.1:9090/api/v1/status/tsdb | jq '.data'
对比同一实例的历史与业务规模,不用单一序列阈值判故障。
df -hT /var/lib/prometheus
journalctl -u prometheus.service --since "1 hour ago" --no-pager
使用已启用的快照 API 备份
仅适用于已审批并限制访问的 --web.enable-admin-api 实例;先确认剩余空间。下面创建快照,不删除数据。
curl -fsS -X POST 'http://127.0.0.1:9090/api/v1/admin/tsdb/snapshot?skip_head=false' | jq .
记录成功响应 data.name。快照位于数据目录的 snapshots/名称 下;复制到独立备份存储并校验。它仍在原盘上时不算异地副本,之后的写入也不在快照中。
恢复演练使用独立数据目录与兼容版本,禁用会产生实际通知的规则/出口。核对代表性历史查询、时间范围与系列,再安排生产恢复;快照目录与配置备份须来自同一次维护记录。
数据目录按 --storage.tsdb.path 确认;观察 head 系列、churn、查询开销与实际 retention,而不是套“超过百万就故障”的统一阈值。retention.size 不等于整个目录的严格硬上限,WAL/head 等还要预留空间。先修高基数来源,删除旧块不能阻止新数据继续增长。
TSDB snapshot/admin 删除 API 需要显式启用 admin API。使用快照备份时确认包含 head 的选择,并另备配置、规则、服务参数和认证材料;恢复到独立目录/实例验证。delete_series 是带 match[] 与可选时间边界的破坏操作,tombstone 与 clean_tombstones 又是不同阶段,不提供“清空全部”的快捷修复。告警通知链交由 Alertmanager 或 Grafana 按实际部署排查。
HTTP API · Querying basics · Metric practices · Storage · Rule testing