Python playground บนเบราว์เซอร์ที่มาพร้อมกับ runtime ขนาด 5.5 MB เริ่มทำงานล้มเหลวโดยไม่แจ้งเตือนสำหรับผู้ใช้ที่เชื่อมต่ออินเทอร์เน็ตช้า สาเหตุเกิดจากการใช้ Network Information API ผิดวิธี และแดชบอร์ดจัดกลุ่มข้อผิดพลาดที่ระบุปัญหาผิดพลาด บั๊กนี้ซ่อนตัวอยู่หลายสัปดาห์ ทำให้เสียเวลาของนักพัฒนา และทำให้ผู้ใช้บางส่วนไม่สามารถรันโค้ดได้
ปัญหาเริ่มปรากฏขึ้นอย่างไร
ตัวติดตามข้อผิดพลาดของ playground แสดงข้อความที่สะดุดตาเพียงข้อความเดียวคือ: “undefined is not an object.” หัวข้อดังกล่าวบ่งบอกว่าเป็นเพียงการพิมพ์ผิดใน JavaScript ทีมงานจึงมุ่งไปแก้โค้ดในส่วนที่ไม่มีอยู่จริง เมื่อพวกเขาตรวจสอบ metadata ดิบ ก็พบว่า 89% ของเหตุการณ์เหล่านั้นจริงๆ แล้วคือปัญหา network timeout แดชบอร์ดได้นำข้อผิดพลาดแรกที่ได้รับมาใช้ตั้งชื่อให้กับกลุ่มเหตุการณ์ทั้งหมด ทำให้มองไม่เห็นประเภทความล้มเหลวที่แท้จริง
บทเรียนที่ 1 – หัวข้อบนแดชบอร์ดอาจหลอกตาได้
แดชบอร์ดที่รวบรวมเหตุการณ์ต่างๆ จะมีประโยชน์ก็ต่อเมื่อตรรกะการรวบรวมข้อมูลนั้นสะท้อนถึงสาเหตุที่แท้จริงของแต่ละเหตุการณ์ ในกรณีนี้ การจัดกลุ่มตามตำแหน่งแทนที่จะเป็นสาเหตุของข้อผิดพลาดทำให้เกิดภาพที่ผิดพลาดว่าเป็นบั๊กฝั่ง client (client-side bug) บทเรียนที่ได้รับคือ: อย่าแก้ไขปัญหาโดยยึดตามหัวข้อบนแดชบอร์ดเพียงอย่างเดียว ให้ดึงตัวอย่างเหตุการณ์ที่เกิดขึ้นจริงมาตรวจสอบว่าเกิดอะไรขึ้นกันแน่ก่อนที่จะเริ่มจัดสรรทรัพยากร
บทเรียนที่ 2 – ค่า Placeholder ไม่ใช่ค่าการวัดผล
เพื่อหลีกเลี่ยงการโหลด runtime ขนาดใหญ่สำหรับผู้ใช้ที่เชื่อมต่ออินเทอร์เน็ตช้า โค้ดจึงเรียกใช้ Network Information API และอ่านค่าคุณสมบัติ downlink ซึ่งรายงานหน่วยเป็นเมกะบิตต่อวินาที ในการเข้าชมครั้งแรก Chrome มักจะคืนค่า placeholder แทนที่จะเป็นค่าการวัดจริง ตรรกะของระบบกลับมองว่าค่า placeholder นั้นเป็นการเชื่อมต่อที่รวดเร็วและข้ามขั้นตอนการปรับแต่ง (optimization) ไป ส่งผลให้เป็นการปิดกั้นผู้ใช้กลุ่มที่ระบบตั้งใจจะช่วยเหลือตั้งแต่แรก
ให้ถือว่าค่าเริ่มต้น (default) หรือค่า sentinel ใดๆ คือ “ไม่มีข้อมูล” ค่า placeholder ควรจะไปกระตุ้นให้ใช้กลยุทธ์สำรอง (fallback strategy) ไม่ใช่ถูกตีความว่าเป็นความเร็วที่วัดได้จริง
บทเรียนที่ 3 – สภาพเครือข่ายมีการเปลี่ยนแปลง ดังนั้นการตรวจสอบเพียงครั้งเดียวจึงเชื่อถือไม่ได้
หลังจากพบปัญหาเรื่อง downlink ทีมงานได้เปลี่ยนมาตรวจสอบ effectiveType ซึ่งจัดประเภทการเชื่อมต่อเป็น “4g”, “3g” เป็นต้น การทดสอบในห้องแล็บผ่านไปได้ด้วยดี แต่เมื่อทดสอบซ้ำในอีกไม่กี่นาทีต่อมากลับล้มเหลว การเชื่อมต่อผ่านมือถือมีความผันผวน ผู้ใช้อาจจะใช้ 4G ที่รวดเร็วในวินาทีหนึ่ง และลดลงเหลือ 3G ที่ช้ากว่าในวินาทีถัดไป การตรวจสอบการเชื่อมต่อเฉพาะตอนโหลดหน้าเว็บจึงเป็นการเสี่ยงดวง
แนวทางที่ถูกต้องคือการ subscribe ไปยัง event change บน Network Information object และตอบสนองต่อการเปลี่ยนแปลงของแบนด์วิดท์ แทนที่จะตัดสินใจเพียงครั้งเดียว
สิ่งที่ทีมงานได้เปลี่ยนแปลง
- การดาวน์โหลดแบบสองขั้นตอน – ตอนนี้ runtime จะเริ่มด้วยไฟล์ bootstrap ขนาดเล็กมาก หากระบุได้ว่าการเชื่อมต่อช้า ไฟล์ bootstrap จะไปดึง runtime ส่วนที่เหลือมาเป็นส่วนย่อยๆ (small chunks) เพื่อลดโอกาสที่จะเกิดการหยุดชะงักทั้งหมด
- การตรวจสอบแบบเรียลไทม์ – แทนที่จะอ่านค่า
downlinkเพียงครั้งเดียว ตอนนี้โค้ดจะคอยฟัง eventchangeและปรับกลยุทธ์การดาวน์โหลดแบบทันทีทันใด - การเลือกแหล่งข้อมูลที่เสถียร – ก่อนหน้านี้ระบบจะสลับ CDN ระหว่างการดาวน์โหลดเมื่อพบ endpoint ที่เร็วกว่า ซึ่งในลิงก์ที่ช้าจะทำให้การดาวน์โหลดต้องเริ่มใหม่จากศูนย์และทำให้ปัญหาหนักกว่าเดิม ตรรกะใหม่จะล็อกแหล่งข้อมูลไว้ตลอดระยะเวลาการดาวน์โหลด
- การเลื่อนการเขียน cache – การทำงานกับ cache ที่หนักหน่วงซึ่งเคยรันก่อนที่แอปจะพร้อมใช้งาน ถูกเลื่อนออกไปจนกว่า runtime จะเริ่มทำงาน เพื่อคืนแบนด์วิดท์ให้กับการดาวน์โหลดที่สำคัญ
ผลกระทบในวงกว้าง
สำหรับนักพัฒนาที่สร้างเครื่องมือบนเว็บ ความผันผวนของเครือข่ายคือเรื่องสำคัญอันดับต้นๆ การล้มเหลวแบบเงียบๆ (silent failure) บนลิงก์ที่ช้าสร้างความหงุดหงิดให้ผู้ใช้และทำให้ข้อมูล telemetry ผิดเพี้ยน นำทีมไปสู่เส้นทางการแก้บั๊กที่ผิด ในกรณีนี้ การตีความข้อมูลผิดพลาดทำให้ต้องเสียเวลาสืบสวนโดยเปล่าประโยชน์นานหลายสัปดาห์
สิ่งที่ควรระวังต่อไป
บทสรุป: เมื่อข้อมูลดูสะอาดเกินไป มันอาจจะเป็นแค่ค่า placeholder; เมื่อหัวข้อบนแดชบอร์ดชี้ไปที่บั๊กเพียงตัวเดียว ให้ขุดลึกลงไปอีก; และเมื่อคุณตัดสินใจโดยอิงจากการอ่านค่าเครือข่ายเพียงครั้งเดียว คุณกำลังเดิมพันกับเป้าหมายที่เคลื่อนที่อยู่ตลอดเวลา การปรับตัวให้เข้ากับความเป็นจริงเหล่านี้จะเปลี่ยนความล้มเหลวที่เงียบเชียบให้กลายเป็นเหตุการณ์ที่คาดการณ์และกู้คืนได้
