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 ฟ้อง:
- แผนเดิมจะย้าย
startupProbeไปที่/health/liveเพื่อกันไม่ให้ pod ตายตอน dependency ล่ม — จะทำให้ ระบบล่มนานกว่าเดิม ต้องแก้ต้นเหตุก่อน AzureArchiveBlobStoreที่เพิ่งเขียน ทิ้งไฟล์ archive ที่ไม่ครบไว้เงียบ ๆ เมื่อ export ล้มกลางทาง
ทั้งสองเรื่อง build ผ่านและ test เขียวหมดทั้งคู่ตอนที่มันยังผิดอยู่
สิ่งที่เกือบพลาด และเป็นบทเรียนของรอบนี้
ทีมตั้งเวลาปิด Redis กับ PostgreSQL เป็นช่วง ๆ โดยตั้งใจ ปัญหาที่เห็นคือ pod ที่เกิดใหม่ระหว่างช่วงนั้นจะ
ไม่ผ่าน startup แล้ววนเกิดใหม่ตลอด เพราะ startupProbe ชี้ไปที่ /health ที่รวมทุก dependency
ทางแก้ที่ตั้งใจไว้คือย้าย startupProbe ไปที่ /health/live ซึ่งดูตรงไปตรงมา แต่กลไกจริงเป็นแบบนี้:
- งานสร้าง partition ล่วงหน้าทำงานรอบแรกตอน pod เกิด ถ้า PostgreSQL ล่มอยู่ รอบนั้นล้ม
- รอบที่ล้มทำให้ health check
requestlog-partitionsค้างสถานะ Unhealthy - check ตัวนั้นติดป้าย
ready⇒ อยู่ใน/health⇒readinessProbe(ซึ่งไม่ได้ย้าย และไม่ควรย้าย) เตะ pod ออกจาก load balancer - งานสร้าง partition ไม่มี retry มันรอรอบถัดไปตาม
IntervalHoursซึ่งค่าเริ่มต้นคือ 12 ชั่วโมง 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/live | 200 Healthy |
GET /health | 503 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 เป็นตัวเดียวที่ต่างจากคนอื่น