3 นาที

ผลกระทบของฐานข้อมูลหลายผู้เช่าต่อความปลอดภัยและประสิทธิภาพ

เรียนรู้ว่าเมื่อต้องใช้ฐานข้อมูลแบบหลายผู้เช่าจะมีผลต่อความปลอดภัยและประสิทธิภาพอย่างไร ความเสี่ยงหลัก (การแยกผู้เช่า, noisy neighbor) และมาตรการปฏิบัติได้เพื่อรักษาความปลอดภัยและความเร็วสำหรับผู้เช่าแต่ละราย

ผลกระทบของฐานข้อมูลหลายผู้เช่าต่อความปลอดภัยและประสิทธิภาพ

ความหมายของฐานข้อมูลแบบหลายผู้เช่า

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

คิดเหมือนอพาร์ตเมนต์: ทุกคนแชร์โครงสร้างและสาธารณูปโภคของตึก แต่แต่ละผู้เช่ามีห้องล็อกของตัวเอง

แบบ multi-tenant เทียบกับ single-tenant (ภาพรวม)

ในแนวทาง single-tenant ลูกค้าแต่ละรายจะได้ ทรัพยากรฐานข้อมูลที่เฉพาะเจาะจง—เช่น อินสแตนซ์ฐานข้อมูลของตัวเองหรือเซิร์ฟเวอร์ของตัวเอง การแยกเป็นเรื่องที่เข้าใจง่ายกว่า แต่โดยทั่วไปจะแพงกว่าและภาระการปฏิบัติการเพิ่มขึ้นเมื่อลูกค้าเยอะขึ้น

ด้วย multi-tenancy ผู้เช่าแชร์โครงสร้างพื้นฐาน ซึ่งมีประสิทธิภาพ—แต่หมายความว่าการออกแบบต้องตั้งใจที่จะบังคับขอบเขตอย่างชัดเจน

ทำไมทีม SaaS ถึงเลือก multi-tenancy

บริษัท SaaS มักเลือก multi-tenancy ด้วยเหตุผลเชิงปฏิบัติ:\n

  • ต้นทุนต่อผู้ใช้ต่ำกว่า (แชร์ compute, storage, ไลเซนส์ และเวลา ops)\n- การปฏิบัติการเมื่อขยายที่ง่ายกว่า เช่น มีฐานข้อมูลให้น้อยรายการให้แพต อัปเกรด และมอนิเตอร์\n- เวลาบนบอร์ดลูกค้าที่เร็วขึ้น (ไม่ต้องเตรียม stack ฐานข้อมูลใหม่ทั้งชุด)

ความคาดหวังหลัก: การออกแบบเป็นตัวกำหนดผลลัพธ์

การทำ multi-tenancy ไม่ได้หมายความว่าจะ "ปลอดภัย" หรือ "เร็ว" โดยอัตโนมัติ ผลลัพธ์ขึ้นกับการตัดสินใจ เช่น จะแยกผู้เช่าอย่างไร (สคีมา แถว หรือฐานข้อมูล), การบังคับควบคุมการเข้าถึงอย่างไร, การจัดการคีย์การเข้ารหัส, และการป้องกันไม่ให้งานของผู้เช่าหนึ่งชะลอผู้อื่น

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

รูปแบบฐานข้อมูล multi-tenant ทั่วไป

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

Database-per-tenant

แต่ละผู้เช่าได้ฐานข้อมูลของตัวเอง (มักอยู่บนเซิร์ฟเวอร์หรือคลัสเตอร์เดียวกัน)

ขอบเขตการแยก: ฐานข้อมูลเอง นี่มักเป็นเรื่องเล่าแยกผู้เช่าที่ชัดเจนที่สุดเพราะการเข้าถึงข้ามผู้เช่าต้องผ่านขอบเขตฐานข้อมูล

ข้อแลกเปลี่ยนเชิงปฏิบัติการ: หนักเมื่อต้องขยาย การอัปเกรดและมิเกรชันสคีมาอาจต้องรันหลายพันครั้ง การจัดการการเชื่อมต่อซับซ้อนขึ้น การสำรอง/กู้คืนง่ายระดับผู้เช่า แต่พื้นที่จัดเก็บและภาระการจัดการอาจเติบโตเร็ว

ความปลอดภัย & การจูน: โดยทั่วไปง่ายสุดในการรักษาความปลอดภัยและจูนเป็นรายลูกค้า เหมาะเมื่อผู้เช่ามีข้อกำหนดการปฏิบัติตามต่างกัน

Schema-per-tenant

ผู้เช่าแชร์ฐานข้อมูล แต่แต่ละผู้เช่ามีสคีมาของตัวเอง

ขอบเขตการแยก: สคีมา เป็นการแยกที่มีความหมาย แต่พึ่งพาสิทธิและเครื่องมือให้ถูกต้อง

ข้อแลกเปลี่ยนเชิงปฏิบัติการ: การอัปเกรดและมิเกรชันยังซ้ำซ้อน แต่เบากว่า database-per-tenant การสำรองซับซ้อนกว่าเพราะเครื่องมือหลายตัวถือว่าหน่วยที่ต้องสำรองเป็นฐานข้อมูลทั้งก้อน

ความปลอดภัย & การจูน: ง่ายกว่า shared tables แต่ต้องมีวินัยเรื่องสิทธิและต้องมั่นใจว่า query ไม่อ้างถึงสคีมาอื่น

Table-per-tenant

ผู้เช่าแชร์ฐานข้อมูลและสคีมา แต่แต่ละผู้เช่ามีตารางของตัวเอง (เช่น orders_tenant123)

ขอบเขตการแยก: ชุดตาราง เหมาะกับจำนวนผู้เช่าจำกัด แต่สเกลไม่ดี: เมตาดาต้าบวม สคริปต์มิเกรตยุ่งยาก และการวางแผนคิวรีอาจเสื่อมลง

ความปลอดภัย & การจูน: สิทธิอาจละเอียด แต่ความซับซ้อนเชิงปฏิบัติการสูง และง่ายที่จะผิดพลาดเมื่อต้องเพิ่มตารางหรือฟีเจอร์ใหม่

Shared-table (shared schema)

ทุกผู้เช่าแชร์ตารางเดียวกัน แยกด้วยคอลัมน์ tenant_id

ขอบเขตการแยก: เลเยอร์ query และการควบคุมการเข้าถึง (เช่น row-level security) โมเดลนี้จัดการด้านปฏิบัติการได้มีประสิทธิภาพ—แก้สคีมาเพียงชุดเดียว จัดการดัชนีเพียงชุดเดียว—แต่ต้องการการรักษาความปลอดภัยและการแยกประสิทธิภาพอย่างเข้มงวดที่สุด

ความปลอดภัย & การจูน: ยากที่สุดเพราะทุกคิวรีต้องรับรู้ผู้เช่า และปัญหา noisy neighbor มีโอกาสเกิดสูงขึ้นหากไม่มีการจำกัดทรัพยากรและการจัดดัชนีอย่างรอบคอบ

กฎที่เป็นประโยชน์: ยิ่งแชร์มาก การอัปเกรดยิ่งง่ายขึ้น—แต่ยิ่งต้องการวินัยในการควบคุมการแยกผู้เช่าและการแยกประสิทธิภาพมากขึ้น

การที่ multi-tenancy เปลี่ยนแบบจำลองความปลอดภัย

Multi-tenancy ไม่ได้หมายถึงแค่ว่า "มีหลายลูกค้าในฐานข้อมูลเดียว" แต่มันเปลี่ยน threat model: ความเสี่ยงที่ใหญ่ที่สุดเปลี่ยนจากผู้โจมตีภายนอกเป็นผู้ที่ได้รับสิทธิ์แล้วโดยบังเอิญ (หรือตั้งใจ) เห็นข้อมูลของผู้เช่าอื่น

การพิสูจน์ตัวตน vs การอนุญาต: บริบทผู้เช่าคือการตัดสินใจด้านการอนุญาต

Authentication ตอบว่า “คุณคือใคร?” Authorization ตอบว่า “คุณเข้าถึงอะไรได้บ้าง?” ในฐานข้อมูลแบบหลายผู้เช่า บริบทผู้เช่า (tenant_id, account_id, org_id) ต้องถูกบังคับใช้ในขั้นตอนการอนุญาต—ไม่ควรเป็นตัวกรองที่เลือกใช้

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

กฎหลัก: ทุกการอ่านและเขียนต้องมีขอบเขตผู้เช่า

กฎง่ายที่สุดแต่สำคัญที่สุดคือ: ทุกการอ่านและเขียนต้องถูกจำกัดให้เป็นของผู้เช่าหนึ่งเดียวเท่านั้น

นั่นรวมถึง:\n

  • SELECT (รวมถึงหน้ารายการและการส่งออก)\n- UPDATE/DELETE\n- งานแบ็กกราวด์และสคริปต์ ETL\n- เครื่องมือแอดมินและเวิร์กโฟลว์สนับสนุน

ถ้าการกำหนดขอบเขตผู้เช่าเป็นตัวเลือก มันจะถูกละเลยในที่สุด

โหมดล้มเหลวที่พบบ่อยซึ่งทำให้เกิดการเข้าถึงข้ามผู้เช่า

การรั่วไหลข้ามผู้เช่ามักมาจากข้อผิดพลาดเล็กๆ ประจำวัน:\n

  • ลืมตัวกรองผู้เช่าใน endpoint หนึ่งหรือเส้นทางโค้ดหนึ่ง\n- join ที่ตารางหนึ่งถูกกำหนดขอบเขต แต่ตารางที่ join ไม่ถูกกำหนด\n- การตอบกลับที่แคชโดยใช้คีย์เป็นผู้ใช้หรือ URL เท่านั้น ไม่รวมผู้เช่า\n- prepared statement ที่ผูก tenant_id ผิดพลาด

ทำไม "ทดสอบผ่าน" อาจยังรั่วใน production

การทดสอบมักใช้ชุดข้อมูลเล็กและสมมติฐานสะอาด Production เพิ่ม concurrency, retry, cache, ข้อมูลผสม และกรณีขอบจริงๆ ฟีเจอร์อาจผ่านเทสต์เพราะมีผู้เช่าแค่คนเดียวในฐานข้อมูลทดสอบ หรือ fixtures ไม่มี ID ทับซ้อน การออกแบบที่ปลอดภัยจะทำให้การเขียนคิวรีที่ไม่มีขอบเขตทำได้ยาก แทนที่จะพึ่งพาการรีวิวโค้ดทุกครั้ง

การควบคุมการแยกที่ป้องกันการเข้าถึงข้ามผู้เช่า

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

ตัวระบุผู้เช่าและรูปแบบการกำหนดขอบเขตเข้มงวด

ระเบียนที่เป็นของผู้เช่าทุกแถวควรมีตัวระบุผู้เช่า (เช่น tenant_id) และเลเยอร์การเข้าถึงข้อมูลของคุณควร เสมอ กำหนดขอบเขตการอ่าน/เขียนโดยใช้มัน

รูปแบบปฏิบัติได้คือ “บริบทผู้เช่าต้องมาก่อน”: แอปแกะบริบทผู้เช่าจาก subdomain, org ID, หรือ token claims, เก็บไว้ใน request context, และโค้ดเข้าถึงข้อมูลจะปฏิเสธการทำงานหากไม่มีบริบทนั้น

เกราะป้องกันที่ช่วยได้:\n

  • บังคับให้มี tenant_id ในคีย์หลัก/คีย์เฉพาะเมื่อเหมาะสม (ป้องกันการชนกันข้ามผู้เช่า)\n- เพิ่ม foreign key ที่รวม tenant_id เพื่อป้องกันการสร้างความสัมพันธ์ข้ามผู้เช่าโดยไม่ตั้งใจ

Row-level security (RLS) และการเข้าถึงตามนโยบาย

ในระบบที่รองรับ (โดยเฉพาะ PostgreSQL) row-level security สามารถย้ายการตรวจสอบผู้เช่าไว้ในฐานข้อมูลได้ นโยบายสามารถจำกัด SELECT/UPDATE/DELETE เพื่อให้เห็นเฉพาะแถวที่ตรงกับผู้เช่าในปัจจุบัน

วิธีนี้ลดการพึ่งพาว่า "นักพัฒนาทุกคนจะจำ WHERE" และช่วยป้องกันบางกรณีของการโจมตีหรือการใช้ ORM ผิดพลาด ถือ RLS เป็นล๊อกสองชั้น ไม่ใช่ชิ้นเดียว

การแยกด้วยสคีมา/ฐานข้อมูลเป็นเครื่องมือหนึ่ง

หากผู้เช่ามีความอ่อนไหวสูงหรือต้องการการปฏิบัติตามที่เข้มงวด การแยกผู้เช่าโดยสคีมา (หรือฐานข้อมูล) จะลด blast radius ได้ แต่ต้องแลกกับภาระการปฏิบัติการที่มากขึ้น

ค่าเริ่มต้นที่ปลอดภัย: ปฏิเสธโดยค่าเริ่มต้นและสิทธิน้อยที่สุด

ออกแบบสิทธิให้ค่าเริ่มต้นเป็น “ไม่มีสิทธิ”:\n

  • role ของแอปควรมีการเข้าถึงตารางเท่าที่จำเป็นเท่านั้น\n- เวิร์กโฟลว์แอดมินควรใช้บัญชีแยกและมีการบันทึกตรวจสอบ\n- หลีกเลี่ยงการเชื่อมต่อ superuser ในโค้ดแอป

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

การเข้ารหัสและการจัดการคีย์ในที่เก็บข้อมูลที่ใช้ร่วมกัน

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

การเข้ารหัสระหว่างทางและขณะพักอยู่

สำหรับข้อมูลระหว่างทาง ให้บังคับ TLS ในทุกการเชื่อมต่อ: client → API, API → database, และการเรียกบริการภายในอื่นๆ บังคับใช้ระดับฐานข้อมูลเมื่อเป็นไปได้ (เช่น ปฏิเสธการเชื่อมต่อที่ไม่ใช่ TLS) เพื่อป้องกันไม่ให้ข้อยกเว้นชั่วคราวกลายเป็นถาวร

สำหรับข้อมูลที่พักอยู่ ให้ใช้การเข้ารหัสระดับดิสก์หรือระดับสตอเรจ (managed disk encryption, TDE, backup ที่เข้ารหัส) ซึ่งปกป้องกรณีการสูญหายของสื่อ การเปิดเผย snapshot และบางกรณีของการเจาะระบบโครงสร้างพื้นฐาน—แต่จะไม่หยุดคำสั่งที่บั๊กให้คืนแถวของผู้เช่าอื่น

คีย์แชร์ vs คีย์ต่อลูกค้า

คีย์เดียวที่ใช้ร่วมกันง่ายต่อการปฏิบัติการ แต่ถ้าถูกเปิดเผยจะกระทบทุกผู้เช่า คีย์ต่อลูกค้าลด blast radius และตอบโจทย์ลูกค้าองค์กรบางราย แต่เพิ่มความซับซ้อนด้านวงจรชีวิตคีย์ การหมุนคีย์ และเวิร์กโฟลว์การสนับสนุน

ทางสายกลางที่ใช้งานได้จริงคือ envelope encryption: คีย์มาสเตอร์เข้ารหัสคีย์ข้อมูลของแต่ละผู้เช่า ช่วยให้การหมุนคีย์จัดการได้ง่ายขึ้น

การจัดการความลับสำหรับ credential ฐานข้อมูล

เก็บ credential ของฐานข้อมูลใน secrets manager ไม่ใช่ใน environment variables ที่อยู่ยาว ๆ ชอบ credential อายุสั้นหรือการหมุนอัตโนมัติ และกำหนดสิทธิการเข้าถึงตามบทบาทของบริการเพื่อให้การถูกโจมตีของคอมโพเนนต์หนึ่งไม่เข้าถึงทุกฐานข้อมูลโดยอัตโนมัติ

การจัดการโทเค็นและเซสชัน: ป้องกันการปลอมบริบทผู้เช่า

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

การตรวจสอบ บันทึก และความพร้อมเมื่อเกิดเหตุ

ทดลองสร้าง SaaS แบบหลายผู้เช่า
สร้างแอป multi-tenant ทำงานได้จากแชท แล้ววนทดสอบอย่างปลอดภัยก่อนเอาไปใช้งานจริง

Multi-tenancy เปลี่ยนความหมายของ “ปกติ” คุณไม่ได้มอนิเตอร์แค่ฐานข้อมูลเพียงชุดเดียว แต่กำลังมอนิเตอร์หลายผู้เช่าที่แชร์ระบบเดียวกัน ความผิดพลาดหนึ่งครั้งอาจกลายเป็นการเปิดเผยข้ามผู้เช่าได้ การมีการตรวจสอบและมอนิเตอร์ที่ดีช่วยลดความน่าจะเป็นและขนาดความเสียหาย

บันทึกตรวจสอบ: บันทึกเรื่องราวทั้งหมด

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

  • ใคร: ตัวตนผู้ใช้/บริการ วิธีการพิสูจน์ตัวตน บทบาท ที่มา IP/อุปกรณ์\n- อะไร: ปฏิบัติการ (SELECT/UPDATE/DELETE), วัตถุที่ได้รับผลกระทบ, ประเภทคิวรี (ไม่จำเป็นต้องเป็น SQL ทั้งหมด), ก่อน/หลังเมื่อเป็นการเปลี่ยนแปลงที่มีสิทธิ์สูง\n- เมื่อไหร่: timestamp พร้อมโซนเวลา และ request/trace ID เพื่อเชื่อมโยง\n- ผู้เช่า: tenant ID เป็นฟิลด์ชั้นหนึ่ง (อย่าอนุมานทีหลัง)

บันทึกการกระทำระดับแอดมินด้วย: การสร้างผู้เช่า เปลี่ยนนโยบายการแยก การแก้ RLS การหมุนคีย์ และการเปลี่ยน connection string

การแจ้งเตือนสำหรับความผิดปกติข้ามผู้เช่าและสิทธิ

มอนิเตอร์ควรตรวจจับรูปแบบที่ผิดปกติ เช่น:\n

  • คิวรีที่คืนแถวของหลาย tenant ID หรือกระโดดขึ้นของข้อความปฏิเสธ "tenant mismatch"\n- การเข้าถึงจาก service account ไปยังผู้เช่าที่ไม่เคยเข้าถึง\n- การเปลี่ยนบทบาท/สิทธิอย่างรวดเร็ว ผู้ดูแลใหม่ การปิดนโยบายความปลอดภัย หรือความพยายามเลี่ยง RLS

เชื่อมโยงการแจ้งเตือนกับ runbook ที่ทำได้จริง: ตรวจอะไร ขั้นตอนกักกัน และใครต้องได้รับการแจ้ง

การควบคุมแอดมินและขั้นตอน break-glass

จัดการสิทธิพิเศษเหมือนการเปลี่ยนแปลง production ใช้ role ที่ให้สิทธิน้อยที่สุด credential อายุสั้น และการอนุมัติสำหรับการปฏิบัติการอ่อนไหว (มิเกรชัน ส่งออกข้อมูล แก้ไขนโยบาย) สำหรับเหตุฉุกเฉิน เก็บบัญชี break-glass ที่ควบคุมอย่างเข้มงวด: credential แยก บังคับตั๋ว/การอนุมัติ การเข้าถึงจำกัดเวลา และบันทึกเพิ่มเติม

การเก็บรักษาและการเข้าถึงบันทึกแบบแยกผู้เช่า

ตั้ง retention ตามความต้องการการปฏิบัติตามและการสืบสวน แต่จำกัดการเข้าถึงให้พนักงานซัพพอร์ตดูได้เฉพาะบันทึกของผู้เช่าตน เมื่อมีคำขอจากลูกค้าให้ส่งรายงานการตรวจสอบ ให้ส่งรายงานที่กรองตามผู้เช่าแทนการส่ง raw log ร่วมกัน

พื้นฐานด้านประสิทธิภาพและปัญหา noisy neighbor

Multi-tenancy เพิ่มประสิทธิภาพโดยให้ลูกค้าหลายรายแชร์โครงสร้างพื้นฐานฐานข้อมูล ข้อแลกคือประสิทธิภาพก็กลายเป็นประสบการณ์ที่แชร์เช่นกัน: สิ่งที่ผู้เช่ารายนึงทำอาจส่งผลต่อผู้อื่น แม้ว่าข้อมูลจะแยกอย่างสมบูรณ์

ปัญหา “noisy neighbor” แบบเข้าใจง่าย

“noisy neighbor” คือผู้เช่าที่กิจกรรมหนักหรือกระชั้นจนใช้ทรัพยากรส่วนกลางมากเกินควร ฐานข้อมูลไม่ได้ "เสีย" เพียงแต่ยุ่งกับงานของผู้เช่านั้น ทำให้ผู้อื่นต้องรอ

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

สิ่งที่แชร์จริงๆ คืออะไร?

แม้แต่เมื่อแต่ละผู้เช่ามีแถวหรือสคีมาแยก หลายส่วนที่สำคัญต่อประสิทธิภาพยังคงถูกแชร์:\n

  • CPU: การรันคิวรี การเรียง การ join การเข้ารหัส/ถอดรหัส งานพื้นหลัง\n- หน่วยความจำ: buffer/cache, memory สำหรับคิวรี, คิวภายใน\n- ดิสก์ / I/O: อ่านไฟล์ข้อมูล เขียนล็อก flush checkpoints compaction\n- การเชื่อมต่อ: ขีดจำกัดการเชื่อมต่อและ thread pool ของ DB\n- แคช: plan cache, buffer cache และบางครั้งแคชฝั่งแอปที่ feed DB

เมื่อพูลเหล่านี้อิ่ม ความหน่วงจะเพิ่มสำหรับทุกคน

ทำไมงานแบบ burst จึงกระทบผู้อื่น

งาน SaaS มักมาถึงเป็นระลอก: การนำเข้าข้อมูล การสรุปรายเดือน แคมเปญการตลาด หรือ cron ที่รันตรงต้นชั่วโมง

การระเบิดทำให้เกิดคอขวดภายในฐานข้อมูล:\n

  • ผู้เช่าหนึ่งเรียกคิวรีหนักหลายรายการพร้อมกัน ดัน CPU ถึง 100%\n- เขียนขนาดใหญ่ก่อให้เกิด I/O เพิ่ม (เขียนล็อก บำรุงดัชนี) ชะลอการอ่านของผู้อื่น\n- กระโดดเชื่อมต่อเต็มพูล ทำให้ผู้เช่าคนอื่นเชื่อมต่อไม่ได้เร็วๆ นี้

แม้การระเบิดจะสั้น ความหน่วงจะเกิดเป็นลูกโซ่ขณะคิวถูกระบาย

ผู้ใช้สังเกตเห็นอะไรบ้าง

จากมุมลูกค้า ปัญหา noisy neighbor รู้สึกไม่แน่นอนและไม่เป็นธรรม อาการทั่วไปได้แก่:\n

  • Timeout ขณะล็อกอิน ค้นหา ชำระเงิน หรือสร้างรายงาน\n- หน้าโหลดช้า โดยเฉพาะหน้ารายการและแดชบอร์ด\n- ความเร็วไม่คงที่ (เร็วตอน 10:05 ช้า 10:10 แล้วเร็วอีก 10:20)\n- งาน background ล่าช้า (การส่งออกใช้เวลานาน webhook ดีเลย์)

อาการเหล่านี้คือสัญญาณเริ่มต้นว่าคุณต้องการเทคนิคการแยกทรัพยากร ไม่ใช่แค่ "เพิ่มฮาร์ดแวร์"

เทคนิคการแยกทรัพยากรและการจำกัด

ทดสอบการแยกผู้เช่าเร็วๆ นี้
สร้างเทสต์เชิงลบที่พยายามอ่านและเขียนข้ามผู้เช่า เพื่อปิดกั้นการเปลี่ยนแปลงที่เสี่ยง

Multi-tenancy ทำงานได้ดีที่สุดเมื่อผู้เช่าหนึ่งไม่สามารถยืมทรัพยากรมากกว่าที่สมควรได้ การแยกทรัพยากรคือชุดเกราะที่ป้องกันไม่ให้ผู้เช่าหนักทำให้คนอื่นช้า

ขีดจำกัด connection pool และโควต้าแยกผู้เช่า

โหมดล้มเหลวทั่วไปคือการเชื่อมต่อไม่จำกัด: การระเบิดการจราจรของผู้เช่าหนึ่งเปิด session เป็นร้อยๆ แล้วทำให้ DB ขาดแคลน

ตั้งขีดจำกัดเข้มงวดสองจุด:\n

  • ที่ pool ของแอป: จำกัด max connections ต่อ instance ของบริการ และสำรองขั้นต่ำสำหรับงานพื้นหลัง\n- ต่อผู้เช่า: บังคับโควต้าเช่น "N คำขอพร้อมกัน" หรือ "M session DB พร้อมกัน" แผนของลูกค้าจะแม็ปไปยังโควต้านี้

แม้ DB คุณจะไม่รองรับการจำกัด "connections ต่อผู้เช่า" โดยตรง คุณสามารถใกล้เคียงได้ด้วยการส่งผู้เช่าแต่ละคนผ่าน pool แยกหรือ partition ของ pool

การจำกัดอัตราและการจัดรูปแบบงาน (app + DB)

Rate limiting คือเรื่องความยุติธรรมเมื่อเวลาผ่านไป นำไปใช้ใกล้ขอบ (API gateway/แอป) และเมื่อรองรับได้ ภายใน DB (resource groups / workload management)

ตัวอย่าง:\n

  • โทเค็นบัคเก็ตต่อผู้เช่า สำหรับ endpoint ที่หนัก (เช่น export, search)\n- ชั้นลำดับความสำคัญ ให้คำขอแบบ interactive ชนะเหนือ batch\n- การจัดคิว เพื่อลดการระเบิดแทนส่งตรงเข้าฐานข้อมูล

timeout ของคิวรี ขีดจำกัดคำสั่ง และ circuit breaker

ปกป้อง DB จากคิวรี runaway:\n

  • timeout ของคิวรี/คำสั่ง เพื่อหยุดสแกนยาวๆ\n- จำกัดแถว/ไบต์สูงสุด สำหรับ endpoint ที่อาจส่งผลลัพธ์ใหญ่เกินคาด\n- circuit breaker ที่บล็อกฟีเจอร์ของผู้เช่าชั่วคราวเมื่ออัตราความผิดพลาดหรือความหน่วงเกินเกณฑ์

การควบคุมเหล่านี้ควรล้มเหลวอย่างสวยงาม: ส่งข้อผิดพลาดชัดเจนและแนะนำ retry/backoff

Read replica และแคชช่วยลดการแย่งชิง

ย้ายงานอ่านหนักออกจาก primary:\n

  • read replica สำหรับแดชบอร์ด รายงาน และคิวรีแนว analytics\n- แคช (คีย์แยกตามผู้เช่า, TTL สั้น) สำหรับการค้นหาซ้ำและข้อมูล configuration

เป้าหมายไม่ใช่แค่ความเร็ว แต่ลดแรงกดดัน lock และ CPU เพื่อให้ noisy tenant มีช่องทางกระทบน้อยลง

ตัวเลือกการออกแบบข้อมูลที่มีผลต่อความเร็ว

ปัญหาด้านประสิทธิภาพของ multi-tenant มักดูเหมือนว่า "ฐานข้อมูลช้า" แต่สาเหตุรากมักเป็นโมเดลข้อมูล: การกำหนดคีย์ การกรอง การจัดดัชนี และการจัดวางเชิงกายภาพ การออกแบบดีจะทำให้คิวรีที่มีขอบเขตผู้เช่าเร็วโดยธรรมชาติ การออกแบบไม่ดีจะบังคับให้ DB ทำงานหนัก

การทำดัชนีสำหรับคิวรีที่กำหนดขอบเขตผู้เช่า

คิวรี SaaS ส่วนใหญ่ควรรวมตัวระบุผู้เช่า โมเดลนั้นอย่างชัดเจน (เช่น tenant_id) และออกแบบดัชนีให้ขึ้นต้นด้วยมัน ในทางปฏิบัติ ดัชนีคอมโพสิตแบบ (tenant_id, created_at) หรือ (tenant_id, status) มีประโยชน์กว่าการทำดัชนี created_at หรือ status เดี่ยวๆ

นี่รวมถึงความเป็นเอกลักษณ์ด้วย: ถ้า email เป็นเอกลักษณ์เฉพาะต่อผู้เช่า ให้บังคับด้วย (tenant_id, email) แทน constraint อีเมลระดับโลก

หลีกเลี่ยง full-table scan (ลืมตัวกรองผู้เช่า)

รูปแบบคิวรีช้าที่พบบ่อยคือการสแกนข้ามตารางขนาดใหญ่เพราะลืม filter ผู้เช่า

ทำให้หนทางปลอดภัยเป็นหนทางง่าย:\n

  • บังคับตัวกรองผู้เช่าในเลเยอร์คิวรี (ORM scopes, repository methods)\n- ใช้การป้องกันใน DB เมื่อมี (เช่น view เริ่มต้นหรือ policy) เพื่อให้การเข้าถึงที่ไม่มีขอบเขตล้มเหลวเร็ว

การแบ่งพาร์ทิชันและการชาร์ด: ตามผู้เช่าหรือเวลา

พาร์ทิชันลดข้อมูลที่แต่ละคิวรีต้องพิจารณา พาร์ทิชันตามผู้เช่าเมื่อผู้เช่าบางรายใหญ่และไม่สมดุล พาร์ทิชันตามเวลาเมื่อการเข้าถึงมักเป็นข้อมูลล่าสุด (events, logs, invoices) โดยมักมี tenant_id เป็นคอลัมน์นำในดัชนีภายในแต่ละพาร์ทิชัน

พิจารณาการชาร์ดเมื่อฐานข้อมูลเดียวรับภาระไม่ไหว หรือเมื่อผู้เช่ารายหนึ่งขู่ว่าจะกระทบผู้อื่น

การจัดการผู้เช่าที่เป็น hot

“Hot tenants” ปรากฏด้วยปริมาณอ่าน/เขียนสูง การแย่งล็อก หรือดัชนีใหญ่

จับตามองโดยติดตามเวลาคิวรีต่อผู้เช่า แถวที่อ่าน และอัตราการเขียน เมื่อผู้เช่ารายหนึ่งครอบงำ ให้แยกพวกเขาออก: ย้ายไป shard/database แยก แยกตารางขนาดใหญ่ตามผู้เช่า หรือเพิ่ม cache เฉพาะและจำกัดอัตราเพื่อให้ผู้อื่นยังคงเร็ว

แนวปฏิบัติการปฏิบัติการที่ปกป้องทั้งความปลอดภัยและประสิทธิภาพ

multi-tenancy มักล้มเหลวไม่ใช่เพราะฐานข้อมูล "ทำไม่ได้" แต่เพราะการปฏิบัติการประจำวันที่อนุญาตความไม่สอดคล้องเล็กๆ ให้สะสมจนกลายเป็นช่องโหว่หรือการเสื่อมของประสิทธิภาพ เป้าหมายคือทำให้แนวทางที่ปลอดภัยเป็นค่าเริ่มต้นสำหรับทุกการเปลี่ยนแปลง งาน และการปล่อย

ทำให้คีย์ผู้เช่าเป็นมาตรฐาน (และบังคับใช้ทุกที่)

เลือกตัวระบุผู้เช่า canonical เดียว (เช่น tenant_id) และใช้สอดคล้องในตาราง ดัชนี บันทึก และ API ความสอดคล้องลดทั้งข้อผิดพลาดด้านความปลอดภัย (คิวรีผู้เช่าผิด) และความประหลาดใจด้านประสิทธิภาพ (พลาดดัชนีคอมโพสิต)

การป้องกันปฏิบัติได้:\n

  • บังคับให้มี tenant_id ในเส้นทางการเข้าถึงหลักทั้งหมด (คิวรี, repository, ORM scopes)\n- เพิ่มดัชนีคอมโพสิตที่ขึ้นต้นด้วย tenant_id สำหรับการค้นหาทั่วไป\n- ใช้ constraint ใน DB เมื่อเป็นไปได้ (foreign key ที่รวม tenant_id หรือ check constraint) เพื่อจับการเขียนผิดเร็ว

ป้องกันการสลับผู้เช่าในงานแบ็กกราวด์

worker แบบ async เป็นแหล่งทั่วไปของเหตุการณ์ข้ามผู้เช่าเพราะมันรัน "นอกบริบท" ของคำขอที่ตั้งบริบทผู้เช่า

รูปแบบปฏิบัติการที่ช่วยได้:\n

  • ส่ง tenant_id อย่างชัดเจนใน payload ของงานทุกงาน; อย่าอาศัยบริบทรอบตัว\n- รวมคีย์ผู้เช่าใน idempotency key และ cache key\n- บันทึก tenant_id ตอนเริ่ม/จบงาน และทุกการ retry เพื่อการสืบสวนที่รวดเร็ว

ทำให้มิเกรชันปลอดภัยสำหรับผู้เช่าเป็นการออกแบบ

มิเกรชันสคีมาและข้อมูลควรปล่อยได้โดยไม่ต้อง rollout ที่สมบูรณ์แบบและพร้อมกันทั้งหมด

ใช้เปลี่ยนแปลงแบบ rolling:\n

  • กลยุทธ์ขยาย/หด (เพิ่มคอลัมน์/ดัชนีใหม่, dual-write/read, แล้วลบเส้นทางเก่า)\n- หลีกเลี่ยงการดำเนินการบล็อกนานๆ; ทำ backfill เป็นชุดตามผู้เช่าเพื่อตัดโหลด\n- ตรวจสอบให้แน่ใจว่า backfill ทุกงานมีการกำหนดขอบเขตผู้เช่าและจำกัดอัตราเพื่อป้องกัน noisy-neighbor ที่เกิดจากตัวเอง

ทดสอบกรณีล้มเหลวการแยก ไม่ใช่แค่เส้นทางสำเร็จ

เพิ่มเทสต์เชิงลบอัตโนมัติที่พยายามเข้าถึงข้อมูลผู้เช่าอื่นทั้งอ่านและเขียน จัดให้เป็นเกณฑ์บล็อกการปล่อย

ตัวอย่าง:\n

  • พยายามดึงระเบียนที่รู้จักของ Tenant A ขณะพิสูจน์ตัวตนเป็น Tenant B\n- รันเทสต์งานแบ็กกราวด์โดยส่ง tenant_id ผิดและยืนยันว่าล้มเหลวอย่างชัดเจน\n- เทสต์ regression สำหรับ helper คิวรีทุกตัวเพื่อยืนยันว่าการกำหนดขอบเขตผู้เช่าถูกนำไปใช้

การสำรอง กู้คืน และการดำเนินการข้อมูลระดับผู้เช่า

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

การสำรองง่ายที่จะอธิบาย ("คัดลอกฐานข้อมูล") แต่ยากที่จะทำอย่างปลอดภัยใน multi-tenant เมื่อหลายลูกค้าแชร์ตาราง คุณต้องมีแผนในการกู้คืน หนึ่ง ผู้เช่าโดยไม่เปิดเผยหรือเขียนทับคนอื่น

กลยุทธ์สำรอง/กู้คืน: หนึ่งผู้เช่า vs ทุกคน

การสำรองฐานข้อมูลเต็มยังคงเป็นพื้นฐานสำหรับการกู้คืนภัยพิบัติ แต่มันไม่พอสำหรับกรณีสนับสนุนรายวัน วิธีทั่วไปรวม:\n

  • สำรองเต็ม + point-in-time recovery สำหรับเหตุการณ์ที่กระทบทุกคน (ข้อมูลเสียหาย, ภูมิภาคล่ม)\n- การส่งออกแบบสโคปผู้เช่า (logical dumps กรองด้วย tenant_id) เพื่อกู้คืนข้อมูลของผู้เช่าเดียว\n- เก็บแยกตามผู้เช่า (เมื่อเป็นไปได้) เพื่อให้การกู้คืนเป็นขอบเขตของผู้เช่าโดยธรรมชาติ

ถ้าคุณพึ่งพาการส่งออกเชิงตรรกะ ให้ปฏิบัติงานส่งออกเหมือนโค้ด production: ต้องบังคับการแยกผู้เช่า (เช่น ผ่าน RLS) แทนการพึ่งพา WHERE clause ที่เขียนครั้งเดียวและลืมไป

การส่งออก/ลบระดับผู้เช่า (คำขอความเป็นส่วนตัว)

คำขอความเป็นส่วนตัว (ส่งออก ลบ) เป็นการปฏิบัติการระดับผู้เช่าที่เกี่ยวข้องทั้งความปลอดภัยและประสิทธิภาพ สร้างเวิร์กโฟลว์ที่ทำซ้ำได้และบันทึกเพื่อตรวจสอบสำหรับ:\n

  • ส่งออกข้อมูลผู้เช่าในสแนปช็อตที่สอดคล้อง\n- ลบข้อมูลผู้เช่าโดยไม่ทิ้งแถวร้าง\n- พิสูจน์การเสร็จสิ้นผ่านบันทึกและ checksum

ป้องกันการกู้คืนผิดผู้เช่าโดยไม่ตั้งใจ

ความเสี่ยงใหญ่ที่สุดมักไม่ใช่แฮกเกอร์ แต่เป็นผู้ปฏิบัติงานที่รีบร้อน ลดความผิดพลาดของมนุษย์ด้วยเกราะ:\n

  • ต้องมี ตัวระบุผู้เช่า และ การยืนยันทุติยภูมิ (ชื่อผู้เช่า, หมายเลขเรียกเก็บเงิน)\n- ตรวจสอบจำนวนแถวและการกระจาย tenant_id ก่อน นำเข้า\n- กู้คืนเข้าไปยังสภาพแวดล้อมกักกันก่อน แล้วค่อยยกระดับ

ฝึกซ้อม DR และยืนยันขอบเขตหลังเหตุ

หลังการฝึกซ้อมการกู้คืน อย่าหยุดที่ "แอปขึ้น" ให้รันเช็กอัตโนมัติที่ยืนยันการแยกผู้เช่า: คิวรีตัวอย่างข้ามผู้เช่า การทบทวนบันทึก และการตรวจสอบจุดว่าคีย์การเข้ารหัสและบทบาทการเข้าถึงยังคงแยกกันถูกต้อง

เมื่อ multi-tenancy ไม่เหมาะอีกต่อไป

Multi-tenancy มักเป็นดีฟอลต์ที่ดีที่สุดสำหรับ SaaS แต่มันไม่ใช่การตัดสินใจตลอดกาล เมื่อผลิตภัณฑ์และกลุ่มลูกค้าเติบโต แนวทาง "ที่เก็บข้อมูลแชร์หนึ่งชุด" อาจเริ่มสร้างความเสี่ยงทางธุรกิจหรือชะลอการปล่อย

สัญญาณที่ควรเพิ่มการแยก

พิจารณาย้ายจากแชร์ทั้งหมดไปสู่การแยกมากขึ้นเมื่อเห็นอย่างสม่ำเสมอ:\n

  • ผลกระทบจากการเติบโต: แอคเคาท์บางรายขับทราฟฟิก พื้นที่เก็บ หรืองานพื้นหลังมากผิดสัดส่วน และการปรับจูนให้ทุกคนทำได้ยากขึ้น\n- ข้อกำหนดการปฏิบัติตาม/สัญญา: ลูกค้าต้องการสภาพแวดล้อมเฉพาะ ที่ตั้งข้อมูล หรือการควบคุมคีย์แยกตามลูกค้า\n- ผู้เช่าหนักที่มีแพตเทิร์นเฉพาะ: import ขนาดใหญ่ รายงานหนาแน่น หรือการรวมระบบพิเศษสร้างการปะทะซ้ำๆ ที่ throttle/การจูนแก้ไม่ได้

โมเดลผสมที่ช่วยให้ต้นทุนสมเหตุสมผล

คุณไม่จำเป็นต้องเลือกระหว่าง "แชร์ทั้งหมด" กับ "เฉพาะทั้งหมด" ทางเลือก hybrid ทั่วไปได้แก่:\n

  • แยกกลุ่มผู้เช่าชั้นยอด ไปยังฐานข้อมูล/คลัสเตอร์แยกในขณะที่ปล่อยคนอื่นบนโครงสร้างร่วม\n- แผนเป็นชั้น: แชร์โดยค่าเริ่มต้น แยกให้ลูกค้าองค์กร\n- แยกตามฟังก์ชัน: เก็บงานธุรกรรมร่วม แต่ย้าย analytics/reporting ของผู้เช่าหนักไปยังสโตร์แยก

ต้นทุนและความซับซ้อนที่ต้องอธิบายให้ผู้มีส่วนได้ส่วนเสีย

การเพิ่มการแยกมักหมายถึง ค่าโครงสร้างพื้นฐานสูงขึ้น, ภาระการปฏิบัติการมากขึ้น (มิเกรชัน มอนิเตอร์ on-call) และ การประสานการปล่อยมากขึ้น แต่แลกมาด้วยข้อเสนอการรับประกันประสิทธิภาพชัดเจนและการสนทนาด้าน compliance ที่ง่ายขึ้น

ขั้นตอนถัดไป

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

ถ้าต้องการต้นแบบ SaaS อย่างรวดเร็วและทดสอบสมมติฐาน multi-tenant ตั้งแต่ต้น (การกำหนดขอบเขตผู้เช่า, สคีมาเป็นมิตรกับ RLS, การจำกัด, และเวิร์กโฟลว์การปฏิบัติการ) แพลตฟอร์ม vibe-coding อย่าง Koder.ai สามารถช่วยให้คุณปั่นแอป React + Go + PostgreSQL ทำงานได้จากแชท ทดลองในโหมดวางแผน และปรับใช้ด้วย snapshot และ rollback—จากนั้นส่งออกซอร์สโค้ดเมื่อพร้อมจะเสริมความแข็งแรงให้สถาปัตยกรรมเพื่อ production.

คำถามที่พบบ่อย

ฐานข้อมูลหลายผู้เช่า คืออะไร อธิบายแบบง่ายๆ?

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

ทำไมทีม SaaS ถึงเลือกใช้ multi-tenancy?

ทีม SaaS มักเลือก multi-tenancy เพราะ:

  • ต้นทุนต่อผู้ใช้ต่ำกว่า (แชร์ compute / storage / ไลเซนส์ / เวลาดูแล)
  • การปฏิบัติการที่ง่ายขึ้นเมื่อต้องขยาย (มีฐานข้อมูลให้น้อยรายการให้แพตและอัปเกรด)
  • ลดเวลาบนบอร์ดลูกค้า (ไม่ต้องเตรียม stack ฐานข้อมูลใหม่ทั้งชุด)

ข้อแลกเปลี่ยนคือคุณต้องตั้งใจสร้างกรอบการแยกและการป้องกันด้านประสิทธิภาพ

มีรูปแบบฐานข้อมูล multi-tenant อะไรบ้าง?

รูปแบบทั่วไปของ multi-tenant (เรียงจากการแยกมากสุดไปหาน้อยสุด):

  • Database-per-tenant: ขอบเขตแยกชัดที่สุด แต่ปฏิบัติการหนัก
  • Schema-per-tenant: แยกได้ดี แต่ยังต้องทำมิเกรตซ้ำๆ
  • Table-per-tenant: อาจใช้ได้ช่วงสั้นๆ แต่เมื่อขยายมักไม่เหมาะ
  • Shared-table (มีคอลัมน์ tenant_id): ปฏิบัติการง่ายสุด แต่ยากที่สุดในการรักษาความปลอดภัยและจูนประสิทธิภาพ

การเลือกโมเดลคือการกำหนดขอบเขตการแยกผู้เช่าและภาระการดำเนินงาน

การใช้ multi-tenancy เปลี่ยน threat model อย่างไร?

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

โดยทั่วไปอะไรเป็นสาเหตุให้ข้อมูลรั่วข้ามผู้เช่า?

สาเหตุที่พบบ่อยของการรั่วไหลข้ามผู้เช่า ได้แก่:

  • ลืมตัวกรองผู้เช่าในโค้ดทางหนึ่งทางใด
  • join ที่ตารางหนึ่งถูกกำหนดขอบเขต แต่อีกตารางไม่ถูกกำหนด
  • แคชที่ใช้คีย์เป็น URL หรือผู้ใช้ แต่ไม่รวม tenant
  • prepared statement ผูกค่า tenant_id ผิดพลาด
  • งาน background ที่สูญเสียบริบทผู้เช่า

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

ควรใช้ RLS เมื่อไหร่ และมันป้องกันอะไรได้บ้าง?

Row-level security (RLS) ย้ายการตรวจสอบผู้เช่าเข้าไปไว้ในฐานข้อมูลผ่านนโยบายที่จำกัด SELECT/UPDATE/DELETE ให้เห็นเฉพาะแถวที่ตรงกับผู้เช่าในปัจจุบัน มันลดการพึ่งพาว่า "ทุกคนจำ WHERE ได้" แต่ควรใช้คู่กับการกำหนดขอบเขตที่ชัดเจนในแอป, สิทธิน้อยที่สุด (least privilege) และการทดสอบที่แข็งแรง ให้ถือว่า RLS เป็นกุญแจล็อกเพิ่ม ไม่ใช่ล็อกเดียว

การควบคุมแบบไหนที่สำคัญที่สุดเพื่อป้องกันการเข้าถึงข้ามผู้เช่า?

ชุดการควบคุมพื้นฐานที่ใช้งานได้จริง เช่น:

  • ใช้ tenant_id ที่เป็น canonical ในตารางที่เป็นของผู้เช่า
  • ความเป็นเอกลักษณ์และ foreign key แบบคอมโพสิตที่รวม tenant_id
  • สิทธิพื้นฐานเป็นปฏิเสธโดยค่าเริ่มต้น และใช้ role ที่ให้สิทธิน้อยที่สุด
  • การเข้าถึง admin ต้องแยกและมีการตรวจสอบ (หลีกเลี่ยง superuser ในโค้ดแอป)
  • เทสต์เชิงลบที่พยายามอ่าน/เขียนข้ามผู้เช่า

เป้าหมายคือทำให้ความผิดพลาดเป็นอันตรายน้อยลง

การเข้ารหัสและการจัดการคีย์ทำงานอย่างไรในที่เก็บข้อมูลที่ใช้ร่วมกัน?

การเข้ารหัสช่วยได้ แต่ครอบคลุมความเสี่ยงที่ต่างกัน:

  • ระหว่างทาง (TLS): ปกป้องข้อมูลระหว่างบริการ
  • ที่พักอยู่ (at rest): ปกป้อง snapshot/ดิสก์/backup แต่ไม่หยุดคำสั่งที่บั๊กให้คืนแถวของผู้เช่าอื่น
  • คีย์ต่อผู้เช่า ลด blast radius แต่เพิ่มความซับซ้อนการบริหารคีย์

อย่ารับค่า tenant_id ดิบจากไคลเอนต์เป็นความจริง จงผูกบริบทผู้เช่ากับโทเค็นที่เซ็นแล้วและตรวจสอบฝั่งเซิร์ฟเวอร์ทุกคำขอ

ปัญหา noisy neighbor คืออะไร และจะแก้ไขอย่างไร?

Noisy neighbor เกิดเมื่อผู้เช่ารายหนึ่งใช้ทรัพยากรร่วมมากเกินควร (CPU, memory, I/O, การเชื่อมต่อ) ทำให้ผู้อื่นช้าลง วิธีบรรเทาได้แก่:

  • จำกัด connection pool อย่างเข้มงวด (และถ้าเป็นไปได้ จำกัดแยกต่อผู้เช่า)
  • จำกัดอัตรา (rate limiting) และจัดลำดับงานสำหรับ endpoint ที่หนัก
  • ตั้ง timeout สำหรับคำสั่ง จํานวนแถว/ไบต์สูงสุด และ circuit breaker
  • ใช้ read replica และ cache ที่มีคีย์แยกตามผู้เช่า

เป้าหมายคือความเป็นธรรม มากกว่าการเพิ่ม throughput เพียวๆ

เมื่อไรควรย้ายออกจาก multi-tenancy เต็มรูป และมีตัวเลือกผสมอะไรบ้าง?

ควรเพิ่มการแยกเมื่อคุณเห็นสัญญาณเช่น:

  • ลูกค้าจำกัดบางรายกินทรัพยากรมากจน tuning ให้ทุกคนยากขึ้น
  • ความต้องการด้าน compliance ที่ต้องการสภาพแวดล้อมเฉพาะหรือการควบคุมคีย์แยกตามลูกค้า
  • งานของลูกค้าบางราย (import ขนาดใหญ่ รายงานหนาแน่น) ทำให้เกิดการปะทะซ้ำๆ ที่ throttle/การจูนแก้ไม่ได้

ทางเลือกแบบ hybrid ที่ใช้กันทั่วไป เช่น แยกกลุ่มลูกค้าใหญ่ไปยังฐานข้อมูล/คลัสเตอร์แยก เกณฑ์แผนบริการเป็นชั้น (shared สำหรับส่วนใหญ่, dedicated สำหรับลูกค้าองค์กร) หรือแยกงาน analytics ไปยังที่เก็บข้อมูลแยก

Related posts