Zero Downtime Database Deployment
เมื่อได้ยินคำว่า “Zero Downtime Deployment” คุณคิดถึงอะไรกันบ้างครับ?
ถ้าอยู่มาวันหนึ่งคุณจะต้องทำเจ้าสิ่งนี้ขึ้นมา คุณจะคิดถึงอะไรเป็นสิ่งแรก คอมเม้นต์แลกเปลี่ยนความเห็นกันสักหน่อย หากไม่เหลือบ่ากว่าแรง
The Question: Explained
เป็นธรรมเนียมที่ผมควรจะบอกเล่าก่อนว่า “Zero Downtime Deployment” คืออะไร? คำตอบแบบที่เข้าใจง่าย ๆ ก็คือการที่เราสามารถแก้ไข ปรับปรุง เปลี่ยนแปลงซอฟต์แวร์หรือระบบของเราโดยที่ไม่ต้องปิดการให้บริการ นั่นแปลว่าผู้ที่กำลังใช้งานซอฟต์แวร์ของเราอยู่จะสามารถใช้งานได้ตามปกติ โดยที่ไม่รู้ว่าเรากำลังอัพเดต อัพเกรด หรือซ่อมแซมอะไรบางอย่างอยู่นั่นเอง
ส่วนคำตอบแบบเข้าใจยาก ๆ นั้นขึ้นอยู่กับการตีความของคำว่า Downtime ครับ ยกตัวอย่างเช่น
- ถ้าระบบ เซอร์วิสหรือโปรเซสยังออนอยู่ แต่ผู้ใช้งานบ่นว่าระบบช้ามาก ๆ (จนไม่สามารถทำงานได้อย่างปกติแล้ว) เรียกว่าเป็น Downtime ไหม?
- ถ้าผู้ใช้งานดึงรายงานหรือค้นหาข้อมูล(ยัง)ได้อยู่ แต่ทำรายการใหม่เข้ามาไม่ได้ นับเป็น Downtime ไหม?
- ถ้าเหตุการณ์กระทบลูกค้าในสัดส่วน 20% ของลูกค้าทั้งหมด เรียกว่าเป็น Downtime ไหม?
- ถ้าเราประกาศว่าจะขอปิดระบบล่วงหน้าแล้ว อย่างนี้นับเป็น Downtime ไหม?
ซึ่งแน่นอนว่าการตีความประเด็นเหล่านี้จะแตกต่างกันไปตามแต่ละองค์กร ตามรูปแบบหรือธุรกิจที่ให้บริการ หรือบางครั้งก็เป็นไปตามนโยบายของผู้พัฒนาระบบหรือเจ้าของซอฟต์แวร์ แต่หากเราทำให้ระบบมันไม่ดาวน์เลยได้อย่างสมบูรณ์ เราก็ไม่จำเป็นจะต้องตอบคำถามเหล่านี้
The Possible Answers
กลับมาที่คำถามที่ผมถามไว้ตั้งแต่ต้นเรื่องกันครับ ระหว่างรอคุณผู้อ่านเม้นตอบ ผมลองนึกคำตอบที่เป็นไปได้ด้วยตัวเองไปพลาง ๆ
เมื่อคุณได้ยินคำว่า “Zero Downtime Deployment” คุณคิดถึงอะไร
- ไม่เคยต้องทำนะ เพราะมีช่วงเวลาที่ดาวน์ระบบได้ ถือว่าคุณโชคดีมาก ถ้าคุณทำงานกับระบบที่ไม่ต้องให้บริการตลอด 24 ชม. โดยปกติหากระบบเปิดให้บริการเฉพาะช่วงกลางวัน ชาวไอทีมักจะใช้เวลาช่วงกลางคืนในการปรับปรุงระบบ ซึ่งคุณแค่ต้องระวังโปรแกรมจำพวก Batch ที่ทำงานตอนกลางคืน และต้องทำงานให้เร็วพอที่จะเปิดระบบให้ทันตอนเช้าของวันใหม่
- ไม่เคยทำเหมือนกัน เพราะดูแลแต่ฝั่ง Front-end อะ หลายบริษัทแบ่งการทำงานออกเป็น Front-end และ Back-end และคนที่ต้องปวดหัวกับฐานข้อมูลมักจะเป็นคนที่ทำงานในฝั่ง Back-end แต่ถ้า Back-end ร่วงบ่อย ๆ ฝั่ง Front-end ก็น่าจะเริ่มปวดหัวเหมือนกันนะ
- DevOps คำตอบนี้ดูจะกว้างไป อาจจะต้องเฉพาะเจาะจงกว่านี้ว่าเป็นเทคนิคหรือเครื่องมืออะไรใน DevOps แต่โดยทั่วไปแล้ว DevOps ทำให้เราทำงานได้สะดวกรวดเร็วขึ้น แต่ไม่ได้หมายความว่าจะทำให้ Downtime นั้นหมดไป
- Blue/Green Deployment ว้าว อันนี้ผมชอบ เราจะพูดถึงเจ้าสิ่งนี้กันอีกนิดหน่อยในย่อหน้าถัด ๆ ไปครับ
- Feature Flags หรือ Feature Toggles ว้าว ๆ ใครที่ตอบข้อนี้ผมว่าต้องเป็น Developer หรือ Designer มือโปรแน่นอน
- Parallel Change หรือ Expand and Contract Pattern ว้าว ๆๆ ใครตอบข้อนี้มา บทความนี้น่าจะเบสิคเกินไปสำหรับคุณละ แต่สำหรับคนที่สนใจจะศึกษาเพิ่มเติมว่าเจ้าสิ่งนี้คืออะไร ผมให้เว็บป๋ามาร์ตินไว้ให้ด้านล่างครับ
คำตอบต่าง ๆ เหล่านี้ก็มาจากประสบการณ์และความถนัดของแต่ละคนที่แตกต่างกันออกไป แต่ผมว่าสิ่งที่แทบจะเหมือนกันคือถ้างาน Deployment ของเรามีส่วนที่เป็น Databases เข้ามาเอี่ยวด้วยแล้ว การ Deployment ครั้งนั้นจะมีความยากและต้องการการดูแลเอาใจใส่มากขึ้นกว่าปกติ
บทความนี้ผมไม่ได้จะมาเล่าเทคนิคพวกนี้ (Blue/Green Deployment, Feature Flags, Expand and Contract Pattern) หรอกครับ ไม่ได้จะมาให้เทคนิคการใช้คำสั่งบน Databases ในสไตล์ DBAs และไม่ได้จะมาสอนเขียน SQL แต่อย่างใด หากจะมาเล่าแนวคิดพื้นฐานของการออกแบบซอฟต์แวร์เพื่อให้เราสามารถที่จะทำ No Downtime Database Deployment ได้ ซึ่งเดี๋ยวจะเห็นว่าแค่การออกแบบในส่วนของ Databases เพียงอย่างเดียวนั้นไม่เพียงพอครับ แต่จะต้องออกแบบตัวโค้ดของแอปพลิเคชันให้สอดคล้องกันไปด้วยถึงจะสัมฤทธิ์ผล
Relational vs NoSQL
ผมเชื่อว่าบางคนอาจจะกำลังสงสัย เจ้าคำว่า Database ที่ผมกล่าวถึงในบทความนี้หมายถึง Databases ประเภท Relational หรือ NoSQL หรือว่าทั้งคู่เลยกันแน่?
คำตอบคือ Relational Databases ครับ สาเหตุก็เพราะคุณสมบัติ Schemaless ของ NoSQL Databases (จริง ๆ ผมอยากให้นึกถึง Document Databases แบบ MongoDB มากกว่าจะกล่าวเหมารวม NoSQL ทั้งหมดครับ เนื่องจาก NoSQL มันมีหลายประเภทมาก)
เมื่อพูดถึงคำว่า Schemaless มีหลายคนยังเข้าใจผิดว่า NoSQL Databases ไม่มีสิ่งที่เรียกว่า Schema แบบชนิดที่ว่าไม่มี้ ไม่มีเลย ไม่มีอย่างสิ้นเชิง อะไรประมาณนั้น แต่แท้ที่จริงแล้ว มันแค่ไม่มีในตัวฐานข้อมูล แต่มัน(ยัง)มีในแอปพลิเคชันที่ใช้งานฐานข้อมูลนั้นครับ นั่นแปลว่าเวลาเราแก้ Schema ของ Document Databases เราแก้ที่โค้ดของแอปพลิเคชัน ไม่ได้ต้องไป ALTER Add Column อะไรให้วุ่นวายเหมือนพวก Relational Databases พอเราไม่ต้องไปยุ่งเหยิงกับตัว Document Databases งาน Deployment เราก็จะง่ายขึ้นเยอะครับ ใช้แค่ Blue/Green Deployment กับแอปพลิเคชัน ก็จบแบบแฮปปี้เอนดิ้งได้เลย
แต่อย่างว่าครับ ทุกอย่างมีข้อยกเว้น ที่ผมกล่าวมาเป็นหลักการและข้อดีโดยทั่วไปของ NoSQL Databases หรือเรียกแบบเฉพาะเจาะจงก็ Document Databases เมื่อเทียบกับ Relational Databases แต่ไม่ใช่ทุกอย่างทุกงานที่เกี่ยวข้องกับ NoSQL Databases จะทำได้แบบ No Downtime นะครับ
Backward Incompatible Changes
ปฏิเสธไม่ได้ว่าเมื่อพูดถึง Zero Downtime Deployment ผมเองก็จะนึกถึง Blue/Green Deployment ก่อนเป็นอันดับแรก แต่ว่ามันใช้กับงาน Databases ได้หรือเปล่า?
Blue/Green Deployment คือแนวคิดของการมีของ 2 ชุด ชุดนึงเป็นชุดใหม่และอีกชุดเป็นของปัจจุบันหรือจะเรียกว่าชุดเก่าก็ได้ เราให้ของชุดปัจจุบันทำหน้าที่บริการลูกค้าไป และเมื่อเตรียมของชุดใหม่พร้อมแล้ว เราก็สับสวิตซ์โยกรายการของลูกค้าให้เข้าไปที่ของชุดใหม่ ซึ่งจะทำหน้าที่บริการลูกค้าแทนของชุดเก่า หลักการก็ประมาณนี้เองครับ เทคโนโลยีในปัจจุบันเอื้อให้เราทำแบบนี้ได้ไม่ยากเลย แต่อาจจะไม่ใช่กับ Databases
การทำงานกับ Databases ยังคงเป็นงานที่ยุ่งยากและมีความเสี่ยงสูงกว่าอย่างอื่น สาเหตุนั้นไม่ใช่เพราะตัวของระบบฐานข้อมูล แต่เพราะ “Data” ที่อยู่ในนั้น การที่ “ข้อมูล” ของเรามีความสำคัญสูงสุด ถ้าระบบฐานข้อมูลหรือ Databases มีปัญหา นั่นแปลว่ามีโอกาสที่ข้อมูลของเราจะมีปัญหาตามไปด้วย ดังนั้นการทำอะไรก็ตามกับระบบฐานข้อมูลจะยังคง “แพง” อยู่เสมอ
ลองนึกดูว่าถ้าเราจะประยุกต์ Blue/Green Deployment เข้าไปให้ถึงชั้นของ Databases คือจัดทำระบบฐานข้อมูลเป็น 2 ชุด ชุดปัจจุบันและชุดใหม่ และสับสวิตซ์โยกไปพร้อมกับชุดของแอปพลิเคชัน อะไรจะเกิดขึ้นบ้าง?
- “ราคา” ของการตั้งระบบฐานข้อมูลอีกชุดหนึ่งจะแพงไหม?
- ระยะเวลาโดยรวมสำหรับเตรียมการ Deployment จนถึง Deployment เสร็จสิ้นจะยาวนานขึ้นขนาดไหน?
- ความถี่ของการ Deployment จะลดลงไหม?
- ถ้ามีการ Rollback จะจัดการกับ Data อย่างไร? จะมี Data Loss หรือ Data Inconsistency เกิดขึ้นหรือไม่?
เราเลยมักเห็นการทำ Blue/Green Deployment เกิดขึ้นเฉพาะที่ Web Layer และ Business (Domain) Layer กันเท่านั้น และเพราะเหตุนี้เวลามี Databases เข้ามาเกี่ยวข้องเมื่อไร ขั้นตอนจะกลายเป็นประมาณนี้ทุกที
- เตรียมแพ็กเกจของระบบเวอร์ชันใหม่
- ปิดระบบปัจจุบัน
- ทำการสำรองข้อมูลไว้กันเหนียว
- รัน Database Migration Scripts
- Deploy แอปพลิเคชันเวอรชันใหม่
- เปิดระบบ
อ้าว… แล้วถ้าอยากให้ Downtime น้อย ๆ แบบ Near Zero หรือ Zero Downtime ล่ะ จะทำอย่างไร ตัวเลือกที่ผมเห็นบ่อยก็จะเป็นประมาณนี้ครับ
Planned Downtime
การจะได้มาซึ่ง No Downtime Deployment ที่ดีที่สุดคือ Planned Downtime ครับ คือไปดาวน์ในเวลาที่ดาวน์ได้ ฟังแล้วขำนิด ๆ ย้อนแย้งหน่อย ๆ แต่มันเรียลมากนะครับ วิธีนี้จะทำให้ความซับซ้อนและต้นทุนของงาน Deployment ของคุณลดลงอย่างมาก
Leverage Disaster Recovery (DR) or Stand-by Database Instance
สำหรับระบบฐานข้อมูลที่มีอีก Instance หนึ่งเผื่อเอาไว้ในยามฉุกเฉิน หรือที่มักพูดติดปากว่าแบบ Active/Stand-by เราอาจจะใช้หลักการคล้าย ๆ กับ Blue/Green Deployment คือเราจะทำการ Deploy ที่ Instance หนึ่งให้เรียบร้อยก่อน แล้วสลับ Connection จากแอปพลิเคชันไปที่ Instance นั้น ถ้าทุกอย่างไปได้สวยก็ค่อยทำการ Deploy ไปยัง Instance ที่เหลือ ซึ่งข้อควรระวังของวิธีนี้คือเรื่องของการ Sync ข้อมูล และขั้นตอนวิธีในการสลับโหมด Active กับ Stand-by ไปมาของระบบฐานข้อมูล รวมไปถึงจังหวะและวิธีที่ใช้ในการสลับ Connection ของแอปพลิเคชันไปมาระหว่าง Database Instance นั้นต้องดีและเป๊ะมาก ๆ ไม่เช่นนั้นผู้ใช้งานอาจจะได้รับ Error แล้วสุดท้ายข้อมูลอาจจะมีปัญหาได้อีกด้วย
Use Clustered Database
ในระบบฐานข้อมูลบางยี่ห้อ ผู้ผลิตเขาออกแบบให้เราสามารถสร้าง Node ของ Database มากกว่าหนึ่ง Node ทำงานร่วมกันเป็นระบบฐานข้อมูลอันเดียวได้ ที่เราเรียกว่า Cluster ซึ่ง Cluster มักจะอำนวยความสะดวกให้เราในเรื่องของการ Sync Data ระหว่าง Nodes หรือที่เรียกว่า Data Replication นั่นเอง จุดนี้ทำให้การ Deployment บน Cluster ของ Database มีความง่ายในแง่ที่เราไม่ต้องเป็นห่วงในการ Sync ข้อมูล เวลา Deploy ก็มักจะทำที่เครื่อง ๆ เดียว แล้วระบบก็จะจัดการเรื่อง Connection ให้เราอีกด้วย
แต่ความซับซ้อนจะเพิ่มขึ้น เพราะงาน Deployment มักทำคู่กัน ระหว่างแอปพลิเคชันและระบบฐานข้อมูล
ในยุคสมัยก่อนหน้านี้ ไม่ว่าจะเป็นการ Deployment ระบบงานใหญ่ ๆ ที่มีความซับซ้อนสูง หรือการ Deployment เพื่อเปลี่ยนระบบแบบ Big Bang ที่ต้องแข่งกับเวลา เหล่าทหารหาญที่ผ่านสนามรบแบบนั้นมาย่อมรู้ดีว่าไม่ว่าเราจะเตรียมตัวกันมาดีขนาดไหนก็ตาม อะไรก็เกิดขึ้นได้ในขณะ Deployment
ตามสามัญสำนึกแล้วการ Deploy Changes บนแอปพลิเคชันและระบบฐานข้อมูลต้องเดินหน้าไปด้วยกัน หากจะ Rollback ก็ต้อง Rollback กลับด้วยกัน แต่ถ้าเราลองสังเกตให้ดี เราจะพบว่าบางทีเราไม่จำเป็นต้อง Rollback Database ก็ได้นี่ การที่เราต้อง Rollback Database เป็นสิ่งที่แสดงว่า Changes ที่เกิดขึ้นกับ Database ของเราเป็นแบบ Backward Incompatible ครับ
การที่เราสามารถ Deploy Changes ไปยัง Database ได้ง่าย ไม่ได้หมายความว่าการ Rollback Changes บน Database นั้นจะทำได้ง่ายไปด้วย
และ Backward Incompatible Changes ก็เป็นของคู่กันกับ Downtime!
Database Design for Backward Compatibility
เวลาเราพูดถึง Deployment Downtime เราไม่ได้หมายถึงแค่ Downtime เพื่อทำการเดินหน้าอัพเดทระบบเป็นเวอร์ชันใหม่เท่านั้น แต่เรายังหมายรวมถึง Downtime ที่อาจจะเกิดขึ้นในกรณีที่เราต้องถอยหลัง Rollback ระบบของเรากลับเป็นเวอร์ชันก่อนหน้าด้วย เมื่อเราเจาะจงลงไปที่ Database Deployment กุญแจสำคัญที่จะทำให้เราไปถึงจุดที่ไม่ต้องมี Downtime จริง ๆ คือสิ่งที่เรียกว่า “Backward Compatibility Design”
Backward Compatibility Database Design คือการออกแบบ Database เพื่อทำงานกับแอปพลิเคชันเวอร์ชันใหม่ แต่ในขณะเดียวกันยังสามารถทำงานได้กับแอปพลิเคชันเวอร์ชันปัจจุบันได้ด้วย นั่นหมายความว่าเมื่อไหร่ก็ตามที่คุณ Rollback คุณจะ Rollback แค่ส่วนของแอปพลิเคชันเท่านั้น คุณไม่จำเป็นต้อง Rollback Database ตามอีกต่อไป!
ข้างบนดูมาร์เก็ตติ้งเกินจริงไปหน่อยนึง บางคนน่าจะเอะใจว่ามันเป็นไปได้จริงหรือ? คำตอบคือถ้าสภาวะแวดล้อมยิ่งเหมาะสม ความเป็นไปได้ก็จะยิ่งมีมากขึ้นครับ โดยเงื่อนไขจะเป็นประมาณนี้
- ออกแบบ Database ให้ Backward Compatible กับแอปพลิเคชันเวอร์ชันปัจจุบันกับเวอร์ชันก่อนหน้า 1 เวอร์ชันเท่านั้น ถ้าเราอยากจะออกแบบ Backward Compatible กับแอปพลิเคชันเวอร์ชันก่อนหน้ามากกว่า 1 เวอร์ชัน งานออกแบบของเราจะซับซ้อนมากขึ้นเป็นทวีคูณ ซึ่งจะไม่ค่อยคุ้มค่าเหนื่อย ถ้าถามว่าแล้วแค่ 1 เวอร์ชันก่อนหน้าจะพอเหรอ ส่วนตัวผมว่าพอ แนะนำให้เก็บแรงส่วนนั้นเอาไว้ปรับปรุงกระบวนการ Quality Assurance จะคุ้มกว่าครับ
- ระบบฐานข้อมูลของเราจะต้องอยู่บน Infrastructure ที่แรงเพียงพอที่จะทำงานได้รวดเร็วบนปริมาณข้อมูลที่เรามีอยู่ในฐานข้อมูลครับ สมมติว่าเรา Deploy Change เป็น UPDATE Statement สักอันหนึ่ง พอรันแล้วกินเวลาเป็นสิบนาที อาจจะเป็นเพราะเครื่องเซิร์ฟเวอร์คุณแรงไม่พอ เน็ตเวิร์คคุณช้า ดิสก์คุณใกล้เต็ม หรือปริมาณข้อมูลคุณเยอะมากแบบสุด ๆ หรือคุณเขียน Statement ไม่ดี หรือตารางคุณไม่ได้ทำอินเด็กซ์ที่เหมาะสม มีสาเหตุต่าง ๆ ที่เป็นไปได้มากมายก่ายกอง แต่สุดท้ายถ้าคุณใช้เวลาทำสิ่งใดสิ่งหนึ่งนานเกินไป คุณจะยิ่งมีความเสี่ยงที่จะเกิด Downtime หรือให้บริการไม่ได้ครับ ซึ่งถ้าเป็นแบบนี้ควรจะไปปรับปรุงแก้ไขเรื่องอื่นให้เหมาะสมก่อน อย่าให้ปัญหาซ้อนกันเยอะเกินครับ
The Prerequisite
สิ่งที่คุณควรจะต้องทำก่อนที่จะเริ่มต้นฝึกวิชา Backward Compatibility Database Design มีอยู่อย่างเดียว
คุณต้องทำ Versioning กับ Database ให้ได้เสียก่อน
เรามักคุ้นเคยกับการกำหนดเลขเวอร์ชันให้กับแอปพลิเคชันของเรา เช่นเดียวกันครับ เราต้องมีเลขเวอร์ชันให้กับระบบฐานข้อมูลด้วย เพื่อบ่งบอกสถานะปัจจุบัน บอกความเข้ากันได้กับเวอร์ชันแอปพลิเคชัน ที่สำคัญเพื่อจะได้รู้ว่าต้องทำกิจกรรมอะไรบ้างในการ Deployment หรือ Rollback จากเวอร์ชันหนึ่งไปยังอีกเวอร์ชันหนึ่ง
ผมเคยเขียนบทความแนะนำ Automated Database Migration Tools ที่ชื่อ Liquibase เอาไว้ เครื่องมือประเภทนี้จะช่วยในการทำ Database Versioning และช่วยให้เรา Deploy หรือ Rollback Changes บน Databases ได้รวดเร็วขึ้นอีกด้วย
The Procedures
หลักปฏิบัติมีแค่ 2 อย่างครับ
#1 แยกให้ออกว่ากิจกรรมอะไร Backward Incompatible กิจกรรมอะไร Backward Compatible
ตัวอย่างกิจกรรมแบบ Backward Incompatible หรือกิจกรรมที่ทำบนระบบฐานข้อมูลแล้วมีผลให้แอปพลิเคชันเวอร์ชันก่อนหน้าไม่สามารถทำงานได้ อาทิเช่น
- การเปลี่ยนชื่อ Column
- การลบ Column
- การเปลี่ยน Data Type ของ Column
- การแตก Table หนึ่งออกเป็น 2 Tables
- ฯลฯ
#2 สำหรับกิจกรรมที่ Backward Incompatible ให้แตกออกมาเป็นกิจกรรมย่อย ๆ ที่ Backward Compatible และทำไปตามลำดับ โดยทำการ Versioning ไว้ให้ดี
กิจกรรมที่มีคุณสมบัติ Backward Compatible โดยธรรมชาติ (ผมมักจะเรียกว่ามันไม่ Break Application) เช่น การสร้าง Table ใหม่, การเพิ่ม Column ใหม่, การ Remove Constraints เป็นต้น กิจกรรมพวกนี้ Deploy แล้วไม่จำเป็นต้อง Rollback ครับ แต่หากปล่อยทิ้งไว้ บางคนอาจจะหงุดหงิดใจนิดนึง
ทีนี้บางคนอาจจะสงสัยว่ามันจะแตกกิจกรรมที่ Backward Incompatible ออกเป็นกิจกรรมย่อย ๆ ที่เป็นแบบ Compatible ได้อย่างไร งั้นเราไปดูตัวอย่างที่จะช่วยให้เข้าใจง่ายขึ้นกันครับ
The Example
สมมติสถานการณ์และของที่มีในมือเป็นดังต่อไปนี้
- ผมมีแอป v1.0.0 ทำงานได้ดีกับฐานข้อมูล v1
- ฐานข้อมูล v1 มีตารางชื่อ CUSTOMER สำหรับเก็บข้อมูลลูกค้า
- ตาราง CUSTOMER มีคอลัมน์ชื่อ INT_RATE ไว้เก็บอัตราดอกเบี้ยของลูกค้าแต่ละคน คอลัมน์นี้จะต้องมีค่าเสมอ (คือเป็น NOT NULL)
- แอป v1.0.0 มีการ INSERT ข้อมูลลูกค้ารายใหม่พร้อมอัตราดอกเบี้ยลงไปที่ตาราง CUSTOMER นี้ด้วย
- อยู่มาวันหนึ่ง มีการวางแผนพัฒนาฟีเจอร์ใหม่ออกเป็นแอป v2.0.0 ไม่ว่าจะด้วยอะไรก็ตาม มันส่งผลให้ผมต้อง Refactor รูปแบบการเก็บค่าอัตราดอกเบี้ยใหม่ กระทบโครงสร้างข้อมูลแน่
- ผมออกแบบตารางเพื่อเก็บข้อมูลใหม่ แล้วต้องการย้ายค่าอัตราดอกเบี้ยที่เก็บ ณ คอลัมน์ INT_RATE ในตาราง CUSTOMER ออกไปอยู่ในคอลัมน์ที่ชื่อว่า DEFAULT_INTEREST_RATE ของตารางใหม่ที่ชื่อ INTEREST_RATE
ถ้าเริ่มต้นด้วยวิธีที่เรียบง่ายที่สุด ผมก็จะทำสคริปต์ประมาณนี้
→ Create Table INTEREST_RATE …
→ Update Table INTEREST_RATE …
→ Alter Table CUSTOMER Drop Column INT_RATE …
และถ้ารันสคริปต์ตามนี้ ผมก็จะได้ Database v2 ที่พร้อมสำหรับให้ผมไปเขียนแอป v2.0.0 ต่อได้ แต่ประเด็นหลักคือการลบคอลัมน์เป็นกิจกรรมประเภท Backward Incompatible ที่ส่งผล 2 อย่าง
- ผลต่อการออกแบบ ไม่มีทางที่ Database v2 ของผมจะทำงานกับแอป v1.0.0 ได้เลย เพราะในตาราง CUSTOMER ไม่มีคอลัมน์ที่ชื่อ INT_RATE แล้ว
- ผลต่อการ Deployment Database v2 ทันทีที่สคริปต์ของผมลบคอลัมน์ CUSTOMER.INT_RATE ออกไป ตัวแอป v1.0.0 ที่ทำงานอยู่จะเริ่มเดี้ยงทันที
ถ้าอยากได้ Zero หรือ Near-Zero Downtime Deployment วิธีนี้ถือว่าไปต่อลำบาก ดังนั้นสิ่งที่เราจะทำคือการแตกขั้นตอนการลบคอลัมน์ออกเป็นขั้นตอนย่อย ๆ ที่ Backward Compatible และสุดท้ายทำให้เราสามารถลบคอลัมน์เดิมทิ้งไปได้ครับ
Step#1: Database v2, Application v2.0.0
อย่าลืมหลักสำคัญของ Backward Compatible Database Design นั่นก็คือ Database v2 ต้องทำงานได้กับทั้งแอปเวอร์ชัน v2.0.0 และเวอร์ชันก่อนหน้าหนึ่งเวอร์ชัน คือ v1.0.0
Database v2
- เรายังไม่ต้องลบคอลัมน์ CUSTOMER.INT_RATE ทิ้งครับ เพื่อให้ฐานข้อมูลเรา Compatible กับแอป v1.0.0
- สร้าง Table ใหม่ที่ชื่อ INTEREST_RATE จุดสำคัญคือคอลัมน์ DEFAULT_INTEREST_RATE ห้ามมี Not Null Constraint เด็ดขาด เพราะเมื่อฐานข้อมูลเราต้องทำงานกับแอป v1.0.0 เวลามีการ Insert ลงตาราง CUSTOMER มันจะไม่มีการ Insert ลงตาราง INTEREST_RATE ที่เป็นตารางใหม่
- รันคำสั่ง Update เพื่อเอาข้อมูลจากคอลัมน์ INT_RATE เทลงไปในคอลัมน์ DEFAULT_INTEREST_RATE จุดนี้ถ้าเราใช้เวลา Migrate ข้อมูลลงไปที่ตารางใหม่นาน อาจจะเกิดอาการ Data Inconsistency ระหว่างที่ยัง Migration ไม่เสร็จได้ (คือ Select ข้อมูลจากตารางเดิมได้ข้อมูล แต่ Select ข้อมูลจากตารางใหม่ยังไม่พบข้อมูล) ตรงนี้ปรับปรุงได้ด้วย Logic ของแอปที่จะอธิบายต่อไป
Application v2.0.0
- แอป v2.0.0 ของเราจะต้องเพิ่ม Logic พิเศษไปนิดนึง นั่นคือหาก Select ค่าจาก คอลัมน์ INTEREST_RATE.DEFAULT_INTEREST_RATE แล้วพบว่าไม่มีค่า ให้ไป Select เอาค่าจาก CUSTOMER.INT_RATE มาใช้แทน เพื่อชดเชยปัญหา Data Inconsistency ที่อาจเกิดขึ้น
- ในการ Insert ข้อมูล แอป v2.0.0 จะต้องบันทึกข้อมูลลงทั้ง 2 คอลัมน์ นั่นก็คือทั้ง INTEREST_RATE.DEFAULT_INTEREST_RATE และ CUSTOMER.INT_RATE เพื่อให้ข้อมูลสอดคล้องกันและทำงานได้ในกรณีที่เราต้อง Backward ไปใช้แอป v1.0.0
Step#2: Database v3, Application v3.0.0
ทบทวนหลักการกันอีกทีครับ Database v3 จะต้องทำงานได้กับทั้งแอปเวอร์ชัน v3.0.0 และเวอร์ชันก่อนหน้าหนึ่งเวอร์ชัน นั่นก็คือ v2.0.0 นั่นเอง ถึงตรงนี้เป้าหมายสุดท้ายของเราคืออยากจะลบคอลัมน์เดิม คือ CUSTOMER.INT_RATE ให้หายไป แต่เรายังไม่พร้อมจะทำตอนนี้ครับ อย่าลืมว่าแอป v2.0.0 ยังมีการใช้งานคอลัมน์ CUSTOMER.INT_RATE อยู่
Database v3
- ทำการ Migrate ข้อมูลจาก CUSTOMER.INT_RATE มาที่ INTEREST_RATE.DEFAULT_INTEREST_RATE อีกครั้งให้ข้อมูล Consistent กันและไม่มีค่าใดเป็น Null
- ปลด Not Null Constraint ของคอลัมน์ CUSTOMER.INT_RATE ทิ้ง
Application v3.0.0
- เลิกบันทึกข้อมูลลงคอลัมน์ CUSTOMER.INT_RATE (เราสามารถทำได้เพราะเราปลด Not Null Constraint ออกแล้วนั่นเอง)
- เอาโค้ดทุกส่วนที่มีการเรียกถึงคอลัมน์ CUSTOMER.INT_RATE ออกให้หมด
Step#3: Database v4, Application v4.0.0
เหมือนเดิมครับ Database v4 นี้จะต้องทำงานได้กับทั้งแอปเวอร์ชัน v4.0.0 และ v3.0.0
Database v4
- Drop คอลัมน์ CUSTOMER.INT_RATE ทิ้งได้แล้วครับ เพราะแอปทั้ง v4.0.0 และ v3.0.0 ไม่ได้ใช้คอลัมน์นี้แล้วทั้งคู่
- และเพิ่ม Not Null Constraint ที่ INTEREST_RATE.DEFAULT_INTEREST_RATE ได้แล้วเช่นกัน
Application v4.0.0
- ไม่ต้องทำอะไรเป็นพิเศษละครับ
งานจริงมักจะซับซ้อนกว่าตัวอย่างเยอะครับ ผมยกตัวอย่างโดยตัดรายละเอียดที่ไม่จำเป็นออก เพื่อเน้นให้เห็นคอนเซ็ปต์ของการออกแบบได้ชัดเจนขึ้น เราจะเห็นว่านอกจากวิธีขั้นตอนของการออกแบบจะซับซ้อนขึ้นกว่าท่าปกติแล้ว เรายังต้องการ Release Management ที่ดี การ Deployment ต้องไว กรณีที่ต้อง Rollback ก็จะทำทีละเวอร์ชัน ซึ่งหมายถึงกระบวนการทดสอบก่อนและหลังการ Deployment จะต้องมีคุณภาพและน่าเชื่ออีกด้วย
Is it practically possible?
จากตัวอย่างในย่อหน้าที่แล้ว บางคนน่าจะมีความเห็นว่าทำงานเดียวต้องแบ่ง Deploy เป็น 3 ครั้ง สู้ขอ Planned Downtime ทำรอบเดียวจบดีกว่า ผมเห็นด้วย 1000% เลยครับ แต่ในสถานการณ์จริงมีความเป็นไปได้มากมายที่ทำให้เราต้องใช้หรือไม่ใช้การออกแบบในลักษณะนี้ บางทีเราอาจจะไม่ได้เป็นคนเลือก ผมเองเคยมีช่วงนึงที่ต้องทำงานกับระบบที่ให้บริการตลอด 24 ชม. ไม่สามารถที่จะดาวน์ได้เลย ซึ่งผมก็ได้เรียนรู้จากช่วงเวลานั้นมาว่า…
แม้สิ่งที่ไม่คาดคิดอาจจะเกิดได้ทุกเมื่อ แต่สิ่งที่เราคิดจะทำจะต้องไม่ทำให้ระบบดาวน์ครับ
อย่างไรก็ตาม…
สิ่งที่เราควรพิจารณาในฐานะผู้ออกแบบซอฟต์แวร์คือความคุ้มทุน ในการทำสิ่งที่ซับซ้อนมากขึ้น เพื่อคงไว้ซึ่ง User Experience
สิ่งที่เราควรพิจารณาในฐานะผู้ออกแบบซอฟต์แวร์คือความเสี่ยง ที่แตกต่างกันระหว่างการ Deploy Database Changes แบบ Big Bang ทุกควอเตอร์ กับการ Deployment Backward Compatible Changes ชิ้นเล็ก ๆ ด้วย DevOps Pipelines ทุกสิ้นวัน
สิ่งที่เราควรพิจารณาในฐานะผู้ออกแบบซอฟต์แวร์คือสภาวะบน Production ที่แตกต่างจากใน Test Environment นั่นคือปริมาณและความหลากหลายของข้อมูลที่ส่งผลต่อความเร็วในการ Deployment
ในชีวิตการทำงานจริง เราไม่ได้ใช้ทฤษฏีหรือเครื่องมือเพียงอย่างเดียวในการพัฒนาซอฟต์แวร์ อย่าลืมว่าเราสามารถมีตัวช่วย มีเครื่องมืออื่น ๆ อีกมากมายที่จะช่วยให้งานของเราง่ายขึ้น อะไรที่เราทำไม่ได้ เราก็ให้คนอื่นทำครับ ผ่าม!
คุยกันช่วงท้าย ผมรู้สึกว่าการเรียบเรียงบทความนี้ทำได้ยากกว่าบทความอื่นที่ผมเขียนมาในปีนี้เลยทีเดียว ผมมีความกังวลที่จะเขียนให้ครอบคลุมใจความหลักครบถ้วน พยายามจัดลำดับการเล่าเรื่อง แต่ประเด็นเกี่ยวกับการออกแบบซอฟต์แวร์มักมีเรื่องปลีกย่อยมากมายที่อยากจะเขียนอธิบาย กลัวว่าถ้าไม่เขียนก็จะไม่สมบูรณ์ แต่ถ้าจะนำมาขยายก็จะใช้พื้นที่เยอะเกินไป และที่สำคัญจะทำให้เนื้อหาย่อยยากขึ้นไปอีก ท้องอืดท้องผูกกันไป เอาเป็นว่าแค่นี้น่าจะพอไหวมั้ง ^^
Happy Deploying Your Application!
สำหรับใครที่ชื่นชอบบทความนี้ อย่าลืมกดติดตาม Medium: KBTG Life เรามีสาระความรู้และเรื่องราวดีๆ จากชาว KBTG พร้อมเสิร์ฟให้ที่นี่ที่แรก