সরাসরি পর্বে যান
পর্ব ৩০ · ৬৬-এর মধ্যে · মডিউল ৫

Distributed cache — partitioning, replication, hot key আর বহু-স্তরের cache

Distributed Cache — Partitioning, Replication, Hot Keys & Multi-Layer Caching

Taught in Bangla, this lesson shows when one cache server is not enough and sizes a cluster from memory, throughput and network, compares client-side hashing, hash-slot clusters with MOVED and ASK, and proxies, cache replication and its lost writes, the database surge when a node dies, gutter pools and rehash storms, hot keys, big keys, multi-layer caching and staleness, multi-region invalidation, and Memcached versus Redis.

এই পর্বে যা শিখবেন

  • কখন একটি cache সার্ভার যথেষ্ট নয়, আর memory, throughput ও network দেখে cluster-এর আকারের হিসাব
  • Client-side hashing, hash slot-এর cluster (MOVED আর ASK) আর proxy স্তর — কোনটি কখন
  • Cache-এর replication, failover-এ হারানো লেখা, আর cache-কে সত্যের উৎস বানানোর বিপদ
  • একটি node বিকল হলে database-এর ধাক্কার হিসাব, gutter pool, warm standby আর rehash storm এড়ানো
  • Hot key, big key, বহু-স্তরের cache-এর staleness, region জুড়ে invalidation, Memcached বনাম Redis আর চালানোর অভ্যাস

১ · একটি cache সার্ভার কখন যথেষ্ট নয় — memory, throughput আর availability

পর্ব ২৮-এ দেখেছেন cache-এ পড়া-লেখার নিয়ম (Caching কৌশল), পর্ব ২৯-এ কখন কী মুছবেন (Eviction আর invalidation) — দুটোতেই cache ছিল একটি বাক্স। এবার বাক্সটিকে একটি মেশিনের বাইরে নিতে হবে। শুরুতে একটি Redis বা Memcached সার্ভারই যথেষ্ট; একদিন তিনটি দেয়ালের কোনো একটিতে ধাক্কা লাগে:

  • Memory: বারবার লাগা ডেটা — working set — আর এক মেশিনের RAM-এ আঁটে না। ফলে eviction বাড়ে, hit ratio নামে, database-এর চাপ বাড়ে।
  • Throughput: একটি সার্ভার সেকেন্ডে নির্দিষ্ট সংখ্যক অনুরোধই সামলায় — CPU-র সীমা (Redis-এ command মূলত একটি thread-এ চলে), আর network card-এর bandwidth.
  • Availability: সার্ভারটি বন্ধ হলে প্রতিটি hit মুহূর্তে miss, পুরো চাপ database-এ। Cache তখন একটি single point of failure, আর database সাধারণত সেই চাপের জন্য বানানোই হয়নি।

পুরান ঢাকার পাইকারি আড়ত (database) সব মাল রাখে, কিন্তু দূরে আর ধীর; পাড়ার খুচরা দোকান (cache) রাখে শুধু চালু জিনিস। পাড়া বড় হলে দোকানের তাক ভরে যায়, এক দোকানির সামনে লাইন শেষ হয় না, আর দোকান বন্ধের দিন সবাই আড়তে ছোটে — আড়তের সামনে জট। সমাধান কয়েকটি দোকান, প্রতিটি কিছু পণ্যের দায়িত্বে; তখনই প্রশ্ন: কোন পণ্য কোন দোকানে, আর একটি দোকান বন্ধ হলে কী হবে?

আকারের হিসাব — working set × overhead থেকে node-এর সংখ্যা

পর্ব ৩ — Back-of-the-envelope হিসাব-এর মতোই, তবে তিনটি আলাদা সীমা দেখতে হয়, আর সবচেয়ে কড়াটিই উত্তর:

shard (memory) = ⌈ item × (key + value + overhead) ÷ ব্যবহারযোগ্য memory প্রতি node ⌉
shard (throughput) = ⌈ peak অনুরোধ/s ÷ নিরাপদ অনুরোধ/s প্রতি primary ⌉
node = সবচেয়ে বড়টি (network-সহ) × প্রতি shard-এর কপি
  • Overhead: প্রতিটি item-এর সাথে store নিজের হিসাব রাখে — hash table-এর ঘর, মেয়াদ, LRU-র pointer, allocator-এর গোল করা মাপ; মোটামুটি কয়েক দশক byte, engine আর সংস্করণভেদে আলাদা। অনুমান না করে নমুনা ভরে মাপুন (Redis-এ MEMORY USAGE command).
  • ব্যবহারযোগ্য memory: পুরো RAM নয়। Fragmentation, client-এর buffer, persistence-এর fork আর OS-এর জন্য ২০–৩০% headroom রাখা একটি প্রচলিত সতর্কতা।
  • কপি: প্রতিটি shard-এ একটি replica থাকলে memory দ্বিগুণ (অংশ ৩); throughput-এ সাধারণত শুধু primary ধরা হয়, কারণ ডিফল্টে পড়া যায় primary-তে।

নিচের হিসাবযন্ত্রে সব সংখ্যা ধরে নেওয়া। দ্বিতীয় অংশটি এই পর্বের সবচেয়ে গুরুত্বপূর্ণ সংখ্যা দেয়: replica ছাড়া একটি shard মরলে database-এ কত চাপ যায়, N = ৩, ৬ আর ১২-তে।

// Cache-cluster sizing from assumed inputs, then: what one dead shard does to the DB.
const items = 200e6;                    // cached items in the working set (assumed)
const keyB = 40, valB = 900, ovhB = 60; // bytes per item: key, value, engine overhead (varies)
const copies = 2;                       // each shard = 1 primary + 1 replica
const ramGB = 64, usable = 0.75;        // keep 25% for fragmentation, buffers, fork, OS
const peak = 600000, perShard = 100000; // peak reads/s; safe reads/s per primary (assumed)
const hit = 0.95;                       // cache hit ratio at peak

const perCopyGB = items * (keyB + valB + ovhB) / 1e9;
const byMem = Math.ceil(perCopyGB / (ramGB * usable));
const byOps = Math.ceil(peak / perShard);
const shards = Math.max(byMem, byOps);
console.log("one copy of the data: " + perCopyGB.toFixed(0) + " GB, usable per node: " +
  (ramGB * usable) + " GB");
console.log("shards for memory: " + byMem + ", for throughput: " + byOps + " -> " + shards +
  " shards x " + copies + " copies = " + shards * copies + " nodes");
console.log("network per shard at peak: " +
  (peak / shards * (keyB + valB) * 8 / 1e9).toFixed(2) + " Gbit/s");

console.log("\nshards  DB normal  DB after 1 dies  x normal  formula 1+h/(N(1-h))");
const dbNormal = Math.round(peak * (1 - hit));
for (const N of [3, 6, 12]) {
  const after = dbNormal + peak * hit / N;      // the dead shard's hits all become misses
  console.log(String(N).padStart(6) + String(dbNormal).padStart(11) +
    String(Math.round(after)).padStart(17) + (after / dbNormal).toFixed(2).padStart(10) +
    (1 + hit / (N * (1 - hit))).toFixed(2).padStart(10));
}

আউটপুট:

one copy of the data: 200 GB, usable per node: 48 GB
shards for memory: 5, for throughput: 6 -> 6 shards x 2 copies = 12 nodes
network per shard at peak: 0.75 Gbit/s

shards  DB normal  DB after 1 dies  x normal  formula 1+h/(N(1-h))
     3      30000           220000      7.33      7.33
     6      30000           125000      4.17      4.17
    12      30000            77500      2.58      2.58

এক কপির ডেটা ২০০ GB, প্রতি node-এ ব্যবহারযোগ্য ৪৮ GB — memory বলে ৫টি shard (২০০ ÷ ৪৮ ≈ ৪.১৭, তাই ৫)। কিন্তু peak-এর ৬ লাখ পড়া ভাগ করতে লাগে ৬টি primary; এখানে সীমা throughput, memory নয়। ৬টি shard × ২ কপি = ১২টি node, আর প্রতিটি primary-তে network প্রায় ০.৭৫ Gbit/s.

দ্বিতীয় টেবিলটিই আসল শিক্ষা। স্বাভাবিক miss Q(1 − h); মৃত shard-এর ভাগ Q/N, যার h অংশ আগে hit ছিল, এখন miss. তাই নতুন চাপ Q(1 − h) + Qh/N, আর অনুপাত 1 + h/(N(1 − h)). N = ৩-এ database পায় স্বাভাবিকের ৭.৩৩ গুণ, N = ১২-তে ২.৫৮ গুণ — অনেক ছোট shard মানে ছোট blast radius. উল্টো দিকটি চমকে দেয়: hit ratio যত ভালো, অনুপাত তত খারাপ — h = ০.৯৯ হলে N = ৬-এও ১৭.৫ গুণ, কারণ database তখন অভ্যস্ত মাত্র ১% চাপে।

নিচের ছবিটি এই পর্বের মানচিত্র: প্রতিটি app server-এর ভেতরে একটি ছোট local cache (L1), সবার ভাগের distributed cache cluster (L2), আর পেছনে database.

App server-এর L1, slot map আর replica-সহ cache cluster

Each app server keeps a small L1 and a copy of the slot map; every L2 shard has a primary and an async replica; misses go to the database

App server, L1 cache, cache cluster আর database App server 1App server 2App server N L1 · ছোট TTLL1 · ছোট TTLL1 · ছোট TTL slot mapslot mapslot map L2 · cache cluster · ১৬,৩৮৪টি slot Shard AShard BShard C 0–54605461–1092210923–16383 PrimaryPrimaryPrimary asyncasyncasync ReplicaReplicaReplica miss Database সত্যের উৎস · buffer pool invalidate নীল: L1 miss → slot → shard-এর primary · ধূসর: miss বেগুনি: লেখার পর সব L1-এ invalidation (pub/sub)
ছবি ৩০.১ — অনুরোধ আগে app server-এর নিজের L1-এ; না পেলে client library key থেকে slot বের করে, নিজের slot map দেখে সঠিক shard-এর primary-তে যায় (অংশ ২); সেখানেও না পেলে database. প্রতিটি shard-এর primary থেকে replica-তে কপি যায় asynchronous-ভাবে (অংশ ৩), আর লেখার পর বেগুনি পথে সব L1-এ invalidation পৌঁছায় (অংশ ৭)।

২ · Key কোন node-এ — client-side hashing, hash slot-এর cluster আর proxy

কয়েকটি cache node হলো; এখন প্রতিটি key-এর একটি ঠিকানা লাগবে, আর সব app server-কে একই ঠিকানা মানতে হবে। নিয়মের গণিত — ring, virtual node, rendezvous — আছে পর্ব ১৬ — Consistent hashing-এ। এখানে প্রশ্ন: সিদ্ধান্তটা কে নেয়? তিনটি প্রচলিত নকশা:

১ · Client নিজে

Memcached-এর সার্ভারগুলো একে অপরকে চেনে না। প্রতিটি app-এর library সার্ভারের তালিকা জানে আর নিজেই consistent hashing করে (client-side সিদ্ধান্ত)। মাঝে কিছু নেই, তাই এক hop. দাম: প্রতিটি ভাষার library-তে একই নিয়ম, আর সব app server-এর কাছে একই তালিকা (membership) — একটির config পুরোনো হলে একই key দুই জায়গায়।

উদাহরণ: Memcached-এর client library

২ · Cluster নিজে

Node-রা নিজেরাই জানে কোন hash slot কার, আর একে অপরকে জানায়। Client শুধু slot map-এর একটি কপি রাখে (cluster-aware client); ভুল node-এ গেলে node নিজেই বলে দেয় কোথায় যেতে হবে। Resharding আর failover cluster-এর ভেতরে।

উদাহরণ: Redis Cluster

৩ · মাঝে একটি proxy

App কথা বলে একটি proxy-র সাথে — প্রায়ই প্রতিটি host-এ একটি sidecar — যে topology জানে, connection জমিয়ে রাখে, কপিতে লেখে, বিকল node এড়িয়ে চলে। Client সরল; দাম একটি বাড়তি hop, আর proxy-কেও scale আর সচল রাখার দায়।

উদাহরণ: twemproxy-, mcrouter-ধাঁচ

Hash slot-এর cluster ভেতর থেকে

Redis Cluster-এর অফিসিয়াল documentation অনুযায়ী (লেখার সময়ে) key-জগৎ ঠিক ১৬,৩৮৪টি hash slot-এ ভাগ: slot = CRC16(key) mod 16384. প্রতিটি primary কিছু slot-এর মালিক, আর node-রা একটি আলাদা cluster bus-এ heartbeat আর gossip দিয়ে মালিকানার খবর ছড়ায় — membership-এর তত্ত্ব পর্ব ৪৪ — Membership আর failure detection — heartbeat, gossip আর SWIM-এ। Slot থেকে node-এর table-টিই এখানে partition map, পর্ব ২৩ — Partitioning আর sharding-এর fixed partition-এর চিন্তা হুবহু। Redis-এর data type আর command আছে DBMS কোর্সের Redis পর্বে; এখানে দেখি একটি অনুরোধ ঠিক node কীভাবে খুঁজে পায় — resharding-এর মাঝপথেও।

GET user:42:cart → CRC16 mod 16384 = slot 12984
Client-এর slot map
Slot-এর আসল মালিক
অনুরোধ যায়
উত্তর
Round trip
Slot সরানোর মাঝপথেশুধু এবারের জন্য D
client → C:  GET user:42:cart
C → client:  -ASK 12984 10.0.0.4:6379
client → D:  ASKING
client → D:  GET user:42:cart
D → client:  value      (slot map বদলাল না)
সরানো শেষস্থায়ী খবর
client → C:  GET user:42:cart
C → client:  -MOVED 12984 10.0.0.4:6379
client:      map-এ লিখল 12984 → D (বা পুরো map আবার আনল)
client → D:  GET user:42:cart
D → client:  value
একসাথে দুটি keyhash tag
MGET user:42:cart user:42:profile
-CROSSSLOT Keys in request don't hash to the same slot
CLUSTER KEYSLOT {user:42}:cart      →  15880
CLUSTER KEYSLOT {user:42}:profile   →  15880   (একই slot, একই node)
  1. শুরু: map আনা। Cluster-aware client চালু হয়ে যেকোনো একটি node থেকে পুরো slot map আনে — CLUSTER SHARDS (পুরোনো সংস্করণে CLUSTER SLOTS) — আর নিজের memory-তে রাখে। তিনটি primary: A, B, C; প্রতিটির একটি replica, ছবিতে দেখানো হয়নি।
  2. সাধারণ পড়া — এক hop. GET user:42:cart: client নিজেই CRC16 করে slot ১২,৯৮৪ পায়, map দেখে সেটি C-তে, সরাসরি C-কে পাঠায়। মাঝে কোনো router নেই, বাড়তি hop নেই।
  3. Resharding-এর মাঝে — ASK. নতুন node D যোগ হলো; C থেকে ১,০২৪টি slot (১২,২৮৮ থেকে ১৩,৩১১) D-তে যাচ্ছে, key-গুলো একটু একটু করে। এই key-টি ইতিমধ্যে D-তে, তাই C উত্তর দেয় ASK: “শুধু এবার D-কে জিজ্ঞাসা করুন।” Client D-কে আগে ASKING, তারপর অনুরোধটি পাঠায়; map বদলায় না, কারণ slot-এর বাকি key এখনো C-তে থাকতে পারে।
  4. সরানো শেষ — MOVED. Slot ১২,৯৮৪ এখন পুরোপুরি D-র। পুরোনো map নিয়ে client আবার C-তে গেল; C বলে MOVED — এবার স্থায়ী খবর। Client map হালনাগাদ করে (একটি slot, বা পুরো map আবার এনে) D-তে পাঠায়; এই অনুরোধে দুটি round trip.
  5. আবার এক hop. পরের সব অনুরোধ সরাসরি D-তে; redirect-এর দাম শুধু map বদলের পরের অল্প কয়েকটি অনুরোধে। যে client map রাখে না, সে প্রায় প্রতিবার এই দাম দেয়।
  6. একসাথে কয়েকটি key — hash tag. MGET user:42:cart user:42:profile: দুটির slot ১২,৯৮৪ (D) আর ৯,১৩৩ (B) — দুই node, তাই cluster ত্রুটি দেয়। Key-এর নামে {user:42} দিলে শুধু বন্ধনীর ভেতরের অংশ hash হয় (hash tag): দুটিই slot ১৫,৮৮০-এ, এক node-এ। সাবধান: খুব বেশি key এক tag-এ মানে একটি slot-এ বিশাল ভার।

দুটি redirect-এর পার্থক্য মনে রাখুন: MOVED মানে “slot স্থায়ীভাবে সরেছে, map হালনাগাদ করুন”; ASK মানে “শুধু এবারের জন্য ওদিকে যান, map বদলাবেন না”। আরেকটি লুকানো খরচ: একটি পাতা একসাথে ১০০টি key চাইলে (multi-get) অনুরোধ যায় প্রায় সব shard-এ, আর উত্তরের সময় ঠিক করে সবচেয়ে ধীর shard — পর্ব ৪ — Latency, throughput আর percentile-এর fan-out. তাই একসাথে লাগা key-কে hash tag-এ এক জায়গায়, বা পাতার জন্য একটি মিলিত key রাখা প্রায়ই ভালো।

দিকClient-side hashingHash slot-এর clusterProxy স্তর
কে সিদ্ধান্ত নেয়প্রতিটি client librarycluster; client কপি রাখেproxy
Network hop১১ (redirect-এ ২)২ (sidecar হলে প্রায় ১)
Node যোগ-বাদসব client-এ নতুন তালিকা পৌঁছাতে হয়slot সরানো, client redirect থেকে শেখেশুধু proxy-র config
Failoverনিজে নেই; client বা বাইরের ব্যবস্থাreplica স্বয়ংক্রিয়ভাবে primaryproxy অন্য pool-এ পাঠায়
Multi-key কাজclient নিজে ভাগ করেএক slot-এ হলে তবেইproxy ভাগ করে জোড়ে
মূল ঝুঁকিconfig-এর অমিল, ভাষাভেদে librarysmart client লাগে, slot-এর নিয়মবাড়তি latency আর একটি নতুন fleet

৩ · Cache-এর replication — primary-replica, হারানো লেখা আর “সত্যের উৎস”-এর ভুল

Cluster-এ প্রতিটি shard সাধারণত একটি primary আর এক বা একাধিক replica — আলাদা মেশিনে, ভালো হলে আলাদা availability zone-এ। লক্ষ্য durability নয়, availability: primary বিকল হলে replica উঠে আসে (failover), আর shard-এর key ঠান্ডা হয় না। পর্ব ২২ — Replication-এর leader-follower এখানে হুবহু, তবে প্রায় সব cache-এ replication asynchronous: primary নিজের memory-তে লিখেই client-কে OK বলে, কপি যায় পরে।

Failover-এ কী হারায় — আর কোন হারানো বেশি বিপজ্জনক

শেষ কয়েক millisecond-এর লেখা replica-তে পৌঁছানোর আগে primary মরলে নতুন primary সেগুলো জানে না। Cache-এ দুই রকম লেখা হারাতে পারে:

  • হারানো SET: নতুন একটি মান লেখা হয়েছিল, হারাল। পরের পড়া miss পায় বা পুরোনো মান পায়, তারপর database থেকে নতুন করে আসে — ক্ষতি সামান্য।
  • হারানো DEL (invalidation): database বদলানোর পর অ্যাপ key মুছেছিল (cache-aside, পর্ব ২৮), মোছাটাই হারাল। Replica-তে পুরোনো মান রয়ে গেল, আর নতুন primary সেটি দিতে থাকবে TTL শেষ না হওয়া পর্যন্ত। তাই প্রতিটি cache entry-তে একটি TTL রাখা নিরাপত্তা-জাল — invalidation হারালেও staleness-এর একটি সীমা থাকে।

Network ভাগ হলেও লেখা হারায়। Redis Cluster-এর documentation নিজেই বলে এটি strong consistency দেয় না: সংখ্যালঘু দিকে আটকে পড়া primary একটি timeout পর্যন্ত লেখা নিতে পারে, আর ওপাশে তার replica primary হলে সেই লেখাগুলো হারায় — split-brain-এর একটি রূপ। WAIT command কয়েকটি replica-র স্বীকৃতির অপেক্ষা করায়, তবু documentation অনুযায়ী তাতে Redis strongly consistent হয় না। Consistency-র অর্থ পর্ব ৪০ — CAP, PACELC আর consistency model-এ; আর এখানকার failover-এর ভোট পূর্ণ consensus নয় — সেই তত্ত্ব পর্ব ৪২-এ।

Cache-এর কি durability লাগে?

সাধারণ উত্তর: না। সত্যের উৎস database; cache হারালে আবার বানানো যায়। তবু persistence বা replica রাখার একটি কারণ আছে — উষ্ণতা: ২০০ GB-এর ঠান্ডা cache ভরতে database-কে সব পড়া নতুন করে দিতে হয়, যা অংশ ১-এর টেবিলের চেয়েও বড় ধাক্কা। তাই replica থাকে availability-র জন্য, আর নিয়মিত snapshot থাকলে restart-এর পর cache উষ্ণ ফেরে; প্রতিটি লেখার log (AOF) বিশুদ্ধ cache-এ সাধারণত লাগে না। খুঁটিনাটি DBMS কোর্সের Redis পর্বে।

Anti-pattern — cache-কে সত্যের উৎস বানানো। শুরুতে সহজ লাগে: cart, counter বা draft শুধু Redis-এ রাখলেন, database-এ লিখলেনই না। তারপর memory ভরে eviction সেই key মুছল, অথবা failover-এ শেষের লেখা হারাল, অথবা কেউ cache “পরিষ্কার” করতে সব key মুছে দিলেন — আর ডেটা ফেরানোর কোনো উৎস নেই। যে ডেটা হারানো চলে না, তা Redis-এ রাখতেই হলে তাকে database হিসেবে চালান: eviction বন্ধ (noeviction), persistence চালু, replica, backup, আর হারানোর জানালা জেনে-বুঝে মেনে নেওয়া। আর তাকে cache-এর cluster থেকে আলাদা রাখুন, যাতে cache-এর eviction নিয়ম তাকে ছুঁতে না পারে।

Replica থেকে পড়া: পড়ার চাপ ভাগ করতে replica থেকেও পড়া যায় (Redis Cluster-এ replica-র connection-এ READONLY দিয়ে); দাম replication lag-এর সময়টুকু পুরোনো মান — প্রায়ই চলে, কিন্তু নিজের সদ্য লেখা দেখার পাতায় নয়।

৪ · একটি node মারা গেলে — database-এর ধাক্কা, gutter pool আর rehash storm

অংশ ১-এর সূত্রটি মনে করুন: ৬টি shard, peak-এ সেকেন্ডে ৬ লাখ পড়া, hit ৯৫% — database স্বাভাবিকভাবে পায় ৩০,০০০, একটি shard মরলে ১,২৫,০০০. ধরা যাক database নিরাপদে নিতে পারে ৫০,০০০. তাহলে cache-এর একটি node-এর বিভ্রাট মানেই পুরো সিস্টেমের বিভ্রাট। নিচের stepper-এ তিনটি প্রতিক্রিয়া — একটি ভুল, দুটি কাজের।

৬টি shard × সেকেন্ডে ১,০০,০০০ পড়া · hit ৯৫% · database-এর নিরাপদ ক্ষমতা ৫০,০০০/s (ধরা যাক)
Shard C3
C3-এর key যায়
Database-এ পড়া/s
ক্ষমতার কত
  1. স্বাভাবিক দিন। প্রতিটি shard সেকেন্ডে ১,০০,০০০ পড়া পায়, ৯৫% hit. Database পায় বাকি ৫% — ৬ লাখের ৫% = ৩০,০০০, ক্ষমতার ৬০%.
  2. C3 মরল, প্রস্তুতি নেই। C3-এর ১,০০,০০০ পড়ার সবই database-এ (৯৫,০০০ আগের hit, ৫,০০০ আগের miss), সাথে বাকি পাঁচটির ২৫,০০০ — মোট ১,২৫,০০০, ক্ষমতার আড়াই গুণ। Database ধীর হলে সব পাতা ধীর, retry বাড়ে — গোটা সেবার বিভ্রাট।
  3. ভুল প্রতিক্রিয়া — rehash. প্রতিটি client C3-কে তালিকা থেকে বাদ দিল, তার key ছড়াল বাকি পাঁচটিতে (প্রত্যেকে সেকেন্ডে আরও ২০,০০০)। সেখানে এই key-গুলো ঠান্ডা, তাই database-এর চাপ একই; নতুন key-এর জায়গা করতে পুরোনো key evict হয়, তাদের hit ratio-ও নামে। C3 ফিরলে key আবার সরবে, আর C3-এর কপি পুরোনো।
  4. Gutter pool. Rehash নয়: C3-এর কোনো অনুরোধ timeout হলে client সেটি পাঠায় একটি ছোট, আলাদা gutter pool-এ। শুরুতে gutter খালি — miss database থেকে এনে gutter-এ বসে; coalescing আর fallback-এর concurrency limit শুরুর ধাক্কা সামলায়। কয়েক সেকেন্ডে gutter-এর hit ৬০% হলে (ধরা যাক) database = ২৫,০০০ + ৪০,০০০ = ৬৫,০০০.
  5. Gutter উষ্ণ। Hit ৯০% হলে database = ২৫,০০০ + ১০,০০০ = ৩৫,০০০ — ক্ষমতার ৭০%, সেবা চলছে। Gutter-এ entry-র TTL খুব ছোট (যেমন ১০ সেকেন্ড), তাই invalidation না পেলেও পুরোনো মান দ্রুত মোছে। বাকি পাঁচটি shard-এ কিছুই নড়েনি।
  6. ফেরা। Replica থাকলে cluster কয়েক সেকেন্ডে সেটিকে primary বানায় — ডেটা আগে থেকেই উষ্ণ, ধাক্কা শুধু বিকল টের পাওয়ার সময়টুকু। না থাকলে নতুন node উষ্ণ করে ফেরানো হয়। Client আবার C3-এর জায়গায় পাঠায়, gutter-এর entry মেয়াদে মুছে যায়; database আবার ৩০,০০০-এ।

Gutter pool-এর ধারণাটি জনপ্রিয় হয় Facebook-এর memcache paper (Nishtala ও সহকর্মীরা, NSDI ২০১৩) থেকে। Paper অনুযায়ী একটি cluster-এর memcached সার্ভারের মোটামুটি ১% gutter হিসেবে রাখা হতো, আর বাকি সার্ভারে rehash না করার কারণ: একটি hot key পাশের সার্ভারে গিয়ে সেটিকেও ডোবাতে পারে।

Rehash storm কেন বিপজ্জনক

Client-side hashing-এ মৃত node-কে তালিকা থেকে বাদ দিলেই তার key-গুলো ring-এ পাশের node-এ যায় — মোটামুটি ১/N ভাগ (পর্ব ১৬)। শুনতে নিরাপদ, কিন্তু এই rehash storm-এর তিনটি সমস্যা: পাশের node-এ key-গুলো ঠান্ডা, তাই database-এর ধাক্কা একই; মৃত node-এর একটি hot key পাশের node-কেও ডোবাতে পারে, তারপর পরেরটিকে — cascading failure; আর node ফিরলে key-ও ফেরে, কিন্তু তার কপি বিভ্রাটের আগের, মাঝের invalidation গেছে পাশের node-এ — পুরোনো মান। বারবার ওঠানামা করা (flapping) node-এ এটি বারবার।

প্রতিকার: একবারের ব্যর্থ health check-এ node বাদ নয় — পরপর কয়েকবার, আর ফেরার আলাদা শর্ত (hysteresis); বাদ দেওয়ার সিদ্ধান্ত একটি কেন্দ্রীয় ব্যবস্থার, প্রতিটি client-এর নিজের নয়; ফিরে আসা node-কে খালি করে তবেই traffic দেওয়া; আর database-এ fallback সীমিত — concurrency limit আর request coalescing.

তিন বিকল্প — কী দিয়ে কী কেনেন

Replica: ডেটা আগে থেকেই উষ্ণ, ধাক্কা শুধু বিকল টের পাওয়ার সময়টুকু — Redis Cluster-এ node timeout setting দিয়ে ঠিক হয় (ডিফল্ট লেখার সময়ে ১৫ সেকেন্ড); দাম memory দ্বিগুণ। ১৫ সেকেন্ডও কম নয়: উপরের উদাহরণে বাড়তি ৯৫,০০০ × ১৫ ≈ ১৪ লাখ পড়া। Gutter pool: অল্প কয়েকটি খালি সার্ভার সবার বিকল্প — সস্তা, কিন্তু শুরুতে ঠান্ডা। Warm standby: আগে থেকে ভরা প্রস্তুত node; নতুন node-কে traffic দেওয়ার আগে cache warming.

৫ · Hot key — চেনা, L1 cache, কপি ছড়ানো আর coalescing

Consistent hashing আর hash slot key-এর সংখ্যা সমান করে, traffic নয়। একটি কাল্পনিক খবরের সাইটে ক্রিকেট ফাইনালের দিন live score-এর key score:live-এ হঠাৎ মোট পড়ার বড় অংশ এলে সেই একটি shard ডোবে, বাকিরা অলস। Shard বাড়িয়েও লাভ নেই — key তো একটাই, থাকে একটি node-এ। এই একটি hot key-ই সমস্যা।

ফাইনালের দিন পাড়ার চায়ের দোকানে সবাই একটিই প্রশ্ন করেন — “স্কোর কত?” দোকানি প্রতিবার রেডিও শুনে উত্তর দিলে তাঁর আর চা বানানো হয় না। বুদ্ধিমান দোকানি একটি বোর্ডে স্কোর লিখে রাখেন আর প্রতি ওভারে মুছে নতুন লেখেন: প্রশ্নের উত্তর বোর্ডে (L1), রেডিও শোনা ওভারে একবার (L2-তে যাওয়া), আর পাশের দুটি দোকানেও একই বোর্ড (কপি)। কেউ কেউ এক ওভার পুরোনো স্কোর দেখবেন — খেলার আনন্দে সেটুকু চলে।

চেনা — কোন key গরম?

প্রথম লক্ষণ shard-এর metric: একটি shard-এর CPU বা network বাকিদের চেয়ে অনেক উঁচু, অথচ key-সংখ্যা সমান। তারপর key ধরে গোনা — client প্রতি ১০০টি অনুরোধে একটির key গুনে কয়েক সেকেন্ডে top-k পায়; key খুব বেশি হলে কম memory-র আনুমানিক গণনা count-min sketch, যার বিস্তারিত পর্ব ৪৫-এ। Store-এরও নিজের সুবিধা আছে, যেমন redis-cli --hotkeys (LFU-ধাঁচের eviction নিয়মে, সংস্করণভেদে)। পাওয়া তালিকা মেয়াদসহ সব app server-এ যায়।

সামলানো — চারটি উপায়

L1 — প্রতিটি app server-এর memory-তে অল্প TTL-এর কপি; কপি ছড়ানো — score:live#0 … #3 আলাদা shard-এ, পড়া এলোমেলো একটি থেকে, লেখা সবগুলোতে; coalescing — একসাথে আসা miss মিলিয়ে একটি অনুরোধ (পর্ব ২৯); আর replica থেকে পড়া, অংশ ৩-এর lag মেনে। কপি ছড়ানো পর্ব ২৩-এর salting-এর উল্টো: সেখানে লেখা ভাগ হয় আর পড়ায় সব লেজ লাগে, এখানে পড়া ভাগ হয় আর লেখায় সব কপি; লেখা-ভারী গরম গণনার পথ sharded counter. নকশাই বদলানোর উদাহরণ — বিখ্যাত অ্যাকাউন্টের পোস্ট পড়ার সময় আনা — পর্ব ৫৬ — Case study: news feed-এ। নিচে একটি live score-এ চারটির তিনটি একে একে:

৮টি shard, প্রতিটির নিরাপদ ক্ষমতা ৬০,০০০/s · অন্য key মিলে প্রতি shard ১৫,০০০/s · score:live-এ ৮০,০০০/s · ৫০টি app server (ধরা যাক)
S5 (score:live-এর ঘর)
S1, S3, S7
L2-তে score:live-এর অনুরোধ/s
সর্বোচ্চ staleness
  1. সমস্যা। score:live একটি key, তাই একটি shard — S5. সেখানে ১৫,০০০ + ৮০,০০০ = ৯৫,০০০, ক্ষমতার ১৫৮%; timeout শুরু, অথচ বাকি সাতটি shard ২৫% ব্যস্ত।
  2. চেনা। প্রতিটি app server প্রতি ১০০টি অনুরোধে একটির key নমুনা নেয় — পুরো সিস্টেমে সেকেন্ডে ২,০০০ নমুনা, তার ৮০০টি (৪০%) score:live. Top-k তালিকায় সে এক নম্বরে; তালিকাটি মেয়াদসহ সব app server-এ পৌঁছায়। চাপ এখনো আগের মতোই।
  3. L1, TTL ১ সেকেন্ড। প্রতিটি app server score:live নিজের memory-তে রাখে, প্রতি সেকেন্ডে একবার L2 থেকে নতুন করে আনে। L2-তে এই key-এর জন্য এখন সেকেন্ডে মাত্র ৫০টি অনুরোধ — S5 নামল ১৫,০৫০-এ। দাম: স্কোর সর্বোচ্চ প্রায় ১ সেকেন্ড পুরোনো। TTL-এ সামান্য jitter (০.৮ থেকে ১.২ সেকেন্ড) থাকলে ৫০টি সার্ভার একই মুহূর্তে ফেরে না, আর coalescing প্রতিটির একসাথে আসা miss-কে একটিতে মেলায়।
  4. L1-হীন পাঠক। আরেকটি সার্ভিস (ধরা যাক অন্য দলের API) L1 ছাড়াই সেকেন্ডে ৪০,০০০ বার score:live পড়ে। S5 আবার ১৫,০০০ + ৫০ + ৪০,০০০ = ৫৫,০৫০ — ক্ষমতার ৯২%, বিপদসীমার কাছে।
  5. চারটি কপি। score:live#0 … #3 রাখা হলো চারটি আলাদা shard-এ — S1, S3, S5, S7 (hash-এ কোথায় পড়ল তা যাচাই করা হয়েছে, চারটিই আলাদা)। প্রতিটি পাঠক এলোমেলো একটি কপি পড়ে: ৪০,০৫০ ÷ ৪ ≈ ১০,০১৩ করে। সবচেয়ে ব্যস্ত shard এখন প্রায় ২৫,০১৩ — ক্ষমতার ৪২%.
  6. লেখার দাম আর শেষ। স্কোর বদলালে updater চারটি কপিই লেখে আর L1-এ invalidation পাঠায়। চারটি লেখার মাঝে মুহূর্তের অমিল থাকতে পারে — এক কপি নতুন, অন্যটি পুরোনো — যা live score-এ চলে, ব্যাংকের balance-এ নয়। ম্যাচ শেষে key তালিকা থেকে বাদ পড়ে, কপিগুলো TTL-এ মোছে।

এবার একই প্রশ্ন একটি simulation-এ: ৮টি shard, সেকেন্ডে ১,০০,০০০ অনুরোধ, ১০,০০০টি key-এ Zipf-ধাঁচের traffic, আর তার ওপরে একটি celebrity key — score:live — যে একাই ৩০% অনুরোধ পায়। তিনটি অবস্থা: কোনো প্রতিকার নেই; ৪০টি app server-এ L1 (TTL ১ সেকেন্ড); আর চারটি কপি। Hash FNV-1a, random সংখ্যা নির্দিষ্ট seed থেকে।

// 8 shards; Zipf traffic over 10,000 keys plus one celebrity key. Load per shard (req/s).
function h32(s) {                // FNV-1a 32-bit, then MurmurHash3's fmix32 to mix bits
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); }
  h ^= h >>> 16; h = Math.imul(h, 0x85ebca6b); h ^= h >>> 13; h = Math.imul(h, 0xc2b2ae35);
  return (h ^ (h >>> 16)) >>> 0;
}
let seed = 30; function rand() { // mulberry32: small seeded PRNG
  seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const S = 8, SEC = 10, RPS = 100000, APPS = 40, HOT = "score:live", SHARE = 0.3, L1TTL = 1;
const cum = []; let tot = 0; for (let k = 1; k <= 10000; k++) { tot += 1 / k; cum.push(tot); }
function zipf() { const x = rand() * tot; let lo = 0, hi = cum.length - 1;
  while (lo < hi) { const m = (lo + hi) >> 1; if (cum[m] < x) lo = m + 1; else hi = m; } return lo + 1; }
const copies = [];                     // 4 copies of the hot key, on 4 different shards
for (let i = 0; copies.length < 4; i++) { const s = h32(HOT + "#" + i) % S; if (!copies.includes(s)) copies.push(s); }
const none = Array(S).fill(0), l1 = Array(S).fill(0), rep = Array(S).fill(0);
const l1Until = Array(APPS).fill(-1);  // per app server: until when its L1 copy is fresh
for (let i = 0; i < SEC * RPS; i++) {
  const t = i / RPS, app = Math.floor(rand() * APPS), hot = rand() < SHARE;
  const home = h32(hot ? HOT : "item:" + zipf()) % S, pick = copies[Math.floor(rand() * 4)];
  none[home]++;
  if (!hot) l1[home]++; else if (t >= l1Until[app]) { l1[home]++; l1Until[app] = t + L1TTL; }
  rep[hot ? pick : home]++;
}
const rows = { "none": none, "L1 1s": l1, "copies x4": rep };
console.log("hot key on shard " + h32(HOT) % S + ", copies on shards " + copies.join(","));
console.log("shard     " + [0, 1, 2, 3, 4, 5, 6, 7].map((x) => String(x).padStart(6)).join("") + "   max/avg");
for (const [name, c] of Object.entries(rows)) {
  const r = c.map((x) => Math.round(x / SEC)), sum = r.reduce((a, b) => a + b);
  console.log(name.padEnd(10) + r.map((x) => String(x).padStart(6)).join("") +
    (Math.max(...r) / (sum / S)).toFixed(2).padStart(10) + "   total " + sum);
}

আউটপুট:

hot key on shard 2, copies on shards 7,5,6,2
shard          0     1     2     3     4     5     6     7   max/avg
none       15966  6109 36697  8008 12616  6395  6313  7896      2.94   total 100000
L1 1s      15966  6109  6712  8008 12616  6395  6313  7896      1.82   total 70015
copies x4  15966  6109 14210  8008 12616 13876 13813 15402      1.28   total 100000

none: shard ২-তে ৩৬,৬৯৭ — গড় ১২,৫০০-এর ২.৯৪ গুণ; এর প্রায় ৩০,০০০ একটিই key-এর। L1 1s: shard ২ নামল ৬,৭১২-তে; hot key-এর জন্য L2-তে আসে সেকেন্ডে মাত্র ৪০টি (৪০টি app server × প্রতি সেকেন্ডে একবার), তাই মোট L2 traffic ১,০০,০০০ থেকে ৭০,০১৫. copies x4: ৩০,০০০ চার ভাগে প্রায় ৭,৫০০ করে shard ৭, ৫, ৬ আর ২-এ; মোট traffic একই, শুধু ছড়ানো।

তিনটি সূক্ষ্ম শিক্ষা। এক, অনুপাত দেখে ভুল বুঝবেন না: দুই প্রতিকারেই সবচেয়ে ব্যস্ত shard ০-এর ১৫,৯৬৬; L1 মোট চাপ কমিয়েছে (তাই গড় কম, অনুপাত বেশি), কপি শুধু ছড়িয়েছে। দুই, shard ০-এ আছে Zipf-এর শীর্ষ দুটি key item:1 আর item:5 — একটি hot key সামলালেই পরেরটি সামনে আসে। তিন, চারটি আলাদা shard পেতে লেজ #৪ পর্যন্ত যেতে হয়েছে, কারণ #০ আর #১ দুটিই shard ৭-এ পড়েছিল — কপির জায়গা যাচাই করুন।

৬ · Big key — বিশাল value আর collection-এর দাম

Hot key-এর আত্মীয় big key: একটি key-এর value কয়েক MB — একটি পুরো পাতার HTML, বিশাল JSON — বা একটি hash, list বা sorted set-এ লাখো সদস্য। তিনটি দাম:

  • Network: ৫ MB-এর একটি value 1 Gbit/s-এ পাঠাতে শুধু তারেই প্রায় ৪০ ms; 10 Gbit/s-এর card-ও সেকেন্ডে ২৫০ বার এটি পড়লেই ভরে যায় — সেই node-এর বাকি সব key-এর অনুরোধ পেছনে আটকে থাকে।
  • এক thread-এর CPU: Redis-এ command মূলত একটি thread-এ পরপর চলে। লাখো সদস্যের একটি collection পুরো পড়া (HGETALL) বা মোছা (DEL) যতক্ষণ চলে, ততক্ষণ সেই node-এর বাকি সব client অপেক্ষায় — tail latency লাফায়। মোছার কাজ পেছনের thread-এ পাঠাতে Redis 4.0 থেকে UNLINK আছে (সংস্করণভেদে)।
  • সরানো আর কপি: resharding-এ বিশাল key একবারে সরাতে হয়, সেই মুহূর্তে দুই node-ই ধীর।

প্রতিকার: বড় value ভেঙে কয়েকটি key-তে রাখা; compression; collection পাতায় পাতায় পড়া (HSCAN, বা ZRANGE key 0 99-এর মতো ছোট পরিসর); ছবি বা বড় ফাইল object storage বা CDN-এ রেখে cache-এ শুধু ঠিকানা (পর্ব ২৬ — Object storage); আর নিয়মিত খোঁজা — redis-cli --bigkeys বা --memkeys (সংস্করণভেদে)। Memcached-এ একটি item-এর ডিফল্ট সীমা ১ MB (বাড়ানো যায়), Redis-এ একটি string সর্বোচ্চ ৫১২ MB — কিন্তু সীমার কাছে যাওয়াই নকশার ভুলের লক্ষণ।

৭ · বহু-স্তরের cache — browser থেকে buffer pool, আর স্তরে স্তরে staleness

একটি পড়ার অনুরোধ database-এ পৌঁছানোর আগে কয়েকটি স্তরের cache পেরোয় — একে বলে tiered caching. ওপরের স্তর ব্যবহারকারীর কাছে আর দ্রুত, কিন্তু ছোট আর কম নিয়ন্ত্রিত। Latency-গুলো মোটামুটি মাত্রা মাত্র:

স্তরকোথায়মোটামুটি latencyকে ভাগ করেহালনাগাদের উপায়দাম ও ঝুঁকি
Browser cacheব্যবহারকারীর যন্ত্রপ্রায় শূন্যএকজনCache-Control, ETagবাইরে থেকে মোছা যায় না
CDN edge (পর্ব ১৭)শহরের কাছের edgeকয়েক থেকে কয়েক দশক msএক এলাকার সবাইTTL, purgepurge-এ দেরি, খরচ
API gateway বা edge cache (পর্ব ১১)data center-এর দরজা১ ms-এর কাছেসব clientTTLব্যক্তিগত উত্তর ভুল করে ভাগ হওয়া
In-process L1app server-এর memorymicrosecondএকটি processTTL, pub/sub invalidationN গুণ কপি, সার্ভারভেদে ভিন্ন মান
Distributed L2cache clusterপ্রায় ১ ms (একটি round trip)সব app serverDEL বা SET, TTLcluster চালানো, node বিকলের ধাক্কা
DB buffer pooldatabase-এর RAMquery-র খরচসহdatabase নিজেdatabase নিজেই ঠিক রাখেসবসময় সঠিক, কিন্তু সীমিত

ধারণাটি CPU-র L1, L2, L3-এর মতো — Computer Architecture কোর্সের multi-level cache দেখুন — তবে একটি বড় পার্থক্য আছে: CPU-র cache-গুলোকে hardware নিজেই coherent রাখে, আর আমাদের স্তরগুলোকে মেলাতে হয় নিজেদেরই। CDN-এর স্তর আছে পর্ব ১৭ — CDN আর edge-এ, gateway-র কথা পর্ব ১১ — Reverse proxy, API gateway আর BFF-এ।

সবচেয়ে খারাপ staleness = TTL-এর যোগফল

ধরুন invalidation নেই, শুধু TTL: L2-এর T₂, L1-এর T₁, CDN-এর Tc. মুহূর্ত t₀-তে database বদলাল। সবচেয়ে খারাপ ক্রম: L2 পুরোনো মানটি t₀-এর ঠিক আগে এনেছিল, তাই রাখবে t₀ + T₂ পর্যন্ত; একটি app server L2-এর মেয়াদ ফুরানোর ঠিক আগে সেটি L1-এ নিল — t₀ + T₂ + T₁ পর্যন্ত; CDN সেই app server-এর উত্তর ঠিক তখনই নিল — t₀ + T₂ + T₁ + Tc পর্যন্ত। প্রতিটি স্তর নিচের স্তরের প্রায় মেয়াদোত্তীর্ণ কপিকে নিজের পুরো TTL দেয়, তাই:

সবচেয়ে খারাপ staleness ≤ T₂ + T₁ + Tc (+ browser-এর TTL)
উদাহরণ: ৬০ + ৫ + ৩০ = ৯৫ সেকেন্ড, যদিও কোনো একটি TTL ৬০-এর বেশি নয়

এটি ঊর্ধ্বসীমা — বাস্তবে প্রায়ই কম — কিন্তু প্রতিশ্রুতি দিতে হলে এটাই হিসাব। প্রতিটি ধরনের ডেটার জন্য আগে ঠিক করুন কতটা staleness চলে, তারপর স্তর আর TTL এমনভাবে বাছুন যাতে যোগফল সেই বাজেটে থাকে।

L1-কে মেলানো — pub/sub invalidation আর ছোট TTL একসাথে

লেখার পথ: database-এ commit → L2-এর key মোছা বা হালনাগাদ → একটি channel-এ “key X বদলেছে” বার্তা (pub/sub) → প্রতিটি app server-এর subscriber নিজের L1 থেকে X মোছে। বার্তা কয়েক ms-এ পৌঁছায়, তাই staleness নামে ms-এ। কিন্তু Redis-এর pub/sub-এর মতো অনেক ব্যবস্থা বার্তা জমিয়ে রাখে না: সেই মুহূর্তে সংযুক্ত নেই এমন subscriber (restart, network-এর ঝাঁকুনি) বার্তাটি হারায় — তত্ত্ব পর্ব ৩২ — Pub/sub আর event-driven architecture আর পর্ব ৩৪ — Delivery semantics-এ। তাই দুটোই লাগে: invalidation দ্রুততার জন্য, ছোট TTL নিরাপত্তা-জাল হিসেবে। Redis 6 থেকে server-এর সাহায্যে client-side caching-ও আছে (CLIENT TRACKING) — কোন client কোন key পড়েছে মনে রেখে বদলালে জানায় (সংস্করণভেদে)।

নিচের simulation-এ ১০টি app server, প্রতিটিতে ১০০টি key-এর L1 (LRU, TTL ২ সেকেন্ড), সবার ভাগের ৫,০০০টি key-এর L2, আর ২০,০০০টি key-এ Zipf traffic, সেকেন্ডে ১,০০০ অনুরোধ, ১২০ সেকেন্ড। ৬০ সেকেন্ডে সবচেয়ে জনপ্রিয় key বদলায়, database আর L2 সঙ্গে সঙ্গে হালনাগাদ। তিনটি অবস্থা: শুধু TTL; pub/sub (বার্তা ১ থেকে ৩১ ms পরে পৌঁছায়); আর pub/sub, কিন্তু সার্ভার ৭ তখন reconnect করছিল বলে বার্তা পায়নি।

// Two tiers: a small L1 (LRU + TTL) in each of 10 app servers, one shared L2 (LRU), then the DB.
// At t = 60 s the hottest key (key 1) changes in the DB and in L2. Who still sees the old v1?
const APPS = 10, KEYS = 20000, L1CAP = 100, L1TTL = 2, L2CAP = 5000, RPS = 1000, END = 120, TU = 60;
function sim(mode) {                      // mode: "TTL only" | "pub/sub" | "pub/sub, 1 missed"
  let seed = 7; const rand = () => {     // mulberry32, re-seeded so every mode sees the same traffic
    seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
  const cum = []; let tot = 0; for (let k = 1; k <= KEYS; k++) { tot += 1 / k; cum.push(tot); }
  const zipf = () => { const x = rand() * tot; let lo = 0, hi = KEYS - 1;
    while (lo < hi) { const m = (lo + hi) >> 1; if (cum[m] < x) lo = m + 1; else hi = m; } return lo + 1; };
  const arrive = Array.from({ length: APPS }, () => TU + 0.001 + rand() * 0.03);  // 1-31 ms later
  if (mode === "pub/sub, 1 missed") arrive[7] = Infinity;      // server 7 was reconnecting
  const L1 = Array.from({ length: APPS }, () => new Map()), L2 = new Map();
  const touch = (m, k, v, cap) => {      // LRU: re-insert at the end, drop the oldest if full
    m.delete(k); m.set(k, v); if (m.size > cap) m.delete(m.keys().next().value); };
  let l1hit = 0, l2hit = 0, db = 0, stale = 0, last = 0, updated = false, got = Array(APPS).fill(false);
  for (let i = 0; i < RPS * END; i++) {
    const t = i / RPS, a = Math.floor(rand() * APPS), k = zipf();
    if (t >= TU && !updated) { updated = true; if (L2.has(1)) L2.set(1, 2); }   // the write
    if (mode !== "TTL only" && !got[a] && t >= arrive[a]) { L1[a].delete(1); got[a] = true; }
    let e = L1[a].get(k), v;
    if (e && e.exp > t) { l1hit++; v = e.v; touch(L1[a], k, e, L1CAP); }
    else {
      v = L2.get(k); if (v !== undefined) l2hit++; else { db++; v = (k === 1 && t >= TU) ? 2 : 1; }
      touch(L2, k, v, L2CAP); touch(L1[a], k, { v, exp: t + L1TTL }, L1CAP);
    }
    if (k === 1 && t >= TU && v === 1) { stale++; last = t - TU; }
  }
  const n = RPS * END, pct = (x) => (100 * x / n).toFixed(1).padStart(6) + "%";
  const col = (x, w) => String(Math.round(x)).padStart(w);
  console.log(mode.padEnd(18) + pct(l1hit) + pct(l1hit + l2hit) + col((n - l1hit) / END, 8) +
    col(db / END, 6) + col(stale, 7) + col(last * 1000, 8) + " ms");
}
console.log("mode                L1hit  total  L2 r/s  DB/s  stale  window");
["TTL only", "pub/sub", "pub/sub, 1 missed"].forEach(sim);

আউটপুট:

mode                L1hit  total  L2 r/s  DB/s  stale  window
TTL only            32.0%  79.9%     680   201    107    1426 ms
pub/sub             32.0%  79.9%     680   201      1      20 ms
pub/sub, 1 missed   32.0%  79.9%     680   201     15     893 ms

TTL only: L1 মেটায় ৩২.০%, L1 আর L2 মিলে ৭৯.৯%; L2-তে সেকেন্ডে ৬৮০, database-এ ২০১. Update-এর পরে ১০৭টি পড়া পুরোনো মান পেল, শেষটি ১,৪২৬ ms পরে — সীমা TTL-এর ২ সেকেন্ড, প্রকৃত জানালা নির্ভর করে কোন সার্ভার শেষবার কখন এনেছিল তার ওপর। pub/sub: মাত্র ১টি পুরোনো পড়া, ২০ ms-এর মধ্যে, বার্তা পৌঁছানোর আগে। pub/sub, 1 missed: সার্ভার ৭-এর কপি মরল শুধু TTL-এ — ১৫টি পুরোনো পড়া, শেষটি ৮৯৩ ms-এ; TTL না থাকলে সে পুরোনো মান দিত অনির্দিষ্টকাল।

একটি সূক্ষ্ম কথা: total hit তিন অবস্থাতেই ৭৯.৯%, database-ও ২০১ — কারণ L1-এ যা আছে তা L2-তেও আছে। L1 database-এর চাপ কমায় না; কমায় L2-এর traffic (১,০০০ থেকে ৬৮০), network আর latency.

প্রতিটি স্তরের দাম

প্রতিটি নতুন স্তর memory নেয় (L1-এ একই ডেটা N গুণ), invalidation-এর আরেকটি পথ চায়, আর “কোন স্তর পুরোনো মানটি দিল?” প্রশ্নকে কঠিন করে — উত্তরে স্তরের নাম একটি header বা log-এ রাখুন। যোগ করার আগে মাপুন সেটি latency, network না database-এর চাপ কতটা কমাবে; মাপ ছাড়া স্তর প্রায়ই over-engineering.

৮ · একাধিক region — প্রতিটির নিজের cache, আর invalidation-এর ক্রম

ব্যবহারকারী কয়েকটি region-এ ছড়ানো হলে প্রতিটি region-এর নিজের cache cluster লাগে; অন্য region-এর cache পড়া মানে প্রতিবার কয়েক দশক থেকে শতাধিক ms-এর round trip. প্রচলিত নকশা: database-এর leader একটি region-এ, বাকিগুলোতে read replica, আর প্রতিটি region-এর cache ভরে কাছের replica থেকে। লেখা হলে সব region-এ invalidation পৌঁছাতে হবে — বিপদ লুকিয়ে আছে ক্রমে। নিচে region B-র replica ৩০০ ms পিছিয়ে, আর invalidation সরাসরি broadcast:

সময়Region A (leader)Region B (replica, lag ৩০০ ms)
০ msলেখা commit — নতুন মান v2replica-তে এখনো v1
১০ msনিজের cache থেকে key মোছাbroadcast-এ cache থেকে key মোছা
১২ ms—একজন পাঠক miss পেলেন, replica থেকে পড়লেন v1, cache-এ বসালেন v1
৩০০ ms—replica-তে v2 এলো, কিন্তু cache-এ v1 — TTL পর্যন্ত পুরোনো

মোছা পৌঁছেছে ডেটার আগে, তাই cache আবার পুরোনো মানে ভরল। প্রতিকার: প্রতিটি region-এ invalidation চালাক সেই region-এর replication stream — replica পরিবর্তনটি প্রয়োগ করার পরেই একটি process সেই region-এর cache থেকে key মোছে (change data capture), ফলে মোছা কখনো ডেটার আগে পৌঁছায় না। Facebook-এর memcache paper অনুযায়ী তাঁরা MySQL-এর commit log পড়া একটি daemon (mcsqueal) দিয়ে প্রতিটি region-এ এভাবেই delete পাঠাতেন। আরও দুটি সহায়ক কৌশল: মানের সাথে version নম্বর রাখা আর পুরোনো version দিয়ে নতুনটিকে ঢাকতে না দেওয়া; আর লেখার ঠিক পরে ব্যবহারকারীর নিজের পড়া leader থেকে আনা (read-your-writes, পর্ব ২২)। পুরো region বিকল হলে অন্য region-এর cache-এ হঠাৎ দ্বিগুণ traffic, সেখানে ঠান্ডা key — traffic ধীরে সরানোর পরিকল্পনা পর্ব ৫১ — Failover আর disaster recovery — backup, RPO-RTO আর multi-region-এ।

৯ · Memcached বনাম Redis, আর চালানোর খুঁটিনাটি

দুটিই in-memory key-value store, কিন্তু distributed cache হিসেবে তাদের চরিত্র আলাদা। নিচের তুলনা লেখার সময়ের অবস্থা; সংস্করণ আর distribution-ভেদে বদলায়, তাই সিদ্ধান্তের আগে অফিসিয়াল documentation দেখুন। ২০২৪-এর লাইসেন্স বদল আর Valkey fork-এর কথা DBMS কোর্সের Redis পর্বে।

দিকMemcachedRedis
ডেটাশুধু byte-এর valuestring, hash, list, set, sorted set, stream…
Threadmulti-threaded — একটি node অনেক core কাজে লাগায়command মূলত এক thread-এ; network I/O-র বাড়তি thread ঐচ্ছিক (6.0 থেকে)
Memoryslab allocator — মাপ-ভিত্তিক ঘরসাধারণ allocator (Linux build-এ সাধারণত jemalloc), active defrag ঐচ্ছিক
Evictionপ্রতিটি slab শ্রেণির ভেতরে LRU-ধাঁচ (নতুন সংস্করণে কয়েক ভাগের segmented LRU)maxmemory-policy: allkeys-lru, allkeys-lfu, volatile-ttl…; ডিফল্টে memory-সীমা নেই আর নীতি noeviction — cache হিসেবে দুটোই নিজে ঠিক করতে হয়
Persistenceমূল প্রকল্পে নেই (নতুন সংস্করণে flash-এ বাড়তি জায়গার মতো কিছু সুবিধা)RDB snapshot, AOF
Replication ও failoverমূল প্রকল্পে নেই — client বা proxy কপি রাখেasync replica; Sentinel বা Cluster-এ স্বয়ংক্রিয় failover
Key ভাগclient-side বা proxyRedis Cluster-এর hash slot, বা client/proxy
একটি item-এর সীমাডিফল্ট ১ MB, setting-এ বাড়ানো যায়একটি string সর্বোচ্চ ৫১২ MB
কখন মানায়বিশাল, সরল, শুধু-cache; অনেক coredata structure, counter, leaderboard, pub/sub — cache-এর বাইরের কাজও

চালানোর পাঁচ অভ্যাস

  • Connection pool: প্রতিটি app process প্রতিটি node-এর সাথে কয়েকটি স্থায়ী connection রাখে (connection pool), তাই প্রতিবার নতুন TCP বা TLS handshake লাগে না। হিসাব রাখুন: ২০০টি app process × প্রতি node-এ ৮টি = প্রতিটি node-এ ১,৬০০টি connection, ১২টি node-এ মোট ১৯,২০০; app দ্বিগুণ হলে connection-ও। Proxy বা multiplexing এই সংখ্যা কমায়।
  • Timeout — ধীর cache মৃত cache-এর চেয়ে খারাপ: cache-এর p99 সাধারণত ১ ms-এর আশেপাশে। Client-এর timeout ১ সেকেন্ড হলে একটি অসুস্থ node প্রতিটি অনুরোধকে ১ সেকেন্ড আটকায়, thread ফুরিয়ে যায়; মরে গেলে অন্তত সঙ্গে সঙ্গে ত্রুটি আসত। তাই ছোট timeout (কয়েক থেকে কয়েক দশক ms), node-ভিত্তিক circuit breaker (পর্ব ৪৮ — Resilience pattern), আর database-এ fallback সীমিত হারে; সীমার বাইরের অনুরোধ fail fast বা পুরোনো মান পায় (পর্ব ১৮ — Overload সামলানো)।
  • Eviction আর hit ratio-র নজর: সেকেন্ডে কত key evict হচ্ছে, hit ratio, ব্যবহৃত memory বনাম সীমা, প্রতি shard-এর অনুরোধ ও network. Eviction হঠাৎ বাড়া মানে working set বেড়েছে বা একটি big key ঢুকেছে; hit ratio এক ধাপ নামলেই database-এর চাপ লাফায়।
  • Memory fragmentation: নানা মাপের value বারবার লেখা-মোছায় RAM-এ ছোট ছোট ফাঁক তৈরি হয়, তাই process ডেটার চেয়ে বেশি RAM ধরে রাখে — একে বলে memory fragmentation. Redis-এর INFO memory-তে mem_fragmentation_ratio দেখুন; বেশি হলে active defrag বা পরিকল্পিত restart. Memcached-এ ভিন্ন রূপ: পুরোনো traffic অনুযায়ী slab-এর ভাগ আটকে থাকা। কোন অনুপাত “বেশি”, তা সংস্করণ আর workload-ভেদে।
  • নিরাপত্তা: cache কখনো ইন্টারনেটে খোলা নয় — private network, password বা ACL, দরকারে TLS.
# Illustrative only: a Redis node used purely as a cache. Check your version's docs.
maxmemory 44gb                  # leave headroom below the machine's RAM
maxmemory-policy allkeys-lfu    # evict any key, least frequently used first
save ""                         # no RDB snapshots: the database is the source of truth
appendonly no                   # no AOF either
lazyfree-lazy-eviction yes      # free evicted memory in a background thread
activedefrag yes                # background defragmentation (jemalloc builds)

আরও গভীরে: Facebook-এর “Scaling Memcache at Facebook” (Nishtala ও সহকর্মীরা, NSDI ২০১৩) এই পর্বের প্রায় প্রতিটি অংশের বাস্তব রূপ দেখায়: লেখার পর set নয়, delete; একসাথে অনেক key চাওয়ার ঢল (incast) থামাতে client-এ sliding window; miss-এ “lease” token দিয়ে পুরোনো set আর thundering herd ঠেকানো; gutter pool; নতুন ঠান্ডা cluster-কে database-এর বদলে একটি উষ্ণ cluster থেকে ভরা; আর region জুড়ে commit log থেকে invalidation. পরে Facebook-এর CacheLib paper (Berg ও সহকর্মীরা, OSDI ২০২০) বহু সার্ভিসের জন্য একটি সাধারণ caching engine-এর অভিজ্ঞতা বর্ণনা করে। এগুলো প্রকাশের সময়ের বিবরণ — আজকের ভেতরের নকশা আলাদা হতে পারে।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • N shard-এর একটি বিকল হলে database-এর চাপ কত গুণ? 1 + h/(N(1 − h)); h = ০.৯৫, N = ৬-এ ৪.১৭ গুণ।
  • MOVED আর ASK-এর পার্থক্য? MOVED: slot স্থায়ীভাবে সরেছে, map হালনাগাদ; ASK: migration চলছে, শুধু এবার নতুন node-এ ASKING-সহ, map অপরিবর্তিত।
  • Hot key কীভাবে সামলাবেন? Sampling বা top-k-এ চেনা; অল্প TTL-এর L1, কয়েকটি shard-এ কপি, coalescing, replica থেকে পড়া।
  • Cache-এর কি durability লাগে? সাধারণত না — database সত্যের উৎস; replica বা snapshot রাখা হয় উষ্ণতা আর availability-র জন্য।
  • কয়েক স্তরের cache-এ সবচেয়ে খারাপ staleness? সব TTL-এর যোগফল; invalidation দিয়ে কমানো, TTL নিরাপত্তা-জাল।

১০ · AI-এর যুগে — distributed cache-এর নকশা যাচাই

“সাইট ধীর, কী করব?” — AI প্রায় সবসময় বলে, “Redis cluster বসান।” পরামর্শটি ভুল নয়, কিন্তু অসম্পূর্ণ; distributed cache-এ AI-এর ভুলগুলো বেশ নির্দিষ্ট:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
হিসাব ছাড়াই “Redis, Kafka আর microservices যোগ করুন”, বা ভুল অঙ্ককোন সীমা কড়া, জানা নেই; overhead, headroom, কপি বাদ, bit আর byte গুলিয়ে ফেলাঅংশ ১-এর তিনটি সূত্র এককসহ নিজে চালান
Node বিকলের কথা নেই১/N ভাগ hit মুহূর্তে miss; database কয়েক গুণ চাপ পায়Failure-mode পর্যালোচনা: “একটি node মরলে database কত পায়?”
“Consistent hashing ভার সমান করে”Key-এর সংখ্যা সমান করে, traffic নয় — hot key থেকে যায়অংশ ৫-এর মতো Zipf simulation চালান
Cache-কে টেকসই ধরে নেওয়াEviction আর async replication-এ লেখা হারায়প্রশ্ন: এই key হারালে কোথা থেকে ফেরাব?
ACID আর CAP-এর “consistency” গুলিয়ে ফেলাCache-এর প্রশ্ন হলো কপিগুলো কতটা পুরোনো (CAP-এর অর্থ), ACID-এর নিয়ম-রক্ষা নয়প্রশ্ন করুন: কোন অর্থে, কত সেকেন্ড? (পর্ব ৪০)
পণ্যের সীমা আর দাম বানানোSlot-সংখ্যা, item-সীমা, ডিফল্ট setting সংস্করণভেদে; দাম প্রতিনিয়ত বদলায়অফিসিয়াল documentation আর প্রদানকারীর দামের পাতা
অতিরিক্ত স্তরছোট অ্যাপে পাঁচ স্তরের cache মানে পাঁচটি invalidation পথপ্রতিটি স্তর কোন মাপ কতটা কমায়, মেপে দেখুন

AI-কে কাজটি এমনভাবে দিন যাতে প্রতিটি সিদ্ধান্তের পেছনে হিসাব, failure আর বিকল্প দেখাতে হয়:

তুমি একজন অভিজ্ঞ backend ইঞ্জিনিয়ার। একটি খবরের সাইটের জন্য distributed cache-এর নকশা করো। তথ্য (ধরে নেওয়া): working set ২০ কোটি item, গড় key ৪০ byte, value ৯০০ byte; peak-এ সেকেন্ডে ৬ লাখ পড়া, hit ratio ৯৫%; database নিরাপদে সেকেন্ডে ৫০,০০০ পড়া নিতে পারে। খেলার দিনে একটি live score key-তে মোট পড়ার ৩০% আসে। ১. Shard আর node-এর সংখ্যা হিসাব করো — memory, throughput আর network তিনটিই; প্রতিটি অনুমান আলাদা করে লেখো। ২. Key কীভাবে ভাগ হবে (client, proxy না cluster) আর কেন। ৩. একটি node বিকল হলে database কত চাপ পাবে, সংখ্যায়; আর কীভাবে সামলাবে। ৪. Live score-এর hot key কীভাবে চিনবে আর সামলাবে; সর্বোচ্চ staleness কত সেকেন্ড। ৫. L1 রাখলে invalidation কীভাবে হবে, আর সব স্তর মিলে সবচেয়ে খারাপ staleness কত। কোনো product-এর সীমা বা ডিফল্ট বললে সূত্র দাও; নিশ্চিত না হলে বলো "জানি না"।

AI-এর cache নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট

AI দেয় দ্রুত খসড়া, যাচাইয়ের দায় আপনার।
Node গুনুন, বিকলের ধাক্কা মাপুন, গরম key খুঁজুন।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। সূত্র মুখস্থ নয়, কেন কোন নকশা বেছে নেওয়া হয় সেটাই আসল।

প্র ০১Facebook-এর memcache paper (NSDI ২০১৩) থেকে বড় cache চালানোর কোন শিক্ষাগুলো আজও খাটে?

“Scaling Memcache at Facebook” (Nishtala ও সহকর্মীরা, NSDI ২০১৩) একটি বিরল দলিল: বিশাল আকারে memcached চালানোর ভুল আর সমাধান খোলাখুলি লেখা। Paper-এ বর্ণিত নকশা (প্রকাশের সময়ের) থেকে কয়েকটি শিক্ষা:

  • লেখার পর set নয়, delete: database হালনাগাদের পর cache-এর key মোছা হতো। Delete বারবার করলেও একই ফল (idempotent), কিন্তু দুই লেখকের দুটি set উল্টো ক্রমে পৌঁছালে পুরোনো মান জিতে যেতে পারে।
  • Lease: miss-এর সময় cache একটি token দিত; পরে set গ্রহণ হতো কেবল সেই token-সহ, আর মাঝে delete এলে token বাতিল — ফলে পুরোনো ডেটা দিয়ে cache ভরা (stale set) আটকায়। একই key-এ token দেওয়ার হার সীমিত রেখে thundering herd-ও থামানো হতো: বাকিরা একটু অপেক্ষা করত বা সামান্য পুরোনো মান পেত।
  • Gutter আর rehash না করা: বিকল সার্ভারের key অল্প কয়েকটি অলস সার্ভারে, ছোট TTL-এ — অংশ ৪-এর stepper.
  • Region আর ঠান্ডা cluster: commit log পড়ে প্রতিটি region-এ invalidation; নতুন cluster ভরা হতো একটি উষ্ণ cluster থেকে, database থেকে নয়।
  • Proxy: mcrouter নামের একটি routing স্তর client আর সার্ভারের মাঝে — পরে open source হয়।
  • Incast: একটি পাতা শত শত key একসাথে চাইলে উত্তরগুলো একসাথে ফিরে network switch-এর buffer উপচে দেয়; তাই client-এ একটি sliding window দিয়ে একসাথে বাকি থাকা অনুরোধের সংখ্যা সীমিত রাখা হতো — TCP-র congestion window-এর মতো চিন্তা।

খেয়াল করুন, এর কোনোটিই memory-তে byte রাখার কৌশল নয়। সব কয়টি consistency, failure আর database-কে রক্ষার প্রশ্ন। এজন্যই আজকের Redis-ভিত্তিক নকশাতেও প্রশ্নগুলো একই: invalidation কোন ক্রমে, miss-এর ঢল কে থামাবে, বিকল node-এর key কোথায় যাবে।

মূল উপলব্ধি: একটি cache-কে দ্রুত বানানো সহজ; কঠিন হলো তাকে সঠিক রাখা আর তার বিভ্রাটে database-কে বাঁচানো। বড় সিস্টেমের প্রকাশিত paper পড়ুন সমস্যার তালিকার জন্য — সমাধান আপনার আকারে আলাদা হতে পারে।

প্র ০২Key-এর ঠিকানা কে ঠিক করবে — client library, proxy না cluster? twemproxy, mcrouter আর Redis Cluster আলাদা পথে গেল কেন?

তিনটি পথই বাস্তবে সফল, কারণ প্রতিটি আলাদা একটি সমস্যা আগে সমাধান করে।

  • twemproxy (nutcracker): Twitter ২০১২ নাগাদ open source করে — memcached আর Redis protocol বোঝা একটি হালকা proxy. হাজারো app process সরাসরি প্রতিটি cache সার্ভারে connection খুললে সার্ভারে connection-এর পাহাড় জমে; proxy সেগুলো জমিয়ে অল্প কয়েকটিতে চালায় আর consistent hashing নিজেই করে। দাম একটি বাড়তি hop.
  • mcrouter: Facebook ২০১৪ সালে open source করে। এটি শুধু ভাগ করে না — key-এর prefix দেখে আলাদা pool-এ পাঠানো, কপি-রাখা pool, বিকল হলে বিকল্প পথ, নতুন cluster উষ্ণ করা — এমন নিয়ম এক জায়গায় রাখে। নিয়ন্ত্রণ কেন্দ্রীয়, client সরল।
  • Redis Cluster: Redis 3.0 (২০১৫) থেকে — proxy ছাড়াই, node-রা নিজেরা slot-এর মালিকানা জানে আর redirect দিয়ে client-কে শেখায়। কোনো বাড়তি fleet নেই, কিন্তু cluster-aware client আর multi-key কাজে hash tag-এর শৃঙ্খলা লাগে।

বেছে নেওয়ার প্রশ্নগুলো: কয়টি ভাষায় client লেখা? (অনেক ভাষা হলে proxy একটি নিয়মকে এক জায়গায় রাখে।) latency-র বাজেট কত? (বাড়তি hop কি সহ্য হবে, নাকি sidecar হিসেবে একই host-এ রাখবেন?) failover আর resharding কে চালাবে — cluster নিজে, না আপনার দল? অনেক দল proxy-কে প্রতিটি host-এ sidecar হিসেবে চালায়, তাতে network hop প্রায় থাকে না, কিন্তু নিয়ম থাকে কেন্দ্রীয় config-এ। আরেকটি সতর্কতা: আলাদা proxy fleet নিজেই একটি নতুন single point of failure হতে পারে, তাই তাকেও কয়েকটি instance-এ health check-সহ চালাতে হয়; আর proxy-র সংস্করণ বদলানো মানে সব cache traffic-এর পথ বদলানো, তাই কাজটি পর্ব ৫৩-এর canary-র মতো ধাপে ধাপে, অল্প traffic দিয়ে শুরু করতে হয়।

মূল উপলব্ধি: “routing-এর বুদ্ধি কোথায় থাকবে” — client-এ, মাঝখানে, না server-এ — এটি latency, নিয়ন্ত্রণ আর client-এর জটিলতার মধ্যে একটি দর-কষাকষি। পর্ব ৪৭-এর service mesh-এ ঠিক একই প্রশ্ন আবার দেখবেন।

প্র ০৩Cache কি কখনো সত্যের উৎস হতে পারে? Session, rate-limit counter বা leaderboard শুধু Redis-এ রাখা কতটা নিরাপদ?

উত্তর নির্ভর করে একটি প্রশ্নে: এই ডেটা হারালে কী হবে, আর কোথা থেকে ফেরাব? ডেটাকে তিন ভাগে ভাবুন।

  • আবার বানানো যায়: database-এর কোনো ফলের কপি — খাঁটি cache. হারালে শুধু ধীরতা।
  • হারালে বিরক্তি, ক্ষতি নয়: session (আবার login — পর্ব ১৫ — Stateless বনাম stateful), rate-limit-এর counter (কয়েক সেকেন্ড সীমা একটু ঢিলা), সাময়িক leaderboard (event থেকে আবার গোনা যায়)। এগুলো শুধু Redis-এ রাখা প্রচলিত ও যুক্তিসংগত — যদি হারানোর ফল মেনে নেওয়া হয়ে থাকে।
  • হারানো চলে না: টাকা, অর্ডার, বার্তা। এগুলোর সত্যের উৎস একটি টেকসই database হওয়া উচিত।

দ্বিতীয় ভাগের ডেটা Redis-এ রাখলে তাকে database-এর মতো চালান: আলাদা cluster (cache-এর allkeys-lfu নিয়ম যেন তাকে না মোছে, তাই noeviction), AOF-এ প্রতি সেকেন্ডে fsync (তাতে মোটামুটি শেষ এক সেকেন্ডের লেখা হারানোর ঝুঁকি), replica আর backup. তবু মনে রাখুন, replication asynchronous; Redis Cluster-এর documentation নিজেই বলে network ভাগ হলে স্বীকৃত লেখাও হারাতে পারে, আর WAIT এই জানালা ছোট করে কিন্তু strong consistency দেয় না। কিছু managed সেবা Redis-সামঞ্জস্যপূর্ণ API-র নিচে আলাদা টেকসই log রাখার দাবি করে — সেই দাবি তাদের documentation-এ যাচাই করুন, আর latency-র দাম মিলিয়ে দেখুন। একটি ব্যবহারিক পরীক্ষা: staging-এ cache cluster পুরো খালি করে বা একটি node বন্ধ করে দেখুন কোন feature ভাঙে। যা ভাঙে, তার ডেটা আসলে database-এ থাকার কথা ছিল — শুধু ধীর হওয়া ঠিক আছে, ভুল বা হারানো নয়।

মূল উপলব্ধি: যে cache হারানো চলে না, সেটি আর cache নয় — একটি database, যার durability-র নিশ্চয়তা আপনাকে মেপে বুঝে নিতে হবে। নাম নয়, হারানোর ফল দেখে সিদ্ধান্ত নিন।

প্র ০৪ধীর cache কেন মৃত cache-এর চেয়েও খারাপ? Timeout আর fallback কীভাবে ঠিক করবেন?

মৃত node-এ connection সঙ্গে সঙ্গে প্রত্যাখ্যাত হয় — client জানে, database-এ যায় বা ত্রুটি দেখায়। ধীর node উত্তরই দেয় না, তাই প্রতিটি অনুরোধ পুরো timeout অপেক্ষা করে। পর্ব ৪-এর Little-এর সূত্রে হিসাব করুন: একটি app server সেকেন্ডে ৫০০ অনুরোধ সামলায়, ৫০টি worker thread নিয়ে। ছয়টি shard-এর একটি ধীর হলে তার ভাগের প্রায় ৮৩টি অনুরোধ প্রতি সেকেন্ডে ১ সেকেন্ড করে আটকায় — একসাথে প্রায় ৮৩টি thread অপেক্ষায়, অথচ আছে ৫০টি। পুরো app server থেমে গেল, এমনকি যে অনুরোধ ধীর shard ছোঁয়ই না সেগুলোও। Timeout ২০ ms হলে একই হিসাবে মাত্র প্রায় ১.৭টি thread আটকায়।

তাই নিয়মগুলো: cache-এর timeout তার স্বাভাবিক p99.9-এর কাছে, database-এর latency-র চেয়ে অনেক কম; প্রতিটি node-এর জন্য circuit breaker, যাতে অসুস্থ node-কে কিছুক্ষণ ডাকাই না হয়; fallback-এ database পর্যন্ত একটি concurrency limit; আর প্রতিটি dependency-র আলাদা connection pool (bulkhead), যাতে একটির সমস্যা সবার সম্পদ না খায়। Fallback-এর সীমা কত হবে? Database-এর নিরাপদ ক্ষমতা থেকে স্বাভাবিক miss-এর চাপ বাদ দিলে যা থাকে, ঠিক সেটুকু — অংশ ১-এর সূত্রে আগে থেকেই হিসাব করে রাখা যায়। Amazon-এর প্রকাশিত Builders' Library-র একটি লেখা (“Caching challenges and strategies”) সতর্ক করে যে cache থাকা আর না থাকা অবস্থায় সার্ভিসের আচরণ সম্পূর্ণ আলাদা — তাই cache ছাড়া বা ঠান্ডা cache-এ চাপ সহ্য করার ক্ষমতা আলাদা করে পরীক্ষা করা দরকার। নইলে database একবার ডুবলে cache ভরার অনুরোধগুলোও ব্যর্থ হয়, আর সিস্টেম খারাপ অবস্থায় আটকে থাকে — metastable failure.

মূল উপলব্ধি: cache কাগজে একটি “গতি বাড়ানোর” অংশ, কিন্তু বাস্তবে ক্ষমতার ভিত্তি। তার খারাপ দিনের নকশা — ধীর, ঠান্ডা, অর্ধেক মৃত — আগে থেকেই করুন, আর load test-এ সেই দিনটি নিজে তৈরি করে দেখুন।

প্র ০৫কয়েক স্তরের cache-এ একটি মান বদলালে সবাই কত দ্রুত নতুনটি দেখবে? Cache invalidation-কে কেন “কঠিন” বলা হয়?

একটি প্রচলিত উক্তি, যা সাধারণত Phil Karlton-এর বলে ধরা হয়: কম্পিউটার বিজ্ঞানে দুটিই কঠিন কাজ — cache invalidation আর নাম দেওয়া। কঠিন কেন, তা এই পর্বেই দেখেছেন:

  • কপির সংখ্যা: একটি মান browser, CDN, gateway, প্রতিটি app server-এর L1 আর L2-এ — কোনোটি (browser) বাইরে থেকে মোছাই যায় না।
  • ক্রমের দৌড়: পাঠক database থেকে পুরোনো মান পড়লেন, এর মধ্যে লেখক database বদলে key মুছলেন, তারপর পাঠক পুরোনো মানটি cache-এ বসালেন — মোছার পরেও পুরোনো মান, TTL পর্যন্ত। Region জুড়ে replica-র lag একই দৌড় তৈরি করে (অংশ ৮)।
  • হারানো বার্তা: pub/sub-এর মতো ব্যবস্থায় সেই মুহূর্তে সংযুক্ত না থাকা subscriber বার্তা পায় না; failover-এ স্বীকৃত DEL-ও হারাতে পারে (অংশ ৩)।
  • যোগফলের সীমা: invalidation না থাকলে সবচেয়ে খারাপ staleness সব TTL-এর যোগফল (অংশ ৭)।

কাজের কৌশলগুলো: প্রতিটি ধরনের ডেটার staleness-এর বাজেট আগে লেখা; key-এর নামে version (যেমন product:42:v17) — মান বদলালে নতুন key, পুরোনোটি আপনিই মেয়াদে মরে, যা CDN-এ cache busting-এর মতোই (পর্ব ১৭); invalidation আর TTL একসাথে; lease বা version-তুলনা দিয়ে পুরোনো set ঠেকানো; আর যে পড়া অবশ্যই সর্বশেষ হতে হবে (যেমন টাকার balance), তা cache এড়িয়ে সরাসরি leader থেকে। একই ডেটার বাজেটও পাতাভেদে আলাদা হতে পারে: একটি পণ্যের দাম বদলানোর পর তালিকার পাতায় ৯৫ সেকেন্ড পুরোনো দাম হয়তো চলে, কিন্তু checkout-এর পাতায় নয় — সেখানে দাম database থেকে নতুন করে যাচাই করা হয়।

মূল উপলব্ধি: “cache কি সঠিক?” প্রশ্নটি ভুল; ঠিক প্রশ্ন দুটি — “কতটা পুরোনো চলবে?” আর “সবচেয়ে খারাপ ক্ষেত্রে কতটা পুরোনো হতে পারে?” দ্বিতীয় সংখ্যাটিকে প্রথমটির মধ্যে রাখাই নকশার আসল কাজ।

অনুশীলন

আগে নিজে হিসাব করুন বা লিখুন, তারপর উত্তর মেলান। শেষ প্রশ্নে আগে অনুমান, তারপর অংশ ৭-এর প্রোগ্রাম বদলে চালিয়ে যাচাই।

  1. হিসাব — একটি cache cluster-এর আকার: working set ১২ কোটি item; গড় key ৩০ byte, value ১,২০০ byte, প্রতি item-এ overhead ৭০ byte (ধরা যাক); প্রতিটি shard-এ ৩টি কপি (primary + ২টি replica); প্রতিটি node-এ ৩২ GB RAM, যার ৭০% ব্যবহারযোগ্য; peak-এ সেকেন্ডে ৯ লাখ পড়া, প্রতিটি primary নিরাপদে ১,৫০,০০০. (ক) memory আর throughput অনুযায়ী কয়টি shard? (খ) প্রতিটি node-এর network card 1 Gbit/s, আর আপনি ৭০%-এর বেশি ব্যবহার করতে চান না — এবার কয়টি shard, কয়টি node? (গ) 10 Gbit/s-এর card হলে?
    • (ক) প্রতি item ৩০ + ১,২০০ + ৭০ = ১,৩০০ byte; এক কপি = ১২ কোটি × ১,৩০০ = ১৫৬ GB. ব্যবহারযোগ্য প্রতি node ৩২ × ০.৭ = ২২.৪ GB; ১৫৬ ÷ ২২.৪ ≈ ৬.৯৬, তাই memory বলে ৭টি shard. Throughput: ৯,০০,০০০ ÷ ১,৫০,০০০ = ৬টি. বড়টি ৭ — এখানে সীমা memory.
    • (খ) প্রতি পড়ায় key + value = ১,২৩০ byte: ৯,০০,০০০ × ১,২৩০ × ৮ ≈ ৮.৮৬ Gbit/s মোট। ৭টি shard-এ প্রতিটি primary প্রায় ১.২৭ Gbit/s — card-এর পুরো ক্ষমতারও বেশি! ৮.৮৬ ÷ (১ × ০.৭) ≈ ১২.৬৫, তাই ১৩টি shard, ৩ কপিতে ৩৯টি node; প্রতিটিতে ডেটা মাত্র ১২ GB — সীমা network. (Replica থেকেও পড়লে চাপ ভাগ হয়, lag মেনে।)
    • (গ) 10 Gbit/s-এ network-এর সীমা ⌈৮.৮৬ ÷ ৭⌉ = ২টি shard — তখন আবার memory-ই সীমা: ৭টি shard × ৩ = ২১টি node. শিক্ষা: তিনটি সীমাই হিসাব করুন; কোনটি কড়া, তা মেশিনের ধরনের ওপর নির্ভর করে।
  2. হিসাব আর নকশা — একটি node হারানোর ধাক্কা: একটি cache cluster-এ ৮টি shard, কোনো replica নেই; peak-এ সেকেন্ডে ৪ লাখ পড়া, hit ratio ৯৭%, key সমানভাবে ছড়ানো; database নিরাপদে নিতে পারে সেকেন্ডে ৪০,০০০. (ক) স্বাভাবিক অবস্থায় database কত পায়? (খ) একটি shard মরলে? (গ) কোনো প্রতিকার ছাড়া database নিরাপদ রাখতে অন্তত কয়টি shard লাগত? (ঘ) কোন প্রতিকারগুলো বেছে নেবেন, আর কেন?
    • (ক) ৪,০০,০০০ × ০.০৩ = ১২,০০০/s.
    • (খ) বাড়তি ৪,০০,০০০ × ০.৯৭ ÷ ৮ = ৪৮,৫০০; মোট ৬০,৫০০ — স্বাভাবিকের প্রায় ৫.০৪ গুণ, ক্ষমতার ১৫১%. সূত্রে 1 + 0.97/(8 × 0.03) ≈ 5.04.
    • (গ) ১২,০০০ + ৩,৮৮,০০০/N ≤ ৪০,০০০ → N ≥ ৩,৮৮,০০০ ÷ ২৮,০০০ ≈ ১৩.৮৬, তাই অন্তত ১৪টি shard — শুধু ছোট blast radius-এর জন্য প্রায় দ্বিগুণ node; সাধারণত সস্তা পথ নয়।
    • (ঘ) প্রথমে প্রতিটি shard-এ একটি replica: ধাক্কা নামে বিকল টের পাওয়ার কয়েক সেকেন্ডে। কিন্তু ১৫ সেকেন্ডের detection-এও ৪৮,৫০০ × ১৫ ≈ ৭.৩ লাখ বাড়তি পড়া, তাই সাথে database পর্যন্ত fallback-এ concurrency limit আর coalescing; সীমার বাইরে অল্প পুরোনো মান বা দ্রুত ত্রুটি। বড় fleet-এ gutter pool: gutter-এর hit ৯০% হলে database = বাকি ৭টির ৭ × ১,৫০০ + ৫০,০০০ × ০.১ = ১০,৫০০ + ৫,০০০ = ১৫,৫০০. আর rehash নয় — অংশ ৪-এর কারণে।
  3. নকশা — একটি live cricket score পাতার hot key: একটি কাল্পনিক খবরের সাইটে ফাইনালের দিন কয়েক লাখ মানুষ score-এর পাতা খোলা রাখেন, পাতাটি প্রতি কয়েক সেকেন্ডে হালনাগাদ চায়। Score বদলায় প্রতি বলে (প্রায় ৩০–৪০ সেকেন্ডে একবার)। Database থেকে browser পর্যন্ত hot key সামলানোর একটি নকশা লিখুন — স্তর, TTL, invalidation আর failure-এর পরিকল্পনাসহ।
    • Staleness-এর বাজেট আগে: live score-এ কয়েক সেকেন্ড পুরোনো চলে; ধরা যাক সর্বোচ্চ ৫ সেকেন্ড। সব স্তরের TTL-এর যোগফল এর মধ্যে রাখতে হবে।
    • Browser আর CDN: public পাতা বা JSON-এ CDN-এ ১–২ সেকেন্ডের micro-caching আর একই URL-এর একসাথে আসা miss জোড়া লাগানো (পর্ব ১৭)। তাহলে origin প্রতিটি edge সার্ভার থেকে মোটামুটি প্রতি TTL-এ একটি অনুরোধ পায়, দর্শক যত লাখই হোন। আরও ভালো: বারবার জিজ্ঞাসার (polling) বদলে SSE বা WebSocket-এ বদল পাঠানো (পর্ব ১০ — Real-time যোগাযোগ)।
    • App server: score key-এর L1, TTL ১ সেকেন্ড, সাথে jitter আর coalescing; score বদলালে pub/sub-এ invalidation.
    • L2: key-টির কয়েকটি কপি (#০ … #৩) যাচাই করা আলাদা shard-এ, L1-হীন client-দের জন্য; লেখার সময় updater সব কপি লেখে। Score-এর উৎস database-এ, আর প্রতিটি বলের ঘটনা একটি event — cache নষ্ট হলে আবার বানানো যায়।
    • যোগফল: CDN ২ + L1 ১ + L2-এর কপি-লেখার ফাঁক — ৫ সেকেন্ডের বাজেটের ভেতরে।
    • Failure: কপির একটি shard মরলে বাকি কপি থেকে পড়া; L2 ধীর হলে L1 আর CDN-এর পুরোনো মান (stale-if-error), database-এ fallback সীমিত। Detection চালু রাখুন — দ্বিতীয় hot key আসবেই।
  4. আউটপুট অনুমান করুন: অংশ ৭-এর two_tier_cache_sim.js-এ L1TTL = 2-কে L1TTL = 5 করলে L1hit, total, L2 r/s, DB/s, stale আর window কোন দিকে যাবে? “TTL only”-র window কি ৫ সেকেন্ডের কাছে যাবে? আগে অনুমান লিখুন, তারপর চালিয়ে মেলান।

    অনুমান: TTL বড় মানে L1-এর কপি বেশিক্ষণ বাঁচে — L1hit বাড়বে, L2 r/s কমবে। Total আর DB/s বদলাবে না, কারণ L1-এ যা আছে তা L2-তেও আছে। TTL only-তে পুরোনো পড়া বাড়বে, window বাড়বে, কিন্তু ৫ সেকেন্ড একটি ঊর্ধ্বসীমা মাত্র। pub/sub-এর সারি বদলাবে না; যে সার্ভার বার্তা হারিয়েছে তার window বাড়বে।

    আসল আউটপুট:

    mode                L1hit  total  L2 r/s  DB/s  stale  window
    TTL only            34.4%  79.9%     656   201    140    2321 ms
    pub/sub             34.4%  79.9%     656   201      1      20 ms
    pub/sub, 1 missed   34.4%  79.9%     656   201     21    1380 ms

    L1hit ৩২.০% থেকে মাত্র ৩৪.৪% — জনপ্রিয় key-গুলো ২ সেকেন্ডেই বারবার পড়া হচ্ছিল, তাই বাড়তি TTL-এর লাভ সামান্য; L2 r/s ৬৮০ থেকে ৬৫৬. পুরোনো পড়া ১০৭ থেকে ১৪০, window ১,৪২৬ থেকে ২,৩২১ ms — ৫ সেকেন্ড নয়। কারণ: দশটি সার্ভারই ০ সেকেন্ডে একসাথে চালু হয়েছিল, তাই তাদের refresh-এর সময় কাছাকাছি, আর update-এর মুহূর্তে কারও কপিরই ৫ সেকেন্ড বাকি ছিল না। আলাদা সময়ে চালু হলে window ৫ সেকেন্ডের কাছে যেত — তাই প্রতিশ্রুতি দিন ঊর্ধ্বসীমা দিয়ে, একটি run-এর সংখ্যা দিয়ে নয়। pub/sub আগের মতোই; বার্তা হারানো সার্ভারের ২১টি পুরোনো পড়া, ১,৩৮০ ms. লেনদেন: L2-এর ৩.৫% traffic বাঁচল, কিন্তু staleness-এর সীমা আড়াই গুণ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি পড়ার অনুরোধ ব্যবহারকারী থেকে database পর্যন্ত যে cache-স্তরগুলো পেরোয়, সেগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🌐 Browser-এর নিজের cache (Cache-Control)
  2. 🛰️ CDN-এর edge সার্ভার
  3. 🚪 API gateway বা edge cache
  4. 🧠 App server-এর in-process L1
  5. 🗄️ Distributed L2 cache cluster
  6. 💾 Database — আগে buffer pool, তারপর disk

একটি cache cluster-এ ৮টি shard, কোনো replica নেই। Peak-এ সেকেন্ডে ৪,০০,০০০ পড়া, hit ratio ৯৫%, key সমানভাবে ছড়ানো। একটি shard হঠাৎ বন্ধ হলে database-এ সেকেন্ডে মোটামুটি কত পড়া যাবে?

  • ২০,০০০
  • ৫০,০০০
  • ৬৭,৫০০
  • ৪,০০,০০০

Redis Cluster-ধাঁচের একটি cluster-এ client একটি key চাইলে উত্তর পেল -ASK 12984 10.0.0.4:6379. Client-এর জন্য ঠিক কাজ কোনটি?

  • Slot map-এ 12984-এর মালিক স্থায়ীভাবে বদলে দেওয়া
  • নতুন node-এ আগে ASKING পাঠিয়ে শুধু এই অনুরোধটি সেখানে পাঠানো, map না বদলানো
  • একটু অপেক্ষা করে একই node-এ আবার চেষ্টা করা
  • Cache বাদ দিয়ে সরাসরি database থেকে পড়া

৫০টি app server-এর প্রতিটিতে একটি hot key-এর L1 কপি, TTL ১ সেকেন্ড, প্রতিটি সার্ভারে coalescing চালু। Key-টিতে মোট সেকেন্ডে ৮০,০০০ পড়া আসে। Distributed cache-এ এই key-এর জন্য সেকেন্ডে মোটামুটি কত অনুরোধ যাবে?

  • প্রায় ৫০
  • প্রায় ৮০,০০০
  • প্রায় ১,৬০০
  • প্রায় ৪০,০০০

একটি cache shard-এর primary একটি key মুছে (DEL) client-কে OK বলল। Asynchronous replication-এ মোছাটি replica-তে পৌঁছানোর আগেই primary বিকল হলো, আর replica নতুন primary হলো। এরপর কী ঘটতে পারে?

  • কিছুই না — OK পাওয়া মানে replica-তেও মোছা হয়ে গেছে
  • নতুন primary database থেকে নিজেই নতুন মান এনে নেবে
  • পুরো shard খালি হয়ে যাবে
  • পুরোনো মানটি আবার পড়া যাবে, যতক্ষণ না তার TTL শেষ হয়

একটি পাতার উত্তর তিন স্তর পেরিয়ে আসে: CDN-এ TTL ৩০ সেকেন্ড, app server-এর L1-এ ৫ সেকেন্ড, distributed cache-এ ৬০ সেকেন্ড; কোনো invalidation নেই। Database-এ মান বদলানোর পর সবচেয়ে খারাপ ক্ষেত্রে কতক্ষণ পুরোনো মান দেখা যেতে পারে?

  • ৬০ সেকেন্ড
  • ৯৫ সেকেন্ড
  • ৩০ সেকেন্ড
  • ৫ সেকেন্ড

স্বাভাবিকভাবে cache-এর p99 ১ ms আর database-এর ২০ ms. একদিন একটি cache node খুব ধীর হয়ে গেল (মরেনি), আর client-এর cache timeout ২ সেকেন্ড। App server-এর thread দ্রুত ফুরিয়ে যাচ্ছে। সবচেয়ে ভালো স্থায়ী প্রতিকার কোনটি?

  • Timeout বাড়িয়ে ৫ সেকেন্ড করা, যাতে ধীর node-ও উত্তর দিতে পারে
  • প্রতিটি cache অনুরোধ ৩ বার retry করা
  • Cache-এর timeout অল্প (কয়েক দশক ms) রাখা, node-ভিত্তিক circuit breaker, আর timeout হলে সীমিত হারে database-এ যাওয়া
  • Database restart করা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. একটি cache সার্ভার ছাড়ার তিন কারণ: memory, throughput, availability. Shard = memory, throughput আর network-এর সবচেয়ে কড়া সীমা; node = shard × কপি।
  2. Key-এর ঠিকানা: client-side hashing, hash slot-এর cluster (১৬,৩৮৪ slot; MOVED স্থায়ী, ASK সাময়িক), বা proxy. Multi-key কাজে hash tag, সাবধানে।
  3. Cache-এর replication asynchronous — failover-এ হারানো DEL পুরোনো মান ফেরায়, তাই সবসময় TTL. Cache সত্যের উৎস নয়; হারানো না চললে সেটি database.
  4. একটি node মরলে database পায় 1 + h/(N(1 − h)) গুণ; rehash নয় — replica, gutter pool, সীমিত fallback আর coalescing. ধীর cache মৃত cache-এর চেয়ে খারাপ: ছোট timeout আর circuit breaker.
  5. Hot key: চেনা, L1, কপি, coalescing; big key: ভাঙা। বহু স্তরের সবচেয়ে খারাপ staleness = TTL-এর যোগফল; L1-এ pub/sub invalidation + ছোট TTL; region-এ invalidation চলুক replication stream থেকে।

পরের পর্বে: মডিউল ৫ এখানে শেষ — cache-এর কৌশল, eviction আর distributed cache. মডিউল ৬-এ messaging: পর্ব ৩১-এ দেখবেন Message queue — producer, consumer, acknowledgement, retry আর dead letter queue — কাজকে পরে করার জন্য সারিতে রাখা, আর বার্তা হারানো বা দুবার পৌঁছানো ঠেকানো। আর পর্ব ৬১-এর ক্যাপস্টোনে এই পর্বের slot, replica আর failover দিয়ে নিজেই একটি distributed key-value store নকশা করবেন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান