เวลาในระบบมีหลายชั้น และมันไม่ตรงกัน — บทเรียนจากบั๊กที่ไล่ 8 วันแล้วสรุปผิดหลายรอบ
เรื่องเวลาดูเหมือนเป็นรายละเอียดเล็ก ๆ จนกระทั่งมันทำให้ทีมไล่หาสาเหตุผิดทางอยู่หลายวัน
บทความนี้รวบรวมสิ่งที่เราเจอจริงเกี่ยวกับเวลาในระบบ — ตั้งแต่นาฬิกาของเครื่อง ไปจนถึงตัวเลขที่ผู้ใช้เห็นบนหน้าจอ
เหตุการณ์ที่ทำให้ต้องจริงจังกับเรื่องนี้
ทีมไล่บั๊กงานสร้างบิลอัตโนมัติที่ค้างอยู่ 8 วัน และสรุปสาเหตุผิดหลายรอบ
สาเหตุของการสรุปผิดไม่ใช่เพราะข้อมูลไม่พอ แต่เพราะเอาเวลาจากบันทึกของระบบไปเทียบกับเวลาในฐานข้อมูลตรง ๆ
ซึ่งเทียบกันไม่ได้ — เพราะบนเครื่องจริงมีเขตเวลาอยู่ 3 ชั้น และไม่ตรงกันสักคู่
| ชั้น | เขตเวลา |
|---|---|
| ตัวเครื่อง | UTC+8 |
| การเชื่อมต่อฐานข้อมูล | Asia/Bangkok (UTC+7) |
| โค้ดที่เขียนข้อมูลลงฐานข้อมูล | UTC |
เวลาสองค่าที่ต่างกัน 1 ชั่วโมงจึงอาจเป็นเวลาเดียวกัน หรือเวลาที่ดูเท่ากันอาจห่างกัน 7 ชั่วโมงจริง ๆ
และสิ่งที่แย่ที่สุดคือ ไม่มีหน้าไหนในระบบบอกเรื่องนี้เลย ต้องเข้าเครื่องไปสั่งดูเองถึงจะรู้ ซึ่งแปลว่าคนที่ไม่ได้เข้าเครื่องจะไม่มีทางรู้ว่ากำลังเทียบของที่เทียบกันไม่ได้
สิ่งที่เราสร้าง — หน้าดูเวลาทั้ง 3 ชั้นพร้อมกัน
เราทำหน้าจอที่แสดงเวลาของทั้งสามชั้นเรียงกัน พร้อมเขตเวลา ระยะห่างจากเวลามาตรฐานสากล และเวลาปัจจุบันของแต่ละชั้น
และเพิ่มคอลัมน์ที่แปลงทุกแถวเป็นเวลามาตรฐานสากล เพื่อให้เทียบด้วยตาได้ทันทีโดยไม่ต้องคำนวณในหัว
เตือนเมื่อไม่ตรงกัน พร้อมประโยคที่เอาไปใช้ได้จริง
เมื่อสามชั้นไม่ตรงกัน หน้าจอจะขึ้นเตือนพร้อมคำแนะนำที่ใช้ได้ทันที
ห้ามเอาเวลาจากคนละชั้นมาลบกันตรง ๆ ให้ฐานข้อมูลคำนวณผลต่างให้
เราตั้งใจเขียนเป็นประโยคสั่งการ ไม่ใช่คำอธิบายทางเทคนิค เพราะคนที่มาอ่านหน้านี้กำลังไล่ปัญหาอยู่และต้องการรู้ว่าทำอะไรต่อ ไม่ใช่รู้ว่าเกิดอะไรขึ้น
แยก “เขตเวลาไม่ตรง” ออกจาก “นาฬิกาเดินไม่ตรง”
นี่คือส่วนที่เราคิดว่ามีค่าที่สุดของหน้านี้
ปัญหาสองอย่างนี้มีอาการเหมือนกันเป๊ะ — เวลาไม่ตรงกัน — แต่แก้คนละทางโดยสิ้นเชิง
- เขตเวลาไม่ตรง — เวลาถูกต้อง แต่แสดงในเขตเวลาต่างกัน แก้ด้วยการตั้งค่าเขตเวลาหรือแปลงก่อนเทียบ
- นาฬิกาเดินไม่ตรง — เวลาผิดจริง ๆ แก้ด้วยการซิงก์กับเซิร์ฟเวอร์เวลา
ระบบจึงตรวจว่าถ้าเวลาของแอปกับของฐานข้อมูลห่างกันเกิน 2 วินาที ให้ขึ้นเตือนคนละอันว่านี่เป็นเรื่องการซิงก์นาฬิกา ไม่ใช่เรื่องเขตเวลา
สองวินาทีเป็นเกณฑ์ที่เลือกมาให้ทนกับความหน่วงตามปกติของการเรียกข้ามระบบ แต่ยังจับความคลาดเคลื่อนที่แท้จริงได้
หน้าจอยังแสดงสถานะว่าเครื่องซิงก์กับเซิร์ฟเวอร์เวลาอยู่หรือไม่ และตั้งเซิร์ฟเวอร์เวลาได้จากหน้าเว็บ
เปลี่ยนเขตเวลาแล้วต้องรีสตาร์ตด้วย
รายละเอียดที่คนมักลืม — บริการที่รันอยู่จะอ่านเขตเวลาตอนเริ่มทำงานครั้งเดียว การเปลี่ยนเขตเวลาของเครื่องจึงไม่มีผลกับบริการที่รันค้างอยู่
เราจึงใส่ปุ่มรีสตาร์ตทุกบริการไว้ในหน้าเดียวกัน เพื่อไม่ให้เกิดสภาพที่ตั้งค่าไปแล้วแต่ระบบยังใช้ค่าเก่าโดยไม่มีใครรู้
ถ้าสิทธิ์ไม่พอ ต้องบอกก่อนกด
การเปลี่ยนเขตเวลาของเครื่องต้องใช้สิทธิ์ระดับระบบ ซึ่งอาจยังไม่ได้ตั้งไว้
เราเลือกตรวจสิทธิ์ล่วงหน้าแล้วปิดปุ่มพร้อมบอกตรง ๆ ว่าต้องเพิ่มอะไรที่ไหน แทนที่จะปล่อยให้กดแล้วเจอข้อความผิดพลาดดิบ ๆ
ส่วนการอ่านค่าไม่ต้องใช้สิทธิ์พิเศษ จึงใช้ได้ทันทีแม้ยังไม่ได้ตั้งอะไร ซึ่งเป็นส่วนที่มีค่าที่สุดของหน้านี้อยู่แล้ว
ทำไม “วันนี้” เป็นคำที่อันตราย
ปัญหาเรื่องเวลาไม่ได้อยู่แค่ที่การตั้งค่าเครื่อง แต่ฝังอยู่ในโค้ดด้วย
เครื่องใช้งานจริงของเรารันด้วยเวลามาตรฐานสากล ซึ่งช้ากว่าเวลาไทย 7 ชั่วโมง
ผลคือคำสั่งที่ถามว่า “วันนี้วันที่เท่าไร” จะได้วันที่ผิดไปหนึ่งวันทุกวัน ในช่วงเที่ยงคืนถึงเจ็ดโมงเช้าเวลาไทย
ลองนึกภาพผลกระทบ — พนักงานเปิดบิลตอนตีสองของวันที่ 5 แล้วเอกสารถูกบันทึกเป็นวันที่ 4 ซึ่งอาจตกอยู่คนละงวดบัญชี คนละเดือนภาษี หรือคนละปีบัญชี
และมันไม่พังให้เห็น มันแค่บันทึกวันที่ผิดเงียบ ๆ
เราแก้ทั้งระบบ ~90 จุด
เราไล่แทนที่ทุกจุดที่ถามเวลาจากระบบโดยตรง ด้วยชั้นกลางที่รู้เรื่องเขตเวลา แล้วจัดลำดับความเสี่ยงเพื่อให้แก้จุดที่อันตรายที่สุดก่อน
จุดที่อันตรายที่สุดคือจุดที่บันทึกข้อมูลลงฐานข้อมูลโดยไม่มีคนตรวจ เช่นการสร้างเอกสารผ่านช่องทางที่ระบบภายนอกเรียกเข้ามา — ไม่มีใครเห็นวันที่ก่อนบันทึก จึงไม่มีใครทักท้วง
รองลงมาคือค่าเริ่มต้นของฟอร์มราว 34 จุด ซึ่งอันตรายน้อยกว่าเพราะผู้ใช้เห็นวันที่บนหน้าจอและแก้ได้ก่อนบันทึก
ความต่างที่สำคัญที่สุด — “เวลาที่เกิดขึ้น” กับ “เวลาบนหน้าปัด”
ระหว่างทำงานนี้เราต้องแยกให้ชัดว่าเวลาที่กำลังจัดการเป็นแบบไหน เพราะสองแบบนี้ต้องปฏิบัติต่างกันโดยสิ้นเชิง
เวลาที่เกิดขึ้น
เช่นเวลาที่ผู้ใช้กดบันทึก เวลาที่ระบบส่งอีเมล เวลาที่การเชื่อมต่อหมดอายุ
เวลาแบบนี้คือจุดเดียวบนเส้นเวลาของโลก ควรเก็บเป็นเวลามาตรฐานสากลเสมอ แล้วค่อยแปลงเป็นเขตเวลาของผู้ใช้ตอนแสดงผล
เวลาบนหน้าปัด
เช่นวันที่บนเอกสาร วันครบกำหนดชำระ เวลาเริ่มโปรโมชันที่แพลตฟอร์มกำหนด
เวลาแบบนี้ไม่ใช่จุดบนเส้นเวลา แต่คือตัวเลขบนปฏิทิน — “วันที่ 5 สิงหาคม” บนใบกำกับภาษีคือวันที่ 5 ไม่ว่าจะดูจากที่ไหนบนโลก การแปลงเขตเวลาให้กับมันคือการทำให้ผิด
เราจึงมีรายการที่จงใจไม่แตะระหว่างงานนี้ — วันที่ตามข้อกำหนดของเอกสารภาษี เวลาเริ่มโปรโมชันของแพลตฟอร์ม และอายุของคุกกี้
การรู้ว่าอะไรไม่ควรแก้ ใช้เวลาคิดมากกว่าการแก้เสียอีก
“วันนี้” ของใคร
เมื่อระบบรองรับลูกค้าหลายราย คำถามต่อมาคือเขตเวลาของใคร
งานอัตโนมัติที่ทำงานรายวันเดิมตรึงไว้ที่เวลาไทย ซึ่งใช้ได้ตราบใดที่ลูกค้าทุกรายอยู่ในไทย แต่เป็นสมมติฐานที่ไม่ควรฝังไว้ในโค้ด
เราเปลี่ยนให้งานรายวันคำนวณคำว่า “วันนี้” และจุดตัดรอบตามเขตเวลาของลูกค้าแต่ละราย ครอบคลุมราว 40 ไฟล์
ผลคือการแจ้งเตือนที่อิงวันที่ เช่นลูกหนี้เกินกำหนดหรือสรุปยอดรายวัน ตรงกับเที่ยงคืนของลูกค้าจริง ไม่ใช่เที่ยงคืนของเซิร์ฟเวอร์
ในทางกลับกัน สิ่งที่ต้องเทียบด้วยเวลามาตรฐานสากลล้วนก็ต้องชัดเจนเช่นกัน เช่นการตรวจว่าโทเคนเชื่อมต่อร้านค้าหมดอายุหรือยัง ซึ่งเป็นข้อตกลงกับระบบภายนอกที่ไม่เกี่ยวกับเขตเวลาของใครเลย
เวลาที่ผู้ใช้กรอกเข้ามาจากเบราว์เซอร์
อีกชั้นที่มักถูกลืมคือเวลาที่เดินทางมาจากเครื่องของผู้ใช้ ซึ่งมีเขตเวลาของตัวเองที่เราควบคุมไม่ได้
กฎของช่องกรอก
- ช่องกรอกวันที่ ส่งค่ามาเป็นรูปแบบมาตรฐานสากลอยู่แล้ว ใช้ได้เลยโดยไม่ต้องแปลง
- ช่องกรอกวันที่พร้อมเวลา ส่งมาเป็นเวลาท้องถิ่นของเครื่องผู้ใช้ โดยไม่มีเขตเวลาติดมา — ถ้ารับไปเก็บตรง ๆ จะได้เวลาที่ไม่รู้ว่าของโซนไหน จึงต้องแปลงเป็นเวลามาตรฐานสากลก่อนส่งที่ฝั่งเบราว์เซอร์
กฎของข้อมูลที่วิ่งบนสาย
เรากำหนดให้ทุกอย่างที่ส่งข้ามระหว่างเบราว์เซอร์กับเซิร์ฟเวอร์ใช้รูปแบบมาตรฐานสากลตายตัวเสมอ
- วันที่เชิงธุรกิจ — ปี-เดือน-วัน ไม่มีเวลา ไม่มีเขตเวลา
- เวลาที่เกิดขึ้น — รูปแบบสากลที่ลงท้ายด้วยสัญลักษณ์บอกว่าเป็นเวลามาตรฐานสากล
- ห้ามส่งปี พ.ศ. ห้ามส่งรูปแบบวัน/เดือน/ปี และห้ามส่งเวลาท้องถิ่นที่ไม่มีเขตเวลากำกับ ข้ามสาย
เหตุผลคือรูปแบบสำหรับมนุษย์มีความกำกวมเสมอ — เลข 03/04 เป็นได้ทั้งวันที่ 3 เมษายน และวันที่ 4 มีนาคม ขึ้นกับว่าใครอ่าน
ข้อยกเว้นที่ต้องกักไว้ที่ขอบ
มีที่เดียวที่เราส่งปี พ.ศ. ออกไปบนสาย คือหัวข้ออีเมลที่ส่งเอกสารภาษีให้กรมสรรพากร ซึ่งเป็นรูปแบบที่ปลายทางบังคับ ไม่ใช่ของเรา
เราจึงกักมันไว้เป็น “ข้อตกลงกับภายนอก” ที่ขอบระบบ พร้อมหมายเหตุห้ามให้รูปแบบนี้รั่วไปใช้ที่อื่น
กับดักการคำนวณวันที่ในเบราว์เซอร์
อันนี้เจอจริงและเป็นกับดักคลาสสิกที่คนเขียนเว็บเจอกันทั้งโลก
เมื่อสร้างวันที่จากข้อความรูปแบบ ปี-เดือน-วัน เบราว์เซอร์จะตีความเป็นเวลามาตรฐานสากล แต่คำสั่งอ่านและบวกวันที่ทำงานเป็นเวลาท้องถิ่น
ผลคือการบวกวันอาจได้วันที่เลื่อนไปหนึ่งวัน ขึ้นกับว่าผู้ใช้อยู่โซนไหน — เราเจอที่หน้าคำนวณวันครบกำหนดชำระ
วิธีแก้คือใช้คำสั่งชุดที่ทำงานเป็นเวลามาตรฐานสากลให้ตรงกันทั้งการอ่านและการเขียน หรือแยกข้อความออกเป็นปี เดือน วัน แล้วคำนวณเองตรง ๆ
เวลาปัจจุบันต้องมาจากที่เดียว
กฎอีกข้อคือเวลาปัจจุบันต้องมาจากโค้ดเสมอ ไม่ใช่จากฐานข้อมูล
เหตุผลคือถ้าปล่อยให้ฐานข้อมูลเป็นคนใส่เวลาให้ เราจะได้เวลาตามเขตเวลาของการเชื่อมต่อฐานข้อมูล ซึ่งเป็นอีกชั้นหนึ่งที่ตั้งไว้ไม่ตรงกับที่อื่นตามที่เล่าไปตอนต้น
และการมีแหล่งเวลาหลายแหล่งแปลว่าไม่มีใครตอบได้ว่าเวลาในระบบเชื่อถือได้แค่ไหน
บังคับให้ทำถูกตั้งแต่ตอนเขียน
การไล่แก้ 90 จุดจะไม่มีความหมายถ้าจุดที่ 91 กลับมาผิดอีก
เราจึงเพิ่มด่านตรวจอัตโนมัติที่ไม่ยอมให้จัดรูปแบบเวลาดิบ ๆ ในหน้าจอ — ทุกการแสดงวันเวลาต้องผ่านชุดเครื่องมือกลางที่รู้เขตเวลาของลูกค้า
วิธีนี้ดีกว่าการเขียนกฎไว้ในเอกสาร เพราะกฎที่ต้องอาศัยความจำจะถูกลืม ส่วนด่านตรวจจะทักท้วงตั้งแต่ก่อนโค้ดถูกรวมเข้าระบบ
เมื่อนาฬิกาเดินล้ำหน้า — งานที่ค้าง 11 วันโดยไม่มีข้อผิดพลาด
เคสสุดท้ายเป็นตัวอย่างว่าเวลาที่ผิดสร้างความเสียหายแบบที่คาดไม่ถึงได้อย่างไร
กลไก
งานเบื้องหลังของเราจะ “จับจอง” ก่อนทำ พร้อมบันทึกเวลาที่เริ่ม ถ้างานค้างนานเกินเพดาน ระบบจะยึดคืนมาให้คนอื่นทำต่อ — กันงานค้างถาวรเมื่อเครื่องที่จองไปตายกลางคัน
ช่องโหว่
เงื่อนไขการยึดคืนคือ “เวลาที่เริ่มเก่ากว่าเพดาน” ซึ่งไม่มีขอบบน
ถ้าเวลาที่บันทึกไว้อยู่ในอนาคต เพราะนาฬิกาเพี้ยน มันจะไม่มีวันเก่ากว่าเพดาน งานนั้นจึงยึดคืนไม่ได้ตลอดกาล
ผลคืองานหยุดเดินถาวร โดยไม่มีข้อผิดพลาดอะไรเลย — ไม่มีอะไรพัง ไม่มีอะไรแจ้งเตือน มีแค่งานที่ไม่ถูกทำ
เกิดขึ้นจริงกับงานของแพลตฟอร์มหนึ่งที่ค้างอยู่ 11 วัน
วิธีแก้
เพิ่มค่าเผื่อความคลาดเคลื่อนของนาฬิกา 5 นาที — เวลาที่ล้ำหน้าเกินกว่านั้นถือว่าเป็นค่าเพี้ยน ไม่ใช่งานที่กำลังทำอยู่ จึงยึดคืนได้
เราใช้ค่าเดียวกับที่ระบบคิวข้อความใช้อยู่แล้ว ไม่ตั้งค่าใหม่ เพราะค่าคงที่สองตัวที่หมายถึงเรื่องเดียวกันแต่ต่างกันคือบั๊กที่รอเวลาเกิด
เราเจอกับดักนี้กับเว็บของเราเองด้วย
ระหว่างทำบล็อกที่คุณกำลังอ่านอยู่นี้ เราเจอปัญหาเดียวกันในรูปแบบเล็กกว่า
ระบบจัดการเนื้อหาของเว็บนี้ตั้งเขตเวลาเป็นเวลามาตรฐานสากล ไม่ใช่เวลาไทย เมื่อเราลงบทความโดยใส่เวลาแบบไทย บทความที่เวลาล้ำหน้านาฬิกาของระบบจะกลายเป็น “ตั้งเวลาเผยแพร่” เงียบ ๆ แล้วหายไปจากหน้าเว็บ
ไม่มีข้อความผิดพลาด ไม่มีอะไรเตือน มีแค่บทความที่ไม่ปรากฏ
เราเลยใส่ด่านตรวจในเครื่องมือลงบทความว่า ถ้าเจอเวลาที่ล้ำหน้านาฬิกาของระบบ ให้หยุดทันทีและบอกว่าเรื่องไหน แทนที่จะปล่อยผ่านแล้วไปงงทีหลัง
เล่าเรื่องนี้เพราะมันย้ำสิ่งเดียวกัน — ปัญหาเรื่องเวลาแทบไม่เคยประกาศตัว มันเงียบเสมอ
สรุปสิ่งที่เราได้เรียนรู้
- เวลาในระบบมีหลายชั้นและอาจไม่ตรงกัน — ต้องมีหน้าจอที่บอกได้ทันทีว่าแต่ละชั้นเป็นอย่างไร ไม่ใช่ต้องเข้าเครื่องไปดู
- “เขตเวลาไม่ตรง” กับ “นาฬิกาเดินไม่ตรง” อาการเหมือนกันแต่แก้คนละทาง — ระบบควรแยกให้ตั้งแต่ตอนเตือน
- แยก “เวลาที่เกิดขึ้น” ออกจาก “เวลาบนหน้าปัด” ให้ชัด — แบบแรกเก็บเป็นเวลาสากลแล้วแปลงตอนแสดง แบบหลังห้ามแปลง
- “วันนี้” ต้องระบุเสมอว่าของใคร — ในระบบหลายลูกค้า คำว่าวันนี้ไม่มีความหมายถ้าไม่บอกเขตเวลา
- เวลาที่อยู่ในอนาคตอันตรายกว่าเวลาที่ผิดในอดีต — เพราะเงื่อนไขที่เขียนว่า “เก่ากว่า” จะไม่มีวันเป็นจริง
- ปัญหาเรื่องเวลาเงียบเสมอ — มันไม่ทำให้ระบบพัง มันแค่ทำให้ตัวเลขผิด ซึ่งกว่าจะรู้ก็สายไปแล้ว
สำหรับระบบที่ลูกค้าใช้ออกเอกสารทางบัญชีและภาษี วันที่ที่ผิดไปหนึ่งวันไม่ใช่เรื่องเล็ก — มันคือเอกสารที่ตกคนละงวด และนั่นคือเหตุผลที่เราลงแรงกับเรื่องนี้มากกว่าที่ดูเหมือนควรจะเป็น
บริหารจัดการข้อมูลในองค์กร ครบในที่เดียว