Data Decomposition Concerns (ข้อกังวลในการแบ่งย่อยข้อมูล)
เมื่อ เราเริ่มแยกฐานข้อมูลออกจากกัน เราอาจก่อให้เกิดปัญหาหลายอย่างได้ ต่อไปนี้คือความท้าทายบางอย่างที่คุณอาจเจอ พร้อมเคล็ดลับบางอย่างที่ช่วยได้
Performance (ประสิทธิภาพ)
ฐานข้อมูล โดยเฉพาะ relational database เก่งเรื่องการ join ข้อมูลข้ามตารางต่าง ๆ เก่งมากด้วย เก่งจนเราถือเป็นเรื่องปกติไปเลย บ่อยครั้งเมื่อเราแยกฐานข้อมูลออกจากกันในนามของ microservices เราก็ต้องย้าย join operation จาก data tier ขึ้นไปยัง microservices เอง และไม่ว่าเราจะพยายามแค่ไหน มันก็ไม่น่าจะเร็วเท่าเดิม
ลองพิจารณา Figure 3-6 ซึ่งแสดงสถานการณ์ที่เราพบตัวเองอยู่เกี่ยวกับ MusicCorp เราตัดสินใจดึงฟังก์ชันการทำงานด้าน catalog ออกมา ซึ่งสามารถจัดการและเปิดเผยข้อมูลเกี่ยวกับศิลปิน เพลง และอัลบั้มได้ ปัจจุบันโค้ดที่เกี่ยวกับ catalog ภายใน monolith ใช้ตาราง Albums เพื่อจัดเก็บข้อมูลเกี่ยวกับ CD ที่เรามีขายอยู่ อัลบั้มเหล่านี้ท้ายที่สุดก็ถูกอ้างอิงในตาราง Ledger ของเรา ซึ่งเป็นที่ที่เราติดตามยอดขายทั้งหมด แถวในตาราง Ledger จะบันทึกวันที่ที่มีการขายสินค้า พร้อมกับ identifier ที่อ้างอิงถึงสินค้าที่ขาย identifier ในตัวอย่างของเราเรียกว่า SKU (stock keeping unit) ซึ่งเป็นแนวปฏิบัติทั่วไปในระบบค้าปลีก 3
เมื่อสิ้นเดือน เราต้องสร้างรายงานสรุป CD ที่ขายดีที่สุด ตาราง Ledger ช่วยให้เราเข้าใจว่า SKU ไหนขายได้มากที่สุด แต่ข้อมูลเกี่ยวกับ SKU นั้นอยู่ในตาราง Albums เราต้องการให้รายงานอ่านง่ายและสวยงาม ดังนั้นแทนที่จะบอกว่า "เราขาย SKU 123 ไป 400 ชุด ทำเงินได้ $1,596" เราจะ เพิ่มข้อมูลเกี่ยวกับสิ่งที่ขายไป โดยบอกแทนว่า "เราขาย Now That's What I Call Death Polka ไป 400 ชุด ทำเงินได้ $1,596" เพื่อทำเช่นนี้ database query ที่ถูกกระตุ้นโดยโค้ดฝ่ายการเงินของเราต้อง join ข้อมูลจากตาราง Ledger เข้ากับตาราง Albums ดังที่ Figure 3-6 แสดงให้เห็น
Figure 3-6. Join operation ในฐานข้อมูล monolithic
ในโลกใหม่ที่อิง microservice ของเรา microservice Finance ใหม่ของเรามีความรับผิดชอบในการสร้างรายงานสินค้าขายดีที่สุด แต่ไม่มีข้อมูลอัลบั้มอยู่ในเครื่อง ดังนั้นมันจะต้องดึงข้อมูลนี้จาก microservice Catalog ใหม่ของเรา ดังที่แสดงใน Figure 3-7 เมื่อสร้างรายงาน microservice Finance จะ query ตาราง Ledger ก่อน ดึงรายการ SKU ที่ขายดีที่สุดของเดือนที่แล้วออกมา ณ จุดนี้ ข้อมูลเดียวที่เรามีในเครื่องคือรายการ SKU และจำนวนที่ขายได้สำหรับแต่ละ SKU
Figure 3-7. แทนที่ database join operation ด้วยการเรียก service
ต่อไป เราต้องเรียก microservice Catalog เพื่อขอข้อมูลเกี่ยวกับแต่ละ SKU เหล่านี้ request นี้จะทำให้ microservice Catalog ทำ SELECT ในเครื่องกับฐานข้อมูลของตัวเอง
ในเชิงตรรกะ join operation ยังคงเกิดขึ้นอยู่ แต่ตอนนี้มันเกิดขึ้นภายใน microservice Finance แทนที่จะเกิดในฐานข้อมูล การ join ได้ย้ายจาก data tier ไปยัง application code tier แล้ว น่าเสียดายที่ operation นี้จะไม่มีทางมีประสิทธิภาพเท่ากับตอนที่ join ยังอยู่ในฐานข้อมูล เราไปจากโลกที่มี SELECT statement เดียว สู่โลกใหม่ที่เรามี SELECT query กับตาราง Ledger ตามด้วยการเรียก microservice Catalog ซึ่งจะกระตุ้น SELECT statement กับตาราง Albums ต่อไป ดังที่เราเห็นใน Figure 3-7
ในสถานการณ์นี้ ผมจะ ประหลาดใจมาก ถ้า latency โดยรวมของ operation นี้ไม่เพิ่มขึ้น นั่นอาจไม่ใช่ปัญหาใหญ่ในกรณีนี้โดยเฉพาะ เนื่องจากรายงานนี้ถูกสร้างขึ้นทุกเดือนและสามารถถูก cache ได้อย่างเข้มข้น (เราจะสำรวจหัวข้อนี้อย่างละเอียดมากขึ้นใน "Caching" ) แต่ถ้านี่เป็น operation ที่เกิดขึ้นบ่อย นั่นอาจเป็นปัญหามากกว่า เราสามารถลดผลกระทบที่อาจเกิดขึ้นจากการเพิ่มขึ้นของ latency นี้ได้ โดยการอนุญาตให้ SKU ถูก lookup ใน microservice Catalog แบบเป็นชุด (bulk) หรือบางทีอาจถึงขั้น cache ข้อมูลอัลบั้มที่จำเป็นไว้ในเครื่อง
Data Integrity (ความสมบูรณ์ของข้อมูล)
ฐานข้อมูล มีประโยชน์ในการช่วยรักษาความสมบูรณ์ของข้อมูลเรา กลับมาที่ Figure 3-6 เมื่อทั้งตาราง Album และ Ledger อยู่ในฐานข้อมูลเดียวกัน เราสามารถ (และมีแนวโน้มจะ) กำหนด foreign key relationship ระหว่างแถวในตาราง Ledger กับตาราง Album ได้ สิ่งนี้จะทำให้แน่ใจว่าเราจะสามารถนำทางจากบันทึกในตาราง Ledger กลับไปยังข้อมูลเกี่ยวกับอัลบั้มที่ขายได้เสมอ เพราะเราจะไม่สามารถลบบันทึกจากตาราง Album ได้ถ้ามันถูกอ้างอิงอยู่ใน Ledger
เมื่อตารางเหล่านี้อยู่ในฐานข้อมูลคนละตัวกันแล้ว เราก็ไม่มีการบังคับใช้ความสมบูรณ์ของโมเดลข้อมูลของเราอีกต่อไป ไม่มีอะไรมาหยุดเราจากการลบแถวในตาราง Album ซึ่งจะก่อให้เกิดปัญหาเมื่อเราพยายามหาว่าสินค้าที่ขายไปจริง ๆ คืออะไรกันแน่
ในระดับหนึ่ง คุณจะแค่ต้องคุ้นเคยกับความจริงที่ว่าคุณไม่สามารถพึ่งพาฐานข้อมูลของคุณในการบังคับใช้ความสมบูรณ์ของความสัมพันธ์ระหว่าง entity ได้อีกต่อไป แน่นอนว่าสำหรับข้อมูลที่ยังคงอยู่ในฐานข้อมูลเดียว นี่ไม่ใช่ปัญหา
มีวิธีแก้ปัญหาชั่วคราวอยู่หลายวิธี แม้ "coping pattern" จะเป็นคำที่เหมาะกว่าสำหรับวิธีที่เราอาจใช้จัดการกับปัญหานี้ เราสามารถใช้ soft delete ในตาราง Album เพื่อที่เราจะไม่ได้ลบบันทึกออกไปจริง ๆ แต่แค่ทำเครื่องหมายว่าถูกลบแล้วเท่านั้น อีกทางเลือกหนึ่งคือคัดลอกชื่ออัลบั้มไปไว้ในตาราง Ledger เมื่อมีการขายเกิดขึ้น แต่เราก็ต้องแก้ไขปัญหาว่าเราต้องการจัดการการซิงค์การเปลี่ยนแปลงชื่ออัลบั้มอย่างไร
Transactions (Transaction)
พวกเรา หลายคนคุ้นเคยกับการพึ่งพาการรับประกันที่เราได้จากการจัดการข้อมูลใน transaction จากความแน่นอนนั้น เราได้สร้างแอปพลิเคชันในรูปแบบหนึ่ง โดยรู้ว่าเราสามารถพึ่งพาฐานข้อมูลให้จัดการหลายสิ่งหลายอย่างให้เราได้ แต่เมื่อเราเริ่มแยกข้อมูลข้ามฐานข้อมูลหลายตัว เราก็สูญเสียความปลอดภัยของ ACID transaction ที่เราคุ้นเคย (ผมอธิบายคำย่อ ACID และพูดถึง ACID transaction อย่างละเอียดมากขึ้นใน Chapter 6 )
สำหรับคนที่ย้ายจากระบบที่การเปลี่ยนแปลง state ทั้งหมดสามารถจัดการได้ในขอบเขต transactional เดียว การเปลี่ยนไปสู่ระบบแบบกระจายอาจเป็นเรื่องช็อกได้ และปฏิกิริยาที่มักเกิดขึ้นคือการมองหาทาง implement distributed transaction เพื่อเรียกการรับประกันที่ ACID transaction เคยให้เรากลับคืนมาด้วยสถาปัตยกรรมที่ง่ายกว่า น่าเสียดายที่อย่างที่เราจะพูดถึงอย่างละเอียดใน "Database Transactions" distributed transaction ไม่เพียงยากที่จะ implement แม้จะทำได้ดีก็ตาม แต่ยังไม่ได้ให้การรับประกันแบบเดียวกับที่เราคาดหวังจาก database transaction ที่มีขอบเขตแคบกว่าด้วย
อย่างที่เราจะสำรวจต่อไปใน "Sagas" มีกลไกทางเลือก (และดีกว่า) สำหรับ distributed transaction ในการจัดการการเปลี่ยนแปลง state ข้าม microservices หลายตัว แต่มันก็มาพร้อมกับความซับซ้อนแหล่งใหม่ เช่นเดียวกับ data integrity เราต้องยอมรับความจริงที่ว่าการแยกฐานข้อมูลของเราออกจากกัน ด้วยเหตุผลที่ดีมาก ๆ ก็ตาม จะทำให้เราเจอกับปัญหาชุดใหม่
Tooling (เครื่องมือ)
การเปลี่ยนแปลงฐานข้อมูล เป็นเรื่องยากด้วยเหตุผลหลายอย่าง หนึ่งในนั้นคือเครื่องมือที่มีอยู่เพื่อช่วยให้เราเปลี่ยนแปลงได้ง่ายนั้นมีจำกัด สำหรับโค้ด เรามีเครื่องมือ refactoring ที่ฝังอยู่ใน IDE ของเรา และเรายังมีประโยชน์เพิ่มเติมที่ระบบที่เรากำลังเปลี่ยนแปลงนั้นเป็น stateless โดยพื้นฐาน สำหรับฐานข้อมูล สิ่งที่เรากำลังเปลี่ยนแปลงมี state และเรายังขาดเครื่องมือประเภท refactoring ที่ดีด้วย
มีเครื่องมือมากมายที่ช่วยคุณจัดการกระบวนการเปลี่ยนแปลง schema ของ relational database แต่ส่วนใหญ่ก็ตามรูปแบบเดียวกัน การเปลี่ยนแปลง schema แต่ละครั้งถูกกำหนดไว้ใน delta script ที่ควบคุมด้วยระบบ version สคริปต์เหล่านี้จะถูกรันตามลำดับที่เข้มงวดในลักษณะ idempotent Rails migration ทำงานแบบนี้ เช่นเดียวกับ DBDeploy ซึ่งเป็นเครื่องมือที่ผมช่วยสร้างขึ้นเมื่อหลายปีก่อน
ทุกวันนี้ผมมักแนะนำให้ใช้ Flyway หรือ Liquibase เพื่อให้ได้ผลลัพธ์เดียวกัน ถ้าคุณยังไม่มีเครื่องมือที่ทำงานแบบนี้อยู่แล้ว
Reporting Database (ฐานข้อมูลสำหรับรายงาน)
เป็น ส่วนหนึ่งของการดึง microservices ออกจากแอปพลิเคชัน monolithic ของเรา เรายังแยกฐานข้อมูลของเราออกจากกันด้วย เพราะเราต้องการซ่อนการเข้าถึง internal data storage ของเรา ด้วยการซ่อนการเข้าถึงฐานข้อมูลของเราโดยตรง เราสามารถสร้าง interface ที่มีเสถียรภาพได้ดีขึ้น ซึ่งทำให้ independent deployability เป็นไปได้ แต่น่าเสียดายที่สิ่งนี้ก่อให้เกิดปัญหาเมื่อเรามี use case ที่ชอบธรรมสำหรับการเข้าถึงข้อมูลจาก microservice มากกว่าหนึ่งตัว หรือเมื่อข้อมูลนั้นเหมาะที่จะมีให้ใช้งานในฐานข้อมูลมากกว่าผ่านสิ่งอย่าง REST API
ด้วย reporting database เราจะสร้างฐานข้อมูลเฉพาะทางที่ออกแบบมาสำหรับการเข้าถึงจากภายนอกแทน และทำให้เป็นความรับผิดชอบของ microservice ในการผลักดันข้อมูลจาก internal storage ไปยัง reporting database ที่เข้าถึงได้จากภายนอก ดังที่แสดงใน Figure 3-8
Reporting database ช่วยให้เราซ่อนการจัดการ internal state ได้ ในขณะที่ยังคงนำเสนอข้อมูลในฐานข้อมูล ซึ่งมีประโยชน์มาก ยกตัวอย่างเช่น คุณอาจต้องการอนุญาตให้คนรัน SQL query ที่กำหนดแบบ ad hoc รัน join ขนาดใหญ่ หรือใช้ toolchain ที่มีอยู่แล้วที่คาดหวังว่าจะมีการเข้าถึง SQL endpoint Reporting database เป็นทางออกที่ดีสำหรับปัญหานี้
Figure 3-8. ภาพรวมของ reporting database pattern
มีจุดสำคัญสองข้อที่ควรเน้นย้ำตรงนี้ อย่างแรก เรายังต้องการปฏิบัติตาม information hiding ดังนั้นเราควรเปิดเผยข้อมูลขั้นต่ำสุดเท่าที่จำเป็นใน reporting database เท่านั้น นี่หมายความว่าสิ่งที่อยู่ใน reporting database อาจเป็นแค่ส่วนย่อยของข้อมูลที่ microservice จัดเก็บไว้เท่านั้น อย่างไรก็ตาม เนื่องจากนี่ไม่ใช่การ map แบบตรง ๆ มันจึงเปิดโอกาสให้คิดค้นการออกแบบ schema สำหรับ reporting database ที่ปรับให้เหมาะสมกับความต้องการของ consumer โดยเฉพาะได้ ซึ่งอาจรวมถึงการใช้ schema ที่แตกต่างไปอย่างสิ้นเชิง หรือแม้แต่เทคโนโลยีฐานข้อมูลประเภทที่ต่างออกไปเลยก็ได้
จุดสำคัญข้อที่สองคือ reporting database ควรได้รับการปฏิบัติเหมือน endpoint ของ microservice อื่น ๆ และเป็นหน้าที่ของผู้ดูแล microservice ที่จะทำให้แน่ใจว่าความเข้ากันได้ของ endpoint นี้ยังคงอยู่แม้ microservice จะเปลี่ยนแปลงรายละเอียด internal implementation ของมัน การ map จาก internal state ไปยัง reporting database เป็นความรับผิดชอบของคนที่พัฒนา microservice นั้นเอง