AI SREClickHouse Workshops

07 测试、失败与修复

模块 07 的讲师笔记,时间安排、讲解要点、常见故障与重置步骤。

Your computer
macOS terminal: Run workshop commands in Terminal using zsh or bash.

学员课程 07 测试、失败与修复 的讲师配套材料。

时间安排

约 20 分钟。这是事故实验;要保证有足够的余量让诊断在模块 08 和总结之前落地。本实验运行的是 故障 01(诊断需 5-15 分钟)。故障 02(5-10 分钟)和故障 03(10-20 分钟,且只有在拥有大约三千万行的完整数据集时才可用)作为可选的额外事故, 供进度快的会场使用。在预演时设定一个硬性收尾时间,确保总结环节不被压缩。

讲解要点

  • 这就是回报环节:动用此前构建的一切来处理一次真实事故。
  • 描述症状,而不是原因,让 agent 从遥测数据出发自行收敛到结论。
  • 可以考虑在投影仪上并排展示几位参会者的诊断过程。
  • 本实验运行故障 01。如果时间允许再来一轮,可以加上故障 02(故障 03 只在完整数据集已预先导入时 才用);在故障之间使用下面的共享 stash-and-switch 重置流程。

答案手册

不要把本节内容分享给学员;它只存在于本 playbook 中,永远不会提交到应用仓库。每个故障都是 从 build-workshop-v1 分出的独立分支上的一处小改动; 修复方法就是把它还原。一次只运行一个故障。本实验的事故是故障 01(5-15 分钟, 出人意料,后端是无辜的)。供进度快的会场使用的可选附加项:故障 02(5-10 分钟, 热身,追踪数据说明一切)和故障 03,仅在完整数据集可用时使用 (10-20 分钟,需要更深入的 ClickHouse 推理)。

适用于每个故障的共享重置流程(stash 会保留参与者的改动,并避免分支切换被阻止):

git stash push --include-untracked -m "module-07-fix"
git switch build-workshop-v1
docker compose --env-file .env.workshop \
  -f docker-compose.workshop.yml -f docker-compose.otel.yml up -d --build

故障 02:zone stats 报 500(fault/02-zone-stats-500)

参与者看到的提交信息:"backend: align zone stats column names with API params" (只改动 backend/app/query_builders.py 中的 zone_stats_sql)。它按 pickup_zone_id 分组,而 taxi_trips 上并不存在这一列,正确的应该是 pickup_location_id。

  • 症状:地图上的分级着色图是空的(多边形能渲染出来,但每个 zone 都落在 最浅的那一档),而且 "Query Nms" 的说明文字缺失;GET /api/metrics/zone_stats 上出现成批的 500(React Query 会重试三次)。其他所有卡片都正常。
  • 信号在哪里:一个 ClickStack clickhouse.query span,带有 error=True、 error.category="query_failed",且 db.statement 中包含 pickup_zone_id AS zone_id; 记录的异常是 ClickHouse Code 47 UNKNOWN_IDENTIFIER。对应的 ERROR 日志: "ClickHouse query failed (category=query_failed, elapsed_ms=...): ... | sql=..."。
  • 诊断路径:找到那个 500 的 span -> 读 db.statement -> 发现 pickup_zone_id 上的 UNKNOWN_IDENTIFIER -> DESCRIBE taxi_trips(或对比同类的查询构造器)-> 真正的 列名是 pickup_location_id。
  • 修复:在 zone_stats_sql 里把 pickup_zone_id 改回 pickup_location_id(或者 对故障提交执行 git revert);重新构建后端。
  • 重置:使用上面的共享重置流程。

故障 03:仪表板变慢(fault/03-slow-dashboard)

提交信息:"backend: window the trend series by bucket in HAVING"(改动 timeseries_sql)。它把时间窗口谓词从 WHERE 移到了对 bucket 别名的 HAVING 上, 于是 WHERE 变成了 1。ClickHouse 再也无法按 (car_type, pickup_datetime) 主键做裁剪,只能对 taxi_trips 全表扫描并维护两个 quantileTDigest 状态,从而突破 5s 的 max_execution_time。

  • 症状:只有 "What's happening now?" 这张趋势卡片失败,它先转圈,然后显示 "504 Query timed out..."。其他同类卡片都依然很快。
  • 信号在哪里:一个 clickhouse.query span,带有 error.category="timeout"、 db.elapsed_ms 在 5000 左右,且 db.statement 显示 WHERE 1 ... GROUP BY ts HAVING ts >= ...;异常是 Code 159 TIMEOUT_EXCEEDED。与其他端点上快速返回 200 的 span 形成的对比就是关键线索。
  • 诊断路径:一堆快速卡片中只有一张慢 -> 打开超时的那个 span -> 读 SQL -> 发现时间过滤条件在 HAVING 里,而 WHERE 里没有 pickup_datetime 范围 -> 这是主键裁剪 / 谓词下推的反模式(同类卡片都把时间窗口放在 WHERE 里)。
  • 修复:把时间窗口恢复到 WHERE(还原故障提交);重新构建后端。
  • 重置:使用上面的共享重置流程。
  • 讲师注意事项:严重程度随数据量和服务规格而变化。在完整导入的数据集(约 3000 万行) 以及实训所用规格的服务上(该服务可能正从空闲状态唤醒),5s 超时会稳定触发; 而在很小的样本种子数据上,只是变慢而已,不会硬性出现 504。 客户端的 send_receive_timeout 和服务端的 max_execution_time 都是 5s,所以 套接字超时的竞争偶尔会表现为 500 而不是干净的 504,这是基线配置的 特性,不是这个故障造成的。

故障 01:地图加载不出来(fault/01-map-not-loading)

提交信息:"frontend: serve map geojson from /static asset path"(改动 frontend/src/ui/ZoneMap.tsx 中的 fetch 路径)。它去取 /static/taxi_zones.geojson, 而这个文件并不存在。关键微妙之处:nginx 的 SPA 回退(try_files ... /index.html)返回的是 HTTP 200 和那份 HTML 文档,而不是 404,所以 r.ok 会通过,而 r.json() 抛出一个 类似 Unexpected token '<' 的 SyntaxError。

  • 症状:地图卡片是死的,底图瓦片能渲染,但没有 NYC 多边形和分级着色, 并显示一条红色的行内错误。其他一切正常。
  • 信号在哪里:后端 ClickStack 里什么都没有,这个静态资源请求根本没到 FastAPI。浏览器控制台会显示指名 /static/taxi_zones.geojson 的 JSON 解析错误; Network 面板显示该请求返回 text/html、状态 200;nginx 访问 日志显示走了回退。
  • 诊断路径:后端追踪很干净 -> 转向浏览器控制台 / Network 面板 -> 发现一个 .geojson 请求返回 200 text/html -> 识别出 SPA 回退这个坑 -> 文件其实位于 web 根目录,即 /taxi_zones.geojson。
  • 修复:把 fetch 路径改回去(两行),或者对故障提交执行 git revert;重新构建 前端。
  • 教学要点:不是每个故障都会在后端追踪中显现,而且一个 404 可能在 SPA 回退背后 伪装成 200,所以要读实际的响应体和 content-type,而不只是看状态码。
  • 备注(浏览器 SDK): 在启用了 HyperDX 浏览器 SDK 的情况下(模块 05 的叠加层), 前端现在会把这个问题呈现在 ClickStack 里,ZoneMap 的解析失败会被捕获为 ServiceName=nyc-taxi-frontend 下的一个 console.error span,并与那个返回 text/html 200 的资源 span 配对。所以 agent 无需离开 ClickStack 就能从遥测数据中定位它; 浏览器控制台 / Network 面板是备用手段,而不是唯一路径。

Agent 回答示例(故障 01,通过 ClickStack MCP):

Agent 对故障 01 的根因分析:它指出缺失的 /static/taxi_zones.geojson 以 200 text/html 返回了 SPA 的 index.html、ZoneMap 的 JSON 解析错误被捕获为 nyc-taxi-frontend 的 console.error span,将其与健康的 /api 和后端 span 作对比,把 SDK 自检错误标记为噪音,并建议发布该静态资源或为解析加上保护

预期的诊断结果:agent 把故障归因于 geojson 请求返回 200 text/html(SPA 回退)、由此在 nyc-taxi-frontend 下捕获到的 JSON.parse 错误,并建议把该静态资源发布到 /static/ 或为 .json() 解析加上保护。

常见故障

  • 遥测基线不足,agent 无法定位问题;模块 05 必须尽早运行过。
  • 参会者在从证据中确认根因之前就急着去修。
  • 故障症状还看不到,因为整个栈刚刚启动,或者,对于故障 03,是因为 加载的历史数据太少。预演中实测: 在默认的 单月种子数据(约 3.17M 行)上,WHERE 与 HAVING 的这个故障是观察不到的,全表 扫描在约 300-560ms 内完成,与基线无从区分。演示故障 03 之前先导入几个月的数据, 或者干脆坚持用故障 01(它能立刻复现),并把故障 03 当作一个大规模场景下的示例。
  • 故障 01 完全不产生后端追踪,而且那个错误请求会经由 SPA 回退返回 200 text/html 而不是 404;参会者可能会在追踪里翻找到天荒地老。把他们引向 浏览器控制台 / Network 面板,那里能看到 JSON 解析错误和 text/html 的 响应。
  • 检出故障分支后忘记加 --build,导致仍在运行旧镜像。

重置步骤

  • 共享的 stash-and-switch 重置流程能恢复完整的应用,同时不丢弃 参与者尝试过的修复。
  • 要重新注入某个故障,检出 fault/01-map-not-loading / fault/02-zone-stats-500 / fault/03-slow-dashboard 之一,并使用带有 otel 配置的 compose 文件重新构建。
  • 各故障的症状、信号和修复方法见上面的"答案手册"一节。答案手册 只保留在本 playbook 中;它永远不会提交到应用仓库。

本页内容

ZH