ปัญหา
service ส่วนใหญ่เริ่มจาก deployment เดียว: load balancer หนึ่งตัว กลุ่ม instance ที่หน้าตาเหมือนกัน และฐานข้อมูลหนึ่งตัว ลูกค้าทุกรายวิ่งผ่านทั้งหมดนี้ การกระจาย deployment นั้นไปหลาย availability zone ช่วยได้ตอนเสีย data centre ไปทั้งศูนย์ แต่ช่วยอะไรไม่ได้เลยกับปัญหาที่เริ่มจากตัว software เอง เพราะทุกสำเนารันโค้ดเดียวกันและเจอ traffic แบบเดียวกัน:
- deploy พลาด หรือแก้ configuration ผิด ก็ไปถึงลูกค้าทุกรายในเวลาเดียวกัน
- poison request คือ request ที่เนื้อหาของมันทำให้ระบบ crash มันจะล้ม instance ที่รับมันไป แล้ว client หรือ load balancer ก็ retry ไปที่ instance ถัดไป ทำให้ตัวนั้น crash ตามไปอีก จนไม่เหลือสักตัว
- ลูกค้ารายเดียวที่ใช้หนักหรือทำตัวผิดปกติ (script ที่วิ่งไม่หยุด, traffic ที่พุ่งขึ้นกะทันหัน หรือ noisy neighbour) ก็ใช้ทรัพยากรของฐานข้อมูลหรือ queue ที่แชร์กันจนหมด ทำให้ทุกคนเดือดร้อนไปด้วยได้
- operator ทำพลาด หรือข้อมูลเสีย ก็โดนสำเนาเดียวที่มีอยู่
พังด้วยเรื่องอะไรก็ตาม blast radius ก็คือ 100% ของลูกค้า แล้วยิ่งโตก็ยิ่งแย่ เพราะทางเดียวที่จะขยาย deployment เดียวคือทำให้มันใหญ่ขึ้น และระบบที่มีแต่ใหญ่ขึ้นเรื่อย ๆ จะไปชนขีดจำกัดของตัวเอง (ฐานข้อมูลตัวใหญ่ที่สุดที่ซื้อได้, quota ของ account หรือ lock ที่กลายเป็นจุด hot ตอน load สูงขึ้นสิบเท่า) เป็นครั้งแรกบน production เพราะไม่มีใครมีเงินพอจะทำ test environment ขนาดเท่ากัน
ทำงานยังไง
แทนที่จะมี deployment ใหญ่ชุดเดียว ให้รัน deployment เล็ก ๆ ที่เหมือนกันหลายชุด เรียกว่า cell แล้วให้แต่ละ cell ดูแลลูกค้าในสัดส่วนที่กำหนดไว้ตายตัว:
- cell คือสำเนาที่ครบทั้ง workload: มี entry point, compute, ฐานข้อมูล, cache และ queue ของตัวเอง cell ไม่แชร์ state กันและไม่เคยเรียกหากัน ปัญหาที่เกิดใน cell หนึ่งเลยไม่มีทางลามไปอีก cell
- request ทุกตัวมี partition key ติดมาด้วย (customer, tenant หรือ account ID) และลูกค้าแต่ละรายอยู่ใน cell เดียวเท่านั้น
- cell router อยู่หน้า cell ทั้งหมด มันอ่าน key เปิดดูว่าลูกค้าอยู่ cell ไหน แล้วส่ง request ต่อไป นอกจากนั้นมันไม่ทำอะไรเลย
- control plane อยู่นอกเส้นทางของ request คอยสร้าง cell ตัดสินว่าลูกค้าใหม่จะไปอยู่ไหน ย้ายลูกค้าระหว่าง cell และ update table ของ router
ถ้ามี cell ขนาดเท่ากัน N ชุด ปัญหาที่ไม่หลุดออกนอก cell จะกระทบลูกค้าแค่ 1/N: 25% เมื่อมีสี่ cell อย่างใน diagram และ 1% เมื่อมีร้อย cell ทาง AWS เองก็บอกว่าทีม service ของตัวเองสร้างระบบแบบนี้มามากกว่าสิบปีแล้ว และ Reliability Pillar ของ Well-Architected ก็จัดวิธีนี้เป็น best practice REL10-BP03 ภายใต้ชื่อ bulkhead architectures
อะไรอยู่ใน cell และอะไรอยู่นอก cell
cell มีทุกอย่างที่ request ต้องใช้ตั้งแต่ต้นจนจบ รวมทั้งข้อมูลด้วย ตัวอย่างใน whitepaper ของ AWS ใช้ Application Load Balancer, กลุ่ม EC2 instance และฐานข้อมูล RDS เป็นหนึ่ง cell ส่วน cell ที่สองก็คือสำเนาชุดที่สองของทั้งสามอย่างนี้ ยิ่งแยกขาดจากกันมากยิ่งดี: แยกฐานข้อมูล bucket และ queue และถ้าเป็นไปได้ก็แยก cloud account ต่อ cell ไปเลย แบบนี้แต่ละ cell ก็ได้ quota ของตัวเองไปด้วย
dependency คือจุดที่การแยกขาดค่อย ๆ รั่วออกไป ถ้าทุก cell เรียก internal service ตัวเดียวกัน identity provider ตัวเดียวกัน หรือ cache cluster เดียวกัน ตัว dependency นั้นก็กลายเป็นของที่แชร์กัน และ blast radius ของมันก็ยังเป็น 100% อยู่ดี ให้แต่ละ cell มี instance ของ dependency เป็นของตัวเองเท่าที่ทำได้ dependency ที่เราดูแลเองก็ทำให้เป็นแบบ cell ไปด้วย ส่วนที่เหลือให้ถือเป็นความเสี่ยงจากการแชร์ที่รู้อยู่แล้ว และใส่ timeout กับ fallback ให้มันโดยเฉพาะ
cell จะไปอยู่ที่ไหนเป็นอีกเรื่องที่ต้องตัดสินใจแยกกัน:
- cell ที่คร่อมหลาย availability zone (regional cell) ได้ความทนทานแบบ multi-zone มาจาก managed service ที่อยู่ข้างใน เสีย zone ไปหนึ่ง zone ก็ไม่ทำให้ cell ล่ม แต่ถ้า zone หนึ่งพังแค่ครึ่ง ๆ ทุก cell จะโดนผลกระทบนิดหน่อย และไม่มี cell ใด cell หนึ่งให้ถอดออกจาก service ได้
- cell ที่อยู่ใน zone เดียว ทำให้ zone กลายเป็นหน่วยของความล้มเหลว: zone ที่ป่วยก็ระบุตัวได้และ drain ออกได้อย่างแม่นยำ แลกกับการที่ routing ต้องเป็นแบบ zone-aware และแต่ละ cell ต้องมีแผนกู้คืนของตัวเองสำหรับตอนเสีย zone ไป ปกติก็คือต้อง replicate ข้อมูลไปอีก zone การแยกขาดเลยหลวมลง
ที่อยู่นอก cell มีแค่สามอย่าง: router, control plane และ feature ที่ต้องใช้ข้อมูลจากทุก cell จริง ๆ (ดูด้านล่าง) ทำทั้งสามอย่างให้เล็กที่สุดเท่าที่ทำได้ เพราะอะไรที่อยู่นอก cell ก็กลับมาเป็นของที่แชร์กันอีก
partition key และลูกค้าจะไปอยู่ที่ไหน
key ควรเลือกตามหน่วยที่เป็นธรรมชาติของ workload: หน่วยที่ request ของมันแทบไม่ต้องใช้อะไรจากหน่วยอื่นเลย และ request ทุกตัวต้องมี key นี้ติดมา หรือคำนวณออกมาได้ ตัวเลือกที่ใช้กันบ่อยคือ customer, tenant และ account ID ส่วน resource ID ก็ใช้ได้ถ้า resource แต่ละตัวเป็นอิสระต่อกัน
ลูกค้าแต่ละรายมีขนาดไม่เท่ากัน และเรื่องนี้กำหนดว่าลูกค้าจะไปอยู่ที่ไหน:
- ลูกค้ารายใหญ่ ถ้าลูกค้ารายหนึ่งโตจนเกิน cell เดียว จะใช้แค่ customer ID เป็น key ไม่ได้แล้ว ทางหนึ่งคือเพิ่มมิติที่สองเข้าไปใน key เพื่อให้ลูกค้ารายใหญ่กระจายไปได้หลาย cell อีกทางคือให้ลูกค้ารายนั้นมี dedicated cell ของตัวเอง ลูกค้าบางรายก็ยอมจ่ายเพิ่มเพื่อให้ได้ single tenancy แบบนี้
- noisy neighbour ลูกค้าที่ใช้งานโตขึ้นเรื่อย ๆ จะเบียดลูกค้ารายอื่นใน cell เดียวกัน ให้วัดว่าลูกค้าแต่ละรายใช้ capacity ของ cell ไปกี่ส่วน แล้วย้ายลูกค้าที่เริ่มกินส่วนแบ่งมากเกินไปออก
- headroom วางลูกค้าใหม่ให้ไม่มี cell ไหนรันใกล้ขีดจำกัดที่ test ไว้ และเผื่อที่ไว้สำหรับลูกค้าที่ต้องย้ายเข้ามาระหว่างเกิด incident
วิธีที่ router แปลง key เป็น cell เรียกว่า mapping ถ้าใช้ table เต็มของ key → cell จะยืดหยุ่นที่สุด และวางหรือย้ายลูกค้าทีละรายได้ง่าย แต่ก็เป็น state ก้อนใหญ่และสำคัญมาก ถ้าใช้ช่วงหรือ prefix ของ key ก็ทำให้ table เล็กลง แต่เสี่ยงที่บางช่วงจะกลายเป็น hot range ถ้า hash key แล้ว modulo ด้วยจำนวน cell ก็ไม่ต้องมี table เลย แต่พอเพิ่ม cell ลูกค้าแทบทุกรายจะต้องย้าย ดีไซน์ส่วนใหญ่เลย hash key ลงไปใน bucket จำนวนมากที่ตายตัว แล้วเก็บ table เล็ก ๆ ของ bucket → cell ไว้ จะเลือกแบบไหนก็ตาม ให้มี override table ที่ปักบาง key ไว้กับบาง cell: ไว้ใช้ตอน test ไว้กักลูกค้าที่น่าสงสัย หรือไว้วางลูกค้ารายใหญ่มาก ๆ
router
วิธีที่ใช้สร้างกันบ่อย:
- DNS: ลูกค้าแต่ละรายได้ hostname ของตัวเอง และ record ชี้ไปที่ cell ของลูกค้ารายนั้น การย้ายลูกค้าก็คือการแก้ record แล้ว client จะเห็นค่าใหม่เมื่อคำตอบที่ cache ไว้หมดอายุ
- proxy บาง ๆ (หรือ API gateway) อ่าน key จากแต่ละ request เปิดดูใน table ที่เก็บไว้ใน memory แล้วส่ง request ต่อไป ส่วน control plane จะเขียน table ลง store อย่าง object storage แล้ว proxy ก็ reload เมื่อ table เปลี่ยน
- client-side routing: client ถามครั้งเดียวว่าตัวเองอยู่ cell ไหน เก็บคำตอบไว้ใน cache แล้วเรียก cell นั้นโดยตรง ส่วนที่แชร์กันมีแค่การ lookup และมันก็อยู่นอกเส้นทางของ request ทุกตัวหลังจากนั้น
- message router สำหรับ API แบบ asynchronous จะ consume queue หรือ topic ที่แชร์กัน แล้วส่ง message แต่ละตัวต่อไปที่ queue ของ cell เจ้าของ message นั้น
ไม่ว่าจะทำในรูปไหน router ก็เป็น component เดียวที่เห็นทุก request และรู้จักทุก cell มันเลยเป็นตัวกำหนด availability ของทั้งระบบ:
- ไม่มี business logic มันแค่อ่าน key, lookup แล้วส่งต่อ อะไรที่มากกว่านี้คือโค้ดที่พังพร้อมกันในทุก cell ได้ ถ้ามี call ที่ต้องข้าม cell จริง ๆ ให้ส่งกลับผ่าน router แทนที่จะเรียกจาก cell หนึ่งไปอีก cell ตรง ๆ
- ยัง route ต่อได้ด้วย table ที่ stale router ควร serve ต่อจากสำเนาที่ cache ไว้ตอนที่ control plane หรือ store ของ table ล่ม ตัว control plane พังบ่อยกว่า data plane และลูกค้าที่มีอยู่แล้วต้องไม่ขึ้นกับมัน
- replicate มัน และแก้มันอย่างระวัง รันหลาย ๆ สำเนา deploy เป็น wave แบบเดียวกับ cell และกำหนด capacity สูงสุดที่ test แล้วให้ router เหมือนที่ทำกับ cell
ทยอย rollout เป็น wave
cell ทำให้ทุกการเปลี่ยนแปลงมีหน่วยของการ expose ที่เป็นธรรมชาติ แทนที่จะ update ทุกอย่างพร้อมกัน deployment จะออกไปเป็น wave: cell เดียวก่อน (อาจเป็น canary cell ที่รับ traffic สังเคราะห์หรือ traffic อื่นที่ไม่ critical) แล้วมีช่วง bake ที่คอยดู alarm ของมัน จากนั้นค่อยขยายเป็น wave ที่ใหญ่ขึ้นเรื่อย ๆ ถ้า wave แรกมีปัญหา ก็หยุด rollout แล้ว rollback และคนที่เห็นปัญหาก็มีแค่ลูกค้าของ cell เดียว ภายในแต่ละ cell การ update เองก็ยังทำเป็น rolling update หรือ canary release ได้ การเปลี่ยน configuration การพลิก feature flag และการแก้ router ก็ควรทำแบบเดียวกัน: การเปลี่ยนที่ลงทุก cell พร้อมกันทำให้การแยกขาดไม่มีความหมาย
ขนาดและการโตของ cell
ทุก cell มี ขนาดสูงสุด ที่กำหนดไว้ล่วงหน้าและพิสูจน์แล้ว: load test cell จนมันพัง แล้วตั้ง limit ให้ต่ำกว่าจุดนั้นโดยเผื่อ safety margin ไว้ จากนั้นจะโตก็คือ เพิ่ม cell ไม่ใช่ขยาย cell เดิม และสิ่งที่ใหญ่ที่สุดที่ต้อง test ก็คือ cell เต็ม ๆ แค่หนึ่ง cell ส่วนขนาดของ cell ถูกดึงด้วยแรงสามทาง: ใหญ่พอจะรับลูกค้ารายใหญ่ที่สุดของคุณได้ เล็กพอจะ test ที่ full scale ได้และอยู่ใต้ quota ของ account ได้สบาย และใหญ่พอที่ overhead ตายตัวของ cell จะไม่สูญเปล่า พอ cell เข้าใกล้ limit ให้ป้องกันมันด้วย load shedding หรือ rate limiting แทนที่จะปล่อยให้มันล้ม
cell ใหม่สร้างจาก template infrastructure-as-code ชุดเดียวกับ cell เก่า ทำให้ cell เป็นทั้งหน่วยของ capacity และหน่วยของความล้มเหลว
ย้ายลูกค้าระหว่าง cell
ลูกค้าต้องย้ายด้วยเหตุผลที่ดี: รายหนึ่งโตเกิน cell ที่ใช้ร่วมกับคนอื่น ต้อง rebalance cell หรือต้องปลด cell ทิ้ง เพราะข้อมูลของลูกค้าอยู่ใน cell ของเขา การย้ายทุกครั้งเลยเป็น data migration ปกติมีสี่ขั้น: ก็อปข้อมูลไป cell ใหม่เป็นสำเนาที่ยังไม่ใช่ตัวจริง (non-authoritative) จากนั้นเปลี่ยนให้สำเนาใหม่เป็นตัวจริง แล้วชี้ router ไปที่ cell ใหม่ สุดท้ายลบสำเนาเก่า จังหวะที่ต้องระวังคือตอนสลับ ช่วงที่ยังมีบาง request กำลังวิ่งไป cell เก่าอยู่ สร้างเครื่องมือนี้และซ้อมใช้ไว้แต่เนิ่น ๆ: วันที่ลูกค้ารายหนึ่งโตเกิน cell ขึ้นมากะทันหัน ไม่ใช่วันที่เหมาะจะมานั่งเขียนมัน
feature ที่ต้องใช้ทุก cell
บาง feature โดยธรรมชาติก็ต้องมองข้ามลูกค้า: global search, analytics และ reporting, billing, console ของ operator และเครื่องมือของทีม support ที่ใช้หาว่าลูกค้าอยู่ cell ไหน มีสองทางที่จะ serve feature พวกนี้:
- แต่ละ cell publish สิ่งที่ feature ต้องใช้ (event, change stream, export ตามรอบ) ไปที่ global store อย่าง data warehouse หรือ search index แล้วให้ store นั้นตอบ query
- query แบบ scatter-gather ถามทุก cell ผ่าน router แล้วรวมคำตอบเข้าด้วยกัน
ไม่ว่าทางไหน ให้ feature พวกนี้อยู่นอก critical path: cell ต้อง serve ลูกค้าของตัวเองต่อได้ระหว่างที่ warehouse, index หรือ console ล่ม อะไรก็ตามที่ cell ต้องรอแบบ synchronous ก็นับเป็นส่วนหนึ่งของ blast radius ของทุก cell
observability ราย cell
ติด tag cell ให้ทุก metric, log line และ trace แล้วทำ dashboard กับ alarm แยกราย cell ภาพรวมแบบ aggregate จะซ่อน cell ที่ตายไปแล้ว: ถ้ามีร้อย cell แล้วมีหนึ่ง cell ที่พังหมด ก็คิดเป็นแค่ราว 1% ของ request ทำให้ยังผ่านเป้า availability 99% ที่คำนวณรวมจากทุก request ได้ ตั้ง objective ราย cell (ดู SLOs & Error Budgets) และเทียบ cell กันเอง: cell ที่ทำตัวต่างจากพี่น้องที่เหมือนกันทุกอย่างคือสัญญาณที่มีประโยชน์ที่สุดที่คุณมี โดยเฉพาะระหว่าง wave
ใช้ตอนไหนดี
- service แบบ multi-tenant ขนาดใหญ่ ที่การล่มพร้อมกันสำหรับลูกค้าทุกรายเป็นเรื่องรับไม่ได้ และการพังแบบจำกัดวงเป็นทางที่ดีกว่า ตามที่ AWS วางโจทย์ไว้ คุณต้องเลือกว่าอยากให้ลูกค้าทุกรายเจอ request ที่ fail ส่วนเล็ก ๆ หรืออยากให้ลูกค้าส่วนเล็ก ๆ เจอ request fail ทั้งหมด
- workload ที่มี partition key ตามธรรมชาติ และ request แทบไม่ต้องใช้ข้อมูลเกินลูกค้ารายเดียว: SaaS ส่วนใหญ่ แพลตฟอร์ม B2B และ API ที่แยกตาม account
- ระบบที่ต้องโตต่อไป จนเกินขนาดของอะไรก็ตามที่คุณ test ได้ หรือที่ชนเพดานของฐานข้อมูล เครื่อง หรือ quota ของ account ที่ใหญ่ที่สุดที่มีให้ใช้อยู่แล้ว
- ลูกค้าที่อยากได้หรือต้องมีการแยกขาด: dedicated cell สำหรับลูกค้ารายใหญ่หรือลูกค้าที่อยู่ภายใต้กฎระเบียบ หรือ cell ใน region เฉพาะเพื่อ data residency (กลไกเดียวกับ Deployment Stamps ด้านล่าง)
ตอนไหนไม่ควรใช้:
- ระบบเล็ก deployment เดียวที่กระจายไปหลาย zone มี rollout ที่รอบคอบ และ timeout ที่ดี จะถูกกว่าและง่ายกว่า ต้นทุนตายตัวของ router, control plane และงาน operation ราย cell จะคุ้มก็ตอนที่ scale ใหญ่แล้ว
- ไม่มี partition key ตามธรรมชาติ ถ้า request ส่วนใหญ่ต้องใช้ข้อมูลจากลูกค้าหลายราย (social graph, marketplace ที่จับคู่ผู้ซื้อคนไหนก็ได้กับผู้ขายคนไหนก็ได้, global leaderboard) ทุก request จะข้าม cell และการแยกขาดก็หายไป
- ข้อมูลที่ต้องแชร์ทั่วทั้งระบบ ถ้าทุก cell ต้องเห็นข้อมูลชุดเดียวกันที่ consistent (inventory เดียว ledger เดียว) cell ก็เป็นเจ้าของข้อมูลเองไม่ได้ ให้ replicate ข้อมูลไปทุก instance แทน แบบที่ Geode pattern ของ Azure ทำ หรือเก็บไว้ใน service เดียวที่ป้องกันไว้อย่างดี
- dependency ที่แชร์กันแต่แยกไม่ได้ ถ้าทุก cell ยังพึ่งฐานข้อมูลตัวเดียว identity service ตัวเดียว หรือ third-party API ตัวเดียว ปัญหาที่ตรงนั้นก็ยังไปถึงทุกคน แล้วคุณก็จ่ายค่า cell โดยไม่ได้การแยกขาดของมันมา
ได้อะไร เสียอะไร
- ค่าใช้จ่าย ทุก cell มี overhead ตายตัว: load balancer, ฐานข้อมูลขนาดขั้นต่ำ, cache, monitoring และแต่ละ cell ก็เก็บ spare capacity ของตัวเองไว้ด้วย ทำให้ utilisation โดยรวมต่ำกว่า pool เดียวที่ใช้ร่วมกัน คำแนะนำของ Azure สำหรับโครงสร้างแบบเดียวกันบอกว่าต้นทุนที่เพิ่มขึ้นนั้นสูงมาก
- งาน operation ทวีคูณ สิบ cell คือสิบ deployment ที่ต้อง rollout, เฝ้าดู, patch, ทำให้เหมือนกัน และคุมให้อยู่ใน quota ถ้าไม่มี automation เต็มรูปแบบ (infrastructure as code, pipeline ที่ deploy เป็น wave, dashboard ราย cell) cell จะค่อย ๆ ต่างกันไปเรื่อย ๆ และ wave ก็ไม่ปลอดภัยอีกต่อไป
- ล่มวงเล็กลง แต่ล่มเต็ม ๆ cell ที่พังจะล่มทั้งหมดสำหรับลูกค้าของมัน ระบบโดยรวมพังเป็นวงแคบกว่าและกู้คืนได้เร็วกว่า เพราะ cell เล็ก ๆ หาสาเหตุและแก้ได้เร็วกว่า แต่ลูกค้าของ cell ที่พังจะไม่ได้รับ service จากที่อื่น เว้นแต่คุณจะทำ cross-cell failover และนั่นก็พาข้อมูลที่แชร์กันกลับมาอีก
- router กับ control plane เป็นของที่แชร์กัน router เป็น component เดียวที่ยังล้มทุกอย่างได้ และ cell ต้องทำงานต่อได้ระหว่างที่ control plane ใช้ไม่ได้ ทำทั้งคู่ให้เล็ก test ไว้ และแก้เป็น wave
- การย้ายลูกค้าคือ migration การ rebalance ไม่เคยเป็นแค่การเปลี่ยน configuration: มันย้ายข้อมูล และเครื่องมือก็ต้องสร้าง test และซ้อมไว้
- feature ข้ามลูกค้าทำยากขึ้น อะไรที่เคยเป็น query เดียวบนฐานข้อมูลเดียว จะกลายเป็นการ aggregate ข้าม cell หรือ global store แยกต่างหากที่มี lag ของตัวเอง
- ความล้มเหลวที่เกิดพร้อมกันหลาย cell ยังมีอยู่ การเปลี่ยนที่ push ไปทุก cell พร้อมกัน dependency ที่แชร์กัน bug ที่ทุก cell เจอในวันเดียวกัน: cell กันได้แค่สิ่งที่อยู่ข้างในมันเท่านั้น
ข้อควรรู้ตอนลงมือทำ
เริ่มจากของที่มีอยู่ ให้ถือ stack เดิมเป็น cell zero วาง router ไว้ข้างหน้า แล้วเพิ่ม cell ที่สองตั้งแต่เนิ่น ๆ ถึงตอนแรกมันจะ serve แค่ลูกค้าภายในหรือลูกค้าทดสอบก็ตาม ทั้ง AWS และ Azure แนะนำให้มีมากกว่าหนึ่ง cell (stamp) ตั้งแต่เริ่ม เพราะโค้ดและ configuration จะค่อย ๆ สมมติเอาเองแบบเงียบ ๆ ว่ามีสำเนาอยู่ชุดเดียว จากนั้นทำ failure mode analysis ของ cell: ไล่ทุก component กับวิธีที่มันพังได้ แล้วเช็กว่าทุกความล้มเหลวอยู่แค่ใน cell
มันเกี่ยวกับ pattern ใกล้เคียงยังไง:
- Bulkhead แบ่ง resource ภายใน service เดียว เช่น thread pool แยกต่อ dependency ส่วน cell ใช้แนวคิดเดียวกันกับทั้ง stack รวมข้อมูลด้วย และนี่คือเหตุผลที่ Well-Architected Framework จัด cell ไว้ใต้ bulkhead architectures
- Load Balancing ส่ง request ไหนก็ได้ไปที่ instance ที่ healthy ตัวไหนก็ได้ แต่ละ cell ก็ยังมี load balancer สำหรับ instance ของตัวเอง แต่ router ที่อยู่ข้างหน้าเป็นแบบ sticky: ลูกค้ารายหนึ่งไปถึง cell เดิมเสมอ เพราะข้อมูลของเขาอยู่ที่นั่น
- Multi-Region Active-Active ให้ทุก region serve ผู้ใช้ทุกคน และ replicate ข้อมูลระหว่าง region เพื่อให้ traffic ย้ายได้ตอนที่ region หนึ่งพัง ส่วน cell ดูแลลูกค้ากลุ่มย่อยที่ตายตัวและไม่แชร์ข้อมูลกัน ความล้มเหลวเลยอยู่กับที่แทนที่จะย้ายไปไหน สองแบบนี้ใช้ร่วมกันได้ดี: รัน cell ภายในแต่ละ region
- Deployment Stamps (ชื่อที่ Azure ใช้) ใช้กลไกเดียวกัน: สำเนาของ stack เต็มชุดที่เป็นอิสระต่อกันหลายชุด แต่ละชุด serve tenant กลุ่มย่อย และ Microsoft ก็นับ cell เป็นหนึ่งในชื่ออื่นของ stamp แต่จุดเน้นต่างกัน ปกติคนเริ่มใช้ stamp เพราะขีดจำกัดด้าน scale การวาง tenant (region, data residency, dedicated stamp) และจังหวะการ update ที่ต่างกัน (deployment ring) ส่วน cell เริ่มใช้เพื่อจำกัด blast radius และสุดท้ายดีไซน์เดียวก็มักตอบได้ทั้งสองเป้าหมาย
- Sharding แบ่งแค่ข้อมูล ทำให้ bug ใน application tier ที่ใช้ร่วมกันยังไปถึงทุก shard ส่วน cell แบ่งทั้ง stack และข้อมูลก็ตามไปด้วย
- Disaster Recovery Strategies ยังต้องมีอยู่ดี cell หนึ่ง ๆ ก็เสียฐานข้อมูลหรือ zone ได้เหมือน deployment อื่น และลูกค้าของมันก็ต้องพึ่ง backup กับแผนกู้คืนของ cell นั้น
Shuffle sharding พาแนวคิดนี้ไปอีกขั้น Amazon Builders' Library อธิบายด้วย worker แปดตัว: ถ้าแบ่งเป็นสี่คู่ตายตัว ลูกค้าที่ request ของเขาทำให้คู่ของตัวเอง crash จะพาลูกค้าหนึ่งในสี่ของทั้งหมดล่มไปด้วย แต่ถ้าให้ลูกค้าแต่ละรายมีคู่ของตัวเอง เลือกจากคู่ที่เป็นไปได้ 28 คู่ ก็จะมีแค่ลูกค้าที่จับได้คู่เดียวกันเป๊ะ ๆ ราวหนึ่งใน 28 ที่เสีย worker ทั้งสองตัว ส่วนที่เหลือยังมี worker ที่ healthy อยู่หนึ่งตัว Amazon Route 53 ให้แต่ละ hosted zone ได้ virtual name server สี่ตัวจาก 2,048 ตัว ทำให้มี combination ได้ราว 730 billion แบบ ส่วน whitepaper ของ AWS ระบุว่าใช้ shuffle sharding ภายใน cell ได้ แต่ข้าม cell ไม่ได้ เพราะ cell ต้องไม่แชร์อะไรกันเลย
ตัวอย่างที่เผยแพร่ไว้ ตามที่บริษัทเหล่านั้นเล่าเอง:
- Amazon Web Services บอกว่าทีม service ของตัวเองใช้ cell มามากกว่าสิบปีแล้ว (ใน whitepaper ที่เผยแพร่ปี 2023)
- Slack (2023) ย้าย service ฝั่งผู้ใช้ที่สำคัญที่สุดมาเป็น cell ในเวลาราวปีครึ่ง หลังจาก network fault ใน availability zone เดียวเมื่อเดือนมิถุนายน 2021 ทำให้ผู้ใช้เห็น error ส่วน cell ของ Slack คือ availability zone ของมัน ไม่ใช่กลุ่มลูกค้า: service ที่อยู่ใน request path จะคุยกับ service ใน zone เดียวกันเท่านั้น (Slack เรียกว่า siloing) edge load balancer เลย drain zone ที่ป่วยได้โดยลด weight ออกจาก zone นั้น ทีละขั้นเล็กได้ถึง 1% โดยมีเป้าว่าจะเอา traffic ส่วนใหญ่ออกไปให้ได้ภายในห้านาที ส่วน service ที่ทำ silo ไม่ได้ถูกยกไปเล่าใน post ถัดไป
- Shopify (2018) แบ่งแพลตฟอร์มออกเป็น pod: แต่ละ pod คือชุดของร้านค้าที่อยู่บน datastore ที่แยกขาดของตัวเอง และมี routing layer ใน load balancer (Sorting Hat) จับคู่ทุก request เข้ากับ pod ของมัน ตัว application server กับ job worker ยังใช้ร่วมกัน แต่งานแต่ละชิ้นคุยกับ pod เดียวเท่านั้น และเครื่องมือชื่อ Pod Mover ย้าย pod ไป data centre สำหรับกู้คืนได้ในเวลาราวหนึ่งนาที
- Roblox (2023) สร้าง cell ขนาดราว 1,400 เครื่องภายใน data centre ของตัวเอง และ replicate service ทั้งภายในและข้าม cell ทำให้ cell ที่หยุดทำงานถูกถอดออกจาก service ได้ หรือแม้แต่สร้างใหม่ตั้งแต่ศูนย์ ระหว่างที่ cell อื่นรับ load แทน ตอนนั้นมีเครื่องเกือบ 30,000 เครื่องที่จัดการในรูป cell แล้ว
cell ของ Slack และ Roblox สลับกันได้: cell ไหนก็ serve ผู้ใช้คนไหนก็ได้ และจำกัดความล้มเหลวด้วยการ drain cell ที่มีปัญหา ส่วน diagram แสดงแบบ partitioned (เหมือนใน whitepaper ของ AWS และ pod ของ Shopify) ที่ลูกค้าแต่ละรายและข้อมูลของเขาอยู่ใน cell เดียวเท่านั้น cell แบบสลับกันได้จะกู้คืนด้วยการย้าย traffic และต้องมีข้อมูลที่ทุก cell เข้าถึงได้ ส่วน cell แบบ partitioned กันปัญหาในข้อมูลได้ด้วย แต่ลูกค้าของ cell ที่พังต้องรอ cell นั้น
อีกความหมายหนึ่งของคำนี้ Cell-Based Architecture ของ WSO2 ที่เผยแพร่ครั้งแรกปี 2018 เป็น reference architecture สำหรับจัดระเบียบ microservice: cell คือกลุ่ม component ที่เกี่ยวข้องกัน โดย deploy, จัดการ และ observe รวมเป็นหน่วยเดียว มีทีมเดียวเป็นเจ้าของ และเข้าถึงได้ผ่าน gateway ของมันเท่านั้น แนวคิดนี้เป็นเรื่อง modularity และขอบเขตของทีม ไม่ใช่ fault isolation และ cell ของมันก็ต่างกันไปแต่ละอัน ส่วน cell ในหน้านี้เป็นสำเนาที่เหมือนกันทุกอย่าง