Photo by Jelleke Vanooteghem on Unsplash

ความน่ารักของ Web Developer สมัยนี้คือมีการแยก Frontend กับ Backend ออกอย่างชัดเจน ถ้าใครย้อนกลับไปสมัยสิบปีก่อน เราไม่มีตำแหน่ง Frontend Developer ด้วยซ้ำ มีแต่เรียกรวมๆว่า Web Developer

การแยกกันของโค้ดสองส่วน ทำให้การ Deployment ทำแยกกันด้วย คำถามที่เกิดขึ้นคือ เวลาจะ Deploy เราจะเอา Frontend ขึ้นก่อน หรือ Backend ขึ้นก่อนดี

แม้จะเป็นคำถามง่ายๆ แต่รายละเอียดข้างในนั้นค่อนข้างเยอะ ถ้าไม่คิดให้ถี่ถ้วน อาจจะเจออาการเว็บพังขณะ Deploy เป็นประจำ

บทความนี้จะมาวิเคราะห์เจาะลึกกันในเรื่องนี้

ทำความเข้าใจปัญหา

มี Static File Server แยกต่างหาก

จากรูปแรก สมมติว่าเรามี Static File Server แยกไปต่างหาก การ Deploy Frontend กับ Backend ของเราจะแยกกันชัดเจน (JavaScript/CSS/HTML ไปเซอร์เวอร์กลุ่มหนึ่ง ส่วน Backend Code ก็ไปอีกกลุ่มหนึ่ง)

สำหรับใครที่ใช้ Cloud และแยก Static File ไปใส่ไว้ในอีก Service หนึ่ง อันนี้ก็จะหน้าตาคล้ายกัน แค่่ Request ของผู้ใช้ไม่ได้ผ่าน Reverse Proxy ของเรา แต่ตรงเข้า Cloud เลย

ลองนึกภาพว่า เราทำการใส่ Feature ใหม่ โดยโค้ดฝั่ง Frontend จะต้องดึงข้อมูลจาก Backend ที่ /api/data ซึ่งเป็น Endpoint ใหม่ที่เราไม่เคยมีมาก่อน

หากเรา Deploy Frontend ขึ้นก่อน แล้วมีคนมาเข้าเว็บเราก่อนที่จะลง Backend เสร็จ ผู้ใช้ใหม่จะเห็น Feature ใหม่นี้ แต่พอกดใช้งานก็จะเจอ Error

หากขั้นตอนการ Deploy ของเราเร็วมาก ช่วงเวลาที่มีปัญหานี้อาจจะไม่ถึง 1 นาที สำหรับเว็บส่วนใหญ่ที่ไม่ได้ซีเรียสเรื่อง Availability มาก ก็ถือว่ายอมรับได้ เอาความง่ายแลกกับ Error ช่วงสั้นๆ

แต่หากนึกภาพว่าเรา Deploy บ่อยๆ หลายครั้งต่อวัน แล้วการ Deploy แต่ละครั้งใช้เวลา 5-10 นาที อันนี้รับไม่ได้แน่นอน (นึกภาพว่าเรามี Server 10 ตัว ที่ต้องค่อยๆ Deploy ทีละ 2 ตัว เพราะจะปิดหมดแล้ว Deploy รวดเดียวไม่ได้ เพราะจะมี Server ทำงานไม่พอต่อจำนวน Request)

คำถามคือ จะแก้ไขอย่างไร?

ถ้าลง Frontend ก่อนมีปัญหา ก็ลง Backend ก่อนสิ

เราสามารถแก้ปัญหาแบบกำปั้นทุบดิน ด้วยการ Deploy Backend ก่อน

สมมติว่า Change ที่เราจะทำการ Deploy เราได้มีการเปลี่ยน API ให้ Return data ในรูปแบบใหม่ เช่น ตัวอย่าง Todo List Application

// Old format
{
  todos: [
    "Fix bug #1278",
    "Develop feature #1279"
  ]
}

// New format: มีการใส่ชื่อคนที่ต้องทำงานนั้นๆขึ้นมา
{
  todos: [
    {
      task: "Fix bug #1278",
      owner: "Suthee"
    },
    {
      task: "Develop feature #1279",
      owner: "Aruj"
    },

  ]
}

ผู้ใช้ที่โหลด JavaScript ไปแล้ว และกำลังใช้งาน Application อยู่ก็จะทำงานไม่ได้ทันที เพราะโค้ด Frontend เก่าไม่ได้ถูกออกแบบให้ทำงานกับ data รูปแบบใหม่

สังเกตว่าเราอยู่ในสถานการณ์ “ไก่กับไข่” คือ ไม่ว่าอะไรเกิดก่อน อีกอันนึงก็ต้องเกิดพร้อมกันด้วยอยู่ดี

ถ้าเราลงทั้ง Frontend และ Backend พร้อมๆกันล่ะ?

“ทำได้ด้วยเหรอ?”

“ทำได้สิ ก็เอาทุกอย่างใส่บน Server แล้ว Deploy พร้อมๆกันรวดเดียวเลยไง ดูนะ”

ใส่ทุกอย่างลงบนเซอร์เวอร์เดียว

อันนี้ดูเผินๆ อาจจะรอดจากปัญหา “ไก่กับไข่” ไปได้ เพราะหากเปลี่ยนทุกอย่างพร้อมกัน ผู้ใช้ที่เข้าเว็บก็จะได้ไฟล์ JavaScript ใหม่ แล้วเรียกใช้ API อันใหม่ได้เลย

แต่เดี๋ยวก่อนนนน มันยังมีช่องว่างอยู่

  1. ถ้าหากผู้ใช้โหลด JavaScript เก่าไปแล้ว ก่อนเราทำการ Deploy พอดีล่ะ?
  2. หากเรามี Backend Server สัก 10 เครื่อง เราจะไม่สามารถลงทุกเครื่องพร้อมกันได้ ในช่วงเวลาที่เรา Deploy อยู่นั้น หากผู้ใช้เข้าเว็บแล้วได้ JavaScript ไฟล์แล้วได้เวอร์ชั่นเก่า แล้ว Request ถัดมาเพื่อดึงข้อมูลจาก /api/data ดันไปตกเครื่องเวอร์ชั่นใหม่ (หรือกลับกัน) เราก็จะยังเจอปัญหาเดิม

ถึงจุดนี้ ผมจะขอสรุปว่า เราไม่มีทางการันตีได้ว่า Frontend กับ Backend จะเป็นเวอร์ชั่นที่ตรงกัน หากจะแก้ปัญหานี้ เราต้อง…

ทำ Backend ให้เป็น Backward Compatible แล้วลง Backend ก่อน

แม้จะเราจะคุม Caching ดีเพียงใด (เซ็ต HTTP Header Expires/Cache-Control ให้ดีแล้ว + ทำ Cache Busting) แต่ก็เป็นไปได้ว่าผู้ใช้อาจจะยังไม่ได้ Refresh หน้าจอเป็นระยะเวลานาน ซึ่งทำให้มี Request จากเวอร์ชั่นเก่าๆหลงมาอยู่ดี

ดังนั้น ให้ตั้งสมมติฐานไว้ก่อนเลยว่า Frontend ยังไงก็ต้องมีเวอร์ชั่นเก่าแน่นอน

จากสมมติฐานนี้ ยังไงเราก็ต้องเขียน Backend ให้ Backward-Compatible อย่างเลี่ยงไม่ได้

พอ Backend เป็น Backward-Compatible แล้ว เราก็สามารถลง Backend ก่อนได้เลยโดยไม่ต้องกังวล

ระหว่างที่กำลัง Deploy Backend เราจะมีเฉพาะ Frontend เวอร์ชั่นเก่า เขียนเป็นตารางก็ประมาณนี้

Version Frontend Backend
เก่า มี มี (บางส่วนขณะ Deploy)
ใหม่ ไม่มี มี (บางส่วนขณะ Deploy)

จากตารางนี้ จะเห็นได้ว่า ทุก Request จาก Frontend ก็จะอยู่รอดปลอดภัย เพราะไม่ว่าจะเป็น Backend เวอร์ชั่นไหน ก็รองรับ Frontend เก่าได้หมด

หลังจาก Backend จบแล้ว เราก็ทำการลง Frontend กัน

Version Frontend Backend
เก่า มี (ผู้ใช้อาจยังไม่ได้กด Refresh) ไม่มี
ใหม่ มี มี

เป็นอันเสร็จสิ้นกระบวนการ ระหว่าง Deployment ก็จะไม่มีปัญหาเรื่องนี้อีก

ส่วนตัวผมแนะนำวิธีนี้ แต่อยากให้ลองคิดต่อกันถึงกรณีอื่นๆกัน

ถ้าทำ Frontend ให้เป็น Backward-Compatible ล่ะ?

คราวนี้ลองมาคิดกันต่อ ว่าเราจะทำแบบอื่นได้หรือเปล่า

ถ้าสมมติเราทำกลับกันล่ะ คือทำ Frontend ให้เป็น Backward-Compatible แล้วลง Frontend ก่อน

ตัวอย่างเช่นในกรณี Todo List เราอาจจะเขียนให้โค้ดเราสามารถรับ Todo ที่เป็นทั้ง String หรือ Object ซึ่งถ้าหากเป็น String ก็จะใส่ค่า Owner ให้เป็น Empty String แล้วก็ไม่ Render ส่วนที่แสดง Owner หากไม่มีค่า

พอ Deploy กลับกัน เราจะได้ตารางหน้าตาแบบนี้

Version Frontend Backend
เก่า มี (ผู้ใช้อาจยังไม่ได้กด Refresh) มี
ใหม่ มี ไม่มี

หลังกจากลง Frontend เสร็จ ก็ถึงตาของ Backend

Version Frontend Backend
เก่า มี (ผู้ใช้อาจยังไม่ได้กด Refresh) มี (บางส่วนขณะ Deploy)
ใหม่ มี มี (บางส่วนขณะ Deploy)

จะเห็นได้ว่า เนื่องจากเราควบคุมเวอร์ชั่นของฝั่ง Frontend ไม่ได้ 100% ทำให้ยังมี Version เก่าหลุดมา พอ Backend ไม่เป็น Backward-compatible ก็จะมีปัญหากรณีที่ Frontend เก่าดันส่ง Request มาเข้า Backend ใหม่

นี่คือสาเหตุที่ผมแนะนำให้ลง Backend ก่อนครับ เพราะไม่ว่ายังไง เราก็ต้องทำให้ Backend เป็น Backward-Compatible อยู่ดี

ถ้าเกิด Frontend เป็น Single Page Application (SPA) ที่ผู้ใช้เปิดทิ้งไว้นานๆล่ะ?

ปัจจุบัน Frontend ซับซ้อนขึ้นมาก ไม่ได้ใช่แค่แสดงหน้าเว็บเพจอย่างเดียว ตัวอย่างง่ายๆก็พวก Messaging Service ที่บางคนเปิดไว้แทบจะถาวรเลย

กรณีนี้จะมีความซับซ้อนเพิ่มขึ้น เพราะ Frontend อาจจะมีเวอร์ชั่นเก่าเป็นชาติค้างอยู่ ดังตารางนี้

Version Frontend Backend
1.1 มี (มีคนนึงไม่ยอม Refresh Page มาเดือนนึงแล้ว) ไม่มี
1.2 มี ไม่มี
1.3 มี มี
2.0 ไม่มี ไม่มี

ตารางข้างบน เรากำลังจะขึ้นเวอร์ชั่น 2.0 ซึ่งฝั่ง Backend ก็ยังต้องทน Support โค้ดสมัยพระเจ้าเหาที่ 1.1 อยู่

สำหรับกรณีที่ Backend ทำมาเพื่อรองรับ Frontend ของเว็บเดียว การจะบังคับให้ Backend ทำ Backward-Compatible อยู่ตลอดก็ดูจะสิ้นเปลืองพลังงานไปหน่อย

กรณีนี้ เรามีทางเลือกสองทาง

  1. เลิกทำ Backward-Compatible ของเวอร์ชั่นที่เก่ามากๆ และให้ผู้ใช้ Refresh หน้าจอเอง
  2. ออกแบบ Frontend ให้บังคับ User ให้ Refresh page หาก Major Version

แม้กรณี 1 จะดูไร้ความรับผิดชอบไปหน่อย แต่ในทางปฏิบัติ หากเรามี Metric เพื่อใช้เช็คเวอร์ชั่นของผู้ใช้ในระบบ เราจะสามารถเช็คได้ว่าคนที่ใช้ 1.1 อยู่มีเยอะแค่ไหน หากมีน้อยมาก ก็น่าจะถือว่ายอมรับได้

กรณีที่ 2 เป็นทางเลือกที่ดีมาก แต่ต้องออกแบบ Application ทั้งสองฝั่งให้มีฟีเจอร์นี้ตั้งแต่แรก ซึ่งเอาเข้าจริง ผมไม่เคยเห็นใครคิดเรื่องนี้ตั้งแต่แรกเลย เพราะช่วง Release แรกๆ มี Feature ที่สำคัญกว่าอีกเยอะให้ทำ

สรุป

เว็บปัจจุบันมีการแยก Frontend กับ Backend ออกจากชัดเจน ทำให้เกิดปัญหาเรื่องของ Incompatible Version ระหว่างทั้งสองส่วน โดยเฉพาะอย่างยิ่งในช่วงเวลาที่ทำการ Deploy อยู่

บทความนี้ยกตัวอย่างให้เห็นว่า เราไม่สามารถการันตีให้ Frontend กับ Backend จะอยู่บนเวอร์ชั่นเดียวกันได้ 100%

ทางแก้ ผมแนะนำให้ใส่ Backward-Compatible ในส่วนของ Backend และ Deploy ฝั่ง Backend ก่อน เพราะยังไงก็จะต้องมี Frontend ที่ผู้ใช้โหลดไปแล้วในเวอร์ชั่นเก่าๆอยู่ วิธีนี้จะทำให้ไม่ต้องกังวลเรื่องทำฝั่ง Frontend ให้เป็น Backward-Compatible ด้วย