Wall of Text #8: เวลา

จริงๆ ก็หมด content แล้ว เพราะน้องที่ทำให้ผมเริ่มซีรีส์นี้เค้าหนี 100 days of code ไปเที่ยวแล้ว แต่พอดีว่าเจอบั๊ก เลยอยากเล่าเรื่องเวลาสักหน่อยเพราะเป็นเรื่องที่น่าปวดหัวมาก

ถ้าขี้เกียจอ่าน ขอแนะนำให้ดูคลิปนี้ซึ่งคงจะเล่าได้สนุกกว่าที่ผมจะเขียนเยอะ

สมัยนึงผมคิดว่าเวลาเป็นเรื่องง่ายๆ โดยเฉพาะเมื่อมาเขียนโค้ดที่ต้องเก็บเวลาแล้วทุกคนน่าจะเคยคุ้นตากับ UNIX Time ซึ่งเก็บจำนวนวินาทีตั้งแต่ 1 มกราคม 1970

ข้อดีอย่างหนึ่งของ UNIX Timestamp คือเราสามารถหาผลต่างของ 2 เวลาได้ง่าย เพียงแค่เอามาลบกันก็จะได้จำนวนวินาทีในช่วงเวลานั้น

ปัญหามันอยู่ที่ว่า เวลาแต่ละที่ในโลกไม่เหมือนกัน…

Timezone

ครั้งหนึ่งผมก็เคยคิดว่า ลูกค้าเราอยู่ในประเทศไทยเท่านั้น ถ้าอย่างนั้นเราก็ตั้ง Timezone ทุกที่ในโค้ดเป็นบวก 7 ชั่วโมง หรือเขตเวลาของประเทศไทยก็น่าจะจบ

ปรากฏว่า ทำไปพักหนึ่งก็วุ่น เนื่องจากระบบหลายอย่างมักจะอ้างอิงจากเขตเวลาสากล (UTC) อยู่ดี เช่น UNIX Time ตามมาตรฐานนั้นกำหนดให้ใช้เวลาที่เขตเวลา UTC แล้ว Library มันก็จะไม่ถามเราเลยว่าเอาเขตเวลาอะไร

เนื่องจากเราเขียนโค้ดโดย assume เขตเวลาประเทศไทยแล้ว เมื่อ user กรอกข้อมูลมาว่าตั้งเวลาแจ้งเตือน 2019-07-06 14:00:00 เราเอาเข้า function convert เป็น UNIX Time มันก็คืนให้เราเป็น 1562421600 ซึ่งก็เป็นเวลาที่ถูกต้องแต่เขตเวลาเป็น UTC พอแปลงเป็นเวลาไทยคือ 3 ทุ่ม แทนที่จะเป็นบ่ายสองตามที่ตั้งใจไว้

สองคือการเก็บวันอย่างเดียวก็ยุ่งยากและมีปัญหา… เพราะถ้าเราเก็บเป็น UNIX Timestamp โดยกำหนดให้เวลาเป็น 0 หมด (เช่น 2019-07-16 00:00:00) ถ้าเราดันไปใช้ค่าเวลานั้นในฟังค์ชั่นที่รับข้อมูลเป็น Date + Time แล้ว มันอาจจะแปลงเขตเวลากลับเป็น UTC ทำให้กลายเป็นวันก่อนหน้าเวลา 17:00น. พอเราแปลงกลับเป็นวันที่อย่างเดียว ก็ผิดวันซะอย่างนั้น

Always use Timezone

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

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

ทุกวันนี้ที่ไล่แก้บั๊กอยู่ ส่วนมากก็จะอยู่สองกรณีนี้แหละ คือ กำหนดเขตเวลาไม่ถูกต้อง หรือพยายามแปลงวันที่อย่างเดียวไปเข้าระบบวันที่ + เวลา

Birthday

ปัญหาถัดมาคือวันเกิด ซึ่งปวดหัวมาก เพราะคน Gen Y วันเกิดสามารถแทนด้วย UNIX Timestamp ได้เป็นเรื่องปกติ เราก็ออกแบบโปรแกรมไปอย่างนั้น พอไปใช้จริงเจอคนคนยุคก่อนหน้านี้ซึ่งเกิดก่อน 1 มกราคม 1970 แล้วจะแทนอย่างไร?

ผมก็ยังไม่เคยหาคำตอบ เพราะส่วนมากภาษาจะมีไลบรารีที่เกี่ยวข้องกับวันเวลาไว้ให้แล้ว เราก็ให้มันเก็บข้อมูลให้เลย ไม่ได้สนใจว่าด้านหลังจะทำงานอย่างไร

แต่ที่ร้ายไปกว่านั้นคือบางคนไม่ทราบวันเกิดตัวเอง ทราบเพียงแต่ปีเกิด (บางทีก็ไม่ทราบ ไปประมาณเอา) ยิ่งแล้วใหญ่ แทนในระบบไม่ได้เลย ส่วนมากผู้ใช้งานก็จะแก้ขัดโดยระบุวันที่เป็นวันที่ 1 มกราคมของปีนั้นๆ ไป แต่บางระบบก็อาจจะจำเป็นจะต้องเพิ่มช่องให้ระบุได้ว่าไม่ทราบวันที่

History is Hard

อันนี้เพิ่งเจอมาสดๆ ร้อนๆ คือเราก็คิดว่าระบุเวลา 2019-07-06 14:00:00 เขตเวลา Asia/Bangkok เราก็คิดว่ามั่นใจไม่มีทางหลุดหรอก

แต่ปรากฏว่าเวลาที่ได้จริงกลับใช้เขตเวลา +06:42 ไม่ใช่ +07:00 แล้วทำให้การประมวลผลข้อมูลที่เหลื่อมกันผิดพลาดไปซะอย่างนั้น

เอ๊ะ ยังไง เวลาประเทศไทยเป็น +07:00 ไม่ใช่หรอ ใครๆ ก็รู้??

ปรากฏว่าในประเทศไทยก่อนวันที่ 31 มีนาคม 2463 ใช้เขตเวลา +06:42 ส่วนตั้งแต่เวลา 1 เมษายนเป็นต้นไปจะใช้เขตเวลา +07:00

โอ้พระสงฆ์

ทีนี้พอเราระบุเขตเวลาเฉยๆ มันไม่ทราบว่าเราหมายถึงเขตเวลาตอนไหน เพราะถ้าเราระบุวันที่ก่อนปี 2463 จะต้องใช้เขตเวลาเดิม แต่ถ้าหลังจากนั้นต้องใช้เขตเวลาใหม่

วิธีแก้ใน Python ก็คือ ไลบรารี pytz จะมีฟังค์ชั่น localize ที่รับเวลาเข้าไปพร้อมเขตเวลา เสร็จแล้วมันจะไปคิดเอาเองว่าวันที่และเวลานี้ สถานที่นี้ควรจะใช้เขตเวลาอะไร

Astronomy is Hard

อันนี้ยังไม่เคยเจอ แต่อยากเล่าให้ฟัง

นักดาราศาสตร์ได้สังเกตว่าโลกเริ่มหมุนช้าลง จึงมีการกำหนดให้เพิ่มเวลาไปอีก 1 วินาทีตามเวลาที่กำหนด ก็คือในวันนั้น นาฬิกาจะเดิน

  • 23:59:58
  • 23:59:59
  • 23:59:60
  • 00:00:00

1 นาทีมี 61 วินาที!

ใน UNIX Time ก็จะใช้วิธีคือย้ำเวลาเดิม คือเวลาจะเดิน 1 2 3 3 4 5 .. ทำให้เวลา UNIX ไม่ตรงกับเวลาสากล

พอเวลาซ้ำแบบนี้ได้แล้ว ก็อาจเกิดปัญหาขึ้น เช่น ถ้ามีรายการหนึ่งที่ทำที่เวลา 3.98 แล้วอีกรายการทำทีหลังแต่วินาทีที่ 3 วนซ้ำ เลยได้ timestamp เป็น 3.14

เมื่อเราเอารายการมาเรียงกัน รายการทำทีหลังกลับ timestamp อยู่ก่อน และถ้าเอาเวลามาลบกันก็จะทำ leap second ตกหายไปอีกด้วย

วิธีที่ Google แก้ปัญหานี้คือ Leap Smear เนื่องจาก Google ต้องการให้รายการที่ทำทีก่อนอยู่ก่อนรายการที่ทำที่หลังเสมอ จึงกำหนดให้วันที่มีการแทรก Leap second นั้นเวลาบน Server จะเดินช้าลงเล็กน้อยตลอดทั้งวันแทน เมื่อ Leap second ผ่านไปแล้วเวลาก็จะกลับมาตรงกันอีกครั้ง วิธีนี้ทำให้เราไม่ต้องมีวินาทีซ้ำ 2 ครั้ง

ถ้าใครใช้เครื่อง Google Compute Engine เวลาบนเครื่องก็จะถูก Leap Smear อัตโนมัติ หรือถ้าอยู่ข้างนอกก็สามารถใช้ NTP Server ของ Google ได้

Monotonic Time

ปัญหานี้ใกล้ๆ กันกับปัญหาข้างบน แต่อันนี้อาจจะเจอได้ด้วยตัวเอง

คือเวลาเรา Benchmark โปรแกรม ท่าที่เราคิดก็คือ Read unix time ที่ละเอียดที่สุดเท่าที่เป็นไปได้มาเก็บไว้, ทำงานบางอย่าง แล้วเอา Unix time ตอนสุดท้ายมาลบออกกับเวลาแรกก็จะได้เวลาที่ใช้ทำงานนั้น

ฟังดูดีไม่มีปัญหา แต่ท่านี้ก็ไม่รอด…

เพราะนาฬิกาในคอมพิวเตอร์มันตั้งเวลาได้ ดังนั้นระหว่างการวัด 2 ครั้งนั้น NTP อาจจะทำงานแล้ว sync เวลาเครื่องถอยหลังกลับไป อาจจะมากถึง 1-2 วินาที พอเราอ่านค่าเวลาใหม่มันก็จะถอยหลังไปตามนั้นด้วย

ปัญหานี้อาจจะเห็นผลยิ่งชัดถ้าหาก operation นั้นใช้เวลานาน เช่น test เรื่อง Network call โปรแกรมบางตัวก็ไม่ชอบเท่าไรนักหากเวลาเดินย้อนหลัง เช่น Dovecot

วิธีแก้ไขคือ ระบบปฏิบัติการต่างๆ จะมีเวลาที่เรียกว่า Monotonic timer ซึ่งจะนับตลอดตั้งแต่เปิดเครื่องคอมพิวเตอร์ โดยการันตีว่าเดินไปข้างหน้าอย่างเดียว ตั้งเวลาถอยหลังไม่ได้

แต่มีข้อแม้คือ Monotonic timer ไม่ได้อ้างอิงกับเวลาใดๆ ทั้งสิ้น เราจึงไม่สามารถแปลงเป็นเวลาตามนาฬิกา (เรียกว่า Wallclock timer) หรือเอาไปเทียบกันระหว่างเครื่องคอมพิวเตอร์ 2 เครื่องได้ ทำได้อย่างเดียวคือใช้เปรียบเทียบระหว่าง 2 ช่วงเวลาบนคอมพิวเตอร์เครื่องนั้น

Use a Library

สุดท้ายนี้คงต้องใช้ประโยคคลาสสิค จำได้ว่าเคยพูดนานมากแล้วในเรื่อง Web Framework ซึ่งก็ยังใช้ได้กับเรื่องนี้เหมือนกัน

ถ้าจะทำ Date time manipulation เนี่ย ไปใช้ Library เถอะครับ ไปนั่งทำเองปวดหัวแน่นอน มันอาจจะเจอกับดักอื่นๆ ที่ผมยังไม่ได้เจอแล้วไม่ได้เล่าในบล็อคนี้ก็ได้

เท่าที่สังเกตดู ภาษายุคใหม่ๆ ก็จะเริ่มบังคับให้ใช้ Library มากขึ้น เช่น Rust หรือ Go ถ้าอยากทราบเวลาปัจจุบันต้องเรียกใช้ time.Now() และมันไม่ได้คืนค่าเป็น UNIX Time แล้ว ถ้ายังอยากใช้ต้องแปลงเอง