失败是数据:工具运行时的失败采集与可观测性实践
如果你也在维护一个工具运行时大概率会对着监控屏跳过这行日志“获取首页数据失败: exception: 伺服器错误 502: !doctype htmlhtml lang...” 更头大的是那句“建立安全连接失败 由于不能验证所收到的数据是否可信无法显示您想要查看的页面”。我在开发 P04 工具运行时的过程中几乎每天都要和这类失败打交道。起初我也习惯看到失败就重试、清缓存后来我彻底换了一个思路失败是数据不是敌人。P04 最大的改版点就是给所有外部调用统一装了一个失败采集器让每一个异常都变成可检索、可统计、可归因的结构化样本。这篇文章就把 P04 在“失败是数据”这条线下的设计思路、落地细节和踩坑记录完整盘一遍适合正在做自动化跑批、爬虫调度、API 网关类工具的同学参考。P04 本身是一套跑批型工具运行时内部负责调度、执行、数据采集、通知分发每天要跑几百个任务大部分时间在做 HTTP 接口调用、公开页面抓取、HTML/JSON 解析这类脏活。外部环境不可控502、证书校验失败、超时、解析断裂几乎是家常便饭。这篇文章的核心不是劝你把失败消灭干净而是说清楚怎么把失败变成数据再用数据驱动重试、熔断、告警和解析器更新。1. 项目概述与设计思路为什么失败必须被当作数据对待1.1 P04 工具运行时到底在解决什么问题P04 解决的核心问题是当外部依赖不稳定时工具运行时不能跟着一起摆烂。早期版本里每个任务都有自己的错误处理逻辑A 任务超时打一行 logB 任务解析失败直接 throwC 任务 502 后原地重试三次。表面上看每个任务都“处理”了失败实际上没有一个地方能把失败汇总成全景视图出了问题只能一台机器一台机器翻日志。后来我把 P04 重新定义成“带失败可观测性的执行环境”。它不关心你的任务逻辑是什么只关心三件事任务有没有按预期跑完、如果没跑完失败发生在哪个阶段、失败样本能不能被结构化留存。这样的设计让运行时和业务解耦业务方只需要遵循统一的异常上报协议其余交给 P04 去收尾。1.2 为什么“失败是数据”而不是“异常”传统编程语言里异常是一个需要被捕获和处理的控制流分支。try/catch 一包打印完 exception 就没了失败信息只存在于日志文件里既不能压缩成指标也不能做多维对比。把失败当数据意味着我们要给每个失败建立字段、写入存储、定期统计、长期追踪。这个差别很像交通事故记录异常模式只记录“发生了碰撞”数据模式则要记录碰撞地点、车速、天气、路况、车辆型号和驾驶员状态。拿热词里的两个典型失败来看。502 和“建立安全连接失败”在异常模式下都是“请求失败”但在数据模式下是两个完全不同的物种一个偏向网关/上游服务问题一个偏向 TLS 证书校验问题。前者适合重试加退避后者重试一百次也没有用。只有把失败标准化成数据才能让后续的自动化策略拥有判断依据。1.3 三个设计目标稳定、可观测、可自愈P04 在“失败是数据”这条线上设了三个目标。第一是稳定任何单点失败都不能拖垮整个运行时第二是可观测任何失败都能在十分钟内被定位到具体阶段和原因第三是可自愈运行时能基于失败数据自己决定是重试、熔断还是告警。其中一个很容易被忽略的点是“失败本身也要稳定”。采集失败的模块如果在高并发下把存储打爆那失败数据反而会变成新的灾难来源。所以 P04 的失败数据链路设计成了两段式写盘先落到本地临时文件再异步合并到中心存储。这样即使中心存储不可用失败样本依然保得住等存储恢复后重放不会漏掉任何一条关键线索。2. 失败数据标准化从一行日志到一条结构化记录2.1 一条失败样本应该包含哪些字段要回答“失败是数据”先得定义什么叫“数据”。P04 里一条失败样本至少包含以下字段样本 ID、时间戳、所属任务、目标 URL、失败阶段、错误类型、错误码、消息指纹、消息摘要、响应头、响应片段、重试次数、耗时、处理动作、额外上下文。不要觉得字段多真正排查问题的时候少一个字段就可能少一条线索。这些字段的设计逻辑很简单时间戳用来做趋势任务和 URL 用来定位范围阶段用来区分“连接坏了”还是“数据坏了”错误类型和错误码用来做分类消息指纹用来聚合相似问题响应片段用来观察上游返回的原始内容。P04 落地时还加了一个“resolved_by”字段记录这次失败最终是由重试解决、熔断解决还是人工介入解决这能让后面的改进策略越跑越准。2.2 用统一结构收纳失败样本一个可直接抄的代码框架统一结构的第一步是把散落的 exception 转成一个 dataclass。下面是一个简化版示例from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any dataclass class FailureRecord: source_task: str target_url: str stage: str # dns / connect / tls / request / parse / validate error_type: str # HttpError, SSLError, Timeout, ParseError error_code: str # 502, 403, CERTIFICATE_VERIFY_FAILED, ... message_hash: str # 对归一化后的msg做hash用于聚类 message_summary: str # 截断后的关键错误文本 raw_headers: dict field(default_factorydict) response_snippet: str # 只保留前512个字符 retry_count: int 0 latency_ms: int 0 resolved_by: str extra: dict field(default_factorydict) created_at: datetime field( default_factorylambda: datetime.now(timezone.utc) )实际捕获异常时不要只做 log 打印。要把requests.exceptions.RequestException这类异常解构成上面的字段。如果是 HTTP 错误从response.status_code和response.headers拿数据如果是 SSL 错误从certificate verify failed这种消息里提取具体原因如果是解析失败把解析器收到的原始片段存进response_snippet。等这一步做完一条失败样本才是“数据”而不是一句人眼才能看懂的日志。2.3 标准化过程中最容易踩的三个坑第一个坑是消息截断。很多人只存异常str(e)的前 200 个字符但像“建立安全连接失败 由于不能验证所收到的数据是否可信”这类消息真正的证书信息在后面。建议截断策略分两层普通错误摘要截 512 字符响应片段单独存且最多 1024 字符关键头部如Server、Via、X-Request-Id必须完整保留。第二个坑是敏感信息混入。目标 URL 可能带 token、签名参数响应片段里可能含用户个人信息或 Cookie直接入库等于把隐私写到磁盘上。P04 做了一层脱敏URL 中的 query 参数按配置过滤响应片段里匹配到token、cookie、authorization等关键词后用***替换。第三个坑是时间戳不带时区。如果用本地时间存储跨机器统计时会发现 0 点到 1 点的数据缺了一截。P04 统一用 UTC 存储展示到页面时再转换成本地时间所有按小时聚合的报表都必须基于 UTC 字段。3. 典型失败样本分诊从 502 到证书验证失败3.1 “伺服器错误 502”的分层判断502 是一个很“虚”的状态码它只能告诉你网关出了问题不能告诉你网关后面发生了什么。可能是上游服务崩溃可能是负载均衡把请求转发到了一个正在滚动的节点可能是网关读超时也可能是上游返回了非法的响应头。P04 拿到 502 后第一件事不是重试而是看失败样本里有没有Via和Server头、有没有网关日志里的upstream_status再结合最近十分钟这个 URL 的失败率判断。如果同一时刻其他任务调同一个上游都失败基本可以判定是服务端故障此时重试只会增加上游压力。P04 对这种场景的默认策略是第一次 502 记录样本并快速重试一次连续三次以上或失败率超过阈值触发熔断停止继续调用并告警。如果只是单个任务偶发 502自动重试加指数退避通常就能消化掉。3.2 “建立安全连接失败 由于不能验证所收到的数据是否可信”到底是哪里出了问题这句话是典型的 TLS 证书校验失败提示。程序里通常对应SSLCertVerificationError或CERTIFICATE_VERIFY_FAILED。出现这个错误意味着本地的 CA 信任链里找不到能验证对方证书的可信根或者证书已过期、域名不匹配、证书链不完整。有些环境的网络设备会替换证书做内容审计也会导致同样的报错。处理这种失败P04 的底线是绝不开verifyFalse去绕过校验。关闭证书校验等于把 HTTPS 降级成明文这个口子一开后续所有失败样本都会失去真实性。正确做法是分环境处理对内网自建服务把对应根证书打包进运行时的ca_bundle用verifycustom_bundle的方式认证对外部公开服务如果证书校验失败当作基础设施级故障记录并告警由运维侧更新信任链。3.3 用失败数据快速定位与自愈的流程当失败数据已经标准化后定位流程可以做到半自动化。P04 的规则引擎是这样跑的先解析错误类型再按“目标域名 错误类型 消息哈希”聚合同类失败接着看该聚合维度最近一小时的成功率和失败量最后执行决策表里的动作。决策表并不复杂核心就几行网络超时且失败率低延迟重试502/503 且失败率高熔断并告警证书错误且不是内网域名告警且不重试JSON 解析错误写入解析器变更队列HTTP 403/404 且之前成功过升级为契约变更。这套动作让运维同学不用再盯着每一条报错只需要处理被数据筛选出来的异常样本。3.4 失败样本统计分析表把热词变成可执行的指标为了直观理解失败分布P04 每天会生成一张统计表。下面是一天内的示例数据错误类型出现次数主要目标建议动作HTTP 502137外部资讯源重试退避失败率超过40%熔断TLS 证书校验失败12某个第三方接口更新信任链不自动重试连接超时58多个慢接口调整超时时间开启慢请求告警JSON 解析失败9内部数据平台通知开发更新解析器字段缺失31爬虫列表页触发内容结构变更工单这张表的价值在于过去“报错多”只能换来一句“网络不好”现在可以用占比、趋势、目标域来指认具体责任人。502 很多不代表网络差可能只是某个上游下午发生了滚动发布证书错误出现 12 次就说明信任链已经坏了一段时间需要马上修。4. 失败驱动的改进闭环把失败数据变成运行时免疫力4.1 失败分类瞬时故障、配置故障、结构变化要做闭环第一步是对失败样本进行业务层面的分类。P04 把失败分成三类瞬时故障、配置故障、结构变化。瞬时故障包括网络抖动、偶发超时、暂时性 502特点是恢复后任务可以继续配置故障包括证书失效、鉴权失败、请求参数被改重试往往没用得主动修配置结构变化是数据和契约层的失败比如 HTML 改版导致字段抓不到、API 少返回了必填字段。这种分类不是人为拍脑袋而是由失败样本里的stage和error_type映射出来的。stageconnect时大概率是瞬时或配置问题stageparse时基本是结构变化。P04 会把两类问题分发给不同的处理通道瞬时故障走运行时策略结构变化自动创建变更工单配置故障直接进运维队列。4.2 动态重试与熔断不要一把梭用“固定重试三次”固定重试三次是最常见的错误姿势。如果上游已经过载三次重试等于三次补刀。P04 改用“按窗口失败率”的熔断策略每个目标域名维护一个滑动窗口窗口默认 120 秒窗口内失败率超过 50% 就熔断 30 秒期间请求直接失败并记录原因不再发送给上游。熔断结束后进入半开状态放少量请求去试探成功率达到预期再把流量恢复。重试间隔用指数退避加随机抖动。基础延迟 1 秒每次翻倍上限 60 秒。增加随机抖动是为了防止大量任务在同一时间点同时重试否则退避算法反而会制造一波新的峰值。这段逻辑不复杂但没有失败数据的支撑阈值根本定不出来因为你不知道“正常波动”和“异常雪崩”的分界线在哪。4.3 告警阈值失败率与失败形态哪个更值得盯很多监控系统只盯全局失败率99% 的时间没有意义真出问题的时候又会因为抖动被淹没。P04 的经验是双维度并行全局维度看失败率持续时间比如失败率超过 3% 并持续 3 分钟才触发 P2 告警单体维度看单任务或单错误类型的失败形态比如同一任务连续失败 5 次或某个错误类型 30 分钟内出现 10 次。失败形态比失败率更能反映真实问题。一个高频接口偶尔失败 5 次失败率不高但对用户的影响不低一个低频任务失败 3 次失败率看起来很高实际影响很小。P04 把所有失败数据投喂给告警判断器用“失败量 失败率 错误类型”三个维度加权计算告警级别这样出来的告警才能直接转给对应负责人。4.4 从失败样本中提取“结构变化信号”这是 P04 最让我惊喜的部分。最初我只把失败当数据来自动重试后来发现失败样本里藏着上游结构变化的信号。当同一个 URL 的历史失败消息是指“超时”或 “502”突然变成“字段缺失”或“HTML 解析为空”说明上游页面或接口刚改过版网络层没有问题是契约变了。P04 会为这种信号单独建一个队列队列里的样本自动带上response_snippet和解析器版本号。开发同学只要打开这个队列就能直接对比“解析器期望的结构”和“上游实际返回的结构”不用再去线上抓包。后来我们甚至给这个队列加了一个简单的差分提示如果原始 HTML 长度比历史成功样本短 30%直接标记为“页面结构异常”。这一步让失败数据真正的价值被用起来了。5. 实操记录P04 失败数据平台的落地细节5.1 数据链路从异常产生到分析视图P04 的失败数据链路可以分为五个环节产生、采集、清洗、落库、分析。异常在任务执行器里产生由统一异常钩子捕获采集器负责把异常转成FailureRecord并先写本地队列清洗器做字段校验、脱敏、补全阶段信息清洗完的数据写入 SQLite 的failures表分析层通过 SQL 视图和定时任务生成统计报表。这个链路看起来繁琐实际跑起来并不重。小规模运行时不需要上 Kafka 或 ClickHouseSQLite 加上几个索引足够支撑每天几万条失败样本。等到单日样本量超过百万级再考虑换成列式存储。P04 的经验是先跑通完整链路再考虑性能优化千万别为了上大数据组件而牺牲迭代速度。5.2 关键配置与参数选择让失败数据链路稳定运行几个参数需要认真调配置项推荐值选择理由失败样本响应片段长度1024 字符足够看到 HTML 结构变更又不会撑爆存储失败样本保留时间明细 7 天汇总 90 天既能排查近期问题又能看趋势本地队列上限5000 条防止采集模块自身拖垮运行时重试基础延迟1 秒对小跑批任务足够快又不会压制上游熔断失败率阈值50%低于这个值多半是抖动高于就要主动保护敏感字段token, cookie, authorization脱敏必须前置不能入库后再补这些参数不是死的。P04 在早期把失败样本保留时间设为 30 天结果磁盘涨得很快后来改成明细 7 天、汇总 90 天的组合排查能力没有下降存储成本却降了一大半。5.3 轻量可视化不引入重型 BI 直接看趋势失败数据如果不能被方便地查看等于没做。P04 没有上 Grafana 以外的重型 BI而是直接用 FastAPI 起了一个轻量看板核心只有两个页面失败清单和失败趋势。失败清单支持按任务名、错误类型、时间范围过滤失败趋势展示最近 24 小时按小时的失败量聚合曲线。背后的 SQL 可以写成这样SELECT error_type, count(*) AS failure_count, count(DISTINCT target_url) AS url_count, date_trunc(hour, created_at) AS hour_slot FROM failures WHERE created_at now() - interval 24 hours GROUP BY error_type, hour_slot ORDER BY hour_slot DESC;这几行 SQL 足够支撑第一次复盘。后续如果要加维度直接在GROUP BY里增加字段不需要改数据采集端。先让数据看得见再谈自动化。5.4 从失败数据中更新解析器的实际案例分享一个 P04 实际经历的例子。某个列表页任务连续成功两周某天开始失败样本全部指向stageparse错误消息是“字段缺失”。看response_snippet发现页面仍然返回了 HTML但列表容器里一件商品都没有。如果只按成功失败判断这个任务会被标记为失败但真正的原因是页面改版后默认展示空态。我们随后更新了解析器逻辑解析前先判断 HTML 长度和空节点特征如果确认是空态页面不再浪费时间解析而是记录一条“上游空态触发”的样本并把任务标记为“成功空数据”。这样一来失败数据被消化成了运行时规则后续同样的问题不会再打扰到告警系统也让业务方更早知道了页面改版的影响范围。6. 常见问题与排查技巧实录6.1 失败数据积压导致存储膨胀怎么办失败数据也是数据不控制就会成为垃圾。最容易膨胀的是response_snippet如果每个样本都存完整 HTML几天就能撑满磁盘。P04 的做法是分级采样详细样本每类错误每天最多保留 200 条其余只保留统计聚合信息对超大响应体只截取出现目标关键字位置前后各 200 字符。另外定期清理一定要纳入调度。SQLite 里可以直接用定时任务删除七天前的明细记录再用汇总表把小时级数据聚合为天级数据。注意清理时不能简单DELETE FROM failures WHERE created_at ...一把梭要按批次删除避免长时间锁表导致写入阻塞。6.2 错误信息相似但根因不同如何区分“ConnectionError”这个词可以同时掩盖 DNS 解析失败、TCP 连接被重置、TLS 握手失败、上游主动断连四种完全不同的根因。如果只看str(e)会把不同问题聚到同一个桶里后面所有统计都会失真。P04 使用“错误指纹”来区分把error_type stage error_code 关键异常信息片段组合成一个哈希用哈希做聚合。比如同样是 “ConnectionError”stagetls和stageconnect会被分开同样是“超时”connect_timeout和read_timeout也要分到不同的小类。这个指纹一旦建好再遇到“看起来一样”的报错就能追到真正的根因分类。6.3 采集模块自身抛异常如何兜底这是一个容易被忽视的边界场景。P04 里失败采集本身也会失败比如中心存储连不上、本地磁盘写满、序列化出错。如果不处理采集器挂了之后新的失败会彻底消失系统反而变得更不可观测。解决办法是两段式写盘加降级。采集器先把FailureRecord序列化成 JSON 行写入本地failures_pending.log每 10 秒批量合并一次。如果合并失败文件继续累积等网络恢复后再从文件末尾继续重放。同时采集器和主执行流程之间用队列隔开采集器的处理时间不阻塞正在跑的业务任务。只要写盘成功这条失败数据就算保住了。6.4 如何向团队解释“失败是数据”如果团队还停留在“异常就是 bugbug 就要消灭”的阶段这套思路推起来会比较吃力。我的建议是用体检来打比方一个人不会因为一次血压偏高就觉得自己完蛋了但需要通过多次血压记录来判断是偶发还是持续问题。失败的日志就像血压读数单看没有意义连成趋势才能说明问题。落地时先不要让大家改所有代码只要求一件事所有外部调用统一走运行时提供的safe_call入口把异常交给入口包装。当新的失败样本开始出现在看板上团队自己就会提出“这个错误类型怎么这么多”“能不能自动重试”。到这一步“失败是数据”就不再是一句口号而是大家每天都在用的工作方式。P04 这套系统跑了大半年我最深的体会是失败并不会消失它只是不停地在变形状。过去我们把失败当成测试没写好的证据现在我把每一条失败样本都当作运行时给出的体检报告。真正有价值的不是那行“伺服器错误 502”而是它背后可以追踪到的时间、目标、阶段、指纹和趋势。如果你也在做类似的工具运行时不妨从明天开始给异常处理加一个结构化字段再坚持存两周数据你会看到另一个世界。