~/blog/one-person-site-observability
工程2026-10-13 · 9 min

单人网站的可观测性:埋点、指纹与缺陷队列

没有 oncall、没有工单系统,失败数据替你值班。埋点契约化、故障指纹归并、失败负缓存熔断与错误文案的克制造就的缺陷处理体系。

一个单人维护的网站,要不要建可观测性?我们的答案是:不但要,而且它的形态和团队版本完全不同——没有 oncall 轮值、没有工单系统,唯一能依赖的是「失败自己会说话」。这篇笔记记录本站分析体系从埋点到缺陷队列的完整设计,以及中间几个值得展开的决策。

第一层:埋点是契约,不是日志

埋点最容易死在两处:事件名随意长、字段随意加。我们的约束反着来——事件名是前后端契约,后端用 pydantic 白名单硬校验,白名单外整批拒绝。听起来危险(数据静默丢失),但正是这个「危险」逼出了纪律:新增事件必须前后端同批部署,谁也别想先斩后奏。

字段设计上有一个后来被证明极值钱的决策:解析类事件只带 requestId,平台和用户输入的链接由后端在解析落库时按同一 id 关联一份。理由:链接和平台只有后端知道全貌(重定向、短链展开、平台识别都在服务端),前端重复上报必然出现两份不一致的真相。缺陷排查时按 requestId 一次 join 拿到「用户输什么、服务端发生什么」,这是后面所有能力的地基。

另一个反直觉的决定:本地开发环境完全不埋点。import.meta.env.DEV 时 track() 直接不入队。测试数据进统计库的污染是洗不掉的——报表的每一个口径从此都要写「剔除测试数据」的补丁。

第二层:从失败排行到缺陷队列

埋点跑起来后,失败数据很快就淹了:同类失败每天几百条,翻日志不如不翻。解法是故障指纹:按 kind + 主体 + 消息前 160 字符 生成指纹,同类失败归并成一条「缺陷」,带 firstSeen/lastSeen/计数/复现样本抽样。失败日志变缺陷队列,状态流转借用了禅道的四态语义:

  • OPEN:未解决,唯一「确认没修」的状态
  • FIXED:开发标了修复但未验证关闭——模糊地带
  • CLOSED:验证通过
  • IGNORED:判定非缺陷(外部风控、自动化流量、环境硬限制)

四态之外还自动推导一个行级标记「待根因」:OPEN 且根因字段为空。实践里它比普通 OPEN 优先级高得多——连成因都没定位的缺陷,和已定位只差修复的缺陷,处理动作完全不同。

复现样本抽样的价值在排查时才显现:每条缺陷带最近 N 条原始事件,含 IP、地区、UA、耗时、服务端日志。一个「独立 IP 数 ≥3 且样本分散」的缺陷和「单 IP 单 UA 连打 20 次」的缺陷,一个是真 bug、一个是爬虫,一眼分诊。

第三层:指纹归并的「回积」问题

跑了一阵发现队列反复回积:同一类失败修完之后,变体的消息(嵌套的包装文案、数字不同)又以新指纹落回 OPEN,人工反复去标 IGNORED。解法是消息归一化:剥掉全部括号内容(含嵌套)、数字折叠成占位符、空白收敛——同一守卫产出的所有消息归一到同一模板。新指纹入库时先查既有非 OPEN 缺陷的模板表,命中即自动继承状态和根因。

设计上留了两个安全阀:冲突时取 lastSeen 最新者(可人工改回);每日邮件日报单列「新指纹缺陷」,人工只审增量。归一化规则用线上真实缺陷消息做单元测试——规则改坏比不改更糟。

第四层:别让系统自己放大故障

缺陷数据里发现一个恶性循环:用户解析失败后 7 分钟连点重试 6 次,每次重试都把完整的降级链重新打一遍穿透到平台——用户越急,出口 IP 的风控画像越差,失败率越高,于是更急。

解法是失败负缓存:同一链接 90 秒内重复提交,直接短路返回同一个错误。两个实现细节:消息原文回填(保证缺陷指纹不变,不产生新簇);SSRF 校验类的 400 放在缓存边界之外(恶意请求不该被缓存「免单」)。第二次提交实测 0.00 秒返回——对用户是即时反馈,对平台是降级链零穿透,对缺陷队列是不产生重复计数。一个 TTLCache 同时救了三方。

第五层:错误信息是产品的一部分

可观测性的另一面是我们给用户看什么。几个实践:

  • 报错要可行动。ffmpeg 退出码非 0 官方语义不抛错,早期用户只看到笼统的 ErrnoError: FS error。现在十几类已知失败模式各配中文翻译:moov 缺失指向「重新下载完整文件」、解码器缺失指向「先转 H.264 MP4」、无音轨指向「该视频本身没有声音」。报错的评价标准只有一条:用户读完知道下一步做什么。
  • 日志要防刷屏。ffmpeg 的 Last message repeated N times 行曾以 4200 行的规模吃光 2000 字符的日志窗口,真实错误被顶出窗外。入库前剔除无信息量行。
  • 包装错误要克制。OOM 自愈重试失败后曾无差别包装成「内存不足」,真实原因是转码参数的确定性失败——用户按「减负重试」的提示操作永远不会好。只有确认是内存问题才说内存,其余原样透传。
  • reason 截断要前置关键信息。失败原因按头 160 字符入库,所以可行动提示必须放在消息最前面——有一条提示排在第 84 字符处,被页面 70 字符的截断吞掉,是靠自测抓出来的。

体系的真实成本

诚实交底:这套东西(埋点采集 + GeoIP + 缺陷队列 + 指纹归并 + 负缓存 + 日报邮件)是一个后端进程加几张表,没有引入任何新组件——缓存用进程内 TTLCache、报表直接查明细表。单人维护的约束下,「少一个组件」比「多一分扩展性」重要得多。

而它的回报周期比预想短:上线后两个月里,几十条线上缺陷从发现到修复没有一条依赖用户报障,多数缺陷在用户开口之前已经有指纹、有样本、有根因假设。单人网站的可观测性不是运维开销,是把「用户替你测试」变成「失败数据替你值班」。

(本站的所有工具与解析都在这套体系上运行,浏览器本地处理文件、服务端只做解析——欢迎使用,也欢迎在反馈页给我们留「缺陷」。)