- ในระบบแบบกระจายสมัยใหม่ แนวทางล็อกแบบเดิมมีข้อจำกัดเชิงโครงสร้างที่ทำให้ไม่สามารถถ่ายทอดความจริงได้
- ล็อกยังคงถูกออกแบบโดยยึดสมมติฐานของ สภาพแวดล้อมเซิร์ฟเวอร์เดี่ยวแบบปี 2005 ทำให้สูญเสียคอนเท็กซ์ของคำขอที่ต้องผ่านหลายบริการ ฐานข้อมูล และแคช
- การค้นหาด้วยสตริงอย่างเดียวไม่เข้าใจ โครงสร้าง ความสัมพันธ์ และความเชื่อมโยง จึงทำให้หาสาเหตุของปัญหาได้ยาก
- ทางแก้คือการบันทึก 'Wide Event' เดี่ยว (หรือ Canonical Log Line) ที่เก็บ ทุกคอนเท็กซ์ ของแต่ละคำขอ
- วิธีนี้ทำให้ล็อกจากข้อความธรรมดาเปลี่ยนเป็น ทรัพย์สินข้อมูลที่วิเคราะห์ได้
ปัญหาพื้นฐานของการทำล็อก
- ล็อกแบบเดิมถูกสร้างขึ้นโดยอิงกับ ยุคเซิร์ฟเวอร์โมโนลิธิก จึงไม่สามารถสะท้อน สถาปัตยกรรมบริการแบบกระจาย ในปัจจุบันได้
- คำขอหนึ่งครั้งอาจผ่านหลายบริการ DB แคช และคิว แต่ล็อกก็ยังถูกบันทึกโดยยึดเกณฑ์ของเซิร์ฟเวอร์เดี่ยว
- ในตัวอย่างล็อก มีการสร้าง 13 บรรทัดต่อ 1 คำขอ ทำให้เมื่อมี ผู้ใช้พร้อมกัน 10,000 คน จะมี 130,000 บรรทัดต่อวินาที แต่ข้อมูลส่วนใหญ่แทบไม่มีความหมาย
- สิ่งที่ต้องการเมื่อเกิดปัญหาคือ คอนเท็กซ์ (context) แต่ล็อกปัจจุบันกลับขาดสิ่งนี้
ข้อจำกัดของการค้นหาด้วยสตริง
- เมื่อผู้ใช้รายงานว่า “ชำระเงินไม่ได้” ต่อให้ค้นหาล็อกด้วยอีเมลหรือ user_id ก็ยากจะได้ผลลัพธ์ที่ใช้งานได้ เพราะ ไม่มีโครงสร้างที่สม่ำเสมอ
- user ID เดียวกันอาจถูกบันทึกในรูปแบบ นับสิบแบบ เช่น
user-123, user_id=user-123, {"userId":"user-123"}
- รูปแบบล็อกระหว่างบริการต่างกัน ทำให้ ติดตามเหตุการณ์ที่เกี่ยวข้องกันข้ามบริการไม่ได้
- ปัญหาสำคัญคือ ล็อกถูกออกแบบโดยเน้นฝั่ง การเขียน (write) และไม่ได้ปรับให้เหมาะกับ การค้นหา/คิวรี (query)
คำจำกัดความของแนวคิดหลัก
- Structured Logging: วิธีบันทึกในรูปแบบ คีย์-ค่า (JSON) แทนสตริง
- Cardinality: จำนวนค่าที่ไม่ซ้ำกันของฟิลด์ เช่น
user_id จะมีค่าสูงมาก
- Dimensionality: จำนวนฟิลด์ภายในเหตุการณ์ล็อก ยิ่งมากก็ยิ่งมีศักยภาพในการวิเคราะห์สูง
- Wide Event / Canonical Log Line: เหตุการณ์ล็อกเดี่ยวที่มีคอนเท็กซ์ครบถ้วน ต่อหนึ่งคำขอ
- ระบบล็อกส่วนใหญ่มัก จำกัดข้อมูลที่มี cardinality สูงเพราะปัญหาต้นทุน แต่ในความเป็นจริง ข้อมูลแบบนี้กลับมีประโยชน์ที่สุดต่อการดีบัก
ข้อจำกัดของ OpenTelemetry
- OpenTelemetry(OTel) คือ ชุดโปรโตคอลและ SDK ที่ให้เพียงมาตรฐานสำหรับการเก็บรวบรวมและส่งข้อมูล
- แต่ OTel ไม่ได้ทำสิ่งต่อไปนี้
- ไม่ได้ตัดสินใจว่าจะล็อกอะไร
- ไม่ได้เพิ่มคอนเท็กซ์ทางธุรกิจโดยอัตโนมัติ (เช่น ระดับสมาชิก ยอดในตะกร้า ฯลฯ)
- ไม่ได้เปลี่ยน วิธีคิดเรื่องการทำล็อก ของนักพัฒนา
- แม้จะใช้ไลบรารีเดียวกัน แต่ประสบการณ์การดีบักระหว่างการทำ instrumentation ที่ ตั้งใจเพิ่มคอนเท็กซ์ กับการทำแบบพื้นฐานนั้นแตกต่างกันอย่างชัดเจน
- OTel เป็นเพียง ท่อส่งข้อมูล (plumbing) และ นักพัฒนาต้องเป็นผู้ตัดสินใจเองว่าจะส่งอะไรผ่านท่อนั้น
แนวทาง Wide Event / Canonical Log Line
- ต้องเลิกทำล็อกแบบที่เน้นว่า “โค้ดกำลังทำอะไร” แล้วหันมาบันทึกว่า “เกิดอะไรขึ้นกับคำขอนี้”
- สำหรับแต่ละคำขอ ให้สร้าง เหตุการณ์แบบกว้างหนึ่งรายการ ในระดับบริการ
- สามารถมีฟิลด์ได้มากกว่า 50 ฟิลด์ เช่น คำขอ ผู้ใช้ การชำระเงิน ข้อผิดพลาด และสภาพแวดล้อม
- ตัวอย่าง JSON มี คอนเท็กซ์สำหรับการดีบักทั้งหมด เช่น
user_id, subscription_tier, service_version, error_code
- ทำให้สามารถค้นหาเพียงครั้งเดียวเพื่อ วิเคราะห์ได้ทันที เช่น “สาเหตุของความล้มเหลวในการชำระเงินของผู้ใช้พรีเมียม”
การใช้คิวรีกับ Wide Event
- Wide Event ไม่ได้ถูกใช้ผ่านการค้นหาข้อความธรรมดา แต่ใช้เป็น การคิวรีข้อมูลแบบมีโครงสร้าง
- ด้วยข้อมูลที่มี cardinality สูงและหลายมิติ จึงสามารถดีบักได้ในระดับ การวิเคราะห์แบบเรียลไทม์
- ตัวอย่างเช่น สามารถรันคิวรีได้ทันทีอย่าง “สรุปอัตราความล้มเหลวของการชำระเงินของผู้ใช้พรีเมียมในช่วง 1 ชั่วโมงที่ผ่านมา แยกตาม error code”
รูปแบบการนำไปใช้
- สร้างเหตุการณ์ตลอดทั้งวงจรชีวิตของคำขอ และ แสดงผลเพียงครั้งเดียวในตอนท้าย
- กำหนดค่าเริ่มต้นของฟิลด์พื้นฐานใน middleware เช่น
request_id, timestamp, method, path
- ค่อย ๆ เพิ่มข้อมูลผู้ใช้ ตะกร้า การชำระเงิน และข้อผิดพลาดจากใน handler
- สุดท้ายบันทึกเหตุการณ์ JSON เดี่ยวด้วย
logger.info(event)
ควบคุมต้นทุนด้วยการทำ Sampling
- หากบันทึกมากกว่า 50 ฟิลด์ต่อคำขอ ต้นทุนจะพุ่งสูง จึงจำเป็นต้องมี sampling
- การสุ่มตัวอย่างแบบสุ่มธรรมดาอาจทำให้พลาดข้อผิดพลาดสำคัญได้
- มีการเสนอแนวทาง Tail Sampling
- เก็บข้อผิดพลาด (เช่น 500) ไว้เสมอ
- เก็บคำขอที่ช้า (p99 ขึ้นไป) ไว้เสมอ
- เก็บผู้ใช้ VIP หรือเซสชันที่มีแฟล็กเฉพาะไว้เสมอ
- ส่วนที่เหลือสุ่มเก็บเพียง 1~5%
- วิธีนี้ช่วยให้ ลดต้นทุนและรักษาเหตุการณ์สำคัญไว้ได้พร้อมกัน
สรุปความเข้าใจผิดที่พบบ่อย
- Structured Logging ≠ Wide Event: แค่ใช้ฟอร์แมต JSON ยังไม่พอ คอนเท็กซ์คือหัวใจสำคัญ
- ใช้ OpenTelemetry ≠ มี observability ครบถ้วน: มันเพียงทำให้การเก็บรวบรวมเป็นมาตรฐาน แต่ จะบันทึกอะไรยังเป็นหน้าที่ของนักพัฒนา
- ไม่ใช่สิ่งเดียวกับ Tracing: tracing แสดงการไหลระหว่างบริการ ส่วน Wide Event ให้ คอนเท็กซ์ภายในบริการ
- การแบ่งว่า ล็อกมีไว้ดีบัก ส่วนเมตริกมีไว้ทำแดชบอร์ด นั้นไม่จำเป็น — Wide Event ตอบโจทย์ได้ทั้งสองแบบ
- ความเชื่อที่ว่า ข้อมูล cardinality สูงมีราคาแพง นั้นล้าสมัยแล้ว เพราะ ClickHouse, BigQuery และฐานข้อมูลสมัยใหม่ จัดการเรื่องนี้ได้อย่างมีประสิทธิภาพ
ผลลัพธ์ของการนำ Wide Event มาใช้
- การดีบักเปลี่ยนจาก การขุดค้น (archaeology) ไปเป็น การวิเคราะห์ (analytics)
- จากเดิมที่ต้องใช้ grep กับล็อกของ 50 บริการเพื่อหาว่า “ผู้ใช้ชำระเงินล้มเหลว”
มาเป็นการวิเคราะห์แบบ คิวรีเดียว ว่า “อัตราความล้มเหลวของการชำระเงินของผู้ใช้พรีเมียมแยกตาม error code”
- ผลลัพธ์คือ ล็อกเปลี่ยนจาก เครื่องมือที่พูดโกหก ไปเป็น ทรัพย์สินข้อมูลที่บอกความจริง
1 ความคิดเห็น
ความเห็นจาก Hacker News
บทความอ่านยากและให้ความรู้สึกว่า เหมือนมี AI ช่วยเขียน ถึงอย่างนั้นสาระก็ยังมีคุณค่า และน่าจะดีกว่านี้ถ้ากระชับกว่านี้
สิ่งที่ฉันคิดไว้ล่าสุดมีดังนี้
สำหรับหัวข้อนี้จะไม่พูดถึง Charity Majors ก็คงไม่ได้ เธอเผยแพร่แนวคิด “wide events” และ “observability” มานานกว่าสิบปี และสร้าง Honeycomb.io ขึ้นบนปรัชญานั้น
ทุกวันนี้มีเครื่องมือหลายแบบที่ทำแนวทางนี้ได้ สิ่งสำคัญคือใช้ structured logs หรือ traces เพื่อเก็บ wide event และใช้เครื่องมือที่มีการแสดงผลอย่าง time series, histogram ฯลฯ อย่างครบถ้วน
ฉันเห็นด้วยกับข้ออ้างบางส่วนของบทความ แต่แนวทางที่เก็บแค่ wide event เดียว มีจุดอันตรายอยู่ ถ้ามี exception หรือ timeout เกิดขึ้นกลางคำขอ ก็อาจไม่เหลืออะไรเลย
ล็อกจากเฟรมเวิร์กพื้นฐานของภาษา หรือจาก dependency ต่าง ๆ ก็อาจหายไปด้วย
เพราะฉะนั้นควรใช้สิ่งนี้เป็น เลเยอร์เสริม ที่วางทับบนล็อกเดิมมากกว่า โดยมี ID ระดับ request/session แล้วค่อย aggregate ใน ClickHouse หรือที่คล้ายกัน
พรีเซนเทชันและ ตัวอย่างแบบโต้ตอบได้ ทำได้ยอดเยี่ยม แต่สุดท้ายแล้วก็สรุปได้ว่า “เพิ่มแท็กที่มีโครงสร้างลงในล็อก”
ฉันรู้สึกว่า wide log ให้ประโยชน์ไม่มากเมื่อเทียบกับความซับซ้อนและ ความอ่านยากที่เพิ่มขึ้น
แค่
grep \"uid=user-123\" application.logก็มักพอแล้ว จะต้องใส่ถึงวิธีจัดส่งของผู้ใช้ลงไปด้วยจริงหรือ(อนึ่ง ในเบราว์เซอร์ Brave บน Android checkbox ใช้งานไม่ได้)
grep '\"uid\": \"user-123\"'ได้อยู่ และใช้ตัวเลือก--contextเพื่อดูบรรทัดรอบ ๆ ได้ฉันเคยทำงานในสภาพแวดล้อมการผลิตเซมิคอนดักเตอร์ที่มี ผู้เข้าร่วมใน message bus หลายพันตัว ระบบสร้างล็อก 300~400MB ต่อชั่วโมง แต่ก็ยัง จัดการได้สบายด้วย grep และเครื่องมือ CLI เท่านั้น
ล็อกเป็นเพียงลำดับเหตุการณ์ตามเวลา ส่วนการวิเคราะห์เชิงรายละเอียดใช้ Oracle query จัดการ ล็อกคือเครื่องมือสำหรับทำความเข้าใจความเป็นเหตุเป็นผลของเหตุการณ์
ล็อกบอกว่า “เมื่อไร มีอะไรเกิดขึ้น” ส่วน “ทำไม” ต้องหาเอาจากการผสมกันของโค้ด ข้อมูล และเหตุการณ์
สำหรับฉัน อินเทอร์เฟซอย่าง ELK stack ไม่ค่อยเหมาะกับการสำรวจแบบสัญชาตญาณ ล็อกควรถูกอ่านแบบไล่ตามด้วยความรู้สึกได้
คำแนะนำช่วงท้ายบทความที่ว่า “ให้ล็อกทุก error, exception และ slow request” เป็นแนวคิดที่ อันตราย
ตัวอย่างเช่น ถ้า dependency ช้าลง ปริมาณล็อกอาจพุ่งขึ้น 100 เท่า
ในช่วงเกิดเหตุขัดข้อง ระบบควรทำงานให้น้อยลงเพื่อฟื้นตัวได้ง่ายขึ้น แต่ล็อกที่พุ่งสูงกลับอาจทำให้เกิด ความล้มเหลวต่อเนื่อง
ยิ่งล็อกเยอะ อัตราการ sampling ก็จะถูกปรับอัตโนมัติเพื่อไม่ให้ระบบโอเวอร์โหลด
ในซอฟต์แวร์สมัยใหม่ การจะอธิบายว่า “เกิดอะไรขึ้น” ให้ครบด้วยล็อกเพียงตัวเดียวนั้นทำได้ยาก
ดังนั้นจึงต้องมีทั้ง Vertical correlation และ Horizontal correlation
ระหว่างชั้นบนล่างในสแตกควรแชร์ค่า correlation เดียวกัน และเมื่อระบบสื่อสารกันก็ควรมี correlation ระหว่าง peer ด้วย
การเพิ่มค่าพวกนี้เข้าไปใน API หรือโปรโตคอลนั้นทำได้ยาก แต่ถ้าออกแบบ transaction ID ไว้ล่วงหน้า ก็จะติดตามทั้งระบบได้
ฉันคิดว่าการ จดโดเมนแยกสำหรับบทความเดียว นั้นไม่ค่อยยั่งยืน
เพราะต้องจ่ายค่าต่ออายุทุกปี ใช้บล็อกส่วนตัวหรือ subdomain น่าจะดีกว่า
ตัวอย่างเช่น logging-sucks.boristane.com ในรูปแบบนี้น่าจะเหมาะกว่า
สำหรับคำกล่าวที่ว่า “ล็อกเป็นของเหลือจากยุค monolithic” ฉันคิดว่า ล็อกในเครื่องยังคงมีประโยชน์
หน้าที่ดั้งเดิมของมันคือบันทึกบทสนทนาภายใน local process และถ้าจะเข้าใจสถานการณ์ของเซิร์ฟเวอร์อื่นก็ต้องใช้ transaction tracing
แค่ดูล็อกในตำแหน่งที่เหมาะสมก็มักไปถึงต้นตอของปัญหาได้
ล็อกที่มีคอนเท็กซ์สมบูรณ์ เมื่อจับคู่กับเอนจินวิเคราะห์ ก็สามารถนำไปใช้ปรับปรุงผลิตภัณฑ์ได้
ฉันเห็นด้วยกับประโยคที่ว่า “อย่าล็อกสิ่งที่โค้ดทำ แต่ให้ล็อกว่าสิ่งใดเกิดขึ้นกับคำขอ” แต่ผู้เขียนดูเหมือนยังมีประสบการณ์ไม่มาก
ฉันเรียกสิ่งนี้ว่า “bug parts logging” และคิดว่าควรใส่ สัญญาณล่วงหน้า อย่างเส้นทางการประมวลผล จำนวนครั้ง และเวลาไว้ด้วย
ล็อกไม่เหมือน metric หรือ audit ถ้าล็อกล้มเหลว การประมวลผลควรดำเนินต่อได้ แต่ถ้า audit ล้มเหลวถือว่าร้ายแรง
เหมือนแนวคิด “historian” ในระบบ SCADA เราควรแยก สิ่งที่สังเกตได้ (observables) ออกจาก สิ่งที่ใช้ประเมิน (evaluatives)
ตัวอย่างเช่น event ละเอียดจากเซ็นเซอร์เชื้อเพลิงมีประโยชน์ต่อการวินิจฉัย แต่ไม่จำเป็นต่อคำถามว่า “ไปถึงจุดหมายได้ไหม”
ท้ายที่สุดสิ่งสำคัญคือการทำให้ชัดว่า จะสังเกตอะไร และจะประเมินอะไร
แม้วิธีจัดเก็บ แปลง และสืบค้นจะแตกต่างกัน แต่ก็สามารถออกแบบ จุดใช้งานและกลไก ให้เหมือนกันได้
แบบนี้จะทำให้การออกแบบระบบง่ายขึ้น และยังสามารถนำล็อกที่เก็บระยะยาวกลับมาประมวลผลใหม่ในภายหลังได้