AI SREClickHouse Workshops

07 ทดสอบ, ล้มเหลว และแก้ไข

บันทึกสำหรับผู้สอนของโมดูล 07 — เวลา, แนวการบรรยาย, ปัญหาที่พบบ่อย และขั้นตอนรีเซ็ต

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

คู่มือประกอบของผู้ดำเนินเวิร์กช็อปสำหรับบทเรียนผู้เรียน 07 ทดสอบ, ล้มเหลว และแก้ไข

เวลา

ประมาณ 20 นาที นี่คือแล็บ incident กันเวลาให้พอเพื่อให้การวินิจฉัย ลงเอยได้ก่อนโมดูล 08 และการสรุปปิดท้าย แล็บจะรัน fault 01 (วินิจฉัย 5-15 นาที) ส่วน fault 02 (5-10 นาที) และ 03 (10-20 นาที และเฉพาะกับชุดข้อมูลเต็มราว 30 ล้านแถวเท่านั้น) ยังคงมีไว้เป็น incident เพิ่มเติมแบบทางเลือกสำหรับห้องที่ไปเร็ว กำหนด hard-stop ตอนซ้อม เพื่อไม่ให้การสรุปปิดท้ายถูกบีบเวลา

แนวการบรรยาย

  • นี่คือผลตอบแทน: ใช้ทุกอย่างที่สร้างมาจนถึงตอนนี้เพื่อรับมือ incident จริง
  • อธิบายอาการ ไม่ใช่สาเหตุ แล้วปล่อยให้ agent ลู่เข้าหาคำตอบจาก telemetry
  • พิจารณาแสดงการวินิจฉัยของผู้เข้าร่วมหลายคนเทียบกันบนโปรเจกเตอร์
  • แล็บจะรัน fault 01 ถ้าเวลาเหลือให้เพิ่มอีกรอบ ก็เพิ่ม fault 02 (และ fault 03 เฉพาะ เมื่อ seed ชุดข้อมูลเต็มไว้ล่วงหน้าแล้ว) ใช้การรีเซ็ตแบบ stash-and-switch ร่วมด้านล่าง ระหว่างแต่ละ fault

เฉลย

อย่าแบ่งปันส่วนนี้กับผู้เรียน มันอยู่ใน playbook เท่านั้นและไม่เคย commit ลง repo ของแอป แต่ละ fault คือการเปลี่ยนแปลงเล็ก ๆ หนึ่งจุดบน branch ของตัวเองที่แยกจาก build-workshop-v1 และการแก้คือ revert มัน รันหนึ่ง fault ต่อครั้ง incident ของแล็บคือ fault 01 (5-15 นาที เป็นลูกโค้ง — backend ไม่ได้ผิด) ส่วนเสริมแบบทางเลือกสำหรับห้องที่ไปเร็ว: fault 02 (5-10 นาที เป็นการวอร์มอัป trace บอกทุกอย่าง) และ fault 03 เฉพาะเมื่อมีชุดข้อมูลเต็ม (10-20 นาที ต้องใช้การให้เหตุผลเรื่อง ClickHouse ที่ลึกกว่า)

การรีเซ็ตร่วมสำหรับทุก fault (การ stash รักษาการแก้ไขของผู้เข้าร่วมไว้และเลี่ยงการสลับ branch ที่ถูกบล็อก):

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

Fault 02 — zone stats 500 (fault/02-zone-stats-500)

ข้อความ commit ที่ผู้เข้าร่วมเห็น: "backend: align zone stats column names with API params" (แตะเฉพาะ zone_stats_sql ใน backend/app/query_builders.py) มันจัดกลุ่มด้วย pickup_zone_id ซึ่งเป็นคอลัมน์ที่ไม่มีอยู่บน taxi_trips แทนที่จะเป็น pickup_location_id

  • อาการ: choropleth บนแผนที่ว่างเปล่า (polygon เรนเดอร์ออกมา แต่ทุก zone อยู่ใน ช่วงสีอ่อนสุด) และคำบรรยาย "Query Nms" หายไป มี 500 พรั่งพรูบน GET /api/metrics/zone_stats (React Query ลองซ้ำสามครั้ง) การ์ดอื่นทั้งหมดปกติดี
  • สัญญาณอยู่ที่ไหน: span clickhouse.query ของ ClickStack ที่มี error=True, error.category="query_failed" และ db.statement ที่มี pickup_zone_id AS zone_id โดย exception ที่บันทึกไว้คือ ClickHouse Code 47 UNKNOWN_IDENTIFIER log ระดับ ERROR ที่ตรงกัน: "ClickHouse query failed (category=query_failed, elapsed_ms=...): ... | sql=..."
  • เส้นทางการวินิจฉัย: หา span ที่เป็น 500 -> อ่าน db.statement -> UNKNOWN_IDENTIFIER ที่ pickup_zone_id -> DESCRIBE taxi_trips (หรือเทียบกับ query builder พี่น้อง) -> คอลัมน์จริงคือ pickup_location_id
  • การแก้: เปลี่ยน pickup_zone_id กลับเป็น pickup_location_id ใน zone_stats_sql (หรือ git revert commit ของ fault) แล้ว rebuild backend
  • การรีเซ็ต: ใช้การรีเซ็ตร่วมด้านบน

Fault 03 — dashboard ช้า (fault/03-slow-dashboard)

ข้อความ commit: "backend: window the trend series by bucket in HAVING" (แตะ timeseries_sql) มันย้าย predicate ของช่วงเวลาจาก WHERE ไปเป็น HAVING บน alias ของ bucket ทำให้ WHERE กลายเป็น 1 ClickHouse จึงไม่สามารถตัดข้อมูลด้วย primary key (car_type, pickup_datetime) ได้อีก และสแกน taxi_trips ทั้งตารางพร้อม state ของ quantileTDigest สองตัว ทำให้ทะลุ max_execution_time 5 วินาที

  • อาการ: มีเพียงการ์ด trend "What's happening now?" ที่ล้มเหลว — มันหมุนแล้วแสดง "504 Query timed out..." การ์ดพี่น้องทุกใบยังเร็วอยู่
  • สัญญาณอยู่ที่ไหน: span clickhouse.query ที่มี error.category="timeout", db.elapsed_ms ราว 5000 และ db.statement ที่แสดง WHERE 1 ... GROUP BY ts HAVING ts >= ... โดย exception คือ Code 159 TIMEOUT_EXCEEDED ความต่างเทียบกับ span ที่เป็น 200 แบบเร็วบน endpoint อื่น ๆ คือเบาะแสสำคัญ
  • เส้นทางการวินิจฉัย: การ์ดช้าใบเดียวท่ามกลางพี่น้องที่เร็ว -> เปิด span ที่ timeout -> อ่าน SQL -> ตัวกรองเวลาอยู่ใน HAVING และ WHERE ไม่มีช่วงของ pickup_datetime -> เป็น anti-pattern เรื่องการตัดด้วย primary key / การผลัก predicate ลงไป (พี่น้องใส่ช่วงเวลาไว้ใน WHERE)
  • การแก้: คืนช่วงเวลากลับไปที่ WHERE (revert commit ของ fault) แล้ว rebuild backend
  • การรีเซ็ต: ใช้การรีเซ็ตร่วมด้านบน
  • ข้อควรระวังสำหรับผู้สอน: ความรุนแรงขึ้นอยู่กับปริมาณข้อมูลและขนาด service บนชุดข้อมูล ที่ seed เต็ม (ราว 30 ล้านแถว) บน service ระดับเวิร์กช็อป (ซึ่งอาจกำลังตื่นจาก idle) timeout 5 วินาทีจะเกิดขึ้นอย่างแน่นอน แต่บน seed ตัวอย่างเล็ก ๆ มันแค่ช้าลง ไม่ถึงกับเป็น 504 เต็มตัว ค่า send_receive_timeout ฝั่ง client และ max_execution_time ฝั่ง server เป็น 5 วินาทีทั้งคู่ ดังนั้น การแข่งกันของ socket timeout อาจโผล่มาเป็น 500 แทน 504 ที่สะอาดได้เป็นครั้งคราว — เป็น คุณสมบัติของค่าตั้งต้น ไม่ใช่ของ fault

Fault 01 — แผนที่ไม่โหลด (fault/01-map-not-loading)

ข้อความ commit: "frontend: serve map geojson from /static asset path" (แตะ path ของ fetch ใน frontend/src/ui/ZoneMap.tsx) มัน fetch /static/taxi_zones.geojson ซึ่ง ไม่มีอยู่ ประเด็นสำคัญ: SPA fallback ของ nginx (try_files ... /index.html) คืน HTTP 200 พร้อมเอกสาร HTML แทนที่จะเป็น 404 ดังนั้น r.ok ผ่าน และ r.json() โยน SyntaxError แบบ Unexpected token '<'

  • อาการ: การ์ดแผนที่ตาย — base tile เรนเดอร์ออกมาแต่ไม่มี polygon ของ NYC หรือ choropleth และมี error สีแดงแสดงในบรรทัด อย่างอื่นปกติดีทั้งหมด
  • สัญญาณอยู่ที่ไหน: ไม่มีอะไรใน ClickStack ฝั่ง backend — asset นั้นไม่เคยไปถึง FastAPI คอนโซลของเบราว์เซอร์แสดง error การ parse JSON ที่ระบุชื่อ /static/taxi_zones.geojson แท็บ Network แสดงคำขอนั้นคืน text/html ด้วยสถานะ 200 และ access log ของ nginx แสดงการ fallback
  • เส้นทางการวินิจฉัย: trace ฝั่ง backend สะอาด -> หันไปที่คอนโซล / แท็บ Network ของเบราว์เซอร์ -> คำขอ .geojson คืน 200 text/html -> จับได้ว่าเป็นกับดัก SPA fallback -> ไฟล์อยู่ที่ web root จริง ๆ คือ /taxi_zones.geojson
  • การแก้: revert path ของ fetch (สองบรรทัด) หรือ git revert commit ของ fault แล้ว rebuild frontend
  • การรีเซ็ต: ใช้การรีเซ็ตร่วมด้านบน
  • จุดสอน: ไม่ใช่ทุกความล้มเหลวจะโผล่ใน trace ฝั่ง backend และ 404 อาจปลอมตัว เป็น 200 อยู่หลัง SPA fallback ได้ — ดังนั้นให้อ่าน response body และ content-type จริง ไม่ใช่ ดูแค่ status code
  • หมายเหตุ (browser SDK): เมื่อเปิดใช้ HyperDX browser SDK (overlay ของโมดูล 05) frontend จะเผยเรื่องนี้ใน ClickStack เองแล้ว — ความล้มเหลวในการ parse ของ ZoneMap ถูกบันทึก เป็น span console.error ภายใต้ ServiceName=nyc-taxi-frontend คู่กับ resource span ของ text/html ที่เป็น 200 ดังนั้น agent สามารถระบุตำแหน่งปัญหาได้จาก telemetry โดยไม่ต้องออกจาก ClickStack คอนโซล / แท็บ Network ของเบราว์เซอร์เป็นทางสำรอง ไม่ใช่ทางเดียว

ตัวอย่างคำตอบของ agent (fault 01, ผ่าน ClickStack MCP):

การวิเคราะห์รากของปัญหา fault 01 โดย agent: มันระบุว่า /static/taxi_zones.geojson ที่หายไปคืน index.html ของ SPA เป็น 200 text/html, error การ parse JSON ของ ZoneMap ถูกบันทึกเป็น span console.error ของ nyc-taxi-frontend, เทียบกับ span ของ /api และ backend ที่ปกติดี, ตีธงว่า error จาก self-test ของ SDK เป็นสัญญาณรบกวน และเสนอให้ส่งไฟล์ asset ไปวางไว้หรือใส่การป้องกันรอบการ parse

การวินิจฉัยที่คาดหวัง: agent เชื่อมโยงความล้มเหลวกับคำขอ geojson ที่คืน 200 text/html (SPA fallback), error JSON.parse ที่ตามมาซึ่งถูกบันทึกไว้ใต้ nyc-taxi-frontend และแนะนำให้ส่งไฟล์ asset ไปที่ /static/ หรือใส่การป้องกันรอบการ parse ด้วย .json()

ปัญหาที่พบบ่อย

  • baseline ของ telemetry ไม่พอให้ agent ระบุตำแหน่งปัญหา โมดูล 05 ต้องถูกรันไว้ก่อนแล้ว
  • ผู้เข้าร่วมกระโดดไปที่การแก้ก่อนที่จะยืนยันรากของปัญหาจากหลักฐาน
  • อาการของ fault ยังไม่ปรากฏเพราะ stack เพิ่งถูกยกขึ้นมา — หรือในกรณีของ fault 03 เพราะโหลดข้อมูลย้อนหลังไว้น้อยเกินไป วัดได้ในการซ้อม: บน seed หนึ่งเดือนแบบ ค่าเริ่มต้น (ราว 3.17 ล้านแถว) fault แบบ WHERE-เทียบ-HAVING สังเกตไม่ได้ — การสแกนทั้งตาราง รันเสร็จในราว 300-560 มิลลิวินาที แยกไม่ออกจาก baseline ให้ seed หลายเดือนก่อน สาธิต fault 03 หรือใช้ fault 01 (ซึ่งเกิดซ้ำได้ทันที) และถือว่า fault 03 เป็นภาพประกอบสำหรับระดับสเกลใหญ่
  • fault 01 ไม่ก่อให้เกิด trace ฝั่ง backend เลย และคำขอที่ผิดคืน 200 text/html ผ่าน SPA fallback แทนที่จะเป็น 404 ผู้เข้าร่วมอาจค้น trace ไม่จบไม่สิ้น ผลักดันพวกเขา ไปที่คอนโซล / แท็บ Network ของเบราว์เซอร์ ที่ซึ่ง error การ parse JSON และการตอบ text/html จะแสดงอยู่
  • ลืมใส่ --build หลัง checkout branch ของ fault ทำให้ image เดิมยังทำงานอยู่

ขั้นตอนรีเซ็ต

  • การรีเซ็ตแบบ stash-and-switch ร่วมจะคืนแอปให้ครบถ้วนโดยไม่ทิ้ง ความพยายามแก้ปัญหาของผู้เข้าร่วม
  • ใส่ fault กลับเข้าไปด้วยการ checkout หนึ่งใน fault/01-map-not-loading / fault/02-zone-stats-500 / fault/03-slow-dashboard แล้ว rebuild ด้วยไฟล์ compose ของเวิร์กช็อป บวกกับของ otel
  • อาการ, สัญญาณ และการแก้ของแต่ละ fault อยู่ในส่วนเฉลยด้านบน เก็บ เฉลยไว้ใน playbook นี้เท่านั้น ไม่เคย commit ลง repo ของแอป

ในหน้านี้

TH