Private Docs

Monitor Log — ความคืบหน้าถึง 04/09/2026

รอบกลางคืน 04/09: Owner ตัดสิน 4 ข้อที่ค้าง และมี 2 เรื่องที่เกือบพลาด — การย้าย startupProbe ตามแผนเดิมจะทำให้ระบบล่มนานกว่าเดิม และ archive ที่ล้มกลางทางทิ้งไฟล์ไม่ครบไว้เงียบ ๆ ทั้งคู่ถูกจับได้ก่อนขึ้นจริง

อัปเดต: 2026-09-04

Monitor Log — ความคืบหน้าถึง 04/09/2026

ต่อจาก ความคืบหน้าถึง 03/09 — หน้านี้เล่าเฉพาะสิ่งที่ เปลี่ยนไปจากวันนั้น สถานะที่ไม่ได้พูดถึงคือยังเหมือนเดิม

“เสร็จ” ในหน้านี้แปลว่า build ผ่าน test ผ่าน มี exit code เป็นหลักฐาน และ push ขึ้น branch แล้ว — ไม่ได้แปลว่า merge หรือ deploy ทุก PR ยังเป็น Draft โดยตั้งใจ

สรุปสั้นที่สุด

Owner ตอบคำถามที่ค้างครบ 4 ข้อ งานที่เคยติดจึงเดินต่อได้ทั้งหมด และมี 2 เรื่องที่เกือบทำพลาด ซึ่งทั้งคู่ ถูกจับได้ด้วยด่านตรวจ ไม่ใช่ด้วยการที่ build หรือ test ฟ้อง:

  1. แผนเดิมจะย้าย startupProbe ไปที่ /health/live เพื่อกันไม่ให้ pod ตายตอน dependency ล่ม — จะทำให้ ระบบล่มนานกว่าเดิม ต้องแก้ต้นเหตุก่อน
  2. AzureArchiveBlobStore ที่เพิ่งเขียน ทิ้งไฟล์ archive ที่ไม่ครบไว้เงียบ ๆ เมื่อ export ล้มกลางทาง

ทั้งสองเรื่อง build ผ่านและ test เขียวหมดทั้งคู่ตอนที่มันยังผิดอยู่

สิ่งที่เกือบพลาด และเป็นบทเรียนของรอบนี้

ทีมตั้งเวลาปิด Redis กับ PostgreSQL เป็นช่วง ๆ โดยตั้งใจ ปัญหาที่เห็นคือ pod ที่เกิดใหม่ระหว่างช่วงนั้นจะ ไม่ผ่าน startup แล้ววนเกิดใหม่ตลอด เพราะ startupProbe ชี้ไปที่ /health ที่รวมทุก dependency

ทางแก้ที่ตั้งใจไว้คือย้าย startupProbe ไปที่ /health/live ซึ่งดูตรงไปตรงมา แต่กลไกจริงเป็นแบบนี้:

  1. งานสร้าง partition ล่วงหน้าทำงานรอบแรกตอน pod เกิด ถ้า PostgreSQL ล่มอยู่ รอบนั้นล้ม
  2. รอบที่ล้มทำให้ health check requestlog-partitions ค้างสถานะ Unhealthy
  3. check ตัวนั้นติดป้าย ready ⇒ อยู่ใน /healthreadinessProbe (ซึ่งไม่ได้ย้าย และไม่ควรย้าย) เตะ pod ออกจาก load balancer
  4. งานสร้าง partition ไม่มี retry มันรอรอบถัดไปตาม IntervalHours ซึ่งค่าเริ่มต้นคือ 12 ชั่วโมง
  5. livenessProbe ชี้ /health/live อยู่แล้ว ⇒ ไม่มีใคร restart pod ตัวนั้น

รวมกันแปลว่า: หลังย้าย probe ตามแผนเดิม pod จะรอดจาก startup แต่ค้างอยู่นอก load balancer ได้ นานถึง 12 ชั่วโมงหลัง PostgreSQL กลับมาแล้ว และ deployment ตั้ง replicas: 1 ⇒ LogService ตาย ยาวกว่าเดิม

พูดอีกอย่างคือ crash-loop ที่กำลังจะแก้ คือกลไกที่ทำให้ระบบฟื้นตัวเองอยู่ทุกวันนี้ — pod ถูกฆ่า รอบใหม่รันงาน partition ใหม่ จึงกลับมาให้บริการภายในไม่กี่วินาทีหลัง DB กลับมา

Owner เลือกทางที่ถูกคือ แก้ต้นเหตุก่อน แล้วค่อยย้าย probe

ทำเสร็จแล้วรอบนี้ พร้อมหลักฐาน

1. งานสร้าง partition ลองใหม่เองเมื่อล้ม (Backend_LogService)

รอบที่ล้มตอนนี้รอ RetryDelaySeconds (ค่าเริ่มต้น 60 วินาที) แล้วเพิ่มเป็นเท่าตัวทุกครั้งที่ล้มซ้ำ แต่ไม่เกินรอบปกติ · รอบที่สำเร็จรีเซ็ตกลับไปที่จังหวะเดิม

ครอบคลุมทั้งกรณีที่โยน exception และกรณีที่ไม่โยนแต่รายงานว่าเดือนไหนไม่มี partition คลุม (เดิมกรณีหลัง ถูกนับเป็น “สำเร็จ” เพราะไม่ได้โยน)

หลักฐาน: dotnet test -c Release 282/282 ผ่าน exit 0 · test ของ retry ถูกรันกับโค้ดตัวเก่าเพื่อ ยืนยันว่ามันจับได้จริง ผลคือ fail ด้วยข้อความ expected at least 2 maintenance attempts, saw 1

2. ย้าย startupProbe แล้ว และพิสูจน์บนเครื่องจริง (Backend_Iac)

startupProbe ของ dev และ uat ชี้ /health/live แล้ว · readinessProbe คงไว้ที่ /health โดยเจตนา เพราะ pod ที่ dependency ตายควรอยู่นอก load balancer — นั่นคือหน้าที่ของ readiness ไม่ใช่บั๊ก

พิสูจน์ด้วยการรัน LogService บนเครื่องโดยให้ Redis, PostgreSQL และ Service Bus ชี้ไปที่ port ที่ไม่มีอะไรฟัง:

endpointผล
GET /health/live200 Healthy
GET /health503 Unhealthy

และใน log ของ run เดียวกันเห็น retry ทำงานจริง: Retrying in 00:00:05 / 00:00:10 / 00:00:20 instead of waiting 12:00:00 · host ไม่ล้ม (ไม่มี unhandled exception เลย) แม้ Service Bus จะต่อไม่ได้

⚠️ นี่คือหลักฐานจากเครื่อง local ไม่ใช่จาก cluster จริง — Owner สั่งให้พิสูจน์บนเครื่องเพราะ dependency บน cluster ถูกปิดเป็นเวลาอยู่แล้ว

3. archive ลง blob มี implementation จริงแล้ว (Backend_LogService)

เดิม IArchiveBlobStore มีแต่ interface ไม่มีตัวจริง ⇒ archive job log ว่าไม่มีปลายทางแล้วไม่ทำอะไรเลย ทุก environment · Owner อนุมัติให้เพิ่ม package Azure.Storage.Blobs รอบนี้จึงเขียนได้

จุดที่สำคัญที่สุดของตัวนี้: ขนาดที่คืนกลับอ่านจาก storage หลัง commit ไม่ใช่ตัวนับของฝั่งที่เขียน เพราะ archive job เอาค่านี้ไปเทียบกับจำนวน byte ที่ตัวเองส่งก่อนตัดสินใจ drop partition ถ้าคืนตัวนับ ของตัวเองไป การเทียบก็จะเท่ากันเสมอและด่านนั้นก็ไม่เหลืออะไร — มี test คุมข้อนี้ไว้โดยเฉพาะ

4. แก้เอกสารที่พูดเกินจริงเรื่องกันข้อมูลซ้ำ

EventId ถูกเขียนไว้ทั้งใน contract ฝั่งผู้ส่ง (Backend_Package) และ entity ฝั่งผู้รับ (Backend_LogService) ว่าเป็น “dedup key” ซึ่งไม่จริง — unique index จริงคือ (EventId, ReceivedAt) และ ReceivedAt ประทับต่อ batch ⇒ ข้อความที่ถูกส่งซ้ำแล้วตกคนละ batch จะได้แถวใหม่

Owner ตัดสินว่า ยอมรับว่าซ้ำได้ ไม่แก้กลไก แต่เอกสารต้องเลิกสัญญาในสิ่งที่ทำไม่ได้ เพราะคนที่เชื่อ ประโยคเดิมจะข้ามการจัดการแถวซ้ำที่เขาต้องทำจริง ๆ

5. MaxDeliveryCount ของ subscription ตั้งเป็น 5 แล้ว

ตรงกับ subscription เพื่อนบ้าน · request log เป็น telemetry ไม่ใช่ transaction การลองซ้ำ 10 ครั้ง มีแต่จะหน่วงสัญญาณ DLQ ที่บอกว่ามีปัญหา

พร้อมกันนั้นสคริปต์ provisioning เตือนแล้วว่า --max-delivery-count ถูกส่งเฉพาะตอน สร้างใหม่ subscription ที่มีอยู่แล้วจะถูกข้ามไปเงียบ ๆ และคงค่าเดิมไว้ (เดิมมีคำเตือนแบบนี้เฉพาะเรื่อง session)

เรื่องที่สองที่เกือบพลาด — archive ทิ้งของครึ่ง ๆ ไว้เงียบ ๆ

หลังเขียน AzureArchiveBlobStore เสร็จและผ่าน review รอบแรกไปแล้ว ด่านตรวจกระบวนการท้วงว่าเราใช้ reviewer ทั่วไปกับ diff ที่เป็น C# เกือบทั้งหมด ทั้งที่มี reviewer เฉพาะทาง .NET อยู่ · พอเรียกตัวเฉพาะทางมาตรวจซ้ำ มันเจอสิ่งที่ตัวทั่วไปมองไม่เห็น:

การปิด stream ที่ใช้อัปโหลดขึ้น blob จะ commit สิ่งที่ buffer ไว้เสมอ ไม่ว่าจะออกมาแบบปกติหรือเพราะ เกิด exception กลางทาง — โค้ดที่ปิด stream แยกสองกรณีนี้ไม่ได้ ⇒ การ export ที่ตายกลางทางจะทิ้งไฟล์ “สั้นแต่ commit แล้ว” ไว้ใต้ชื่อเดียวกับ partition ที่กำลัง archive

ที่ทำให้มันน่ากลัวคือ ด่านตรวจความครบของข้อมูลรันเฉพาะตอนที่ export สำเร็จ — งาน archive เทียบจำนวน byte ที่เขียนกับที่ storage ยืนยันก่อนจะ drop partition ทิ้ง แต่ถ้า export ล้ม เส้นทางนั้นไม่ถูกเรียกเลย ไฟล์ที่ไม่ครบ จึงนอนอยู่เฉย ๆ โดยไม่มีอะไรบอกว่ามันไม่ครบ

แก้แล้ว: เส้นทางที่ล้มลบไฟล์ทิ้งเองก่อนโยน error ต่อ · ถ้าการลบล้มด้วยก็บันทึกไว้แล้วกลืน เพราะคนเรียกต้องเห็นว่า การ export พังเพราะอะไร ไม่ใช่ว่าการเก็บกวาดพังเพราะอะไร · มีเทสต์คุมทั้งสองกรณี

ยังไม่เสร็จ และรออะไรอยู่

  • ยังไม่ merge ยังไม่ deploy ⇒ อาการเดิมบน dev/uat ยังเปิดอยู่จนกว่าจะขึ้นจริง · 🔴 ลำดับ merge บังคับ: PR ของ LogService ต้องเข้าก่อน PR ของ Iac เสมอ สลับลำดับ = probe ชี้ไปที่ 404 แล้ว pod เข้า CrashLoopBackOff ถาวร
  • topic ยังไม่มีจริงบน Azure ต้องรัน provision-servicebus-topics.ps1 ต่อ namespace ของแต่ละ env ก่อน service ตัวแรกเปิดใช้ ไม่งั้นข้อความหายเงียบ
  • read API ยังไม่มี — รอบนี้ร่าง contract เสนอไว้แล้ว (route, filter, จะคืน body ในลิสต์ไหม, แถวซ้ำ เลือกยังไง, ใครเข้าถึงได้) รอ Owner ตัดสิน ยังไม่เขียนโค้ดสักบรรทัด
  • PDPA ลบรายบุคคล ยังไม่มีทาง — ตาราง append-only + archive ลง blob
  • log DB ยังใช้ context เดียวกับ AuditLogs ขัดกับแผนเดิมที่ให้แยก instance
  • ยังไม่มีตัวเลขจริง request/วัน · ขนาด body · tier ของ Service Bus · version และ IOPS ของ PostgreSQL

ข้อสังเกตที่ควรยกให้ทีมทั้งทีม

startupProbe ของ deployment อื่นอีก 14 ตัวยังชี้ /health เหมือนกันหมด และเหตุผลที่ Owner ยกมา (Redis กับ PostgreSQL ถูกปิดเป็นเวลา) ใช้ได้กับทุกตัวเท่ากัน · รอบนี้ตั้งใจไม่ขยายไปแตะ เพราะอยู่นอก ขอบเขตที่ตกลงกันไว้ แต่ควรเป็นเรื่องที่ทีมตัดสินร่วมกัน ไม่ใช่ปล่อยให้ log-service เป็นตัวเดียวที่ต่างจากคนอื่น