ทำไมผมถึงยอมให้ข้อมูลของคนแปลกหน้าอยู่ในฐานข้อมูลเดียวกัน

U-PRO Build Journal — ตอนที่ 2

ตอนจบของตอนที่ 1 ผมเพิ่งย้ายระบบที่รันอยู่จริงไปเป็นสถาปัตยกรรม multi-tenant เสร็จ — เป็นการตัดสินใจแบบที่ทำครั้งเดียวแล้วต้องอยู่กับมันไปอีกหลายปี ทุกอู่ที่จะมาใช้ U-PRO ระบบบริหารจัดการหน้าร้านสำหรับอู่ซ่อมรถ จะมีข้อมูลอยู่ในฐานข้อมูลเดียวกันกับทุกอู่อื่น ตารางเดียวกัน แถวเดียวกัน แยกกันแค่ตรงว่าเป็นของอู่ไหน เจ้าของต่างคน ช่างต่างกัน ลูกค้าต่างกัน ที่ไม่ควรเห็นข้อมูลของกันแม้แต่ไบต์เดียว — ทั้งหมดนี้อยู่ในที่เดียวกันจริงๆ ทางกายภาพ

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

ถ้าอธิบายสถาปัตยกรรมนี้ให้คนนอกวงการซอฟต์แวร์ฟัง — "ผมจะเอาธุรกิจของลูกค้าทุกรายไปใส่ในฐานข้อมูลเดียวกัน แล้วสิ่งเดียวที่กั้นพวกเขาออกจากกันคือโค้ดที่ผมเขียนเอง" — ปฏิกิริยาธรรมชาติมักจะประมาณว่า "ฟังดูเสี่ยงนะ ทำไมไม่ให้แต่ละคนมีฐานข้อมูลของตัวเองไปเลยล่ะ" เป็นคำถามที่สมเหตุสมผล และเป็นคำถามที่ผมถามตัวเองเหมือนกัน คำตอบที่ตรงไปตรงมาไม่ใช่ "เพราะฐานข้อมูลร่วมปลอดภัยกว่าโดยธรรมชาติ" มันเจาะจงกว่านั้น และแทบไม่เกี่ยวกับความปลอดภัยเลย แต่เกี่ยวกับรูปแบบธุรกิจที่ผมพยายามสร้างเกือบทั้งหมด

โครงสร้างฐานข้อมูลเรืองแสงโปร่งใสแตกร้าวแยกออกเป็นห้องแยกต่างหาก ในความมืดเกือบสนิท

กำแพงสองแบบ

กำแพงที่แบ่งครึ่งทแยงมุมระหว่างคอนกรีตหยาบกับกำแพงเรืองแสงที่ทำจากเส้นกฎ ในความมืดเกือบสนิท

โดยกว้างๆ แล้วมีวิธีกันไม่ให้ข้อมูลของอู่หนึ่งรั่วไปหาอีกอู่อยู่สองแบบ

แบบแรกคือการแยกทางกายภาพ ให้แต่ละอู่มีฐานข้อมูลของตัวเอง หรืออย่างน้อยก็มี schema ของตัวเอง เพื่อให้ต่อให้โค้ดแอปพลิเคชันมีบั๊กร้ายแรงแค่ไหน ก็ข้ามเส้นแบ่งไปไม่ได้ — เพราะไม่มีเส้นแบ่งให้ข้าม ในเมื่อข้อมูลไม่ได้อยู่ที่เดียวกันตั้งแต่แรก นี่คือกำแพงที่สร้างจากคอนกรีต เข้าใจง่ายโดยสัญชาตญาณ และในบางบริบทก็เป็นทางเลือกที่ถูกต้อง

แบบที่สองคือการแยกทางตรรกะ ฐานข้อมูลเดียวกัน ชุดตารางเดียวกัน และกำแพงที่บังคับด้วยกฎล้วนๆ — ทุก query ต้องมีตัวกรองบังคับติดมาด้วยเสมอ เป็นการเช็คที่บอกว่า "คุณเห็นได้แค่แถวที่เป็นของอู่ที่คุณสังกัดอยู่เท่านั้น" ทำงานอัตโนมัติทุกครั้ง ไม่ว่าคนที่เขียนโค้ดส่วนนั้นจะนึกถึงเรื่องนี้หรือไม่ก็ตาม นี่คือกำแพงที่สร้างจากนโยบาย เข้าใจยากกว่าเมื่อมองจากภายนอก เพราะชี้ไม่ได้ว่ามันอยู่ตรงไหน มันมีอยู่ในรูปตรรกะที่รันอยู่ในฐานข้อมูลเอง ไม่ใช่กำแพงที่ถ่ายรูปได้

ผมเลือกแบบที่สอง ไม่ใช่เพราะคิดว่ามันปลอดภัยกว่าโดยธรรมชาติ — กำแพงที่อิงนโยบายขึ้นอยู่กับว่านโยบายนั้นถูกต้องครบทุกจุด บังคับใช้ทุกที่ และไม่มีวันถูกข้ามไปแบบเงียบๆ เลย ซึ่งเป็นคุณสมบัติที่รับประกันได้ยากกว่า "สองสิ่งนี้ไม่ได้อยู่ในตึกเดียวกัน" จริงๆ ผมเลือกมันเพราะต้นทุนของแต่ละทางเลือก ที่สเกลที่ผมกำลังสร้างอยู่จริง

ต้นทุนที่แท้จริงของ "ฐานข้อมูลแยกรายอู่"

นี่คือส่วนที่มองไม่เห็นจนกว่าจะลองนึกภาพว่าต้องรันมันจริง ฐานข้อมูลเฉพาะต่ออู่ไม่ได้แค่กินทรัพยากรคอมพิวเตอร์มากขึ้นเท่านั้น มันกิน ตัวผม มากขึ้นด้วย

ทุกอู่ที่สมัครเข้ามาต้องได้รับการจัดเตรียมฐานข้อมูลของตัวเองให้ — เป็นปฏิบัติการแยกที่ต้องสำเร็จอย่างน่าเชื่อถือทุกครั้ง แม้แต่ตอนตี 2 ถ้าบังเอิญมีคนสมัครเสร็จตอนนั้น ทุกการเปลี่ยน schema ที่ผมปล่อยออกไป ไม่ว่าจะเป็นคอลัมน์ใหม่ ตารางใหม่ หรือแก้ฟังก์ชัน ต้องรันกับฐานข้อมูลทุกตัวเหล่านั้น ไม่ใช่แค่ครั้งเดียว ทุกนโยบายสำรองข้อมูลต้องแยกใช้ต่ออู่ ไม่ใช่ใช้รวมทีเดียวทั้งระบบ ทุก dashboard ตรวจสอบ ทุกสคริปต์ migration ทุกการเช็คว่า "แก้ตรงนั้นไปแล้วใช้ได้จริงทุกที่ไหม" — ทั้งหมดนี้คูณเพิ่มตามจำนวนอู่ ตลอดไป ตราบใดที่ธุรกิจนี้ยังอยู่

การคูณเพิ่มแบบนี้ไม่มีปัญหา หรือแม้แต่ควรเลือกด้วยซ้ำ ถ้าขายให้ลูกค้าองค์กรใหญ่จำนวนน้อยรายที่จ่ายเงินมากพอจะคุ้มกับการมี infrastructure เฉพาะและทีมดูแล แต่เป็นการแลกที่แย่มาก ถ้าโจทย์ทั้งหมดของธุรกิจ — ซึ่งเป็นแบบนั้นจริงๆ สำหรับผม — คืออู่เล็กๆ ที่มีช่างสามคนกับคอมพิวเตอร์เครื่องเดียวต้องสมัครใช้ ลองระบบ แล้วดูว่าคุ้มเงินไหม โดยที่ผมไม่ต้องตั้งและดูแล infrastructure เฉพาะให้อู่นั้นทีละราย เศรษฐศาสตร์ของ "ลูกค้ารายเล็กจำนวนมาก ต้นทุนต่อรายต่ำ" กับเศรษฐศาสตร์ของ "ฐานข้อมูลเฉพาะต่อลูกค้า" ดึงกันไปคนละทาง ผมเลือกทั้งสองอย่างพร้อมกันไม่ได้ เหตุผลทั้งหมดที่ U-PRO มีอยู่ — ต้องถูกพอ เร็วพอ แรงเสียดทานต่ำพอที่เจ้าของอู่เล็กจะเริ่มใช้ได้โดยไม่ต้องผ่านกระบวนการจัดซื้อ — ชี้ไปทางโมเดลฐานข้อมูลร่วมทั้งหมด

มีกรอบคิดจากนอกวงการซอฟต์แวร์ที่จับภาพนี้ได้ดีกว่าคำอธิบายเชิงเทคนิคใดๆ คือการตัดสินใจแบบ build-versus-buy เพียงแต่สิ่งที่ผม "ซื้อ" ไม่ใช่โปรดักต์จากผู้ขายรายอื่น แต่คือความเรียบง่ายในการดำเนินงาน ที่จ่ายด้วยสกุลเงินแบบรับภาระเพิ่มไว้ในโค้ดแอปพลิเคชันเองแทน ต้นทุนรวมในการเป็นเจ้าของ ถ้าคิดอย่างตรงไปตรงมา ไม่ใช่แค่ค่า infrastructure แต่คือต้นทุนต่อเนื่องของการดูแลระบบนั้นตามจำนวนลูกค้าที่หวังว่าสุดท้ายจะมี — และสำหรับโปรดักต์ที่ตั้งใจให้อู่เล็กจำนวนมากสมัครใช้ในต้นทุนถูก โมเดลฐานข้อมูลเฉพาะต่อลูกค้าคือเส้นต้นทุนที่จะย้อนกลับมาทำร้ายธุรกิจตั้งแต่วันแรก ก่อนที่มันจะกลายเป็นปัญหาทางเทคนิคเสียอีก

กำแพงต้องรับน้ำหนักได้จริง

ทั้งหมดนี้ไม่ใช่ข้อโต้แย้งว่าการแยกทางตรรกะปลอดภัยโดยธรรมชาติ มันไม่ใช่ มันเป็นข้อโต้แย้งว่านี่คือการเดิมพันทางเศรษฐศาสตร์ที่ถูกต้อง — และการเดิมพันไม่เหมือนกับการพิสูจน์ว่ามันรับน้ำหนักได้จริง การเลือกกำแพงที่ถูกกว่าผูกมัดให้ต้องลงแรงจริงเพื่อให้แน่ใจว่ามันรับน้ำหนักไหว เพราะต่างจากกำแพงคอนกรีต ไม่มีใครเดินไปดูด้วยตาแล้วยืนยันได้ว่ามันอยู่ตรงนั้น

ตรงนี้แหละที่งานหลังจาก multi-tenant migration เกิดขึ้นจริง ในไม่กี่วันหลัง migration นั้นลงเสร็จ มีชุดการเปลี่ยนแปลงที่ทำขึ้นเฉพาะเพื่อล็อกการเข้าถึงข้อมูลรถที่ระดับฐานข้อมูล — จำกัดว่าฟังก์ชันเข้าถึงข้อมูลตัวไหนบ้างที่เรียกใช้ได้ และซ่อนส่วนต่างๆ ของหน้าจอทั้งหมดจากใครก็ตามที่ไม่ได้อยู่ใน role ผู้บริหารของอู่นั้น ทั้งสองการเปลี่ยนแปลงนี้ไม่ได้เพิ่มฟีเจอร์ที่ผู้ใช้จะสังเกตเห็นหรือร้องขอ มันมีอยู่เพียงเพื่อลดช่องว่างระหว่าง "กำแพงควรจะรับน้ำหนักได้" กับ "กำแพงพิสูจน์แล้วว่ารับน้ำหนักได้จริง" ทีละเส้นทางการเข้าถึง

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

นี่คือส่วนของ multi-tenancy แบบฐานข้อมูลร่วมที่คนพูดถึงไม่มากพอ เวลาบรรยายว่ามันคือสถาปัตยกรรมประหยัดต้นทุนที่ฉลาด — การเลือกมันคือส่วนที่ง่าย เป็นการตัดสินใจที่ทำได้ในบ่ายวันเดียวหลังอ่านเรื่อง row-level security แต่สิ่งที่มันผูกมัดให้ทำจริงๆ คือวินัยต่อเนื่องแบบถาวร — ทุกตารางใหม่ต้องมี logic แยกข้อมูลแบบเดียวกันติดไปด้วย ทุกฟังก์ชันใหม่ที่แตะข้อมูลลูกค้าต้องเช็คอย่างชัดเจนว่าอู่ไหนกำลังถามอยู่ ทุกครั้ง ตลอดไป ไม่มีจุดไหนที่งานนี้จะ "เสร็จ" มีแค่จุดที่ตามทันทุกอย่างที่มีอยู่ตอนนี้ ก่อนที่ฟีเจอร์ถัดไปจะเพิ่มของใหม่ที่ต้องการการดูแลแบบเดียวกันเข้ามาอีก

ตอนตัดสินใจ ผมยังไม่เข้าใจเรื่องนี้เต็มที่ ผมเข้าใจมันแค่ในฐานะการตัดสินใจเรื่องต้นทุน — ดูแลถูกกว่า สร้างต่อยอดได้เร็วกว่า เหมาะกับลูกค้ารายเล็กจำนวนมากมากกว่ารายใหญ่ไม่กี่ราย สิ่งที่ผมยังไม่ซึมซับตอนนั้นคือ ผมกำลังเลือก "ภาษี" ต่อเนื่องที่จะเก็บจากทุกฟีเจอร์ในอนาคตด้วย — ไม่มีอะไรสร้างเร็วๆ ได้อีกแล้วโดยไม่ต้องถามด้วยว่า "ของใหม่นี้เคารพกำแพงถูกต้องไหม" เพราะกำแพงนี้ไม่ได้บังคับใช้ตัวเองแบบที่การแยกทางกายภาพจะเป็น

ภาษีนี้ทบต้นแบบเงียบๆ

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

ตาชั่งโบราณเอียงลงภายใต้กองน้ำหนักเล็กๆ สีเข้มที่เพิ่มขึ้นเรื่อยๆ ในความมืดเกือบสนิท

ทุกตารางใหม่ที่ผมเพิ่มเข้าระบบจากนี้ไปต้องต่อ logic แยกข้อมูลแบบเดียวกันเข้าไปด้วย — คอลัมน์ที่บอกว่าแถวนั้นเป็นของอู่ไหน และกฎที่บังคับใช้ที่ระดับฐานข้อมูล ไม่ใช่แค่ในโค้ดแอปพลิเคชัน ที่บอกว่าไม่มีใครอ่านหรือเขียนแถวนั้นได้ เว้นแต่จะเป็นสมาชิกที่ยืนยันแล้วของอู่ที่แถวนั้นเป็นของ ทุกฟังก์ชันใหม่ที่แตะข้อมูลลูกค้าต้องเช็คความเป็นสมาชิกนี้อย่างชัดเจนภายในตัวมันเอง ทุกครั้ง เพราะฟังก์ชันไหนก็ตามที่มีสิทธิ์มากพอจะข้ามกฎการเข้าถึงปกติได้ ก็มีสิทธิ์มากพอจะรั่วข้ามกำแพงได้เช่นกัน ถ้าการเช็คนั้นขาดหายไปแม้แค่ครั้งเดียว ทั้งหมดนี้ผู้ใช้ที่คลิกไปมาในโปรดักต์มองไม่เห็นเลย มันคือโครงที่มองไม่เห็นซึ่งต้องถูกต้องทุกที่ตลอดไป ไม่งั้นกำแพงนั้นจะไม่ใช่กำแพงจริงๆ — มันจะเป็นกำแพงที่มีประตูที่ไม่มีป้ายบอกซ่อนอยู่ตรงไหนสักที่ และจะไม่รู้ว่าตรงไหนจนกว่าจะมีคนเดินผ่านเข้าไป

นี่คือต้นทุนทางวิศวกรรมที่ต่างไปจากภาพที่คนมักนึกถึงเวลาได้ยินคำว่า "technical debt" อย่างแท้จริง technical debt แบบทั่วไปคือสิ่งที่ชี้ได้ จัดลำดับความสำคัญได้ แล้วทยอยจ่ายคืนตามตารางเวลาที่สะดวก แต่นี่ไม่ใช่แบบนั้นเสียทีเดียว มันใกล้เคียงกับภาษีต่อเนื่องที่ถูกเก็บอัตโนมัติทุกครั้งที่ฟังก์ชันใหม่แตะข้อมูลลูกค้า ไม่ว่าผมจะนึกถึงมันในตอนนั้นหรือไม่ก็ตาม ต้นทุนนี้ไม่ปรากฏเป็นรายการในบัญชี แต่ปรากฏเป็นนิสัยที่ผมต้องฝึกไว้ในวิธีตรวจงานตัวเอง — สัญชาตญาณที่ต้องถามทุกครั้งที่มี logic ฐานข้อมูลใหม่ ว่า "สิ่งนี้เคารพเส้นแบ่งไหม" ก่อนจะถามด้วยซ้ำว่าฟีเจอร์นั้นทำงานถูกต้องหรือเปล่า

ผมไม่คิดว่าภาษีนี้เป็นเหตุผลให้เสียใจกับการตัดสินใจนี้ ผมคิดว่ามันคือราคาที่แท้จริงและตรงไปตรงมาของการแลกเปลี่ยนที่อธิบายไว้ข้างบน — infrastructure ถูกกว่า แต่วินัยแพงกว่า — และการแกล้งทำเป็นว่าราคานี้ไม่มีอยู่จะทำให้เรื่องราวทั้งหมดฟังดูเรียบร้อยกว่าความเป็นจริง ที่กำแพงนี้ยังรับน้ำหนักได้จนถึงตอนนี้ ไม่ใช่เพราะเลือกสถาปัตยกรรมที่ปลอดภัยกว่า แต่เพราะจ่ายภาษีนั้นอย่างตั้งใจทุกครั้ง และเต็มใจจะจ่ายมันต่อไปตราบใดที่โปรดักต์นี้ยังอยู่

ทำไมผมถึงคิดว่าจะไม่เลือกทางอื่น

ถ้าจะให้ความเป็นธรรมกับตัวเองเวอร์ชันที่ตัดสินใจเรื่องนี้ในเดือนกรกฎาคม ผมจะเลือกแบบเดิมอีกครั้ง และไม่ใช่แค่เพราะมันเป็นทางที่เลือกไปแล้วและต้องอยู่กับมันตอนนี้ ทางเลือกอื่น — infrastructure เฉพาะต่ออู่ — แก้ปัญหาที่ผมไม่ได้มีอยู่จริง ผมไม่ได้ขายให้ผู้ประกอบการ fleet รายใหญ่ไม่กี่รายที่ต้องการการแยกทางกายภาพแบบรับประกันได้และยินดีจ่ายเงินเพื่อสิ่งนั้น ผมกำลังพยายามสร้างของบางอย่างที่อู่เล็กอิสระจะเริ่มใช้ได้ถูก เร็ว แรงเสียดทานในการติดตั้งต่ำ แล้วตัดสินใจได้ภายในสัปดาห์เดียวว่าคุ้มเก็บไว้ใช้ต่อไหม โปรไฟล์ลูกค้าแบบนี้ไม่มีทางเกิดขึ้นในโลกที่ผมต้องตั้งและดูแลฐานข้อมูลแยกให้ทีละราย เศรษฐศาสตร์มันไม่รองรับที่สเกลนี้เลย

สิ่งที่อยากบอกตัวเองต่างออกไป ถ้าส่งข้อความย้อนกลับไปหาสัปดาห์นั้นในเดือนกรกฎาคมได้ ไม่ใช่ "เลือกสถาปัตยกรรมอื่น" แต่เป็น "รีบเข้าใจให้เร็วกว่านี้ว่าเพิ่งสมัครรับวินัยถาวร ไม่ใช่แค่การตัดสินใจครั้งเดียว" การเลือกใช้ฐานข้อมูลร่วมนั้นตัดสินใจครั้งเดียวจบ แต่การรักษาให้ฐานข้อมูลนั้นปลอดภัยสำหรับทุกอู่ที่อยู่ข้างในต้องตัดสินใจต่อเนื่องในทุกฟีเจอร์ ตราบใดที่โปรดักต์นี้ยังอยู่ ส่วนแรกผมเข้าใจทันที ส่วนที่สองต่างหากที่ต้องเรียนรู้ใหม่ซ้ำแล้วซ้ำอีก

มีอีกเรื่องหนึ่งที่อยากพูดตรงๆ เพราะเล่าเรื่องนี้แบบที่ทำให้ฟังดูเหมือนตรรกะทางธุรกิจอย่างเดียวทำให้การตัดสินใจดูชัดเจนย้อนหลังนั้นทำได้ง่าย แต่ตอนนั้นมันไม่ได้รู้สึกชัดเจนขนาดนั้น มันรู้สึกเหมือนการเดิมพันที่ทำด้วยข้อมูลไม่ครบ โดยคนที่ยังไม่รู้ว่าสุดท้ายจะมีกี่อู่สมัครเข้ามาจริง หรือเร็วแค่ไหน ผมเลือกทางที่ถูกกว่าถ้าธุรกิจโตตามที่หวังไว้ และแพงกว่าในแง่วินัยทางวิศวกรรมไม่ว่าจะโตแบบไหนก็ตาม นี่ไม่ใช่การตัดสินใจที่มีเหตุผลรองรับเรียบร้อยน่าพอใจ แต่เป็นการตัดสินใจที่สมเหตุสมผลเมื่อพิจารณาจากสิ่งที่ผมมองเห็นได้ ณ จุดที่ยืนอยู่ตอนนั้น และเป็นสิ่งที่ผมต้องคอยปกป้องกับตัวเองอย่างเงียบๆ ทุกครั้งที่ฟีเจอร์ใหม่ทำให้กำแพงนี้ดูแลรักษายากขึ้นอีกนิดหนึ่ง

สิ่งที่การตัดสินใจนี้ยังไม่ได้ตอบ

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

ก่อนเข้าสู่ช่วงนั้นของโปรเจกต์ ผมสมมติไว้ว่าฟีเจอร์จริงตัวแรกหลัง migration จะเป็นอะไรที่มุ่งตรงไปที่ผู้ใช้งานประจำวัน — ค้นหาสต็อก ติดตามงานซ่อม อะไรบางอย่างที่ช่างจะเปิดใช้ทุกวัน แต่ไม่ใช่แบบนั้นที่เกิดขึ้นจริง ฟีเจอร์สำคัญตัวแรกที่ผมสร้างหลังฐานราก multi-tenant พร้อมแล้ว ไม่ใช่ของช่างที่ทำงานซ่อมประจำเลย มันเป็น use case ที่แคบกว่ามากและแปลกกว่ามากที่ผมไม่ได้วางแผนจะให้ความสำคัญเร็วขนาดนี้ — ซึ่งกลับเผยให้เห็นบางอย่างว่าลูกค้ายุคแรกที่เรียกร้องมากที่สุดของ U-PRO เป็นใครกันแน่ และต้องการอะไรจริงๆ ซึ่งผมคาดไม่ถึงเลยจากมุมมองภายนอก

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


นี่คือตอนที่ 2 ของ U-PRO Build Journal ตอนที่ 1 เล่าเรื่อง multi-tenant database migration เอง — กลไกและความยุ่งเหยิงของการเสริมการแยกข้อมูลระดับอู่เข้าไปในระบบที่รันอยู่จริง ตอนที่ 3 จะเล่าถึงฟีเจอร์แรกที่สร้างขึ้นจริงบนฐานรากนั้น และทำไมมันถึงไม่ใช่ตัวที่คาดไว้