来自内核计数器的进程级磁盘占用实况

iotop 能看出是哪个进程在烧磁盘

磁盘突然飙升却找不到明显的写入者时,iotop 把吞吐直接指回那个在捣乱的进程。系统级工具只给症状——await 高、util 打满——不给嫌疑人:五十个进程共享磁盘,平均值平等地怪每一个。终结调查的永远是按进程的问题——谁、多少、何时开始——iotop 从内核计数器实时回答,应用藏都没处藏。 把它放在系统级视图旁边,而不是代替。iostat 或 vmstat 说磁盘是瓶颈,iotop 点名是谁造成的。两者合起来把存储悬案压成两条命令:确认资源,锁定消费者。 逐个进程的计数器 iotop 从内核读每个进程的 IO,于是实时读写速率能对应到一个 pid,而不是一个说不清来源的负载均值。加 -o 只显示真正在做 IO 的进程,一眼看清。默认视图读写分开,还带 swap-in 百分比,把真吃磁盘的和仅仅在场的分开——DISK READ 高、写零的是在扫,写高的在产; throttle、迁移还是杀,取决于这个区分。 注意计数器量的到底是什么:穿过 IO 层的按进程字节,含还没落盘的缓冲写。进程写速率巨大、磁盘看着却闲(页缓存吸收),或速率平平、磁盘打满(大量小同步写),都可能。iotop 跟设备 util 一起读:进程侧定罪加设备侧饱和才是完整画面,任一半单独都误导。 看到谁在扫盘 按带宽排序,那个罪魁就现形了,无论是日志囤积、索引器还是失控备份。跟住这个 pid,就能定位到确切的进程(往往还有文件),再动手不迟。惯犯名单:debug 级别没轮转的日志、排在高峰的重建索引、同盘读写备份、坏镜像上循环解包的容器层。修法各不同——轮转、改期、分开、清理——杀进程治标,病因明天定时再来。 动手前把 pid 解析到完整上下文:命令行、工作目录、lsof 看开文件、父进程。一个烧写的 java 进程背后可能是一个 PID 命名空间里的十个应用;修要修对那一个。先定罪后动手——每个以遗憾收场的存储 war story 都跳过了第一步。 ...

三月 15, 2028 · 1 分钟 · XB Field Notes
三个相连的方块,依次标注观察、决策与行动。

Grafana 变量必须与查询语言的语义一致

Grafana 仪表盘变量让同一组面板能够切换环境、服务或实例,而不必复制多份仪表盘。查询变量从数据源获得选项,自定义变量则可以维护一份有意固定的列表。这两种方式没有绝对优劣:当选项应跟随数据源发现结果变化时,可以使用查询变量;当选择范围就是一份较小且稳定的约定时,维护静态列表同样合理。变量是否合适,取决于它如何表达实际使用需求。 数据源执行查询之前,变量会先被代入查询表达式,所以变量格式本身属于查询正确性的一部分。Prometheus 多选场景应使用正则标签匹配,例如 environment=~"$environment"。Grafana 的 Prometheus 插值逻辑会把选中值处理为正则候选项,并执行所需转义。如果改用精确等号匹配,组合后的文本只会被当成一个标签值,并不会自动变成希望表达的多个环境选择。变量编辑器里的 label_values 是发现选项的辅助语法,不是可以直接复制到面板表达式中使用的 PromQL 函数。 检查最终送出的表达式 这里存在两层语法:正则表达式,以及包裹它的 PromQL 字符串。production.eu 中的点号应表示字面字符,不能成为同时匹配 productionXeu 的通配符。应让数据源按其约定完成插值;使用显式格式覆盖时,要检查真正发送出去的查询。raw 格式会绕过数据源格式化,并不是解决复杂变量值的通用办法。表面上省掉反斜线,可能只是把错误隐藏在返回的数字中。 全部选项也有明确语义。本例没有设置自定义的全部值,因此会展开经过筛选的可用选项。菜单只包含 production.eu 和 staging,全部选择就应覆盖这两个环境。如果把全部值改成点星号等自定义表达式,范围则可能扩大到后台正则允许匹配的其他标签。需要先决定这里的全部究竟指当前菜单中的值,还是更广的查询范围,再用刻意排除的数据验证这种决定。 坏掉的样子 查询变量不会保证永远新鲜,刷新设置决定了 Grafana 何时重新执行选项查询。选项依赖时间范围时,通常还需要随时间范围变化刷新。存在链式变量,也不保证每组上游选择都能找到下游组合;数据本来不存在时,结果完全可能为空。应当区分真实的空集合、已经陈旧的选择和查询错误,不能看到无数据提示,就直接把它解释为服务故障。 变量筛选不是权限控制。菜单里隐藏一个环境,并不会从数据源删除它,也不会阻止其他获准查询请求这些数据。访问边界需要在对应的 Grafana 配置与后台数据服务中落实。分享链接携带的同样只是选中状态,不是权限声明。多个环境通过重复的 var-environment 参数传递,不能随意发明一个逗号拼接的权限列表,再期望后台自动按访问规则理解它。 快速验证 保存代码为 grafana-variables.py,在准备好 Python 和 Docker 后,运行 python3 grafana-variables.py –serve。脚本在专用桥接网络中创建可删除的 Grafana 12.1.0 与 Prometheus 3.5.1 容器,仅通过本机回环地址暴露 Grafana,不挂载宿主机目录。打开输出的仪表盘地址,结束后按控制键加 C,脚本会删除两个容器及本次网络。不带服务参数运行时,只执行后台检查并立即清理。 ...

二月 23, 2028 · 3 分钟 · XB Field Notes
计数器速率估计主题封面,展示观察、判断与行动三个步骤。

Prometheus rate 估计计数器每秒的增长速度

请求计数器回答进程累计记录了多少事件,速率则回答这些事件到来得有多快。Prometheus 的 rate 估计指定范围内每秒平均增长量,修正能够观测到的计数器重置,并向范围边界进行外推。结果具有明确的时间单位,适合观察负载变化;但它不是两次抓取之间全部事件的精确还原,更不能凭空补回没有采到的重启历史。 采样决定能够推断多少事实 普通计数器在重置前持续增长,进程重启是常见的重置原因。相邻已观测样本出现下降时,计数器函数会按重置处理。不过,如果两次抓取之间已经重启并重新累计,而且新值超过旧样本,重置可能完全不可见。提高抓取频率可以提升观测分辨率,但任何查询都不能确定地恢复一段没有被记录的历史。 应先对每条计数器序列求 rate,然后聚合结果。如果先求和,一台实例的增长可能掩盖另一台实例的重置。标签决定序列身份,替换实例使用新标签时,是开始另一条序列,不是自动接续旧实例。队列长度等仪表值本来就允许正常下降,把它们交给计数器速率函数,会把真实下降错误解释为重置。 选择的范围也是指标含义的一部分 范围选择器左开右闭。在第五分钟求 [5m] 时,恰好位于第零分钟的样本不在范围内。对普通浮点计数器求速率至少需要两个可用样本;只有一个样本时得到空结果,并不表示业务流量为零。因此,范围不仅要覆盖正常抓取周期,还需要考虑偶发漏抓后的样本数量,不能仅凭希望曲线更灵敏就无限缩短。 较长窗口通常会平滑短促突发,也让突发离开平均值的时间更晚;较短窗口响应更快,但观测更少,而且可能变成空结果。应依据事件持续时间与检测目标选择范围,并在面板上明确标注。irate 使用最后两个样本,rate 使用所选范围,两者可以针对同一指标回答不同问题,只要面板清楚说明各自含义,不把数值当成同一种口径比较。 规则评估周期决定何时提出查询问题,不决定目标已经被抓取了多少次。如果目标每分钟才采集一次,即使面板每秒刷新,也不会凭空产生新的观测。分析告警响应速度时,需要分别检查采集与评估这两个周期,不能只缩短页面刷新时间。 坏掉的样子 刚创建的序列可能要等第二个样本到来后才出现在速率面板。把所有空值都补成零,可能掩盖采集故障。遇到空白,应先检查原始序列、时间戳和抓取状态,再判断业务是否空闲。另一种问题是窗口太长:短时间过载已经影响用户,但平均线被长窗口稀释,看起来始终平稳,告警因而没有达到阈值。 重置修正不代表缺口对准确性没有影响。漏抓可能隐藏突发或多次重启,范围边界的外推本身也属于估计。increase 以事件数量单位表达相应的范围估计,即使原始计数器只有整数,也可能返回小数。如果场景需要精确账务或计费总数,应依靠业务记账记录,不能把监控面板描述成可以完整重建交易流水的工具。 快速验证 把程序保存为 rate.py,执行 python3 rate.py。实验需要 Docker 与 Prometheus 3.5.1 镜像,镜像未缓存时可由 Docker 下载。程序调用真实 promtool 查询引擎,输入每分钟一份的合成样本。一次性容器没有外部网络,也不挂载宿主机目录,结束时会自动删除;运行结果由引擎断言决定,不是程序自己模拟查询公式。 import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. tests = r"""evaluation_interval: 1m tests: - interval: 1m input_series: - series: "demo_requests_total{instance=\"steady\"}" values: "0+60x5" - series: "demo_requests_total{instance=\"restart\"}" values: "100 160 220 20 80 140" - series: "demo_requests_total{instance=\"gaps\"}" values: "0 60 _ 180 _ 300" - series: "demo_requests_total{instance=\"one\"}" values: "_ _ _ _ _ 7" promql_expr_test: - expr: "rate(demo_requests_total{instance=\"steady\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"steady\"}" value: 1 - expr: "abs(rate(demo_requests_total{instance=\"restart\"}[5m]) - 5/6) < bool 1e-12" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 1 - expr: "increase(demo_requests_total{instance=\"restart\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 250 - expr: "irate(demo_requests_total{instance=\"restart\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"restart\"}" value: 1 - expr: "rate(demo_requests_total{instance=\"gaps\"}[5m])" eval_time: 5m exp_samples: - labels: "{instance=\"gaps\"}" value: 1 - expr: "rate(demo_requests_total{instance=\"one\"}[5m])" eval_time: 5m exp_samples: [] """ rules = r"""""" script = "set -eu\ncat > /tmp/tests.yml <<'TEST_DATA'\n" + tests + "\nTEST_DATA\n" if rules: script += "cat > /tmp/rules.yml <<'RULE_DATA'\n" + rules + "\nRULE_DATA\n" script += "exec /bin/promtool test rules /tmp/tests.yml\n" name = "xbcatm-prom-unit-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 断言全部通过时输出 SUCCESS。稳定序列每秒增加一个请求。对于 100 160 220 20 80 140,五分钟 rate 为 5/6,increase 为二百五十,irate 为一。这些结果不同,是因为范围排除了最初样本,随后修正可见重置,再把所得到的平均增长外推到请求时长;二百五十不能被描述为原始样本直接观测到的增量。 ...

十二月 24, 2027 · 2 分钟 · XB Field Notes
告警状态与通知送达主题封面,展示观察、判断与行动三个步骤。

Prometheus 规则先跟踪告警状态,再交给通知链路

告警规则评估 PromQL 表达式,并持续跟踪表达式返回的标签集。Prometheus 判断每个告警是等待还是触发,Alertmanager 接收告警信息后负责通知路由、分组、静默与抑制。排查时应分别追踪这些环节:规则显示正在触发,是规则状态的证据,不能直接证明某个人已经收到消息。把状态和送达混为一谈,会使漏通知问题很难定位。 表达式返回元素就会激活告警候选项 表达式返回的每个元素代表一个活动告警候选项。queue_depth > 5 这样的比较会过滤不满足条件的数值;但加上 bool 修饰符之后,不满足条件的比较仍会留下数值为零的元素。告警规则依据元素是否存在来激活候选项,因此误用该修饰符,可能让数值已经为零的结果也进入告警流程。这是查询看似合理、规则却持续激活的常见原因。 标签定义告警身份。服务、队列与严重程度等稳定路由维度适合放在标签中,变化中的说明值则适合放在注解中。如果把当前队列大小写进标签,每次数值变化都可能创建一个新身份,重新开始等待。把大小写进注解则可以解释现场状态,同时保留原有告警身份。需要审阅的不只是阈值,还有标签和注解承担的职责。 等待时长通过评估时刻观察 配置 for: 2m 后,一个新出现的标签集会先等待,直到它在评估中持续活动两分钟。如果规则每分钟评估一次,第一分钟首次看到异常,就可能在第三分钟进入触发状态。引擎观察的是评估结果,不是样本之间连续发生的全部物理事实。因此,实际发现时间还包括等待下一次评估的时间,不能只用配置的等待时长计算最坏延迟。 省略 for 时,活动表达式可以在第一次评估就触发。这可能适合含义明确且必须立即处理的故障,但也意味着一个异常样本便足够触发。应根据失败语义主动选择,而不是由于忘记配置而意外采用默认行为。 若表达式在触发前不再返回该标签集,等待状态会清除。本例采用默认恢复行为,已经触发的告警也会在条件消失后清除。显式配置 keep_firing_for 可以在条件消失后继续保持一段触发时间,但本实验没有使用这个选项。不能把所有告警都描述为必须人工确认才能消除的事故记录;规则状态和事故管理系统的关闭流程是不同概念。 坏掉的样子 对缺失序列做阈值比较通常返回空结果。这可能让告警清除,但应用其实已经停止输出指标。应明确缺失是否需要独立规则,不能一律当成恢复。在测试阈值上下变化的同时,也要测试标签变更与样本缺失。语法检查只能证明文件能够解析,并不能揭示这类条件含义错误,更不能证明规则在真实数据中一定符合预期。 告警已经触发却没有页面通知,原因可能是静默、抑制、路由选择、分组等待或发送失败。这些机制属于通知链路,不会改变 Prometheus 对阈值的评估。应先查看活动告警标签,再查看 Alertmanager 匹配了哪个接收器,以及是否真正产生送达证据。延长 for 不会修复接收器配置错误,也不会自动补上路由所需但缺少的标签。 快速验证 把下面程序保存为 alert-state.py,在 Docker 可用时运行 python3 alert-state.py。程序使用 Prometheus 3.5.1,镜像未缓存时可能先下载。真实 promtool 按每分钟一份样本评估队列值 0 6 6 6 0 6 0 0。实验没有启动 Alertmanager,也没有配置任何外部通知目标,所以不会向他人发送测试消息。 ...

九月 1, 2027 · 2 分钟 · XB Field Notes
有界标签与请求标识形成的高基数标签,产生不同规模的直方图序列。

基数是指标的预算

一个很短的指标名,背后可能对应大量存储与查询成本。Prometheus 使用指标名及完整标签集合标识一条时间序列,每出现一种新的实际标签组合,就形成另一条需要采集、建立索引、保留和查询的序列。基数预算关注的是,哪些区别值得付出这笔成本,以及服务增长时,这些区别的数量是否仍有明确上限。它不是单纯要求标签越少越好,而是要求每个维度都有可解释的用途。 统计真正出现的组合 标签维度会扩大可能的组合数,但只有每种组合都实际出现时,各维度取值数的乘积才等于真实数量。两条路由、两种方法、两类状态最多可以形成八种组合;如果每条路由只允许一种方法,导出的组合就可能更少。因此既要计算理论上界,也要核对实际输出,并把实例等采集目标标签纳入服务端最终接收的序列估计。 取值集合比标签名字是否好听更重要。标准化路由可以把同一类订单操作归在一起,而包含每个订单编号的原始路径会持续增加取值。请求编号、时间戳、用户标识以及任意查询字符串,都可能随着事件量增长。开发者把标签叫作路由,并不意味着它真的有界;还要检查真实流量中的值,尤其是错误路径、未匹配路径和很少出现的边界请求,避免正常测试掩盖高基数来源。 把指标表示方式一起算进去 对于每组实际标签,普通计数器通常产生一条主要序列,另外可能存在伴随指标。经典直方图则会展开成多个桶、总和与计数组件。如果有六条有限桶边界,再加正无穷桶、总和及计数,每组标签就对应九条核心序列。有些客户端还会导出创建时间序列,真实清单必须把它算上,不能将用于理解原理的简化公式当成完整埋点总量。 多个副本还会增加实例维度。采集间隔决定每条序列多久写入一个样本,保留期限决定历史数据保存多久,它们与不同序列的数量不是同一概念。不断变化的标识还会制造时序更替:生产端已经消失的短命组合,历史样本仍可能继续留在存储里。因此应分别观察活跃序列、新建速度、写入量、内存和查询表现,不能拿一个固定的“每条序列占多少字节”覆盖所有配置与工作负载。 坏掉的样子 看似方便排障的标签,可能把聚合指标变成逐请求事件仓库。只在仪表盘查询中去掉该标签,无法撤销已经发生的采集和保留成本。对每个标识计算哈希,也不会消除其唯一性;截断随机标识通常只是推迟增长问题,同时引入碰撞。如果业务真正需要的是分类统计,就应选择有明确上限的类别,而不是给无限集合换一种编码方式。 也不能为了降低成本,把识别用户影响所必需的区别全部压平。保留有意义且有界的结果或服务维度,把单次请求调查交给具有相应访问控制的日志和链路记录。如果在采集过程中删除标签,还要检查一次采集内是否出现重复的最终序列,以及诊断信息是否丢失。能够修改生产端时,应优先在那里修正,再验证新标签集合,并明确旧历史序列如何随保留策略逐步退出。 快速验证 保存为 cardinality.py 并使用 Python 3 执行。人为构造的事件把路由、方法和状态的每一种组合重复五十次。一种表示只保留这些有界维度,另一种再增加唯一的请求标识。计算假定二十个副本都导出相同的组合,并使用六条有限边界的经典直方图;这个前提在代码旁明确列出,便于读者核对乘法来源。 from itertools import product routes = ["/orders/{order_id}", "/products/{product_id}"] methods = ["GET", "POST"] statuses = ["200", "500"] events = list(product(routes, methods, statuses, range(50))) bounded = {(route, method, status) for route, method, status, _ in events} with_request_id = {(route, method, status, f"request-{i}") for i, (route, method, status, _) in enumerate(events)} finite_buckets = 6 # Core classic-histogram series: finite buckets, +Inf, _sum, _count. # Optional _created series are deliberately excluded from this calculation. series_per_combination = finite_buckets + 3 replicas = 20 bounded_series = len(bounded) * series_per_combination * replicas request_id_series = len(with_request_id) * series_per_combination * replicas assert len(events) == 400 and len(bounded) == 8 assert len(with_request_id) == 400 assert bounded_series == 1440 and request_id_series == 72000 print(f"observed label sets: bounded={len(bounded)}, request_id={len(with_request_id)}") print(f"core classic-histogram series: bounded={bounded_series}, request_id={request_id_series}") print(f"multiplier={request_id_series // bounded_series}x") 四百条事件在有界表示下被聚合为八种组合,带请求标识时则保留四百种。扩展到二十个副本后,两者分别产生一千四百四十条与七万二千条核心直方图序列,相差五十倍。示例明确排除了可选创建时间序列和其他指标族,因此输出是给定假设下的序列数量模型,不是对 Prometheus 内存或磁盘占用进行过真实压测的结果。 ...

七月 21, 2027 · 1 分钟 · XB Field Notes
加权服务概览、故障区域与明确标注的数据缺失状态并列。

看板是总结,不是放大镜

一张有用的看板,应该帮助读者决定接下来检查什么。它把大量观测归纳成少数几个问题:用户操作是否成功,影响发生在哪里,资源是否正在逼近限制。但归纳不能抹掉重要区别。全局成功率仍然很好看时,一个区域可能已经严重失败;面板空白也可能是遥测缺失,而不是服务处于安静且健康的状态。看板的价值在于减少重新推理的负担,同时保留做判断必需的上下文。 给页面安排清楚的阅读顺序 先展示服务结果和观测覆盖,再给出流量、延迟、错误以及有关资源的饱和信息。如果区域、操作类型或依赖的差异会改变下一步行动,就提供对应下钻入口。变更标记和诊断资料链接也应放在相关视图附近。曲线同时变化可以帮助选择调查方向,但仅凭发生时间接近,还不能证明某次部署或某个依赖导致了事故,仍需进一步验证真实链路。 每个面板应该回答明确的问题,但不要求只放一个指标。队列长度与配置上限使用相同单位,并列比较就很有意义;把不同单位放进复杂双轴图,则容易制造没有根据的视觉关系。应直接说明单位、聚合窗口、统计总体以及阈值含义,让读者不必凭记忆猜测坐标表示毫秒、秒、零到一比例还是百分数。含义清楚通常比增加面板数量更重要。 保留请求权重和观测覆盖 计算服务错误比例时,应使用相同窗口中的全部不良事件数除以全部符合条件的事件数。直接平均区域百分比,会让很安静的区域与繁忙区域获得相同权重,这已经换成另一种统计量,不是无关紧要的显示方式。即使全局值按请求正确加权,当小范围用户仍属于服务承诺的一部分时,也需要展示能定位影响的区域或操作细分。 新鲜度同样决定统计范围。应展示最近一次成功观测的时间,或者明确的覆盖信号,并约定何时把陈旧数据视为未知。业务看板选择的新鲜度阈值,与存储系统处理时序失效的机制是不同概念。把缺口一律补零,可能让已经不可用的采集器表现成完美错误率或空队列。缺失状态应被保留下来,并提供下一步排查采集链路或服务本身的方向。 坏掉的样子 自动缩放的纵轴可能把微小变化放大成重大事故,过宽的固定范围又可能掩盖真正越过阈值的变化。应根据运维问题选择刻度,让需要比较的面板保持一致。平均延迟还会藏起慢请求群体,需要合适的分布与观测数量作为补充;不能直接平均不同实例已经计算出的高分位数,再把它当成整个服务的尾部延迟。 外观漂亮的页面也可能只是一些互不相关的快照。时区、过滤条件、刷新频率和聚合窗口不同,会妨碍连贯比较。共享变量应有明确默认值和可见范围,尤其要让读者知道当前是否查看生产环境以及哪些区域。应拿区域故障、低流量和观测缺失来检查看板,如果三种场景都表现为绿色,就说明页面在压缩信息时丢掉了做决策所需的关键事实。 快速验证 将代码保存为 dashboard.py 并用 Python 3 执行。它基于一个完整六十秒窗口中的模拟计数,计算可以用于面板展示的值。两个区域有新鲜观测,另一个区域既缺少数值,最近观测时间也较旧。九十秒阈值是示例看板的业务规则,并不是 Prometheus 默认行为;验证这部分聚合逻辑不需要安装 Grafana,也不会操作任何现有仪表盘。 import json from fractions import Fraction # Counts cover the same completed 60-second interval. rows = [ {"region": "west", "requests": 9800, "errors": 0, "age_seconds": 10}, {"region": "east", "requests": 200, "errors": 100, "age_seconds": 10}, {"region": "south", "requests": None, "errors": None, "age_seconds": 240}, ] fresh = [r for r in rows if r["age_seconds"] <= 90 and r["requests"] is not None and r["errors"] is not None] missing = [r["region"] for r in rows if r not in fresh] requests = sum(r["requests"] for r in fresh) errors = sum(r["errors"] for r in fresh) regional = {r["region"]: Fraction(r["errors"], r["requests"]) for r in fresh} known_ratio = Fraction(errors, requests) unweighted = sum(regional.values()) / len(regional) assert known_ratio == Fraction(1, 100) assert regional["east"] == Fraction(1, 2) and unweighted == Fraction(1, 4) assert missing == ["south"] print(json.dumps({ "coverage": "incomplete", "fresh_requests_per_second": round(requests / 60, 3), "fresh_error_percent": float(known_ratio * 100), "regional_error_percent": {k: float(v * 100) for k, v in regional.items()}, "wrong_unweighted_percent": float(unweighted * 100), "unknown_regions": missing, }, indent=2)) 新鲜观测合计一万次请求、一百次错误,对应每秒约一百六十六点六六七次请求,以及按请求加权后的百分之一错误率。然而流量较小的东部区域有一半请求失败。若直接平均两个区域的百分比,会错误地把合并请求错误比例写成百分之二十五。正确方式是让百分之一的总体值与区域细分共同出现,而不是挑选某一个数字来遮住另一个事实。 ...

七月 18, 2027 · 1 分钟 · XB Field Notes
对外采集器标签主题封面,展示观察、判断与行动三个步骤。

外部标签标识 Prometheus 对外发送的数据

多个 Prometheus 向同一系统提供数据时,下游需要知道信息来自哪个采集器或环境。全局 external_labels 可以在与外部系统通信时补充这些上下文。但它不会重写每条本地存储序列,也不会覆盖对外序列中已经存在的同名标签。理解这两条边界,才能避免用错误的查询位置判断配置是否生效,也能避免误以为全局值一定压过目标值。 本地存储与对外身份并不相同 本地查询 up 通常看到抓取和重新标记产生的标签,外部标签 prom_region 并不一定出现。它没有出现在本地查询中,不能证明配置失败。应将适当的对外接口与本地结果比较;联邦接口提供了一种直接观察差异的方法,无需为了验证这个行为先接入远端存储系统。检查位置必须与希望验证的机制对应。 配置契约也涉及远端存储和告警等外部通信,但各条下游路径还可能有自己的重新标记与处理步骤。验证某条路径时,需要观察它实际到达的消费者。本地评估的记录规则不会因为保存了一条新序列,就自动获得所有外部标签。外部标签在相关通信边界补充,不是统一改写所有本地查询结果的机制。 序列已有的标签优先 如果目标序列已经具有 cluster="target-cluster",全局外部标签配置为 cluster="collector-cluster",不会把目标值替换掉。外部标签补充的是缺少的标签名称。这是已定义的行为,不能编造成新版本遇到冲突必定拒绝或记录错误。因此,本地指标对外导出时保留的集群值,可能不同于操作人员以为全局配置会强制设置的值。 需要区分目标身份与采集器身份时,应约定独立的采集器标签名,并检查导出器和重新标记规则是否与之冲突。联邦场景中,目标端的 honor_labels 还决定如何处理源标签与本端抓取标签的冲突。这个接收端行为,与源端如何向联邦响应补充外部标签是两件事,不能因为一侧正确便跳过另一侧检查。 稳定标签不会自动实现去重 改变全局标签值,会改变之后导出序列的身份。已经保存在下游的历史仍在旧标签下,不会被重写。修改选择器和面板时,应考虑这条历史边界。添加很多标签也不必然有益:固定标签提供上下文,不同采集器的不同取值则增加消费者需要处理的身份区别,应根据实际查询需求决定,而不是把能写的字段都写进去。 两个采集器抓取相同目标时,可能在不同副本标签下导出重复观测。直接跨副本求和,会让流量看起来翻倍。外部标签让这些副本可以被区分,却不会替下游配置高可用去重。标签也不是租户授权边界;如果存在隔离要求,应核对存储系统真实的权限和查询行为,不能仅依赖名称看起来来自不同环境。 坏掉的样子 团队可能在本地查询中寻找外部标签,找不到便反复修改本来有效的配置。相反,也可能看到联邦输出带有预期标签,就认为远端写入和告警路由都已验证。应检查具体路径和最终标签,包括下游重新标记的影响,而不是把一个接口的结果当成全部集成已经成功的证据。目标标签与全局值相同的测试,还会掩盖真正的冲突优先级。 快速验证 保存为 external-labels.py 后执行 python3 external-labels.py。实验需要 Docker 与 Prometheus 3.5.1 镜像,未缓存时可能由 Docker 下载。单个一次性 Prometheus 抓取自己,随后比较本地查询响应与文本格式的联邦输出。所有 HTTP 请求都停留在容器回环地址,不发布宿主机端口,也没有外部网络访问。 import json import re import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. # All HTTP traffic stays on loopback inside one disposable container. script = r"""set -eu cat > /tmp/prom.yml <<'CONFIG' global: scrape_interval: 1s scrape_timeout: 500ms external_labels: cluster: collector-cluster prom_region: lab-region replica: one scrape_configs: - job_name: self static_configs: - targets: ['127.0.0.1:9090'] labels: cluster: target-cluster CONFIG /bin/prometheus --config.file=/tmp/prom.yml --storage.tsdb.path=/tmp/data \ --web.listen-address=127.0.0.1:9090 > /tmp/prom.log 2>&1 & for attempt in $(seq 1 30); do if wget -qO /tmp/ready.json 'http://127.0.0.1:9090/api/v1/query?query=up%20%3D%3D%201'; then if grep -q '"status":"success"' /tmp/ready.json && ! grep -q '"result":\[\]' /tmp/ready.json; then wget -qO /tmp/local.json 'http://127.0.0.1:9090/api/v1/query?query=up' wget -qO /tmp/federated.txt --header='Accept: text/plain; version=0.0.4' \ 'http://127.0.0.1:9090/federate?match%5B%5D=up' cat /tmp/local.json printf '\n' cat /tmp/federated.txt exit 0 fi fi sleep 1 done cat /tmp/prom.log >&2 exit 1 """ name = "xbcatm-prom-live-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if result.returncode: raise RuntimeError(result.stderr or result.stdout) local_json, _, exported = result.stdout.partition("\n") payload = json.loads(local_json) assert payload["status"] == "success" local = next(a["metric"] for a in payload["data"]["result"] if a["metric"]["job"] == "self") line = next(line for line in exported.splitlines() if line.startswith("up{")) # The fixture's controlled label values contain no quotes or escape sequences. outbound = dict(re.findall(r'(\w+)="([^"]*)"', line)) assert outbound["prom_region"] == "lab-region" and outbound["replica"] == "one" assert outbound["cluster"] == local["cluster"] == "target-cluster" assert "prom_region" not in local and "replica" not in local print(json.dumps({"federated_labels": outbound, "local_labels": local}, indent=2)) 本地标签包含 cluster="target-cluster",但没有 prom_region 和 replica。联邦输出补入 prom_region="lab-region" 与 replica="one",同时保留原有集群值。程序对这三个事实分别断言。解析器仅处理本实验中不含引号和转义序列的受控标签值,不是能够解析任意指标响应的通用组件,不能直接拿去处理未知导出内容。 ...

六月 9, 2027 · 2 分钟 · XB Field Notes
指标失效与告警等待主题封面,展示观察、判断与行动三个步骤。

Prometheus 序列失效与告警等待是不同机制

指标缺失不等于数值为零。Prometheus 会区分具有近期值的序列、已经标记失效的序列,以及找不到可用样本的选择器;告警又在查询结果之上使用自己的评估和等待规则。如果把这些机制统称为一个宽限时间,就可能漏掉被移除的目标,或者没有弄清到底缺少什么数据便宣布服务故障。准确的缺失告警首先需要准确描述观测对象。 失效标记不是统一的五分钟计时器 即时选择器通常使用回看范围内最新的合格样本,默认回看范围是五分钟。失效标记会告诉引擎:在标记之后,不再让这条序列出现在即时选择结果中。它不要求旧值再保留五分钟。历史样本仍然存在于存储中,也可能继续参与范围查询;因此,即时查询已经看不到某条序列,与历史窗口里完全没有样本,并不是同一件事。 具体采集路径会影响行为。导出器不再输出某个指标、目标从服务发现消失,以及导出器自己提供样本时间戳,不一定遵循相同的时间过程。显式时间戳和失效跟踪配置也会改变结果。在依据抓取周期推测消失时刻之前,应先确认导出器使用哪条路径。可配置的回看范围本质上是查询选择规则,不是能够统一判定所有服务故障的计时器。 先确定要询问哪一种缺失 absent(selector) 在选择器没有任何元素时返回一;只要仍有匹配数据,即使数值为零,也返回空向量。absent_over_time(selector[3m]) 则询问所选三分钟范围内是否存在样本。失效标记之前的真实样本,可能继续留在这个范围内,使范围缺失表达式保持为空,直到该样本移出窗口。这不是查询互相矛盾,而是问题的时间范围不同。 告警规则的 for 又是一层机制:相关标签集必须在连续的评估中持续由表达式返回,经过等待时长后才进入触发状态。它不会配置序列失效,也不会延长查询回看范围。把三分钟缺失窗口再配上额外等待时间,会进一步延后通知,评估节奏和采集时机也影响最终延迟。选择这些参数时,应从允许的发现时间反推,而不是只把它们当作降噪旋钮。 坏掉的样子 absent(up{job="api"}) 不是抓取失败告警。若已配置目标仍存在,只是 up=0,选择器仍有元素,缺失条件便不成立。与零比较才能检查这个场景。但如果目标彻底从发现清单消失,只检查零又可能在其序列消失之后漏报。对目标是否应当存在有要求时,需要另行监测预期成员关系,不能让一个表达式混合承担所有职责。 宽泛的选择器也不会自动列举所有缺少的成员。一个副本仍有数据时,对整个任务求 absent 仍为空,即使其他几个预期副本已消失。可以匹配明确已知的身份,或者将观测序列与权威清单比较。标签被改名同样可能使旧匹配条件看起来像发生故障,而进程其实正在以新身份正常工作,所以规则与标签变更需要一起审阅。 快速验证 把程序保存为 stale.py,在 Docker 可用时执行 python3 stale.py。程序使用公开的 Prometheus 3.5.1 镜像和真实 promtool 查询;镜像未缓存时,Docker 可能先进行下载。输入由显式失效标记、缺失位置和正常零值组成。本实验验证查询语义,不测量真实服务发现系统在墙上时钟中需要多久才移除一个目标。 import subprocess import uuid # Requires Docker and the public Prometheus 3.5.1 image. tests = r"""evaluation_interval: 1m tests: - interval: 1m input_series: - series: "up{job=\"api\",instance=\"gone\"}" values: "1 0 0 0 0" - series: "service_events_total{job=\"api\",instance=\"gone\"}" values: "10 stale _ _ _" - series: "service_events_total{job=\"api\",instance=\"zero\"}" values: "0 0 0 0 0" promql_expr_test: - expr: "service_events_total{job=\"api\",instance=\"gone\"}" eval_time: 1m exp_samples: [] - expr: "up{job=\"api\",instance=\"gone\"}" eval_time: 1m exp_samples: - labels: "{__name__=\"up\",job=\"api\",instance=\"gone\"}" value: 0 - expr: "absent(service_events_total{job=\"api\",instance=\"gone\"})" eval_time: 1m exp_samples: - labels: "{job=\"api\",instance=\"gone\"}" value: 1 - expr: "absent_over_time(service_events_total{job=\"api\",instance=\"gone\"}[3m])" eval_time: 2m exp_samples: [] - expr: "absent_over_time(service_events_total{job=\"api\",instance=\"gone\"}[3m])" eval_time: 4m exp_samples: - labels: "{job=\"api\",instance=\"gone\"}" value: 1 - expr: "absent(service_events_total{job=\"api\",instance=\"zero\"})" eval_time: 4m exp_samples: [] - expr: "absent(up{job=\"api\"})" eval_time: 4m exp_samples: [] """ rules = r"""""" script = "set -eu\ncat > /tmp/tests.yml <<'TEST_DATA'\n" + tests + "\nTEST_DATA\n" if rules: script += "cat > /tmp/rules.yml <<'RULE_DATA'\n" + rules + "\nRULE_DATA\n" script += "exec /bin/promtool test rules /tmp/tests.yml\n" name = "xbcatm-prom-unit-" + uuid.uuid4().hex[:12] try: result = subprocess.run( ["docker", "run", "--rm", "--name", name, "--network", "none", "-i", "--entrypoint", "/bin/sh", "prom/prometheus:v3.5.1", "-s"], input=script, text=True, capture_output=True, timeout=45, ) finally: subprocess.run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 成功时输出 SUCCESS。第一分钟,已经标记失效的业务序列不再被即时选择,但单独提供的 up 序列仍存在,数值为零;即时 absent 返回一。第二分钟,三分钟范围仍包含最初的业务样本,因此 absent_over_time 不返回元素;到了第四分钟,这个样本已不在范围内,表达式才返回一。程序分别断言每种结果,而不是只检查命令退出。 ...

二月 3, 2027 · 2 分钟 · XB Field Notes