Part 03 · High Level Design
CAP Theorem and Consistency Models
CAP আসলে কী বলে আর কী বলে না, PACELC যে latency এর দাম যোগ করে, strong বনাম eventual consistency, আর quorum দিয়ে read আর write।
⏱ 2-3 ঘণ্টা · Station 27 / 80
একই প্রশ্নের দুইটা উত্তর
একটা mobile wallet প্রতিটা balance এর তিনটা কপি রাখে: দুইটা ঢাকার একটা data centre এ, একটা সিঙ্গাপুরে, যাতে এক building এ আগুন লাগলে কারো টাকা মুছে না যায়। এক সন্ধ্যায় সিঙ্গাপুরের সমুদ্রের নিচের cable এ সমস্যা হলো, আর দুই site এর মধ্যের link চার মিনিটের জন্য বন্ধ।
সিঙ্গাপুরে একজন customer app খুলে ৳500 পাঠাল। সিঙ্গাপুরের কপিটা কী করবে? সে ঢাকায় পৌঁছাতে পারছে না, জানে না ওই একই টাকা এইমাত্র সেখানে খরচ হয়েছে কিনা। Transfer টা নিলে দুই দিকের balance আলাদা হয়ে যেতে পারে, আর কেউ একই ৳500 দুইবার খরচ করে ফেলতে পারে। না নিলে customer চার মিনিট "Service unavailable" দেখবে, যদিও তার সামনের server দিব্যি চলছে।
দুইটাই রাখা যায় এমন তৃতীয় কোনো পথ নেই। এটাই CAP theorem এর মূল কথা। আর network ঠিক থাকলেও, যত বেশি কপির জন্য অপেক্ষা করবে, প্রতিটা request তত slow হবে। এটা PACELC। এই chapter হলো "consistent" আসলে কী বোঝায়, কোন কোন trade-off এর মধ্যে বাছতে হয়, আর quorum, যেটা বেশিরভাগ replicated database তোমাকে বাছাই করার জন্য দেয়।
সমস্যা: এমন কপি যারা একমত না হতে পারে
| কী হয় | কেন |
|---|---|
| User একটা value দেখল, refresh দিল, পুরনোটা দেখল | Read গেছে আলাদা আলাদা replica তে, যারা ভিন্ন ভিন্ন পর্যায়ে catch up করছিল |
| দুইজন মানুষ শেষ seat book করে ফেলল | Partition এর দুই দিকই write নিয়েছে |
| একটা region এ পৌঁছানো না গেলে system read-only হয়ে যায় | Availability এর বদলে consistency বেছে নিয়েছে |
| Database অলস, তবু write slow | প্রতিটা write আরেক region এর একটা replica র জন্য অপেক্ষা করে |
| "আমাদের DB CP" অথচ stale read হচ্ছে | Consistency নির্ভর করে read আর write কীভাবে configure করা সেটার উপর, শুধু product এর নামের উপর না |
| যে post এর reply, তার আগেই comment দেখা যায় | সম্পর্কিত write গুলোর মধ্যে কোনো causal ক্রম নেই |
একই bank এর দুইটা শাখার কথা ভাবো
একটা bank এর দুইটা শাখা phone এ একটাই খাতা মিলিয়ে চলে। সাধারণত এক শাখায় যেকোনো জমা বা তোলা customer চলে যাওয়ার আগেই phone করে অন্য শাখাকে জানানো হয়। দুইটা খাতা সবসময় মেলে।
তারপর phone line মরে গেল। একজন customer শাখা B তে এসে ৳10,000 তুলতে চাইল। শাখা B এর সামনে দুইটা পথ:
- না বলা ("দুঃখিত, এই মুহূর্তে আপনার balance নিশ্চিত করতে পারছি না")। খাতা ঠিক থাকে, কিন্তু customer ফেরত যায়। এটা consistency বেছে নেওয়া।
- টাকা দিয়ে দেওয়া আর লিখে রাখা, পরে মেলানো হবে। Customer সেবা পায়, কিন্তু একই মানুষ যদি এইমাত্র শাখা A থেকে তুলে থাকে, bank দুইবার দিয়ে ফেলল। এটা availability বেছে নেওয়া।
Phone line চালু থাকলে প্রশ্নটা অন্য রকম: cashier কি প্রতিবার phone এ অন্য শাখার জন্য অপেক্ষা করবে (slow, সবসময় মেলানো), নাকি নিজের খাতায় লিখে পরে phone করবে (দ্রুত, কিছুক্ষণ অমিল)? এটা latency বনাম consistency এর trade-off, আর এটা প্রতিদিন খাটে, শুধু outage এর সময় না।
তবে একটা জায়গায় মেলে না। Bank এর লোক পরে customer কে phone করে ভুল ঠিক করতে পারে। Software অনেক সময় পারে না: দুইটা system যদি শেষ seat দুজনকেই "বিক্রি" করে ফেলে, কাউকে না কাউকে হারতেই হবে। তাই consistency আর availability এর মধ্যে বাছাই হয় প্রতিটা কাজ ধরে, conflict হলে কতটা খারাপ হবে সেটা দেখে।
তিনটা replica, N = 3। প্রতিটা write W = 2টা acknowledgement এর জন্য অপেক্ষা করে, প্রতিটা read R = 2টা replica কে জিজ্ঞেস করে আর সবচেয়ে নতুন value টা রাখে। একটা replica অনেক পিছিয়ে। Write শেষ হওয়ার ঠিক পরে কি read পুরনো value দিতে পারে?
Consistency এর পেছনের নিয়মগুলো
- CAP: একটা network partition replica দের আলাদা করে দিলে system কে প্রতিটা request এর জন্য বাছতে হয় consistency (প্রতিটা read সবশেষ write দেখবে, নাহলে error পাবে) আর availability (প্রতিটা request error ছাড়া উত্তর পাবে, হয়তো পুরনো) এর মধ্যে। আসল network এ partition এড়ানো যায় না, তাই CAP আসলে বলে "partition এর সময় C নাকি A?"
- CAP অনেক সময় ভুলভাবে ব্যবহার হয়। এটা শুধু partition নিয়ে, consistency এর একটা নির্দিষ্ট কড়া সংজ্ঞা (linearizability) নিয়ে, আর "availability" মানে প্রতিটা সচল node উত্তর দেবে। বেশিরভাগ আসল system পুরোপুরি CP বা AP না; তারা প্রতিটা কাজ আর configuration অনুযায়ী বাছে।
- PACELC স্বাভাবিক সময়টাও যোগ করে: Partition হলে A নাকি C বাছো; Else (partition না থাকলে) Latency নাকি Consistency বাছো। প্রতিটা write এ দূরের replica র জন্য অপেক্ষা করলে কপি মেলানো থাকে, কিন্তু আরেক region এ একটা round trip এর খরচ হয় (প্রায়ই 50 থেকে 200 ms)।
- Consistency একটা ধারাবাহিক মাপ। সবচেয়ে কড়া থেকে দুর্বল: linearizable (একটা কপির মতো আচরণ; একটা read তার শুরুর আগে শেষ হওয়া প্রতিটা write দেখে); sequential; causal (কারণের পরে ফল: post এর আগে কখনো reply দেখা যাবে না); read-your-writes আর monotonic reads (একজন user এর জন্য session এর নিশ্চয়তা); eventual (write থামলে একসময় সব কপি মিলে যায়)।
- Quorum হলো নিয়ন্ত্রণের knob। N টা কপি থাকলে একটা write W টা acknowledgement এর জন্য অপেক্ষা করে আর একটা read R টা replica কে জিজ্ঞেস করে। W + R > N হলে প্রতিটা read সবশেষ write এর সাথে মেলে। কম W মানে দ্রুত write; কম R মানে দ্রুত read; W = N হলে কোনো একটা replica down থাকলেই write আটকে যায়।
- Majority quorum failure সহ্য করে। N = 3 আর W = R = 2 হলে একটা replica down থাকলেও system চলে, আর কখনো stale read দেয় না। N = 5 আর W = R = 3 হলে দুইটা down থাকতে পারে।
- AP system কে conflict মেটাতে হয়। দুই দিকই write নিয়ে থাকলে কাউকে ঠিক করতে হবে: timestamp ধরে last-write-wins (সহজ, চুপচাপ একটা write ফেলে দেয়), version vector যেগুলো একসাথে হওয়া write ধরে ফেলে আর দুইটাই রাখে, application এর নিজের মেলানোর rule, বা CRDT (এমন data type যেগুলো নিজে থেকেই মিলে যায়, যেমন counter আর set)।
- Company ধরে না, কাজ ধরে বাছো। Seat booking, টাকা transfer বা unique username এ strong consistency লাগে। Like এর সংখ্যা, "last seen" সময় বা product view counter eventual হলে চলে। অনেক system দুইটাই পাশাপাশি ব্যবহার করে।
- Clock কাজটা সহজ করে না। Machine জুড়ে দেয়ালঘড়ির সময় দিয়ে "সবশেষ" ঠিক করা ভরসার না (clock সরে যায়; পরের chapter)। Strong consistency সাধারণত একজন leader বা একটা consensus protocol এর উপর দাঁড়ায়, timestamp এর উপর না।
Partition এর সময় একটা transfer
সিঙ্গাপুর বিচ্ছিন্ন হলে একটা CP wallet কী করে
ধাপ 1/5 Customer ৳500 পাঠায়
App সবচেয়ে কাছের replica কে call করে, সিঙ্গাপুরে। Balance এ strong consistency লাগে, তাই write যায় leader দিয়ে, majority quorum সহ (N = 3, W = 2)।
- কাজ
- ৳500 transfer
- লাগবে
- 3 এর majority
ধাপে ধাপে কীভাবে বাছবে
- কাজগুলোর list করো আর জিজ্ঞেস করো: এখানে stale read বা conflicting write এর সবচেয়ে খারাপ ফল কী?
- টাকা, inventory, uniqueness, lock: strong consistency। Single leader বা consensus, majority quorum; partition এর সময় error মেনে নাও।
- Counter, feed, presence, analytics: সাধারণত eventual consistency চলে। Availability আর কম latency বেছে নাও; conflict কীভাবে মিলবে ঠিক করো।
- একজন user এর নিজের data: অন্তত read-your-writes আর monotonic reads, যাতে কেউ নিজের change হারিয়ে যেতে না দেখে।
- প্রতিটা কাজের জন্য N, W, R ঠিক করো (বা database এর consistency level), আর যেখানে fresh read লাগে সেখানে W + R > N কিনা দেখো।
- Partition এর সময় user কী দেখবে ঠিক করো: একটা error, read-only mode, নাকি label সহ পুরনো data।
- Replication lag মাপো, আর production খুঁজে বের করার আগে নিজেরা জেনেবুঝে partition test করো।
নিজে বলো: consistency র বাছাই ঠিক নাকি না?
Strong নাকি eventual: ঠিকটা বেছেছে?
partition এর সময় ছোট দিকটা book করতে পারে না; কেউ double-book করে না
চালিয়ে দেখার মতো quorum
তিনটা replica: A 4 ms দূরে, B 20 ms, আর C 80 ms দূরে আর replication এ 400 ms পিছিয়ে। Program টা ভিন্ন ভিন্ন W দিয়ে x = 2 লেখে, ভিন্ন ভিন্ন R দিয়ে আবার পড়ে, আর read যে যে replica set কে জিজ্ঞেস করতে পারে তার প্রতিটা check করে: write এ কত লাগল, read এ কত লাগতে পারে, আর কয়টা read set পুরনো value দেবে। তারপর C কে বিচ্ছিন্ন করে দেখায় CP আর AP আচরণ কেমন দেখায়।
package main
import (
"fmt"
"sort"
)
// Three replicas of one key, x = 1. A client writes x = 2 and reads it straight back.
// rtt is the round trip to each replica in ms; lag is how far behind its replication queue
// runs. C sits in another region and is behind.
type replica struct {
name string
rtt, lag int
}
var replicas = []replica{{"A", 4, 0}, {"B", 20, 0}, {"C", 80, 400}}
// has: the moment replica r holds x = 2 (the write reaches it after half a round trip, then waits
// in its queue). ack: when the client hears back from it.
func has(r replica) int { return r.rtt/2 + r.lag }
func ack(r replica) int { return r.rtt + r.lag }
// subsets returns every way to pick k of the n replicas.
func subsets(k int) [][]replica {
var out [][]replica
n := len(replicas)
for mask := 0; mask < 1<<n; mask++ {
var s []replica
for i := 0; i < n; i++ {
if mask&(1<<i) != 0 {
s = append(s, replicas[i])
}
}
if len(s) == k {
out = append(out, s)
}
}
return out
}
// quorum: the write waits for w acks; the read then asks r replicas and keeps the newest value.
func quorum(w, r int) {
acks := []int{}
for _, rep := range replicas {
acks = append(acks, ack(rep))
}
sort.Ints(acks)
wrote := acks[w-1] // the w-th fastest ack
stale, worst, total := 0, 0, 0
for _, s := range subsets(r) {
total++
fresh, slowest := false, 0
for _, rep := range s {
if wrote+rep.rtt/2 >= has(rep) { // the read reaches it after it got the write
fresh = true
}
slowest = max(slowest, rep.rtt)
}
if !fresh {
stale++
}
worst = max(worst, slowest)
}
fmt.Printf(" W=%d R=%d (W+R=%d): write %3d ms, read up to %2d ms, stale reads: %d of %d\n", w, r, w+r, wrote, worst, stale, total)
}
func main() {
fmt.Println("3 replicas: A 4 ms away, B 20 ms, C 80 ms and 400 ms behind; write x=2, read it back")
for _, wr := range [][2]int{{1, 1}, {2, 1}, {2, 2}, {1, 3}, {3, 1}} {
quorum(wr[0], wr[1])
}
// A network partition cuts C off from A and B.
fmt.Println("partition: C can't reach A or B")
reachable := 2
for _, w := range []int{3, 2} {
if w > reachable {
fmt.Printf(" W=%d: write refused (needs %d acks, %d reachable): consistent, not available\n", w, w, reachable)
} else {
fmt.Printf(" W=%d: write accepted by A and B; R=2 reads still see it\n", w)
}
}
// If both sides keep taking writes (W=1), they drift apart.
sideAB, sideC := 2, 3 // a client near A writes x=2, a client near C writes x=3
fmt.Printf(" W=1 on both sides: A,B say x=%d, C says x=%d: available, but they disagree until the partition heals\n", sideAB, sideC)
}# Three replicas of one key, x = 1. A client writes x = 2 and reads it straight back.
# rtt is the round trip to each replica in ms; lag is how far behind its replication queue
# runs. C sits in another region and is behind.
from typing import NamedTuple
class Replica(NamedTuple):
name: str
rtt: int
lag: int
REPLICAS = [Replica("A", 4, 0), Replica("B", 20, 0), Replica("C", 80, 400)]
def has(r: Replica) -> int:
"""The moment r holds x = 2 (half a round trip, then its queue)."""
return r.rtt // 2 + r.lag
def ack(r: Replica) -> int:
"""When the client hears back from r."""
return r.rtt + r.lag
def subsets(k: int) -> list[list[Replica]]:
"""Every way to pick k of the n replicas."""
n = len(REPLICAS)
out = []
for mask in range(1 << n):
s = [REPLICAS[i] for i in range(n) if mask & (1 << i)]
if len(s) == k:
out.append(s)
return out
def quorum(w: int, r: int) -> None:
"""The write waits for w acks; the read then asks r replicas and keeps the newest value."""
wrote = sorted(ack(rep) for rep in REPLICAS)[w - 1] # the w-th fastest ack
stale = worst = total = 0
for s in subsets(r):
total += 1
fresh = any(wrote + rep.rtt // 2 >= has(rep) for rep in s) # the read reaches it after it got the write
if not fresh:
stale += 1
worst = max(worst, max(rep.rtt for rep in s))
print(f" W={w} R={r} (W+R={w + r}): write {wrote:3} ms, read up to {worst:2} ms, stale reads: {stale} of {total}")
print("3 replicas: A 4 ms away, B 20 ms, C 80 ms and 400 ms behind; write x=2, read it back")
for w, r in [(1, 1), (2, 1), (2, 2), (1, 3), (3, 1)]:
quorum(w, r)
# A network partition cuts C off from A and B.
print("partition: C can't reach A or B")
reachable = 2
for w in (3, 2):
if w > reachable:
print(f" W={w}: write refused (needs {w} acks, {reachable} reachable): consistent, not available")
else:
print(f" W={w}: write accepted by A and B; R=2 reads still see it")
# If both sides keep taking writes (W=1), they drift apart.
side_ab, side_c = 2, 3 # a client near A writes x=2, a client near C writes x=3
print(f" W=1 on both sides: A,B say x={side_ab}, C says x={side_c}: available, but they disagree until the partition heals")// Three replicas of one key, x = 1. A client writes x = 2 and reads it straight back.
// rtt is the round trip to each replica in ms; lag is how far behind its replication queue
// runs. C sits in another region and is behind.
type Replica = { name: string; rtt: number; lag: number };
const REPLICAS: Replica[] = [
{ name: "A", rtt: 4, lag: 0 },
{ name: "B", rtt: 20, lag: 0 },
{ name: "C", rtt: 80, lag: 400 },
];
/** The moment r holds x = 2 (half a round trip, then its queue). */
const has = (r: Replica) => Math.floor(r.rtt / 2) + r.lag;
/** When the client hears back from r. */
const ack = (r: Replica) => r.rtt + r.lag;
/** Every way to pick k of the n replicas. */
function subsets(k: number): Replica[][] {
const out: Replica[][] = [];
const n = REPLICAS.length;
for (let mask = 0; mask < 1 << n; mask++) {
const s = REPLICAS.filter((_, i) => mask & (1 << i));
if (s.length === k) out.push(s);
}
return out;
}
/** The write waits for w acks; the read then asks r replicas and keeps the newest value. */
function quorum(w: number, r: number) {
const wrote = REPLICAS.map(ack).sort((a, b) => a - b)[w - 1]!; // the w-th fastest ack
let stale = 0;
let worst = 0;
let total = 0;
for (const s of subsets(r)) {
total++;
const fresh = s.some((rep) => wrote + Math.floor(rep.rtt / 2) >= has(rep)); // the read reaches it after it got the write
if (!fresh) stale++;
worst = Math.max(worst, ...s.map((rep) => rep.rtt));
}
console.log(` W=${w} R=${r} (W+R=${w + r}): write ${String(wrote).padStart(3)} ms, read up to ${String(worst).padStart(2)} ms, stale reads: ${stale} of ${total}`);
}
console.log("3 replicas: A 4 ms away, B 20 ms, C 80 ms and 400 ms behind; write x=2, read it back");
for (const [w, r] of [[1, 1], [2, 1], [2, 2], [1, 3], [3, 1]] as const) quorum(w, r);
// A network partition cuts C off from A and B.
console.log("partition: C can't reach A or B");
const reachable = 2;
for (const w of [3, 2]) {
if (w > reachable) console.log(` W=${w}: write refused (needs ${w} acks, ${reachable} reachable): consistent, not available`);
else console.log(` W=${w}: write accepted by A and B; R=2 reads still see it`);
}
// If both sides keep taking writes (W=1), they drift apart.
const [sideAB, sideC] = [2, 3]; // a client near A writes x=2, a client near C writes x=3
console.log(` W=1 on both sides: A,B say x=${sideAB}, C says x=${sideC}: available, but they disagree until the partition heals`);// Three replicas of one key, x = 1. A client writes x = 2 and reads it straight back.
// rtt is the round trip to each replica in ms; lag is how far behind its replication queue
// runs. C sits in another region and is behind.
#[derive(Clone, Copy)]
struct Replica {
rtt: i32,
lag: i32,
}
const REPLICAS: [Replica; 3] = [Replica { rtt: 4, lag: 0 }, Replica { rtt: 20, lag: 0 }, Replica { rtt: 80, lag: 400 }];
/// The moment r holds x = 2 (half a round trip, then its queue).
fn has(r: Replica) -> i32 {
r.rtt / 2 + r.lag
}
/// When the client hears back from r.
fn ack(r: Replica) -> i32 {
r.rtt + r.lag
}
/// Every way to pick k of the n replicas.
fn subsets(k: usize) -> Vec<Vec<Replica>> {
let n = REPLICAS.len();
(0..1 << n)
.map(|mask: usize| (0..n).filter(|i| mask & (1 << i) != 0).map(|i| REPLICAS[i]).collect::<Vec<_>>())
.filter(|s| s.len() == k)
.collect()
}
/// The write waits for w acks; the read then asks r replicas and keeps the newest value.
fn quorum(w: usize, r: usize) {
let mut acks: Vec<i32> = REPLICAS.iter().map(|&rep| ack(rep)).collect();
acks.sort();
let wrote = acks[w - 1]; // the w-th fastest ack
let (mut stale, mut worst, mut total) = (0, 0, 0);
for s in subsets(r) {
total += 1;
let fresh = s.iter().any(|&rep| wrote + rep.rtt / 2 >= has(rep)); // the read reaches it after it got the write
if !fresh {
stale += 1;
}
worst = worst.max(s.iter().map(|rep| rep.rtt).max().unwrap());
}
println!(" W={w} R={r} (W+R={}): write {wrote:3} ms, read up to {worst:2} ms, stale reads: {stale} of {total}", w + r);
}
fn main() {
println!("3 replicas: A 4 ms away, B 20 ms, C 80 ms and 400 ms behind; write x=2, read it back");
for (w, r) in [(1, 1), (2, 1), (2, 2), (1, 3), (3, 1)] {
quorum(w, r);
}
// A network partition cuts C off from A and B.
println!("partition: C can't reach A or B");
let reachable = 2;
for w in [3, 2] {
if w > reachable {
println!(" W={w}: write refused (needs {w} acks, {reachable} reachable): consistent, not available");
} else {
println!(" W={w}: write accepted by A and B; R=2 reads still see it");
}
}
// If both sides keep taking writes (W=1), they drift apart.
let (side_ab, side_c) = (2, 3); // a client near A writes x=2, a client near C writes x=3
println!(" W=1 on both sides: A,B say x={side_ab}, C says x={side_c}: available, but they disagree until the partition heals");
}import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// Three replicas of one key, x = 1. A client writes x = 2 and reads it straight back.
// rtt is the round trip to each replica in ms; lag is how far behind its replication queue
// runs. C sits in another region and is behind.
public class Main {
record Replica(String name, int rtt, int lag) {
/** The moment this replica holds x = 2 (half a round trip, then its queue). */
int has() { return rtt / 2 + lag; }
/** When the client hears back from it. */
int ack() { return rtt + lag; }
}
static final List<Replica> REPLICAS = List.of(new Replica("A", 4, 0), new Replica("B", 20, 0), new Replica("C", 80, 400));
/** Every way to pick k of the n replicas. */
static List<List<Replica>> subsets(int k) {
List<List<Replica>> out = new ArrayList<>();
int n = REPLICAS.size();
for (int mask = 0; mask < 1 << n; mask++) {
List<Replica> s = new ArrayList<>();
for (int i = 0; i < n; i++) if ((mask & (1 << i)) != 0) s.add(REPLICAS.get(i));
if (s.size() == k) out.add(s);
}
return out;
}
/** The write waits for w acks; the read then asks r replicas and keeps the newest value. */
static void quorum(int w, int r) {
int[] acks = REPLICAS.stream().mapToInt(Replica::ack).toArray();
Arrays.sort(acks);
int wrote = acks[w - 1]; // the w-th fastest ack
int stale = 0, worst = 0, total = 0;
for (List<Replica> s : subsets(r)) {
total++;
boolean fresh = false;
for (Replica rep : s) {
if (wrote + rep.rtt() / 2 >= rep.has()) fresh = true; // the read reaches it after it got the write
worst = Math.max(worst, rep.rtt());
}
if (!fresh) stale++;
}
System.out.printf(" W=%d R=%d (W+R=%d): write %3d ms, read up to %2d ms, stale reads: %d of %d%n", w, r, w + r, wrote, worst, stale, total);
}
public static void main(String[] args) {
System.out.println("3 replicas: A 4 ms away, B 20 ms, C 80 ms and 400 ms behind; write x=2, read it back");
int[][] combos = {{1, 1}, {2, 1}, {2, 2}, {1, 3}, {3, 1}};
for (int[] wr : combos) quorum(wr[0], wr[1]);
// A network partition cuts C off from A and B.
System.out.println("partition: C can't reach A or B");
int reachable = 2;
for (int w : new int[] {3, 2}) {
if (w > reachable) System.out.printf(" W=%d: write refused (needs %d acks, %d reachable): consistent, not available%n", w, w, reachable);
else System.out.printf(" W=%d: write accepted by A and B; R=2 reads still see it%n", w);
}
// If both sides keep taking writes (W=1), they drift apart.
int sideAB = 2, sideC = 3; // a client near A writes x=2, a client near C writes x=3
System.out.printf(" W=1 on both sides: A,B say x=%d, C says x=%d: available, but they disagree until the partition heals%n", sideAB, sideC);
}
}// Three replicas of one key, x = 1. A client writes x = 2 and reads it straight back.
// rtt is the round trip to each replica in ms; lag is how far behind its replication queue
// runs. C sits in another region and is behind.
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>
struct Replica {
const char* name;
int rtt, lag;
int has() const { return rtt / 2 + lag; } // the moment it holds x = 2 (half a round trip, then its queue)
int ack() const { return rtt + lag; } // when the client hears back from it
};
const std::vector<Replica> kReplicas = {{"A", 4, 0}, {"B", 20, 0}, {"C", 80, 400}};
// Every way to pick k of the n replicas.
std::vector<std::vector<Replica>> subsets(size_t k) {
std::vector<std::vector<Replica>> out;
size_t n = kReplicas.size();
for (size_t mask = 0; mask < (1u << n); mask++) {
std::vector<Replica> s;
for (size_t i = 0; i < n; i++)
if (mask & (1u << i)) s.push_back(kReplicas[i]);
if (s.size() == k) out.push_back(s);
}
return out;
}
// The write waits for w acks; the read then asks r replicas and keeps the newest value.
void quorum(int w, int r) {
std::vector<int> acks;
for (const auto& rep : kReplicas) acks.push_back(rep.ack());
std::sort(acks.begin(), acks.end());
int wrote = acks[w - 1]; // the w-th fastest ack
int stale = 0, worst = 0, total = 0;
for (const auto& s : subsets(r)) {
total++;
bool fresh = false;
for (const auto& rep : s) {
if (wrote + rep.rtt / 2 >= rep.has()) fresh = true; // the read reaches it after it got the write
worst = std::max(worst, rep.rtt);
}
if (!fresh) stale++;
}
std::printf(" W=%d R=%d (W+R=%d): write %3d ms, read up to %2d ms, stale reads: %d of %d\n", w, r, w + r, wrote, worst, stale, total);
}
int main() {
std::printf("3 replicas: A 4 ms away, B 20 ms, C 80 ms and 400 ms behind; write x=2, read it back\n");
for (auto [w, r] : std::vector<std::pair<int, int>>{{1, 1}, {2, 1}, {2, 2}, {1, 3}, {3, 1}}) quorum(w, r);
// A network partition cuts C off from A and B.
std::printf("partition: C can't reach A or B\n");
int reachable = 2;
for (int w : {3, 2}) {
if (w > reachable)
std::printf(" W=%d: write refused (needs %d acks, %d reachable): consistent, not available\n", w, w, reachable);
else
std::printf(" W=%d: write accepted by A and B; R=2 reads still see it\n", w);
}
// If both sides keep taking writes (W=1), they drift apart.
int sideAB = 2, sideC = 3; // a client near A writes x=2, a client near C writes x=3
std::printf(" W=1 on both sides: A,B say x=%d, C says x=%d: available, but they disagree until the partition heals\n", sideAB, sideC);
}ছয়টাই হুবহু এটা print করে:
3 replicas: A 4 ms away, B 20 ms, C 80 ms and 400 ms behind; write x=2, read it back
W=1 R=1 (W+R=2): write 4 ms, read up to 80 ms, stale reads: 1 of 3
W=2 R=1 (W+R=3): write 20 ms, read up to 80 ms, stale reads: 1 of 3
W=2 R=2 (W+R=4): write 20 ms, read up to 80 ms, stale reads: 0 of 3
W=1 R=3 (W+R=4): write 4 ms, read up to 80 ms, stale reads: 0 of 1
W=3 R=1 (W+R=4): write 480 ms, read up to 80 ms, stale reads: 0 of 3
partition: C can't reach A or B
W=3: write refused (needs 3 acks, 2 reachable): consistent, not available
W=2: write accepted by A and B; R=2 reads still see it
W=1 on both sides: A,B say x=2, C says x=3: available, but they disagree until the partition heals
Output টা পড়ো:
- W=1 R=1: সবচেয়ে দ্রুত write (4 ms), কিন্তু যে read ঘটনাক্রমে C কে জিজ্ঞেস করে সে পায় x = 1। সম্ভাব্য তিনটা read এর একটা stale।
- W=2 R=1: write B এর জন্যও অপেক্ষা করেছে (20 ms), তবু W + R = 3, N এর বেশি না, তাই শুধু C থেকে পড়লে এখনো stale।
- W=2 R=2: W + R = 4 > 3। কোনো read set পুরনো value দেয় না, আর write এ এখনো মাত্র 20 ms। এটাই সাধারণ সেরা জায়গা।
- W=1 R=3: দ্রুত write, আর read সবাইকে জিজ্ঞেস করে: সবসময় fresh, কিন্তু প্রতিটা read সবচেয়ে slow replica র জন্য অপেক্ষা করে।
- W=3 R=1: read fresh আর দ্রুত হতে পারে, কিন্তু write আরেক region এর পিছিয়ে থাকা replica র জন্য 480 ms অপেক্ষা করেছে। PACELC এর "L" এটাই।
- partition: W = 3 এ write বাতিল (consistent, available না); W = 2 এ majority র দিক চলতে থাকে; দুই দিকই W = 1 দিয়ে write নিলে তারা এখন একমত না।
- কোন language এ কী বদলালো: bit mask দিয়ে subset বের করা ছয়টাতেই একই। Java র
recordআর C++ এর structhas()আরack()replica র উপরেই রাখে; Rust এরReplicaথেকে name field বাদ, কারণ কেউ ওটা পড়ে না আর Rust সেটা নিয়ে warning দিত।
আসল project এ consistency
- তোমার database এর default জানো। Async replica সহ PostgreSQL: primary তে strong, replica তে stale read। MongoDB: read আর write concern (
majority)। Cassandra আর DynamoDB: প্রতি request এ consistency level (QUORUM,ONE, strongly consistent read)। Spanner আর CockroachDB: default এ strong, তার দাম latency তে দিয়ে। - Session এর নিশ্চয়তা সস্তায় পাওয়া লাভ: user লেখার ঠিক পরে তার read primary তে পাঠাও, বা একটা "অন্তত version X থেকে পড়ো" token দাও, যাতে মানুষ সবসময় নিজের change দেখে।
- Multi-region: যেখানে পারো strongly consistent data প্রতি user বা প্রতি record এর একটা home region এ রাখো, যাতে বেশিরভাগ write সমুদ্র পার না হয়। যেটা বেশি পড়া হয় সেটা সব জায়গায় replicate করো।
- Conflict: কয়েক জায়গায় write নিতে দিলে মেলানোর rule আগেই ঠিক করো, test করো, আর conflict log করো। যেখানে একটা write হারালে ক্ষতি, সেখানে last-write-wins এড়াও।
- Test করো। Fault-injection tool (Toxiproxy, Chaos Mesh, বা Jepsen ধাঁচের test) দিয়ে network কেটে দেখো তোমার system আসলে কী করে, brochure যা বলে তা না।
যে design গুলো পরে problem create করে
Consistency র ভুলটা ধরো
CP database ও follower থেকে stale read দিল
একটু থামো, ভাবো
তুমি একটা data centre এ একটা Cassandra cluster চালাও, প্রতি key এ N = 3 replica। Write গতির জন্য ONE (W = 1) ব্যবহার করে। একজন product manager বলছেন, product edit করার পরে admin page মাঝে মাঝে এক সেকেন্ডের জন্য পুরনো title দেখায়। কেউ প্রস্তাব দিল read কে ALL (R = 3) করে দিতে। এতে কী পাবে, কী খরচ হবে, আর এর চেয়ে ভালো setting আছে?
উত্তর দেখো
W = 1 আর R = 3 এ W + R = 4 > 3, তাই read সবসময় সবশেষ write দেখবে। কিন্তু R = ALL মানে প্রতিটা read সবচেয়ে slow replica র জন্য অপেক্ষা করে, আর যেকোনো একটা replica down থাকলে প্রতিটা read fail করে: তুমি read কে একসাথে slow আর কম available বানিয়ে ফেললে। ভালো বাছাই হলো দুইটার জন্যই QUORUM: W = 2 আর R = 2 ও W + R > N দেয়, write আর read প্রত্যেকে মাত্র দুইটা replica র জন্য অপেক্ষা করে, আর একটা replica down থাকলেও system চলে। শুধু admin page এর fresh read লাগলে, বাকি সব জায়গায় ONE রেখে শুধু editor এর write আর read এ QUORUM দিতে পারো।
ছোট্ট quiz
1. এক বাক্যে CAP কী বলে?
উত্তর
একটা network partition replica দের আলাদা করে দিলে প্রতিটা request কে হয় consistent থাকতে অপেক্ষা/fail করতে হবে, নয়তো পৌঁছানো যায় এমন দিক থেকে উত্তর দিয়ে inconsistent হওয়ার ঝুঁকি নিতে হবে; partition এর সময় দুইটা একসাথে পাওয়া যায় না।
2. PACELC CAP এর সাথে কী যোগ করে?
উত্তর
Partition না থাকলেও একটা trade-off আছে: replica দের কড়াভাবে মিলিয়ে রাখতে latency খরচ হয়, কারণ প্রতিটা কাজে অন্য replica (হয়তো অনেক দূরের) র জন্য অপেক্ষা করতে হয়।
3. W + R > N কেন fresh read দেয়?
উত্তর
W + R > N হলে N এর মধ্যে যেকোনো W টা replica র set আর যেকোনো R টা replica র set এ অন্তত একটা replica মিলতেই হবে। ওই replica র কাছে সবশেষ শেষ হওয়া write আছে, আর read যা দেখে তার মধ্যে সবচেয়ে নতুনটা রাখে।
4. Strong consistency লাগে এমন একটা কাজ আর eventual consistency তে চলে এমন একটা কাজের নাম বলো।
উত্তর
Strong: টাকা transfer, শেষ seat book করা, একটা unique username নেওয়া। Eventual: like এর সংখ্যা, view counter, "last seen" সময়, recommendation feed।
তোমার challenge: C যদি পিছিয়ে না থাকে?
ঝামেলা এসেছিল C এর 400 ms পিছিয়ে থাকা থেকে। C এর lag 400 থেকে 0 করো (সে এখনো 80 ms দূরে) আর program আবার চালাও। চালানোর আগে আন্দাজ করো পাঁচটা quorum line এর কোনগুলো বদলাবে।
- শুধু lag এর value বদলাও।
- পুরনো আর নতুন output line ধরে ধরে মেলাও।
তুমি পেরেছো যখন W=1 R=1 আর W=2 R=1 দুইটা line ই শেষ হবে stale reads: 0 of 3 দিয়ে, আর W=3 R=1 line হবে W=3 R=1 (W+R=4): write 80 ms, read up to 80 ms, stale reads: 0 of 3। Replication lag না থাকলে এই run এ W=1 এর read ও fresh, কারণ read write এর পরে পৌঁছায়। Lag ই "মোটামুটি ঠিক" কে stale read বানিয়ে দেয়, আর আসল lag load এর সময়, deploy এর সময় আর region জুড়ে হঠাৎ বেড়ে যায়। তখন তোমাকে বাঁচায় W + R > N।
এক লাইনে
কপিরা একমত না হতে পারে: partition এর সময় বাছতে হয় না বলবে (consistency) নাকি উত্তর দিয়ে conflict এর ঝুঁকি নেবে (availability), আর বাকি সময় latency দিয়ে consistency কেনো; প্রতিটা কাজ ধরে বাছো, যেখানে পুরনো data ক্ষতি করে সেখানে মিলে যাওয়া quorum (W + R > N) বা একজন leader রাখো, প্রতিটা user কে read-your-writes দাও, আর যেখানে eventual consistency মেনে নাও সেখানে conflict কীভাবে মিলবে ঠিক করো।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →