← Blog

เวลาในระบบมีหลายชั้น และมันไม่ตรงกัน — บทเรียนจากบั๊กที่ไล่ 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 นาที — เวลาที่ล้ำหน้าเกินกว่านั้นถือว่าเป็นค่าเพี้ยน ไม่ใช่งานที่กำลังทำอยู่ จึงยึดคืนได้

เราใช้ค่าเดียวกับที่ระบบคิวข้อความใช้อยู่แล้ว ไม่ตั้งค่าใหม่ เพราะค่าคงที่สองตัวที่หมายถึงเรื่องเดียวกันแต่ต่างกันคือบั๊กที่รอเวลาเกิด

เราเจอกับดักนี้กับเว็บของเราเองด้วย

ระหว่างทำบล็อกที่คุณกำลังอ่านอยู่นี้ เราเจอปัญหาเดียวกันในรูปแบบเล็กกว่า

ระบบจัดการเนื้อหาของเว็บนี้ตั้งเขตเวลาเป็นเวลามาตรฐานสากล ไม่ใช่เวลาไทย เมื่อเราลงบทความโดยใส่เวลาแบบไทย บทความที่เวลาล้ำหน้านาฬิกาของระบบจะกลายเป็น “ตั้งเวลาเผยแพร่” เงียบ ๆ แล้วหายไปจากหน้าเว็บ

ไม่มีข้อความผิดพลาด ไม่มีอะไรเตือน มีแค่บทความที่ไม่ปรากฏ

เราเลยใส่ด่านตรวจในเครื่องมือลงบทความว่า ถ้าเจอเวลาที่ล้ำหน้านาฬิกาของระบบ ให้หยุดทันทีและบอกว่าเรื่องไหน แทนที่จะปล่อยผ่านแล้วไปงงทีหลัง

เล่าเรื่องนี้เพราะมันย้ำสิ่งเดียวกัน — ปัญหาเรื่องเวลาแทบไม่เคยประกาศตัว มันเงียบเสมอ

สรุปสิ่งที่เราได้เรียนรู้

  1. เวลาในระบบมีหลายชั้นและอาจไม่ตรงกัน — ต้องมีหน้าจอที่บอกได้ทันทีว่าแต่ละชั้นเป็นอย่างไร ไม่ใช่ต้องเข้าเครื่องไปดู
  2. “เขตเวลาไม่ตรง” กับ “นาฬิกาเดินไม่ตรง” อาการเหมือนกันแต่แก้คนละทาง — ระบบควรแยกให้ตั้งแต่ตอนเตือน
  3. แยก “เวลาที่เกิดขึ้น” ออกจาก “เวลาบนหน้าปัด” ให้ชัด — แบบแรกเก็บเป็นเวลาสากลแล้วแปลงตอนแสดง แบบหลังห้ามแปลง
  4. “วันนี้” ต้องระบุเสมอว่าของใคร — ในระบบหลายลูกค้า คำว่าวันนี้ไม่มีความหมายถ้าไม่บอกเขตเวลา
  5. เวลาที่อยู่ในอนาคตอันตรายกว่าเวลาที่ผิดในอดีต — เพราะเงื่อนไขที่เขียนว่า “เก่ากว่า” จะไม่มีวันเป็นจริง
  6. ปัญหาเรื่องเวลาเงียบเสมอ — มันไม่ทำให้ระบบพัง มันแค่ทำให้ตัวเลขผิด ซึ่งกว่าจะรู้ก็สายไปแล้ว

สำหรับระบบที่ลูกค้าใช้ออกเอกสารทางบัญชีและภาษี วันที่ที่ผิดไปหนึ่งวันไม่ใช่เรื่องเล็ก — มันคือเอกสารที่ตกคนละงวด และนั่นคือเหตุผลที่เราลงแรงกับเรื่องนี้มากกว่าที่ดูเหมือนควรจะเป็น

Freeable

บริหารจัดการข้อมูลในองค์กร ครบในที่เดียว

ดูรายละเอียด →

อ่านต่อ

Freeable

ค่าหนึ่งค่ามี 4 หน้าที่ — มาตรฐานการแทนค่า เก็บ · ส่ง · คำนวณ · แสดง

ปัญหาเรื่องวันที่ไม่ใช่ปัญหาของวันที่ แต่เป็นหนึ่งตัวอย่างของปัญหาตระกูลเดียวกัน — ค่าหนึ่งค่าถูกใช้ทำ 4 หน้าที่ที่ต่างกัน พอรูปแบบของหน้าที่หนึ่งรั่วไปอีกหน้าที่ ตัวเลขก็เพี้ยนเงียบ ๆ

Freeable

ระบบเฝ้าระวังข้อผิดพลาดที่ใช้งานได้จริง — บทเรียนจากข้อความที่หายวันละ 113 ฉบับ

ติดตั้งระบบเฝ้าระวังแล้วไม่ได้แปลว่าจะเจอปัญหา บทความนี้เล่าเคสที่ error หนึ่งกินไป 88% ของทั้งหมดแต่ไล่ไม่เจอ 9 วันเพราะชื่อของมันเดาสาเหตุผิด และเล่าว่าเราจัดการเสียงรบกวนอย่างไร

Freeable

ตั้งรหัส error อย่างไรให้ใช้งานได้จริง — รหัสสองประเภทที่ทำงานคู่กัน

ระบบส่วนใหญ่มีรหัส error ไว้ให้ผู้ใช้แจ้ง support แต่ไม่ค่อยมีใครคิดว่ารหัสควรตอบคำถามอะไร บทความนี้เล่าว่าเราแยกรหัสเป็นสองประเภทเพราะอะไร และกฎข้อเดียวที่ใช้ตัดสินว่าเมื่อไรควรตั้งรหัสใหม่