এই পর্বে যা শিখবেন
- কখন ডেটা ভাগ করতে হয়, partitioning আর replication কীভাবে একসাথে কাজ করে, আর request কীভাবে ঠিক shard-এ পৌঁছায়
- Shard key বাছাইয়ের চার শর্ত, আর range, hash, compound key, directory ও geo — কোন নিয়ম কখন
- Hot spot আর skew চেনা, salting-এর হিসাব আর hot partition ভেঙে সরানো
- নির্দিষ্ট সংখ্যক partition, dynamic split আর consistent hashing দিয়ে rebalancing; চালু সিস্টেমে dual write আর backfill-এ resharding
- Local বনাম global secondary index, scatter-gather-এর দাম, আর UUID v4, UUID v7, Snowflake ও ticket server দিয়ে unique ID
১ · কেন ভাগ করবেন — একটি মেশিনে যখন আর আঁটে না
একটি কাল্পনিক chat অ্যাপের কথা ভাবুন। শুরুতে সব message একটি database সার্ভারে। ব্যবহারকারী বাড়তে বাড়তে একদিন তিনটি দেয়ালে ধাক্কা: disk ভরে আসছে (table কয়েক TB); প্রতি সেকেন্ডে হাজারো নতুন message, সব লেখা একটিমাত্র সার্ভারে; আর backup ও restore-এ এখন ঘণ্টার পর ঘণ্টা।
প্রথম চেষ্টা সাধারণত বড় মেশিন — vertical scaling, পর্ব ১৩ — Vertical বনাম horizontal scaling-এ। তারপর কয়েকটি read replica — পর্ব ২২ — Replication-এ। কিন্তু replica শুধু পড়া ভাগ করে; প্রতিটিতে পুরো ডেটা, আর প্রতিটি লেখা সবগুলোতে পৌঁছায়। ডেটার আকার আর লেখার চাপ যখন একটি মেশিনের সীমা ছাড়ায়, তখন একটাই পথ: ডেটাকেই ভাগ করা।
বড় ডাকঘরে সব চিঠি এক টেবিলে বাছাই হলে লাইন বাড়তেই থাকে। তাই পোস্ট কোড অনুযায়ী আলাদা টেবিল, প্রতিটিতে আলাদা কর্মী; প্রতিটি চিঠি ঠিক একটি টেবিলে, আর দরজার একজন নিয়ম দেখে বলে দেন কোন টেবিল। প্রতিটি টেবিলের রেজিস্টারের ফটোকপি পাশের ঘরে, যাতে একজন অসুস্থ হলেও কাজ না থামে। টেবিলে ভাগ হলো partitioning, ফটোকপি replication, দরজার মানুষটি routing.
Partitioning মানে ডেটাকে এমনভাবে টুকরো করা যাতে প্রতিটি record (সারি, document বা key) ঠিক একটি টুকরোর অংশ হয়। টুকরোগুলো আলাদা মেশিনে বসলে সাধারণত তাকে sharding বলে। এখানে ভাগ হচ্ছে সারি ধরে (horizontal partitioning); column ধরে ভাগ (vertical partitioning) আর database তত্ত্বের fragmentation আছে DBMS কোর্সের Distributed database — partitioning, sharding আর CAP পর্বে।
নামের গোলমাল: একই ধারণার নাম সিস্টেমভেদে আলাদা — MongoDB আর Elasticsearch-এ shard, HBase-এ region, Bigtable-এ tablet, Cassandra-য় token range, DynamoDB আর Kafka-য় partition. আবার PostgreSQL-এর “table partitioning” সাধারণত একই সার্ভারের ভেতরে একটি table-কে কয়েক ভাগ করা — মেশিন একটাই। এই পর্বে partition আর shard প্রায় একই অর্থে।
বাস্তবে partitioning আর replication একসাথে থাকে: ডেটা ভাগ হয় কয়েকটি shard-এ, আর প্রতিটি shard-এর থাকে একটি leader আর কয়েকটি replica. একটি মেশিনে সাধারণত কয়েকটি shard-এর কপি থাকে — কোনোটির leader, কোনোটির replica. নিচের ছবিতে পুরো চিত্রটি; এই পর্বের প্রায় প্রতিটি অংশ এর কোনো একটি টুকরো নিয়ে।
Router, partition map আর replica-সহ shard
Router looks up key → partition → shard in a versioned partition map; each shard is a leader with replicas
hash(conv:42) mod 256 = 128), তারপর partition map দেখে জানে P128 আছে S3-এ, আর অনুরোধ পাঠায় S3-এর leader-এ। Map-এর মূল কপি coordination service-এ (পর্ব ৪৩), router রাখে তার একটি কপি; প্রতিটি shard-এ leader আর দুটি replica.পান: ডেটা, লেখা আর পড়ার ক্ষমতা মোটামুটি node-এর সংখ্যার সাথে বাড়ে, কারণ প্রতিটি shard নিজের ডেটা নিজেই সামলায় (shared-nothing); একটি shard বিকল হলে শুধু তার অংশ প্রভাবিত। দেন: একাধিক shard ছোঁয়া query ধীর, shard জুড়ে transaction কঠিন, আর ডেটা সরানো, monitoring ও backup-এ কাজ বাড়ে। তাই আগে হিসাব করুন (পর্ব ৩ — Back-of-the-envelope হিসাব): একটি বড় মেশিন আর কয়েকটি replica যদি আগামী কয়েক বছর চালায়, এখনই sharding করা প্রায়ই over-engineering.
২ · Shard key — সবচেয়ে বড় সিদ্ধান্ত
কোন record কোন partition-এ যাবে, তা ঠিক করে record-এর এক বা একাধিক field — এটিই shard key, অনেক সিস্টেমে partition key. ভার সমান হবে কি না, query কয়টি shard ছোঁবে, hot spot হবে কি না — সবই এর ফল; আর বদলাতে হলে পুরো ডেটা নতুন করে ভাগ (অংশ ৮)। চারটি শর্ত:
১ · অনেক ভিন্ন মান
ভিন্ন মানের সংখ্যা (cardinality) বেশি হতে হবে। “দেশ” দিয়ে ভাগ করলে মান কয়েকটি — shard-ও তার বেশি নয়।
ভালো: user_id · খারাপ: country, status২ · সমান ছড়ানো
ডেটা আর অনুরোধ দুটোই মানগুলোর মধ্যে মোটামুটি সমান ছড়াতে হবে। একজন বিখ্যাত ব্যবহারকারী এক মানেই অনেক ভার আনেন।
ঝুঁকি: celebrity, বড় tenant৩ · Query এক shard-এ
সবচেয়ে ঘন query-র শর্তে shard key থাকলে সেটি একটি shard-এই শেষ হয়। না থাকলে প্রতিটি query সব shard-এ যায় (অংশ ৭)।
প্রশ্ন: কোন query সবচেয়ে বেশি?৪ · বৃদ্ধিতে টেকে
সময়ের সাথে ক্রমাগত বাড়ে এমন key (timestamp, auto-increment) range-এ ভাগ করলে সব নতুন লেখা এক জায়গায়। একটি tenant বছরে দশ গুণ হলে তার shard-ও।
ভাবুন: ৩ বছর পরে কী হবেনিচের stepper-এ chat অ্যাপের messages table-এর জন্য চারটি প্রার্থী key একে একে যাচাই করা হলো — আগে খারাপগুলো, কেন খারাপ, তারপর ভালোটি।
| shard key | |
| লেখা ছড়ায়? | |
| একটি conversation খুলতে | |
| রায় |
- আগে query, পরে key. Table-এ পাঁচটি field. সবচেয়ে ঘন কাজ দুটি: একটি message পাঠানো (লেখা), আর একটি conversation খুললে শেষ ৫০টি message দেখানো (পড়া)। Shard key বাছার আগে এই access pattern কাগজে লিখে ফেলুন।
- created_at দিয়ে range — বাতিল। সময় শুধু সামনে যায়, তাই আজকের সব message শেষ range-এর shard-এ; বাকি ১৫টি প্রায় অলস — ভাগের উদ্দেশ্যটাই হারাল।
- message_id দিয়ে hash — বাতিল। লেখা এবার সমান, কিন্তু একটি conversation-এর message ১৬টি shard-এ ছড়ানো — প্রতিটি conversation খুলতে ১৬টি অনুরোধ আর সবচেয়ে ধীরজনের অপেক্ষা (অংশ ৭)। সবচেয়ে ঘন পড়াটাই সবচেয়ে দামি হলো।
- sender_id দিয়ে hash — বাতিল। একই conversation-এর message প্রেরক অনুযায়ী ছড়ায়: দুজনের chat-এ ২টি shard, বড় group-এ প্রায় সবগুলো। আর একটি bot বা বিখ্যাত অ্যাকাউন্ট লাখো message পাঠালে তার shard গরম হয়।
- conversation_id দিয়ে hash — ভালো। কোটি conversation, তাই cardinality যথেষ্ট আর লেখা ছড়ায়। একটি conversation-এর সব message এক shard-এ, আর shard-এর ভেতরে message_id-এর ক্রমে সাজানো, তাই “শেষ ৫০টি” একটিমাত্র range scan. বাকি ঝুঁকি: লাখো সদস্যের একটি channel-এর partition সীমাহীন বড় আর গরম হতে পারে।
- (conversation_id, bucket) — বড় conversation-ও ভাগ। bucket মানে সময়ের একটি জানালা, যেমন কয়েক দিন; বড় channel-এর ইতিহাস কয়েকটি partition-এ ভাগ হয়, আর সাম্প্রতিক message সাধারণত একটিতেই। Discord-এর প্রকাশিত engineering blog (২০১৭) অনুযায়ী তাঁরা Cassandra-তে channel আর সময়ের bucket মিলিয়ে partition key বানিয়েছিলেন। “একজন ব্যবহারকারীর সব conversation”-এর তালিকা আলাদা table, যার shard key user_id — প্রতিটি table-এর key তার নিজের query দেখে।
শেষ ধাপের key-টির দুটি অংশ: partition ঠিক করে প্রথম অংশ, আর partition-এর ভেতরে সারির ক্রম ঠিক করে দ্বিতীয় অংশ (clustering key). Cassandra-ধাঁচের database-এ লেখাটি এমন:
CREATE TABLE messages (
conversation_id bigint,
bucket int, -- time window, splits very large conversations
message_id bigint, -- Snowflake-style id, grows with time
sender_id bigint,
body text,
PRIMARY KEY ((conversation_id, bucket), message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);ভেতরের বন্ধনী (conversation_id, bucket) হলো partition key — এর hash ঠিক করে কোন node. বাইরের message_id ভেতরে নতুন থেকে পুরোনো ক্রমে সাজায় — একটি composite key-এর দুই অংশের দুই কাজ।
৩ · ভাগের নিয়ম — range, hash, compound key, directory আর geo
Shard key ঠিক হলো; এবার নিয়ম — key-এর কোন মান কোন partition-এ। পাঁচটি প্রচলিত নিয়ম আছে, আর প্রতিটির সুবিধা অন্যটির দুর্বলতা।
Range partitioning — অভিধানের খণ্ডের মতো
Range partitioning-এ key-গুলো সাজানো ক্রমে পরপর পরিসরে ভাগ হয় — অভিধানের খণ্ডের মতো: “ক–ঘ” এক খণ্ডে, “ঙ–ত” আরেকটিতে। পাশাপাশি key একই partition-এ, তাই “১ থেকে ৭ মার্চের সব অর্ডার”-এর মতো range scan অল্প কয়েকটি partition-এই শেষ। সীমা ঠিক করতে হয় ডেটা দেখে। দুর্বলতা: ক্রমাগত বাড়তে থাকা key-এ সব নতুন লেখা শেষ partition-এ — একটি hot spot.
Google-এর Bigtable paper (Chang ও সহকর্মীরা, OSDI ২০০৬) range partitioning-এর বিখ্যাত উদাহরণ: সারিগুলো row key-এর ক্রমে সাজানো, আর পরপর সারির একটি পরিসর একটি tablet. Paper-এর Webtable উদাহরণে web পাতার URL উল্টো করে key বানানো হয় — com.cnn.www — যাতে একই domain-এর পাতাগুলো পাশাপাশি বসে আর একসাথে পড়া যায়। shard key-এর রূপ বদলে query locality আনার এটি একটি চমৎকার কৌশল।
Hash partitioning — সমান ছড়ানো, ক্রম হারানো
Hash partitioning-এ key-এর hash দেখে partition ঠিক হয় — hash(key) mod P, বা hash-এর পরিসরকে ভাগ করে। ভালো hash পাশাপাশি key-কেও দূরে ছড়ায়, তাই ক্রমিক key-তেও hot spot নেই। দাম: range scan হারায় — “১ থেকে ৭ মার্চ” এখন সব partition-এ। Hash থেকে node বাছাইয়ের নিয়ম (ring, virtual node, rendezvous) পর্ব ১৬ — Consistent hashing-এ; এখানে P মানে partition-এর সংখ্যা, node-এর নয় (অংশ ৫)।
Compound key আর salting — দুটির মাঝামাঝি
আগের CQL উদাহরণটি মাঝামাঝি পথ: প্রথম অংশের hash-এ partition, দ্বিতীয় অংশে ভেতরের ক্রম। আরেকটি কৌশল salting: key-এর সাথে একটি ছোট লেজ (যেমন #0 থেকে #7) জুড়ে একটি key-কে কয়েকটি key বানানো, যাতে সেগুলো আলাদা partition-এ যায়। লেখা ছড়ায়, কিন্তু পড়ার সময় সবগুলো লেজের key পড়ে মেলাতে হয় — দাম অংশ ৪-এ।
Directory (lookup table) — নমনীয়, কিন্তু আরেকটি সার্ভিস
Directory-ভিত্তিক sharding-এ সূত্র নেই; একটি table লিখে রাখে কোন key কোথায় — যেমন tenant 17 → shard 3. বড় গ্রাহককে নিজস্ব shard দেওয়া বা একটি tenant সরানো খুব সহজ। দাম: প্রতিটি অনুরোধে বাড়তি lookup, আর table-টি বন্ধ হলে পুরো সিস্টেম বন্ধ — তাই একে replicate আর cache করতে হয়। প্রায়ই মিশ্র রূপ: বেশিরভাগ hash দিয়ে, directory-তে শুধু ব্যতিক্রম।
Geo-partitioning — ব্যবহারকারীর কাছাকাছি, আইনের ভেতরে
Geo-partitioning-এ ডেটা ভাগ হয় এলাকা ধরে — বাংলাদেশের ব্যবহারকারীর ডেটা কাছের একটি region-এ, ইউরোপেরটা ইউরোপে। লাভ: latency কম, আর data residency-র শর্ত পূরণ — নির্দিষ্ট ডেটা নির্দিষ্ট দেশের ভেতরে রাখার আইনি বা চুক্তির বাধ্যবাধকতা। ইউরোপীয় ইউনিয়নের GDPR (২০১৮ থেকে কার্যকর) এলাকার বাইরে ব্যক্তিগত ডেটা পাঠানোয় শর্ত দেয়; বাংলাদেশসহ বহু দেশে এমন নিয়ম তৈরি ও বদলের প্রক্রিয়ায়, আর তা দেশ, খাত ও সময়ভেদে আলাদা — নকশার আগে সর্বশেষ সরকারি নির্দেশনা আর আইনি পরামর্শ নিন। প্রকৌশলের প্রশ্ন থেকেই যায়: ব্যবহারকারী দেশ বদলালে ডেটা সরবে কি না, আর দুই এলাকার মানুষের chat কোথায় থাকবে।
| নিয়ম | ভার ছড়ানো | Range scan | প্রধান ঝুঁকি | যেখানে মানায় |
|---|---|---|---|---|
| Range | ডেটা দেখে সীমা ঠিক করলে ভালো | সস্তা | ক্রমিক key-এ hot spot | সময়ের সিরিজ বিশ্লেষণ, সাজানো key, Bigtable/HBase-ধাঁচ |
| Hash | সমান | সব partition-এ | ঘন range query ধীর | key দিয়ে খোঁজা, user বা অর্ডার |
| Compound (hash + ক্রম) | প্রথম অংশে সমান | প্রথম অংশের ভেতরে সস্তা | একটি প্রথম-অংশ বিশাল হলে | chat, timeline, IoT সেন্সর |
| Directory | যেমন খুশি | নিয়মভেদে | বাড়তি lookup, সেটিও availability-র অংশ | multi-tenant SaaS, বড় গ্রাহক আলাদা |
| Geo | জনসংখ্যা অনুযায়ী অসমান | এলাকার ভেতরে | আন্তঃএলাকা ডেটা, আইন বদল | কম latency, data residency |
৪ · Hot spot আর skew — সমান ভাগেও কেন একটি shard ডোবে
অসম ভাগের (skew) দুটি রূপ। ডেটার skew: একটি partition-এ অন্যগুলোর চেয়ে অনেক বেশি ডেটা — যেমন একটি বিশাল tenant. চাপের skew: ডেটা সমান, কিন্তু একটি partition-এ অনেক বেশি অনুরোধ — যেমন একজন তারকা খেলোয়াড়ের profile, বা ভাইরাল হওয়া একটি পোস্টের like-count. Hash partitioning প্রথমটি অনেকটা সামলায়, কিন্তু দ্বিতীয়টি নয়: একটি key যত অনুরোধই পাক, সেটি এক partition-এই থাকে।
ঈদের আগে রেলস্টেশনে দশটি টিকিট কাউন্টার, যাত্রীরা নামের প্রথম অক্ষর অনুযায়ী লাইনে — সাধারণ দিনে সব লাইন সমান। কিন্তু একজন প্রতিনিধি একাই পাঁচশো জনের টিকিট কাটতে এলে তাঁর কাউন্টারের লাইন আর শেষ হয় না, বাকি নয়টি যত খালিই থাকুক। একটি key-তে বিশাল চাপ ঠিক এই সমস্যা।
জনপ্রিয়তা প্রায়ই Zipf distribution-এর কাছাকাছি: ক্রমে k-তম key চাওয়ার সম্ভাবনা 1/k-এর সমানুপাতিক। তখন সবচেয়ে জনপ্রিয় key পায় মোট অনুরোধের 1/Hn ভাগ, যেখানে Hn = 1 + 1/2 + … + 1/n. n = ১,০০০-এ Hn ≈ ৭.৪৯, অর্থাৎ একটিমাত্র key একাই প্রায় ১৩.৪% — ৮টি shard-এর গড় ভাগ ১২.৫%-এর চেয়েও বেশি!
নিচের প্রোগ্রাম ৮টি shard-এ দুটি পরীক্ষা চালায়: (A) আজকের ৮,০০০টি message — range-এ (৬৪ দিনের ইতিহাস দেখে প্রতিটি shard ৮ দিনের, শেষটি খোলা) আর hash-এ; (B) ১,০০০ জন ব্যবহারকারীর ওপর Zipf-ধাঁচের ১৬,০০০টি অনুরোধ, তারপর শুধু user:1-কে ৮ ভাগে salting. Hash পর্ব ১৬-এর মতোই FNV-1a, আর random সংখ্যা নির্দিষ্ট seed থেকে।
// 8 shards. (A) time-ordered keys: range vs hash. (B) a Zipf hot key, then salting.
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 = 23; 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, DAY = 86400000, T0 = Date.UTC(2026, 0, 1), SALT = 8;
const show = (name, c) => console.log(name.padEnd(12) + c.map((x) => String(x).padStart(6)).join(""));
const byRange = (t) => Math.min(S - 1, Math.floor((t - T0) / (8 * DAY))); // shard i = days 8i..8i+7
const range = new Array(S).fill(0), hash = new Array(S).fill(0);
for (let i = 0; i < 8000; i++) { // 8,000 messages written today (day 64)
const t = T0 + 64 * DAY + Math.floor(rand() * DAY);
range[byRange(t)]++; hash[h32("msg:" + t + ":" + i) % S]++;
}
show("shard", [0, 1, 2, 3, 4, 5, 6, 7]); show("range(time)", range); show("hash(id)", hash);
const U = 1000, cum = []; let tot = 0; // Zipf: user k is requested with weight 1/k
for (let k = 1; k <= U; k++) { tot += 1 / k; cum.push(tot); }
function zipfUser() { // binary search in the cumulative weights
const x = rand() * tot; let lo = 0, hi = U - 1;
while (lo < hi) { const m = (lo + hi) >> 1; if (cum[m] < x) lo = m + 1; else hi = m; } return lo + 1;
}
const plain = new Array(S).fill(0), salted = new Array(S).fill(0); let hot = 0;
for (let i = 0; i < 16000; i++) { // 16,000 requests
const u = zipfUser(), suffix = Math.floor(rand() * SALT); if (u === 1) hot++;
plain[h32("user:" + u) % S]++;
salted[h32(u === 1 ? "user:1#" + suffix : "user:" + u) % S]++; // salt only the hot key
}
show("zipf", plain); show("salted x" + SALT, salted);
console.log("user:1 = " + hot + " of 16000 requests; busiest shard / average: " +
(Math.max(...plain) / 2000).toFixed(2) + " -> " + (Math.max(...salted) / 2000).toFixed(2));আউটপুট:
shard 0 1 2 3 4 5 6 7
range(time) 0 0 0 0 0 0 0 8000
hash(id) 996 1013 1005 1016 1028 978 1051 913
zipf 1317 1336 2271 2203 1847 1284 2345 3397
salted x8 1572 1630 2271 2728 2629 1559 2345 1266
user:1 = 2131 of 16000 requests; busiest shard / average: 1.70 -> 1.36
(A): range-এ আজকের ৮,০০০টি লেখার সবই shard ৭-এ; hash-এ ৯১৩ থেকে ১,০৫১ — গড় ১,০০০-এর কাছে। (B): user:1 একাই ২,১৩১টি অনুরোধ (১৩.৩%, সূত্রে ১৩.৪%), তাই তার shard ৭-এ ৩,৩৯৭ — গড় ২,০০০-এর ১.৭০ গুণ। Salting-এর পরে shard ৭ নামল ১,২৬৬-তে, সবচেয়ে ব্যস্ত shard গড়ের ১.৩৬ গুণ। দুটি শিক্ষা: ৮টি sub-key পড়েছে মাত্র ৫টি ভিন্ন shard-এ (০, ১, ৩, ৪, ৫) — sub-key-এর hash-ও এলোমেলো, ৮টি shard-এ গড়ে প্রায় ৫.২৫টি ভিন্ন shard পড়ে; আর Zipf-এ গরম key একটি নয় — user:2 আর user:3-ও প্রায় ৬.৭% আর ৪.৫%.
প্রতিকার — প্রতিটির দাম আছে
- Salting: লেখার সময় গরম key-এর সাথে লেজ (
user:1#0…#7); পড়ার সময় r-টি লেজের সবগুলো পড়ে মেলাতে হয় — পড়া r গুণ। তাই এটি মানায় লেখা-ভারী, চেনা গরম key-এ (like বা view গোনা); sub-key-গুলো সত্যিই আলাদা shard-এ পড়ছে কি না যাচাই করুন। - Cache: গরম key-এর পড়া database-এর আগেই থামানো — অল্প TTL-এর local cache, কয়েকটি কপি, request coalescing; বিস্তারিত পর্ব ৩০ — Distributed cache-এ।
- Hot partition ভেঙে সরানো: গরম পরিসরের partition দুই ভাগ করে একটি অংশ অন্য node-এ — নিচের stepper. একটিমাত্র key-কে অবশ্য ভাঙা যায় না।
- নকশা বদল: বিখ্যাত অ্যাকাউন্টের পোস্ট follower-দের feed-এ লেখার বদলে পড়ার সময় আনা — পর্ব ৫৬ — Case study: news feed-এ।
| Node N1 | |
| Node N2 | |
| Node N3 | |
| Node N4 | |
| Partition map |
client (map v7) → N2: GET shop:1700
N2 → client: MOVED P2b → N4 (map v9)
client: নতুন map v9 আনল → N4: GET shop:1700
N4 → client: OK
- স্বাভাবিক দিন। একটি কাল্পনিক marketplace-এ দোকানের ডেটা shop_id-এর range ধরে তিনটি partition-এ, তিনটি node-এ; প্রতিটিতে প্রতি সেকেন্ডে প্রায় ৩,০০০ অনুরোধ। Partition map-এর সংস্করণ v7.
- Flash sale — P2 গরম। ১০০০ থেকে ২০০০-এর মধ্যের কয়েকটি বড় দোকানে sale, তাই P2-তে হঠাৎ ১২,০০০ — N2-এর CPU প্রায় পুরো ভরা, latency বাড়ছে। মোট চাপ ১৮,০০০, কিন্তু তার দুই-তৃতীয়াংশ একটি node-এ।
- চাপ দেখে ভাঙা। Key ধরে চাপ মেপে দেখা গেল ১২৫০-এ ভাঙলে দুই দিকে প্রায় সমান: P2a [১০০০–১২৫০) ৬,০০০, P2b [১২৫০–২০০০) ৬,০০০ — শুধু আকার দেখে মাঝখানে ভাঙলে গরম দোকানগুলো এক ভাগেই থেকে যেত। এই ধাপে কেবল metadata বদলায়; দুটিই এখনো N2-তে, map v8.
- P2b নতুন node N4-এ। P2b-এর snapshot কপি, তারপর কপির সময়ের নতুন লেখা log থেকে মিলিয়ে নেওয়া, শেষে এক মুহূর্ত লেখা থামিয়ে মালিকানা বদল — map v9. সবচেয়ে ব্যস্ত node-এ এখন ৬,০০০, আগের অর্ধেক।
- পুরোনো map-ওয়ালা client. একটি app server-এর কাছে এখনো v7, তাই
shop:1700পাঠাল N2-তে। N2 জানে সে আর মালিক নয়, তাই উত্তর দেয় “MOVED — N4-এ যান”; client নতুন map এনে আবার পাঠায়। Map-এ সংস্করণ নম্বর থাকায় পুরোনো তথ্য চেনা যায় (অংশ ৬)। - সীমা — একটি key ভাঙা যায় না। যদি পুরো ১২,০০০-এর উৎস একটিমাত্র দোকান হতো, তাহলে যত ছোট করেই ভাঙুন, সেই দোকানের key একটি partition-এ — সবচেয়ে ছোট partition হলো একটি key. তখন উপায় salting, cache, বা পড়ার কাজ replica-তে ছড়ানো।
৫ · Rebalancing — ডেটা সরানোর তিন কৌশল
Node যোগ হয়, বিকল হয়, ডেটা বাড়ে, partition গরম হয় — তখন ডেটা নতুন করে ভাগ করে সরাতে হয়; এটিই rebalancing. ভালো rebalancing-এর তিনটি চাওয়া: শেষে ভার সমান; সরানোর সময়েও পড়া-লেখা চলে; আর যতটুকু না সরালেই নয়, তার বেশি ডেটা নড়ে না।
যা করবেন না — key থেকে সরাসরি hash mod N, যেখানে N = node-এর সংখ্যা। পর্ব ১৬-এ প্রমাণ করা হয়েছে, N থেকে N + 1 করলে N/(N + 1) ভাগ key-এর node বদলায় — ৪ থেকে ৫-এ ৮০%. Cache-এ এর মানে হঠাৎ miss-এর ঢল; database-এ মানে প্রায় পুরো ডেটা নেটওয়ার্কে কপি।
কৌশল ১ — নির্দিষ্ট সংখ্যক partition, node-এর চেয়ে অনেক বেশি
শুরুতেই node-এর চেয়ে অনেক বেশি partition — ধরুন ৪টি node-এ ২৫৬টি (fixed partitions). ঠিকানা দুই ধাপে: partition = hash(key) mod 256, যা কখনো বদলায় না; আর node = map[partition], যা rebalancing-এ বদলায়। নতুন node এলে সে অন্যদের কাছ থেকে কয়েকটি পুরো partition নেয়; কোনো key-এর partition বদলায় না, বদলায় শুধু partition-এর মালিক। Redis Cluster-এর ১৬,৩৮৪টি hash slot ঠিক এই চিন্তা (পর্ব ১৬)।
// 256 fixed partitions on 4 nodes; add a 5th node. Compare with hash mod N.
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 = 5; 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 P = 256, K = 100000, TB = 8; // 8 TB of data in total
const owner = Array.from({ length: P }, (_, p) => p % 4); // partition map: owner[p] = node
const before = owner.slice();
const perNode = (n) => Array.from({ length: n }, (_, i) => owner.filter((o) => o === i).length);
console.log("before: partitions per node " + perNode(4).join(" "));
let moved = 0;
while (perNode(5)[4] < Math.floor(P / 5)) { // node 4 takes whole partitions, busiest donor first
const c = perNode(4), donor = c.indexOf(Math.max(...c));
owner[owner.lastIndexOf(donor)] = 4; moved++;
}
console.log("after: partitions per node " + perNode(5).join(" ") + " (moved " + moved + " of " + P + ")");
let fixedMoved = 0, modMoved = 0;
for (let i = 0; i < K; i++) {
const h = h32("order:" + Math.floor(rand() * 1e9));
if (before[h % P] !== owner[h % P]) fixedMoved++; // key -> partition never changes
if (h % 4 !== h % 5) modMoved++; // key -> node directly by mod N
}
const pct = (x) => (100 * x / K).toFixed(1) + "%";
console.log("keys moved: fixed partitions " + pct(fixedMoved) + ", hash mod N " + pct(modMoved) + ", ideal 1/5 = 20.0%");
console.log("data copied (8 TB total): fixed " + (TB * 1024 * moved / P).toFixed(0) + " GB, mod N about " +
(TB * 1024 * modMoved / K).toFixed(0) + " GB");আউটপুট:
before: partitions per node 64 64 64 64
after: partitions per node 51 51 51 52 51 (moved 51 of 256)
keys moved: fixed partitions 19.8%, hash mod N 80.2%, ideal 1/5 = 20.0%
data copied (8 TB total): fixed 1632 GB, mod N about 6571 GB
নতুন node ৪ নিয়েছে ঠিক ⌊২৫৬ ÷ ৫⌋ = ৫১টি partition — প্রত্যেক পুরোনো node থেকে ১২ বা ১৩টি; শেষে ৫১ থেকে ৫২, প্রায় সমান। সরানো partition ৫১/২৫৬ = ১৯.৯%, আর ১,০০,০০০টি key-এর মধ্যে সরেছে ১৯.৮% — আদর্শ ১/৫-এর কাছে। একই কাজ mod N-এ সরাত ৮০.২%. ৮ TB ডেটায় (কোডে ১ TB = ১,০২৪ GB ধরা) কপি প্রায় ১,৬৩২ GB বনাম ৬,৫৭১ GB — চার গুণ কাজ, চলতি traffic-এর মাঝখানে।
P কত হবে? P-ই node-এর সর্বোচ্চ সংখ্যা — ২৫৬টি partition-এ ২৫৬-এর বেশি node কাজে আসে না; আবার P খুব বড় হলে প্রতিটি partition-এর metadata আর replication-এর খরচ জমে। ডেটা বাড়লে partition-ও বড় হয়, আর বড় partition সরাতে সময় বেশি — বাছাইয়ের হিসাব ভাবনার প্রশ্ন ২-এ।
কৌশল ২ — চাহিদামতো ভাঙা আর জোড়া (dynamic partitioning)
Range partitioning-এ সীমা আগে ঠিক করা কঠিন, তাই অনেক সিস্টেম dynamic partitioning করে: partition নির্দিষ্ট মাপ বা চাপ ছাড়ালে ভেঙে দুটি (partition split), খুব ছোট হলে পাশেরটির সাথে জোড়া — Bigtable-এর tablet আর HBase-এর region এভাবে বাড়ে। ফাঁদ: খালি database শুরু হয় একটি partition দিয়ে, তাই আগে থেকে কয়েকটি সীমা বসানো হয় (pre-splitting).
কৌশল ৩ — consistent hashing আর virtual node
প্রতিটি node ring-এর কয়েকটি বৃত্তাংশের মালিক; নতুন node এলে গড়ে মাত্র প্রায় 1/(N + 1) ভাগ সরে, আর partition-এর সংখ্যা node-এর সমানুপাতিক — Dynamo-ধাঁচের store আর Cassandra-র token ring এই পথে (পর্ব ১৬ — Consistent hashing-এ)।
| কৌশল | Partition-এর সংখ্যা | কী সরে | ভালো | সাবধান |
|---|---|---|---|---|
| Fixed partitions | শুরুতেই ঠিক, স্থির | পুরো partition | সরল map, key-এর ঠিকানা স্থির | P বাছাই; ডেটা বাড়লে partition বড় |
| Dynamic split/merge | ডেটার সাথে বাড়ে-কমে | ভাঙা অর্ধেক | range-এ স্বাভাবিক, গরম পরিসর ভাঙা যায় | শুরুতে একটি partition; pre-split |
| Consistent hashing | node-এর সমানুপাতিক | ring-এর বৃত্তাংশ | ন্যূনতম নড়াচড়া, কেন্দ্রীয় map কম | range scan নেই; vnode-এর হিসাব |
একটি partition কীভাবে সরে: snapshot কপি, কপির সময়ের পরিবর্তন replication log থেকে মিলিয়ে নেওয়া, শেষে অল্প সময় লেখা থামিয়ে মালিকানা বদল আর map-এর সংস্করণ বাড়ানো; কপির গতি বাঁধা থাকে। পুরোপুরি স্বয়ংক্রিয় rebalancing ঝুঁকির: একটি node সাময়িক ধীর হলে তাকে “মৃত” ভেবে ডেটা সরাতে শুরু করলে বাড়তি চাপে অন্যরাও ধীর হয় — একটি cascading failure. তাই অনেক দল প্রস্তাব স্বয়ংক্রিয় রাখে, অনুমোদন দেন মানুষ।
৬ · Request routing — কোন key কোন node-এ, সে খবর কে রাখে
Partition সরে, node বদলায় — অনুরোধ ঠিক node-এ পৌঁছাবে কীভাবে? এই কাজের নাম request routing, আর এর কেন্দ্রে থাকে partition map: কোন partition এখন কোন node-এ, তার সংস্করণ-নম্বরসহ তালিকা। তিনটি প্রচলিত নকশা:
১ · যেকোনো node-এ পাঠান
Client যেকোনো node-এ যায়; সেটি মালিক না হলে অনুরোধ মালিকের কাছে পাঠিয়ে উত্তর ফেরত দেয়। Node-রা map জানে gossip-এর মতো প্রোটোকলে (পর্ব ৪৪)।
Cassandra-র coordinator node২ · Routing tier
মাঝে একটি router বা proxy-স্তর map দেখে অনুরোধ ঠিক shard-এ পাঠায় (ছবি ২৩.১)। Client সরল; দাম একটি বাড়তি hop আর আরেকটি স্তর চালানো।
MongoDB-র mongos, Vitess-এর vtgate৩ · Partition-aware client
Client library নিজেই map রাখে আর সরাসরি মালিকের কাছে যায় — সবচেয়ে কম hop; ভুল node-এ গেলে উত্তরে আসে নতুন ঠিকানা।
Redis Cluster-এর MOVED উত্তরMap-এর সত্যের উৎস কে? সব router আর client-কে একই সময়ে একই map দেখাতে হলে একটি নির্ভরযোগ্য উৎস লাগে। প্রায়ই সেটি একটি coordination service — যেমন etcd বা ZooKeeper — যা consensus দিয়ে নিজের ডেটা কয়েকটি মেশিনে মিলিয়ে রাখে আর বদল হলে সবাইকে জানায় (watch). Lease আর fencing token দিয়ে “দুজন একসাথে মালিক” পরিস্থিতি ঠেকানোর বিস্তারিত পর্ব ৪৩ — Coordination — distributed lock, lease আর fencing token-এ।
তবু কিছু client কিছুক্ষণ পুরোনো map দেখবেই। তাই map-এ সংস্করণ নম্বর (epoch), আর প্রতিটি node যাচাই করে partition-টি এখনো তার কি না — না হলে redirect (অংশ ৪-এর stepper, ধাপ ৫)। সবচেয়ে বিপজ্জনক partition সরানোর মুহূর্ত: পুরোনো আর নতুন মালিক দুজনেই লেখা নিলে split-brain.
৭ · Secondary index আর cross-shard query
Messages ভাগ হয়েছে conversation_id দিয়ে; নতুন feature চায় “একজন ব্যবহারকারীর পাঠানো সব message” — শর্তে sender_id, shard key নয়। Secondary index (shard key ছাড়া অন্য field-এর index; একটি database-এর ভেতরে এর মূল কথা পর্ব ২১ — Index আর query performance-এ) shard-এ ভাগ করা ডেটায় দুই রকম হতে পারে:
| দিক | Local index (document-partitioned) | Global index (term-partitioned) |
|---|---|---|
| কোথায় থাকে | প্রতিটি shard শুধু নিজের সারিগুলোর index রাখে | Index নিজেই indexed মান (যেমন sender_id) দিয়ে ভাগ করা, আলাদা shard-এ |
| লেখা | সারি আর index একই shard-এ, তাই একসাথে আর সস্তা | সারি এক shard-এ, index-এর entry আরেকটিতে — দুই জায়গায় লেখা |
| পড়া (sender_id = 42) | সব shard-এ জিজ্ঞাসা, তারপর ফল জোড়া — scatter-gather | একটি index shard-এ খোঁজা, তারপর শুধু যেসব data shard-এ সারি আছে |
| Consistency | সারির সাথেই হালনাগাদ | সাধারণত async — লেখার পরে কিছুক্ষণ index-এ না-ও দেখা যেতে পারে |
| উদাহরণ | Elasticsearch-এ প্রতিটি shard নিজের document-এর index রাখে | DynamoDB-র global secondary index (documentation অনুযায়ী শুধু eventually consistent পড়া) |
Local index-এ লেখা সহজ, পড়া ছড়ায়; global index-এ পড়া এক জায়গায়, লেখা ছড়ায়। Global index-কে সারির সাথে একই মুহূর্তে হালনাগাদ রাখতে দুই shard জুড়ে transaction লাগে (পর্ব ২৫), তাই বেশিরভাগ সিস্টেম একে async-ভাবে হালনাগাদ করে — বিনিময়ে eventual consistency. নামগুলো Martin Kleppmann-এর “Designing Data-Intensive Applications” (২০১৭) বইয়ের।
Scatter-gather-এর আসল দাম — সবচেয়ে ধীর shard
Local index-এ sender_id দিয়ে খোঁজা মানে প্রতিটি shard-এ একটি অনুরোধ (scatter), তারপর সব উত্তর জোড়া (gather). মোট সময় ঠিক করে সবচেয়ে ধীর উত্তর। পর্ব ৪ (Latency, throughput আর percentile)-এর fan-out সূত্র: প্রতিটি shard স্বাধীনভাবে p সম্ভাবনায় ধীর হলে N-টির অন্তত একটি ধীর হওয়ার সম্ভাবনা 1 − (1 − p)N. নিচের simulation ১৬টি shard দিয়ে তা যাচাই করে; প্রতিটি shard-এর latency ধরে নেওয়া — সাধারণত ৫ থেকে ১৫ ms, কিন্তু ০.৫% অনুরোধে ৪০ থেকে ১০০ ms-এর একটি বিরতি (যেমন garbage collection).
// Scatter-gather: a query that needs 1 shard vs one that must wait for all 16 shards.
let seed = 1; 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;
}
function shardMs() { // assumed: 5 ms + exponential (mean 3 ms); 0.5% of calls hit a 40-100 ms pause
const base = 5 - 3 * Math.log(1 - rand());
return rand() < 0.005 ? base + 40 + 60 * rand() : base;
}
const pctl = (a, q) => a.slice().sort((x, y) => x - y)[Math.ceil(q * a.length) - 1]; // nearest rank
const Q = 20000, one = [], all = [];
for (let i = 0; i < Q; i++) {
one.push(shardMs());
let slowest = 0;
for (let s = 0; s < 16; s++) slowest = Math.max(slowest, shardMs()); // wait for every shard
all.push(slowest);
}
const row = (name, a) => console.log(name.padEnd(18) + pctl(a, 0.5).toFixed(1).padStart(7) +
pctl(a, 0.99).toFixed(1).padStart(8) + pctl(a, 0.999).toFixed(1).padStart(8));
console.log("query p50 ms p99 ms p99.9ms");
row("1 shard", one); row("all 16 shards", all);
const cut = pctl(one, 0.99), hit = all.filter((x) => x > cut).length / Q;
console.log("slower than the 1-shard p99: simulated " + (100 * hit).toFixed(1) + "%, formula 1 - 0.99^16 = " +
(100 * (1 - 0.99 ** 16)).toFixed(1) + "%");
console.log("for a 16-shard p99, each shard must be fast at its p" + (100 * 0.99 ** (1 / 16)).toFixed(2));আউটপুট:
query p50 ms p99 ms p99.9ms
1 shard 7.1 20.8 93.8
all 16 shards 14.9 100.5 108.0
slower than the 1-shard p99: simulated 15.0%, formula 1 - 0.99^16 = 14.9%
for a 16-shard p99, each shard must be fast at its p99.94
একটি shard-এর query-র p99 প্রায় ২১ ms; ১৬টি shard-এর অপেক্ষায় p99 প্রায় ১০০ ms — প্রায় পাঁচ গুণ, কারণ ১৬টির কোনো একটিতে বিরতি পড়ার সম্ভাবনা 1 − 0.99516 ≈ ৭.৭% — p99-এর ১% সীমার অনেক ওপরে। p50-ও দ্বিগুণ (৭.১ থেকে ১৪.৯)। একক-shard p99-এর চেয়ে ধীর হয়েছে ১৫.০% query, সূত্রে ১৪.৯%. উল্টো দিক থেকে: ১৬-shard query-র p99 ঠিক রাখতে প্রতিটি shard-কে তার p99.94 পর্যন্ত দ্রুত হতে হবে। আর shard বাড়লে ক্ষতি আরও বাড়ে — তাই shard key-এর ঘন query যেন এক shard-এ থাকে।
Cross-shard join আর transaction
দুই table-এর join আলাদা shard-এ পড়লে নেটওয়ার্কে সারি আনা-নেওয়া লাগে। প্রচলিত কৌশল:
- একসাথে রাখা (co-location): যে table-গুলো একসাথে join হয়, তাদের একই shard key — যেমন
ordersআরorder_itemsদুটোই customer_id দিয়ে ভাগ, তাই একজন গ্রাহকের সব এক shard-এ। - ছোট table সবখানে: দেশ বা পণ্যের ধরনের মতো ছোট, কম-বদলানো table-এর কপি প্রতিটি shard-এ (reference table).
- আগে থেকে জুড়ে রাখা: দরকারি তথ্যের কপি সারির ভেতরেই — denormalization (DBMS কোর্সের Denormalization পর্ব); বিনিময়ে কপি মিলিয়ে রাখার দায়।
- বিশ্লেষণ আলাদা জায়গায়: বড় aggregate আর report চলুক একটি data warehouse-এ, চলতি shard-গুলোতে নয় (DBMS কোর্সের Data warehousing পর্ব)।
দুই shard-এর ডেটা একসাথে বদলাতে — যেমন দুই shard-এর দুই অ্যাকাউন্টে টাকা পাঠানো — লাগে two-phase commit বা saga: পর্ব ২৫ — Distributed transaction; এক shard-এর ভেতরের isolation পর্ব ২৪ — Transaction আর isolation level-এ। নিয়ম: যা একসাথে atomic হতে হবে, তার ডেটা একই shard key-তে রাখুন।
৮ · চালু সিস্টেমে resharding — dual write আর backfill
কখনো পুরো ভাগটাই নতুন করে করতে হয় — shard key ভুল ছিল, বা fixed partition না থাকায় node বাড়ানো যাচ্ছে না। একে বলে resharding, আর কঠিন অংশ সিস্টেম না থামিয়ে করা — চলন্ত বাসের চাকা বদলানোর মতো। ধারাটি দুটি কৌশলে দাঁড়িয়ে: dual write (নতুন লেখা দুই জায়গাতেই) আর backfill (পুরোনো ডেটা পেছনে কপি)। নিচের ধাপগুলো একটি সাধারণ রূপরেখা; বাস্তব tool আর সিস্টেমভেদে ধাপ ও নাম আলাদা।
| লেখা যায় | |
| পড়া আসে | |
| Backfill | |
| Source of truth | |
| ফেরার পথ |
- শুরুর অবস্থা। একটি কাল্পনিক mobile banking সেবার লেনদেনের ডেটা user_id mod 4 দিয়ে ৪টি shard-এ; আর node বাড়ানো যাচ্ছে না। লক্ষ্য: নতুন cluster, ১,০২৪টি নির্দিষ্ট partition — যাতে পরের বার পুরো resharding না লাগে।
- নতুন cluster প্রস্তুত। Schema, partition map আর routing-এর নতুন নিয়ম তৈরি, কিন্তু কোনো traffic নেই। এখনো ভুল হলে ক্ষতি নেই — নতুনটি মুছে আবার শুরু।
- Dual write চালু। প্রতিটি নতুন লেখা আগে পুরোনোতে (এখনো সত্যের উৎস), তারপর নতুনে; নতুনে ব্যর্থ হলে লেখাটি queue-তে জমে পরে আবার চেষ্টা হয়। অনেক দল app-এর কোডে দুবার লেখার বদলে database-এর পরিবর্তনের log পড়ে নতুনে পাঠায় (change data capture) — তাতে দুই লেখার মাঝে ব্যর্থ হওয়ার ফাঁক থাকে না।
- Backfill. পুরোনো ডেটা ছোট ব্যাচে, গতি বেঁধে কপি হয়; প্রতিটি ব্যাচের পরে অগ্রগতি লেখা থাকে (checkpoint), যাতে থামলে সেখান থেকেই শুরু। সবচেয়ে সূক্ষ্ম ঝুঁকি: dual write-এ আসা নতুনতর মান যেন backfill-এর পুরোনো কপিতে চাপা না পড়ে — তাই প্রতিটি সারিতে version, আর নিয়ম “নতুনে নতুনতর version থাকলে কপি বাদ”; দুবার চললেও ফল এক।
- যাচাই। প্রতিটি পরিসরে সারি গোনা আর checksum মেলানো; তারপর shadow read — চলতি পড়ার একটি অংশ দুই জায়গা থেকেই পড়ে তুলনা, কিন্তু ব্যবহারকারী পান পুরোনোর উত্তর। শূন্য অমিল না পাওয়া পর্যন্ত পরের ধাপ নয়।
- পড়া ধাপে ধাপে নতুনে। আগে ১% পড়া নতুন থেকে, latency আর error দেখে ১০%, তারপর ১০০% — canary-র মতো। লেখা তখনও দুই জায়গায়, তাই সমস্যা হলে এক মুহূর্তে পড়া পুরোনোতে ফেরানো যায়।
- লেখার মালিক বদল। এখন নতুনই সত্যের উৎস: লেখা আগে নতুনে, আর কিছুদিন উল্টো dual write, যাতে বড় ভুল ধরা পড়লে ফেরা যায়। বদলের মুহূর্তে কয়েক সেকেন্ড লেখা থামানো লাগতে পারে, যাতে একই সারি দুই দিকে একসাথে না বদলায়।
- পুরোনো বন্ধ। ফেরার জানালা (ধরুন কয়েক সপ্তাহ) পেরোলে উল্টো dual write বন্ধ, আর একটি শেষ backup রেখে পুরোনো ৪টি shard বন্ধ। এখন থেকে বৃদ্ধি মানে শুধু partition সরানো।
এই ধারা schema বদলের expand–contract পদ্ধতির আত্মীয় (পর্ব ৫৩ — Deployment আর release): নতুনটি পাশে দাঁড় করানো, দুটো একসাথে চালানো, যাচাই, তারপর পুরোনোটি সরানো — প্রতিটি ধাপে ফেরার পথ খোলা রেখে।
৯ · কেন্দ্রীয় counter ছাড়া unique ID
একটি database-এ নতুন সারির ID দেয় auto-increment — ১, ২, ৩ … কিন্তু ১৬টি shard-এর প্রত্যেকের নিজের counter থাকলে সবাই একটি করে “ID ৪২” দেবে। আর সবাই একটি কেন্দ্রীয় counter-কে জিজ্ঞেস করলে প্রতিটি লেখায় বাড়তি নেটওয়ার্ক অনুরোধ, আর counter-টি একটি single point of failure. ভালো ID হবে অনন্য, বানাতে কারও সাথে সমন্বয় লাগবে না, সম্ভব হলে সময়ক্রমে সাজানো (index আর “নতুন আগে” query সস্তা) আর ছোট।
UUID v4 — পুরোপুরি এলোমেলো
128-bit UUID-এর version 4-এ ১২২টি bit এলোমেলো; যেকোনো মেশিন একাই বানায়। মিলে যাওয়ার ভয় নগণ্য: birthday সমস্যার হিসাবে ৫০% সম্ভাবনায় একটি মিল পেতে লাগে প্রায় √(2 ln 2 · 2122) ≈ ২.৭ × ১০১৮টি ID — সেকেন্ডে ১০০ কোটি করে বানালেও প্রায় ৮৬ বছর। আসল সমস্যা index-এ: ID এলোমেলো বলে B-tree-তে প্রতিটি নতুন সারি পড়ে কোনো এলোমেলো পাতায় — memory-র পাতাগুলো কাজে লাগে না, পাতা ভাঙে বেশি। যেসব database primary key-এর ক্রমে সারি রাখে (যেমন MySQL-এর InnoDB), সেখানে ক্ষতি আরও বেশি; B-tree-র ভেতরের কথা পর্ব ১৯ — Storage engine-এ।
UUID v7 — সময়ক্রমে সাজানো UUID
২০২৪ সালে প্রকাশিত RFC 9562 (পুরোনো RFC 4122-এর জায়গায়) UUID v7 সংজ্ঞায়িত করে: প্রথম 48 bit-এ Unix সময় millisecond-এ, বাকিটা মূলত এলোমেলো। নতুন ID প্রায় সবসময় আগেরগুলোর চেয়ে বড়, তাই B-tree-র ডান প্রান্তে বসে — অথচ বানাতে সমন্বয় লাগে না। দাম: ID থেকে সৃষ্টির সময় পড়া যায়, আর আকার 128 bit; ভাষা আর database-এ সমর্থন সংস্করণভেদে আলাদা। সাবধান: সময়ক্রমের ID দিয়ে range partitioning করলে আবার অংশ ৩-এর hot spot — এমন ID hash করে ভাগ করুন।
Snowflake-ধাঁচের 64-bit ID
প্রকাশিত engineering blog অনুযায়ী Twitter ২০১০ সালে Snowflake নামের ID-সার্ভিস ঘোষণা ও open-source করে। Snowflake-ধাঁচের ID-র গঠন: ওপরের 1 bit ফাঁকা (সংখ্যা ধনাত্মক রাখতে), 41 bit সময় — নিজের বাছা epoch থেকে millisecond, 10 bit worker-এর নম্বর, 12 bit ক্রমিক সংখ্যা।
প্রতি worker প্রতি ms-এ 212 = ৪,০৯৬টি · worker 210 = ১,০২৪টি · সময় 241 ms ≈ ৬৯.৭ বছর
সময় সবচেয়ে ওপরের bit-গুলোতে, তাই ID-গুলো মোটামুটি সময়ক্রমে সাজানো — k-sorted. নিচের প্রোগ্রাম একটি generator বানায় (নির্ভুল হিসাবে BigInt), ID ভেঙে দেখায়, ক্রম যাচাই করে, এক millisecond-এ ৪,০৯৭টি ID চায়, আর ক্ষমতা ও শেষ হওয়ার তারিখ হিসাব করে।
// Snowflake-style 64-bit ID = 41-bit ms timestamp | 10-bit worker | 12-bit sequence
const EPOCH = BigInt(Date.UTC(2024, 0, 1)); // custom epoch: 2024-01-01 00:00 UTC
const make = (ms, w, seq) => ((BigInt(ms) - EPOCH) << 22n) | (BigInt(w) << 12n) | BigInt(seq);
const parse = (id) => ({ ms: Number((id >> 22n) + EPOCH), worker: Number((id >> 12n) & 1023n), seq: Number(id & 4095n) });
function generator(worker) { // one per machine: no network call, no central counter
let last = -1, seq = 0;
return (ms) => { // ms = this machine's clock reading
if (ms <= last) { // same ms (or clock behind): next sequence number
ms = last; seq = (seq + 1) & 4095;
if (seq === 0) ms = last + 1; // all 4,096 used: move to the next ms
} else seq = 0;
last = ms; return make(ms, worker, seq);
};
}
const t = Date.UTC(2026, 9, 28, 10, 0, 0); // a fixed clock reading for the demo
const w7 = generator(7), w300 = generator(300);
const made = [["w300", w300(t)], ["w7", w7(t)], ["w7", w7(t)], ["w300", w300(t + 1)], ["w7", w7(t + 1)]];
made.forEach(([who, id], i) => { const p = parse(id);
console.log("#" + i + " " + who.padEnd(5) + String(id).padStart(19) + " +" + (p.ms - t) + "ms worker=" + p.worker + " seq=" + p.seq); });
const order = made.map((m, i) => [m[1], i]).sort((a, b) => (a[0] < b[0] ? -1 : 1)).map((x) => "#" + x[1]);
console.log("sorted by ID: " + order.join(" ") + " (exact order only between different ms)");
let id; for (let i = 0; i < 4097; i++) id = w7(t + 5); // burst: 4,097 IDs asked in one ms
const q = parse(id); console.log("4,097th ID asked at +5ms: +" + (q.ms - t) + "ms seq=" + q.seq + " (sequence was full)");
console.log("capacity: " + 4096 * 1000 + " IDs/s per worker, " +
(4096 * 1000 * 1024 / 1e9).toFixed(2) + " billion/s for 1,024 workers");
const years = 2 ** 41 / (365.25 * 86400000);
console.log("41-bit ms timestamp lasts " + years.toFixed(1) + " years -> overflows on " +
new Date(Number(EPOCH) + 2 ** 41).toISOString().slice(0, 10));আউটপুট:
#0 w300 373772884378828800 +0ms worker=300 seq=0
#1 w7 373772884377628672 +0ms worker=7 seq=0
#2 w7 373772884377628673 +0ms worker=7 seq=1
#3 w300 373772884383023104 +1ms worker=300 seq=0
#4 w7 373772884381822976 +1ms worker=7 seq=0
sorted by ID: #1 #2 #0 #4 #3 (exact order only between different ms)
4,097th ID asked at +5ms: +6ms seq=0 (sequence was full)
capacity: 4096000 IDs/s per worker, 4.19 billion/s for 1,024 workers
41-bit ms timestamp lasts 69.7 years -> overflows on 2093-09-06
#০ বানানো হয়েছে সবার আগে, কিন্তু সাজালে তৃতীয় — একই millisecond-এ ক্রম ঠিক করে worker-এর নম্বর (৭ < ৩০০), আসল সময় নয়; আলাদা millisecond-এর মধ্যে ক্রম নিখুঁত। এক millisecond-এ ৪,০৯৭তম ID চাইলে sequence ফুরিয়ে যায়, তাই সে পরের millisecond-এর seq ০ পায় — বাস্তব generator এখানে ঘড়ি এগোনোর অপেক্ষা করে। ক্ষমতা: প্রতি worker সেকেন্ডে ৪০ লাখের বেশি, ১,০২৪টি মিলে প্রায় ৪১৯ কোটি; ২০২৪-এর epoch থেকে 41-bit সময় ফুরাবে ২০৯৩ সালের সেপ্টেম্বরে। ID-গুলো ১৮ অঙ্কের, আর JavaScript-এর Number নির্ভুল থাকে 253 (প্রায় ৯ × ১০১৫) পর্যন্ত — তাই JSON-এ এমন ID string হিসেবে পাঠান।
দুটি দুর্বল জায়গা। ঘড়ি: NTP ঘড়ি পেছালে একই সময় আবার আসে, ID মিলে যেতে পারে — তাই generator শেষ ব্যবহৃত সময় মনে রাখে আর ঘড়ি পেছালে অপেক্ষা করে বা ID দিতে অস্বীকার করে; clock skew-এর গভীরে পর্ব ৩৯ — সময় আর ক্রম-এ। Worker নম্বর: দুটি মেশিন একই নম্বর পেলে ID মিলবে; নম্বর আসে config থেকে বা coordination service-এর lease থেকে (পর্ব ৪৩) — container আসা-যাওয়ার যুগে এটিই সবচেয়ে সাধারণ ভুল।
Ticket server আর ID-র ব্লক
আরেকটি সরল পথ: কেন্দ্রীয় counter একবারে একটি ব্লক দেয় — একটি app server ১,০০০টি ID নিয়ে (৫০,০০১ থেকে ৫১,০০০) নিজের memory থেকে বিলোয়। কেন্দ্রীয় অনুরোধ হাজার গুণ কম; দাম restart-এ ফাঁক, আর সার্ভারভেদে সময়ক্রম নেই। Flickr-এর প্রকাশিত engineering blog (২০১০) অনুযায়ী তাঁরা দুটি MySQL ticket server চালাতেন — একটি বিজোড়, অন্যটি জোড় সংখ্যা দেয়। ছোট আর অনুমান-অযোগ্য ID-র নকশা পর্ব ৫৪ — Case study: URL shortener-এ।
| পদ্ধতি | আকার | সময়ক্রম | সমন্বয় | মূল ঝুঁকি |
|---|---|---|---|---|
| Auto-increment (এক DB) | 64 bit | নিখুঁত | প্রতিটি ID-তে এক DB | shard-এ সংঘর্ষ, ব্যবসার পরিমাণ ফাঁস |
| UUID v4 | 128 bit | নেই | নেই | index-এ এলোমেলো লেখা, বড় আকার |
| UUID v7 | 128 bit | ms পর্যন্ত | নেই | সৃষ্টির সময় দেখা যায় |
| Snowflake-ধাঁচ | 64 bit | ms পর্যন্ত (k-sorted) | শুধু worker নম্বর বিলি | ঘড়ি পেছানো, worker নম্বরের সংঘর্ষ |
| Ticket server / ব্লক | 64 bit | মোটামুটি | প্রতি ব্লকে একবার | কেন্দ্রীয় সার্ভিস, ফাঁক |
আরও গভীরে: Google-এর Spanner paper (Corbett ও সহকর্মীরা, OSDI ২০১২) key-এর পরিসর ভাগ করে প্রতিটি ভাগ একটি Paxos-দলে রাখে, আর চাপ বা আকার দেখে ভাগগুলো দলের মধ্যে স্বয়ংক্রিয়ভাবে সরায়, ভাঙে ও জোড়ে — dynamic partitioning আর consensus একসাথে। YouTube-এ শুরু হওয়া open-source Vitess MySQL-কে shard করে; তার documentation-এ resharding-এর কাজ — নতুন shard-এ কপি, পরিবর্তন অনুসরণ, তুলনা, তারপর আগে পড়া আর পরে লেখা সরানো — এই পর্বের অংশ ৮-এর ধাপের কাছাকাছি।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:
- Partitioning আর replication-এর পার্থক্য? একটি ভাগ করে ক্ষমতা বাড়ায়, অন্যটি কপি রেখে টিকে থাকা আর পড়া; দুটো একসাথে চলে।
- Range বনাম hash partitioning — কখন কোনটি? Range scan ঘন হলে range (ক্রমিক key-এ hot spot সাবধান); key দিয়ে খোঁজা আর সমান ছড়ানো চাইলে hash.
- Node ৪ থেকে ৫ করলে fixed partition-এ কত ডেটা সরে, mod N-এ কত? প্রায় ১/৫ = ২০% বনাম ৪/৫ = ৮০%.
- Local আর global secondary index-এর trade-off? Local: লেখা সস্তা, পড়া scatter-gather; global: পড়া এক জায়গায়, লেখা দুই shard-এ আর সাধারণত async.
- Snowflake ID-র ক্ষমতা আর আয়ু হিসাব করুন। 212 = ৪,০৯৬/ms/worker; 241 ms ≈ ৬৯.৭ বছর; ঘড়ি পেছালে অপেক্ষা বা প্রত্যাখ্যান।
১০ · AI-এর যুগে — sharding-এর নকশায় AI-কে যাচাই
“Database কীভাবে shard করব?” — AI কয়েক সেকেন্ডে উত্তর দেয়, প্রায়ই “user_id দিয়ে hash, সাথে Kafka আর Redis”. আপনার access pattern না জেনে দেওয়া এমন উত্তরের ভুলগুলো নির্দিষ্ট:
| AI-এর ভুল | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
| Query না দেখেই “user_id দিয়ে shard” | ঘন query-তে key না থাকলে প্রতিটি পড়া scatter-gather | শীর্ষ ৫টি query লিখে দেখুন প্রতিটি কয়টি shard ছোঁয় |
| হিসাব ছাড়াই sharding | একটি বড় মেশিন আর replica-তে আঁটলে এটি over-engineering | ডেটা, লেখার QPS আর বৃদ্ধি নিজে হিসাব করুন (পর্ব ৩) |
Node-এর ওপর সরাসরি hash % N | Node বদলালে N/(N + 1) ভাগ ডেটা সরে | অংশ ৫-এর মতো simulation চালিয়ে সরানো key গুনুন |
| Replication আর partitioning গুলিয়ে ফেলা | Replica লেখার চাপ বা ডেটার আকার ভাগ করে না | প্রশ্ন করুন: প্রতিটি মেশিনে কত ডেটা, কত লেখা? |
| ACID আর CAP-এর “consistency” গুলিয়ে ফেলা | ACID-এর C মানে নিয়ম রক্ষা; CAP-এর C মানে সব কপি একই সর্বশেষ মান দেখায় | প্রশ্ন করুন: কোন অর্থে? (পর্ব ৪০) |
| Snowflake-এর হিসাবে ভুল, বা পণ্যের সীমা বানানো | 241 ms, 212-এর ভুল গুণ; সংখ্যা সংস্করণভেদে বদলায় | নিজে হিসাব; অফিসিয়াল documentation |
| Hot key আর resharding-এর পরিকল্পনা নেই | Zipf-এ একটি key একাই গড় shard-এর চেয়ে বেশি চাপ আনতে পারে | Failure-mode পর্যালোচনা: “একটি key-তে ১০ গুণ চাপ এলে?” |
AI-কে কাজটি এমনভাবে দিন যাতে প্রতিটি সিদ্ধান্তের পেছনে হিসাব আর বিকল্প দেখাতে হয়:
AI-এর sharding নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট
Query গুনুন, সরানো ডেটা মাপুন, গরম key খুঁজুন।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। সূত্র মুখস্থ নয়, কেন কোন নকশা বেছে নেওয়া হয় সেটাই আসল।
প্র ০১Bigtable আর HBase range partitioning বেছে নিল, Dynamo আর Cassandra hash — দুই ঘরানার পেছনে কোন প্রয়োজন ছিল, আর Cassandra কীভাবে দুটো মেলায়?
দুই ঘরানার জন্ম দুই রকম কাজের চাপ থেকে। Google-এর Bigtable (OSDI ২০০৬) বানানো হয়েছিল web পাতা, মানচিত্র আর বিশ্লেষণের মতো কাজের জন্য, যেখানে প্রায়ই ক্রমানুসারে অনেক সারি একসাথে পড়তে হয়। তাই সারি row key-এর ক্রমে সাজানো, আর পরপর সারির পরিসর একেকটি tablet. Paper অনুযায়ী একটি tablet সাধারণত প্রায় ১০০–২০০ MB; বড় হলে ভেঙে যায়, আর master tablet-গুলো সার্ভারে বিলি করে। Range scan এখানে সবচেয়ে সস্তা কাজ, আর key-এর রূপ বদলে locality আনা যায় — উল্টো URL-এর কৌশল তারই উদাহরণ। HBase এই নকশারই open-source রূপ, তার region একই ধারণা।
Amazon-এর Dynamo paper (SOSP ২০০৭) এসেছিল অন্য প্রয়োজনে: shopping cart-এর মতো সেবা, যেখানে প্রায় সব কাজ একটি key দিয়ে পড়া-লেখা, আর সবচেয়ে জরুরি সবসময় লেখা নিতে পারা ও ভার সমান রাখা। সেখানে range scan প্রায় অপ্রয়োজনীয়, তাই key hash করে ring-এ বসানো — ক্রমিক key-এও hot spot নেই, কোনো master-এর দরকার নেই।
Apache Cassandra দুটির মিশ্রণ: Dynamo-র মতো ring আর replication, কিন্তু Bigtable-এর মতো সাজানো সারির কাঠামো। Primary key-এর প্রথম অংশ (partition key) hash হয়ে node ঠিক করে; বাকি অংশ (clustering column) partition-এর ভেতরে সারিগুলো সাজায়। তাই “একটি sensor-এর গত এক ঘণ্টার মাপ” বা “একটি conversation-এর শেষ ৫০টি message” একটি node-এর ভেতরে range scan, অথচ sensor বা conversation-গুলো সব node-এ ছড়ানো। দাম: partition key ছাড়া range query আবার সব node-এ, আর একটি partition key-এর নিচে সীমাহীন সারি জমলে বিশাল partition — যে কারণে সময়ের bucket যোগ করা হয়।
তাই ইন্টারভিউতে “range না hash?” প্রশ্নের ভালো উত্তর শুরু হয় একটি পাল্টা প্রশ্নে: সবচেয়ে ঘন query কোনগুলো, আর সেগুলো কি পরপর key-এর পরিসর চায়?
মূল উপলব্ধি: partition নিয়ম বাছাই আসলে প্রশ্ন বাছাই — কোন query সস্তা হবে আর কোনটি দামি, সেটি নকশার শুরুতেই ঠিক হয়ে যায়।
প্র ০২“নির্দিষ্ট অনেকগুলো partition” কৌশলে শুরুতে কয়টি partition বানাবেন? বাস্তব সিস্টেমগুলো কোন সংখ্যা বেছেছে, আর কেন?
Fixed partition-এর মূল শর্ত: key থেকে partition-এর নিয়ম কখনো বদলায় না, বদলায় শুধু partition-এর মালিক। তাই P বাছাই প্রায় স্থায়ী সিদ্ধান্ত, আর দুই দিকেই ভুল করা যায়।
- P খুব ছোট: P-এর বেশি node কোনো কাজে আসে না। আর ডেটা বাড়লে প্রতিটি partition বিশাল — একটি সরাতে ঘণ্টা লাগে, আর একটি গরম partition ভাগ করার উপায় থাকে না।
- P খুব বড়: প্রতিটি partition-এর metadata, খোলা ফাইল, replication-এর সংযোগ আর পর্যবেক্ষণ — সবকিছু গুণ হয়। Map বড় হয়, আর ছোট partition-এ প্রতিটি কাজের স্থির খরচ বেশি।
বাস্তব উদাহরণগুলো এই ভারসাম্যের নানা রূপ। Redis Cluster-এর অফিসিয়াল documentation অনুযায়ী সব key ১৬,৩৮৪টি hash slot-এ ভাগ হয়; সংখ্যাটি এমন যে slot-এর মালিকানার bitmap node-দের heartbeat বার্তায় আঁটে। Couchbase-এর documentation অনুযায়ী একটি bucket-এর ডেটা ১,০২৪টি vBucket-এ ভাগ (প্ল্যাটফর্মভেদে আলাদা হতে পারে)। Notion-এর প্রকাশিত engineering blog (২০২১) অনুযায়ী তাঁরা PostgreSQL-কে ৪৮০টি logical shard-এ ভাগ করে ৩২টি database-এ বসান — ৪৮০ অনেক সংখ্যা দিয়ে ভাগ যায়, তাই পরে ভিন্ন সংখ্যক database-এও সমান ভাগ সহজ। উল্টো দিকে Elasticsearch-এর documentation অনুযায়ী একটি index-এর primary shard-এর সংখ্যা তৈরির সময়েই ঠিক করতে হয়; পরে বদলাতে split বা reindex-এর মতো আলাদা কাজ লাগে — তাই সেখানে বাছাইটি আরও সাবধানে করতে হয়।
একটি কাজের নিয়ম: আগামী কয়েক বছরের সম্ভাব্য সর্বোচ্চ node-সংখ্যার কয়েক গুণ থেকে শত গুণ; এমন সংখ্যা যার অনেক উৎপাদক; আর প্রতিটি partition-এর প্রত্যাশিত আকার এমন যে একটি partition সরানো কয়েক মিনিট থেকে এক-দুই ঘণ্টায় শেষ হয়। হিসাবটি ডেটার বৃদ্ধির সাথে মিলিয়ে নিন।
মূল উপলব্ধি: partition-এর সংখ্যা হলো ভবিষ্যৎ scaling-এর ছাদ আর আজকের overhead-এর মাঝে একটি আপস — বাছার সময় তিন বছর পরের ছবিটি ভাবুন।
প্র ০৩একজন তারকা ব্যবহারকারী বা একটি ভাইরাল পোস্ট কেন পুরো sharding নকশাকে চ্যালেঞ্জ করে? Salting আর cache-এর দাম কোথায়?
Sharding-এর পুরো প্রতিশ্রুতি দাঁড়িয়ে আছে একটি অনুমানে: অনেকগুলো key, আর প্রতিটির চাপ মোটের তুলনায় ছোট। জনপ্রিয়তা কিন্তু Zipf-ধাঁচের — ১,০০০ জনের মধ্যে শীর্ষজন একাই প্রায় ১৩.৪% অনুরোধ পান, যা ৮টি shard-এর গড় ভাগের চেয়েও বেশি। কোটি ব্যবহারকারীর সিস্টেমে শীর্ষ কয়েকজনের follower কয়েক কোটি; তাঁদের একটি পোস্ট মুহূর্তে লাখো পড়া আর like আনে। Hash যত ভালোই হোক, একটি key একটি partition-এ — আর সেই partition-এর leader একটি মেশিন।
প্রতিকারগুলোর প্রতিটির নিজস্ব দাম:
- লেখায় salting: like-গণনাকে r-টি sub-key-তে ভাগ করলে লেখার চাপ r ভাগ হয়। কিন্তু মোট like দেখাতে r-টি পড়া আর যোগ; আর অংশ ৪-এর পরীক্ষায় দেখেছেন, sub-key-এর hash নিজেই মিলে গিয়ে কম shard-এ পড়তে পারে। AWS-এর DynamoDB documentation-এ partition key-এর সাথে এলোমেলো বা হিসাব করা লেজ জুড়ে “write sharding”-এর পরামর্শ আছে — একই চিন্তা।
- পড়ায় cache আর replica: গরম key-এর পড়া app server-এর local cache বা কয়েকটি cache কপিতে থামানো; দাম কয়েক সেকেন্ড পুরোনো তথ্য আর invalidation-এর জটিলতা (পর্ব ২৯ আর ৩০)। Replica থেকে পড়লে replication lag (পর্ব ২২)।
- নকশা বদল: সাধারণ ব্যবহারকারীর পোস্ট follower-দের feed-এ লেখার সময় ছড়ানো, কিন্তু তারকার পোস্ট পড়ার সময় টেনে আনা — একটি মিশ্র fan-out (পর্ব ৫৬)।
- আলাদা ঘর: directory-তে সবচেয়ে বড় tenant বা অ্যাকাউন্টকে নিজস্ব shard দেওয়া।
কোন key গরম, তা আগে থেকে জানা যায় না — তাই পর্যবেক্ষণ লাগে: প্রতি partition আর প্রতি key-এর অনুরোধ মাপা, শীর্ষ key-এর একটি চলমান তালিকা (count-min sketch-এর মতো কাঠামো, পর্ব ৪৫), আর হঠাৎ চাপ এলে স্বয়ংক্রিয় সতর্কতা।
মূল উপলব্ধি: sharding গড় চাপ ভাগ করে, চূড়ার চাপ নয়। চূড়ার জন্য আলাদা পরিকল্পনা — salting, cache, নকশা বদল — শুরু থেকেই রাখতে হয়।
প্র ০৪Global secondary index কেন প্রায় সবসময় async? “লেখার পরে নিজের লেখা না দেখা”-র সমস্যা কীভাবে সামলাবেন?
ধরুন messages ভাগ হয়েছে conversation_id দিয়ে, আর sender_id-এর একটি global index আছে, যা sender_id দিয়েই ভাগ। একজন ব্যবহারকারী message পাঠালেন: সারিটি যায় conversation-এর shard-এ, আর index-এর entry যায় sender-এর index shard-এ — দুটি ভিন্ন মেশিন। দুটিকে একই মুহূর্তে, একসাথে সফল বা একসাথে ব্যর্থ করতে হলে লাগে একটি distributed transaction, যেমন two-phase commit (পর্ব ২৫): প্রতিটি লেখায় বাড়তি নেটওয়ার্ক round trip, lock ধরে রাখা, আর index shard ধীর বা বিকল হলে মূল লেখাও আটকে যাওয়া। একটি লেখা যত বেশি index ছোঁয়, ঝুঁকি তত বাড়ে।
তাই বেশিরভাগ সিস্টেম মূল লেখা আগে শেষ করে, আর index-এর হালনাগাদ পাঠায় পেছনে — একটি log বা queue দিয়ে। AWS-এর documentation অনুযায়ী DynamoDB-র global secondary index থেকে শুধু eventually consistent পড়া যায়; লেখার পরে কিছুক্ষণ index-এ নতুন মান না-ও দেখা যেতে পারে। Martin Kleppmann-এর বই (২০১৭) এই trade-off-কে document-partitioned বনাম term-partitioned index নামে ব্যাখ্যা করে।
ব্যবহারকারীর চোখে সমস্যা: “আমার পাঠানো message” পাতায় এইমাত্র পাঠানোটি নেই। প্রচলিত সমাধান:
- নিজের লেখা নিজের কাছে: সদ্য লেখা ID-গুলো client বা session-এ মনে রেখে ফলের সাথে জুড়ে দেখানো (read-your-writes-এর একটি রূপ, পর্ব ২২ আর ৪০)।
- অপেক্ষার সীমা: index-এর পিছিয়ে থাকা (lag) মাপা আর সতর্কসীমা রাখা; lag বেশি হলে মূল shard থেকে scatter-gather-এ ফিরে যাওয়া।
- Index মিলিয়ে দেখা: async হালনাগাদ হারিয়ে গেলে index চিরকাল ভুল থাকে — তাই পর্যায়ক্রমে মূল ডেটার সাথে মিলিয়ে মেরামত।
আরেকটি সূক্ষ্মতা: global index-ও partition, তাই তারও hot spot হয় — “status = pending” বা একটি জনপ্রিয় tag-এর মতো কম cardinality-র মানে সব entry একটি index shard-এ।
মূল উপলব্ধি: shard-এ ভাগ করা ডেটায় প্রতিটি index একটি আলাদা ডেটা-কাঠামো, যার নিজের consistency আর নিজের hot spot আছে — বিনামূল্যের “আরও একটি index” আর থাকে না।
প্র ০৫Flickr-এর ticket server, Twitter-এর Snowflake, Instagram-এর ID আর UUID v7 — unique ID-র ইতিহাস থেকে কী শেখার আছে?
প্রায় প্রতিটি দ্রুত বাড়তে থাকা সেবা একই মোড়ে পৌঁছেছে: একটি database-এর auto-increment আর চলে না। প্রকাশিত engineering blog-গুলোতে পথগুলো বেশ স্পষ্ট (খুঁটিনাটি সময়ের সাথে বদলেছে):
- Flickr (২০১০): দুটি MySQL “ticket server” — একটি বিজোড়, অন্যটি জোড় ID দেয় — শুধু ID বিলানোর জন্য। সরল আর পরিচিত প্রযুক্তি, কিন্তু প্রতিটি ID-তে একটি নেটওয়ার্ক অনুরোধ, আর সার্ভার দুটি নিজেই গুরুত্বপূর্ণ অংশ।
- Twitter-এর Snowflake (২০১০): প্রকাশিত ঘোষণা অনুযায়ী, MySQL থেকে অন্য storage-এ যাওয়ার সময় তাঁদের দরকার ছিল সমন্বয় ছাড়া, মোটামুটি সময়ক্রমে সাজানো 64-bit ID. ফল: সময় + মেশিন + ক্রমিক সংখ্যা। প্রকাশিত কোডের epoch ২০১০ সালের নভেম্বরের একটি মুহূর্ত, তাই 41-bit সময় তাঁদের হিসাবে ২০৮০ সালের কাছাকাছি ফুরাবে।
- Instagram (২০১২): নতুন সার্ভিস না বানিয়ে PostgreSQL-এর ভেতরেই একটি function দিয়ে ID — সময়, logical shard-এর নম্বর আর প্রতি shard-এ একটি ক্রমিক সংখ্যা; ID দেখেই বোঝা যায় সারিটি কোন shard-এ।
- UUID v7 (RFC 9562, ২০২৪): সময়ক্রমের সুবিধা একটি মানদণ্ডে — যেকোনো ভাষা আর database একই নিয়মে বানাতে পারে, worker নম্বর বিলির ঝামেলা নেই, দাম 128 bit.
একটি ছোট কিন্তু শিক্ষণীয় ঘটনা: 64-bit ID JavaScript-এর Number-এ নির্ভুল থাকে না, কারণ সেখানে নির্ভুল পূর্ণসংখ্যা 253 পর্যন্ত। Twitter-এর API documentation-এ তাই ID-র একটি string রূপ (id_str) দেওয়া হতো, যাতে browser-এ ID-র শেষ অঙ্ক বদলে না যায়। এই পর্বের প্রোগ্রামেও সেজন্যই BigInt.
আর নিরাপত্তার দিক: ক্রমিক বা সময়-ভিত্তিক ID অনুমানযোগ্য। ID অনুমান করে অন্যের ডেটা দেখা ঠেকানোর আসল উপায় প্রতিটি অনুরোধে authorization যাচাই (পর্ব ৫২); ID-কে গোপন রাখা নিরাপত্তা নয়। তবে দিনে কতটি অর্ডার হলো, তা প্রতিযোগীকে জানাতে না চাইলে বাইরের দিকে এলোমেলো ID দেখানো যায়।
মূল উপলব্ধি: ID বাছাই মানে কয়েকটি গুণের আপস — আকার, সময়ক্রম, সমন্বয় আর তথ্য ফাঁস। প্রতিটি পদ্ধতি আলাদা গুণকে অগ্রাধিকার দেয়; আপনার সিস্টেমে কোনটি জরুরি, সেটি আগে ঠিক করুন।
অনুশীলন
আগে নিজে লিখুন বা হিসাব করুন, তারপর উত্তর মেলান। শেষ প্রশ্নে আগে অনুমান, তারপর অংশ ৪-এর প্রোগ্রাম চালিয়ে যাচাই।
-
নকশা — তিনটি table-এর shard key: প্রতিটির জন্য shard key, ভাগের নিয়ম আর প্রধান ঝুঁকি লিখুন। (ক) একটি কাল্পনিক mobile banking সেবার
transactionstable; ঘন query: “একটি অ্যাকাউন্টের শেষ ২০টি লেনদেন” আর “এক মাসের statement”. (খ) একটি কাল্পনিক ride-sharing অ্যাপেরtripstable; ঘন query: যাত্রী আর চালকের নিজ নিজ trip-এর ইতিহাস। (গ) একটি কাল্পনিক e-commerce সাইটেরproductstable; ঘন query: product_id দিয়ে পাতা খোলা আর category দিয়ে তালিকা।- (ক) transactions: partition key account_id (hash), ভেতরে সময় বা Snowflake-ধাঁচের ID-র ক্রমে সাজানো — দুটি query-ই এক shard-এ একটি range scan. ঝুঁকি: বড় ব্যবসায়ীর অ্যাকাউন্টে বিপুল লেনদেন — মাসের bucket যোগ করা যায়। দুই অ্যাকাউন্টের মধ্যে টাকা পাঠানো দুই shard ছোঁয়, তাই পর্ব ২৫-এর saga বা distributed transaction. created_at দিয়ে range — বাতিল, hot spot.
- (খ) trips: একটি key দুই দিক মেলাতে পারে না — rider_id দিয়ে ভাগ করলে চালকের ইতিহাস scatter-gather, উল্টোটাও। সমাধান: rider_id দিয়ে মূল table, আর driver_id দিয়ে ভাগ করা একটি global index বা আলাদা denormalized table, async-ভাবে হালনাগাদ। শহর দিয়ে ভাগ লোভনীয়, কিন্তু cardinality কম আর ঢাকার মতো বড় শহরের shard বিশাল; চলমান trip-এর geohash-ভিত্তিক ভাগ পর্ব ৫৮-এ।
- (গ) products: product_id (hash) — পাতা খোলা এক shard-এ। Category-র তালিকা হলে category দিয়ে local index মানে scatter-gather, আর category দিয়ে global index-এ জনপ্রিয় category একটি গরম index shard. সাধারণত পণ্যের তালিকা আর খোঁজার কাজ আলাদা search cluster-এ দেওয়া হয় (পর্ব ২৭ — Search), আর catalog কয়েক লাখ পণ্যের হলে প্রায়ই sharding-ই লাগে না।
-
হিসাব — partition কতটি সরে: একটি cluster-এ ১,০২৪টি নির্দিষ্ট partition, ৮টি node, মোট ডেটা ১৬ TB (হিসাব সহজ রাখতে ১ TB = ১,০২৪ GB ধরুন), সব partition সমান মাপের। দুটি নতুন node যোগ হলো। (ক) প্রতিটি নতুন node কতটি partition নেবে, মোট কতটি সরবে, কত শতাংশ? (খ) কত GB কপি হবে; প্রতিটি নতুন node সেকেন্ডে ২৫০ MB নিতে পারলে (ধরা যাক, 1 GB ≈ 1000 MB) কত সময়? (গ) একই কাজ node-এর ওপর সরাসরি hash mod N-এ (৮ থেকে ১০) কত ডেটা সরাত? (ঘ) পরে ১০টির একটি স্থায়ীভাবে বিকল হলে?
- (ক) ন্যায্য ভাগ ১,০২৪ ÷ ১০ = ১০২.৪, তাই প্রতিটি নতুন node ১০২টি নেয়; মোট সরে ২০৪টি ≈ ১৯.৯%. শেষে পুরোনো চারটিতে ১০২, চারটিতে ১০৩, নতুন দুটিতে ১০২ করে (মোট ১,০২৪) — অংশ ৫-এর প্রোগ্রামের নিয়মেও তাই।
- (খ) প্রতিটি partition ১৬ TB ÷ ১,০২৪ = ১৬ GB; মোট ২০৪ × ১৬ = ৩,২৬৪ GB. প্রতিটি নতুন node পায় ১,৬৩২ GB; দুটি একসাথে কপি হলে ১৬,৩২,০০০ MB ÷ ২৫০ MB/s = ৬,৫২৮ সেকেন্ড ≈ ১.৮ ঘণ্টা।
- (গ) key থাকে শুধু যখন h mod 8 = h mod 10. lcm(8, 10) = ৪০টি পরপর মানে এমন মান মাত্র ৮টি (০ থেকে ৭), অর্থাৎ থাকে ২০%, সরে ৮০% — প্রায় ১২.৮ TB, চার গুণ।
- (ঘ) বিকল node-এর ১০২টি partition বাকি ৯টিতে ভাগ হয় — প্রত্যেকে ১১ বা ১২টি; সরে ১০২/১,০২৪ ≈ ১০% (replica থেকে নতুন কপি বানিয়ে)। অন্য কোনো partition নড়ে না।
-
একটি Snowflake ID ভেঙে পড়ুন: গঠন: 41 bit সময় (epoch ২০২৪-০১-০১ 00:00 UTC থেকে millisecond) · 10 bit worker · 12 bit sequence. ID =
291488190695477290. সময় (UTC), worker আর sequence বের করুন। কাজটি browser-এর console-এBigIntদিয়ে, বা অংশ ৯-এর প্রোগ্রামেরparseদিয়ে করতে পারেন।- সময়: ID ÷ 222 (৪১,৯৪,৩০৪)-এর পূর্ণ অংশ = ৬৯,৪৯৬,২০০,২৫০ ms epoch থেকে। ৮০৪ দিন = ৬৯,৪৬৫,৬০০,০০০ ms; বাকি ৩০,৬০০,২৫০ ms = ৮ ঘণ্টা ৩০ মিনিট ০.২৫ সেকেন্ড। ২০২৪-০১-০১ থেকে ৮০৪ দিন (২০২৪ অধিবর্ষ, ৩৬৬ দিন; ২০২৫-এর ৩৬৫; তারপর ৭৩ দিন) = ২০২৬-০৩-১৫, 08:30:00.250 UTC.
- Worker: ID mod 222 = ২১,০১,২৯০; একে ৪,০৯৬ দিয়ে ভাগ করলে ভাগফল ৫১৩ — কোডে
(id >> 12n) & 1023n. - Sequence: সেই ভাগের ভাগশেষ ৪২ — কোডে
id & 4095n. অর্থাৎ worker ৫১৩ সেই millisecond-এ এর আগে আরও ৪২টি ID দিয়েছিল।
parse(291488190695477290n) → { ms: 1773563400250, worker: 513, seq: 42 } // 2026-03-15T08:30:00.250Z -
আউটপুট অনুমান করুন: অংশ ৪-এর
skew_range_hash_salt.js-এSALT = 8-কেSALT = 4করলে কী বদলাবে?zipfসারিটি কি বদলাবে?saltedসারি আর শেষ লাইনের “busiest / average” মোটামুটি কত হবে? আগে অনুমান লিখুন, তারপর চালিয়ে মেলান।অনুমান: প্রতিটি অনুরোধে
rand()একই সংখ্যকবার ডাকা হয়, তাইzipfসারি হুবহু এক।user:1-এর ২,১৩১টি অনুরোধ এবার ৪টি sub-key-তে, প্রতিটিতে প্রায় ৫৩০; চারটি আলাদা shard-এ পড়লে অনুপাত ১.৩–১.৪. কিন্তু ৮টি shard-এ ৪টি sub-key সব আলাদা হওয়ার সম্ভাবনা মাত্র (৮ × ৭ × ৬ × ৫) ÷ ৮৪ ≈ ৪১%, গড়ে ভিন্ন shard প্রায় ৮ × (১ − (৭/৮)৪) ≈ ৩.৩টি।আসল আউটপুট (শেষ তিন লাইন):
zipf 1317 1336 2271 2203 1847 1284 2345 3397 salted x4 1317 1336 2271 3270 2911 1284 2345 1266 user:1 = 2131 of 16000 requests; busiest shard / average: 1.70 -> 1.64দুর্ভাগ্য: চারটি sub-key
user:1#0…#3পড়েছে shard ৪, ৩, ৪, ৩-এ — মাত্র দুটিতে। তাই shard ৩ হলো ৩,২৭০ (সেখানে আগে থেকেইuser:3), আর অনুপাত নামল মাত্র ১.৬৪-এ। শিক্ষা: sub-key-গুলোকে hash-এর ভাগ্যে না ছেড়ে সরাসরি আলাদা shard-এ বসান (যেমন মূল shard থেকে পরপর r-টি), অথবা নকশার সময় যাচাই করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমসিস্টেম না থামিয়ে পুরোনো shard থেকে নতুন cluster-এ resharding-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🏗️ নতুন cluster, partition map আর routing নিয়ম তৈরি — কোনো traffic ছাড়া
- ✍️ Dual write চালু — নতুন লেখা পুরোনো আর নতুন দুই জায়গায়
- 📦 Backfill — পুরোনো ডেটা ব্যাচে কপি, নতুনতর version থাকলে বাদ
- 🔍 যাচাই — সারি গোনা, checksum আর shadow read
- 📖 পড়া ধাপে ধাপে নতুন cluster-এ — ১% থেকে ১০০%
- 🔁 লেখার সত্যের উৎস নতুনে, ফেরার জানালা শেষে পুরোনো বন্ধ
একটি messages table range partitioning-এ created_at (timestamp) দিয়ে ৮টি shard-এ ভাগ করা, প্রতিটি shard একটি সময়ের পরিসর। লেখার চাপ কেমন হবে?
- ৮টি shard-এ মোটামুটি সমানভাবে ছড়াবে
- প্রায় সব নতুন লেখা একটি shard-এ পড়বে
- লেখা পালা করে প্রতিটি shard-এ যাবে
- পুরোনো shard-গুলোতে বেশি লেখা পড়বে
একটি cluster-এ ২৫৬টি নির্দিষ্ট partition, ৪টি node-এ সমানভাবে (প্রতিটিতে ৬৪টি)। পঞ্চম node যোগ করে সবাইকে প্রায় সমান ভাগ দিতে নতুন node কোনো partition না ভেঙে মোটামুটি কয়টি partition নেবে?
- ৬৪টি
- প্রায় ২০৫টি
- প্রায় ৫১টি
- ১২টি
একটি table conversation_id দিয়ে ১৬টি shard-এ ভাগ, আর sender_id-এর একটি local (document-partitioned) secondary index আছে। “sender_id = 42-এর সব message” query কী করবে?
- ১৬টি shard-এর সবগুলোতে যাবে, তারপর ফল জোড়া হবে (scatter-gather)
- sender_id-এর hash দেখে একটি index shard-এ যাবে
- sender_id-এর hash দেখে একটি data shard-এ যাবে
- Query চালানো যাবে না
একটি Snowflake-ধাঁচের 64-bit ID-তে 41 bit millisecond-এর সময়, 10 bit worker আর 12 bit sequence. একটি worker এক millisecond-এ সর্বোচ্চ কয়টি অনন্য ID দিতে পারে?
- ১,০২৪টি
- ৪১টি
- প্রায় ৪২ লাখ
- ৪,০৯৬টি
একটি database primary key-এর ক্রমে B-tree-তে সারি রাখে। এমন table-এ UUID v4-এর বদলে UUID v7 নিলে মূল লাভ কী?
- ID-র আকার অর্ধেক হয়
- নতুন ID মোটামুটি সময়ক্রমে বাড়ে, তাই insert index-এর এক প্রান্তে জমে আর পাতা-ছড়ানো কমে
- Collision-এর সম্ভাবনা দূর হয়
- ID থেকে আর কোনো তথ্য পড়া যায় না
একটি query-কে ১৬টি shard-এর উত্তরের অপেক্ষা করতে হয়। প্রতিটি shard স্বাধীনভাবে ১% সম্ভাবনায় তার নিজের p99-এর চেয়ে ধীর। মোটামুটি কত শতাংশ query অন্তত একটি ধীর shard-এর জন্য আটকাবে?
- ১%
- ঠিক ১৬%
- প্রায় ১৫%
- প্রায় ০%
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- ডেটা বা লেখার চাপ একটি মেশিনে না আঁটলে partitioning; replica শুধু পড়া বাড়ায়। বাস্তবে প্রতিটি shard-এ leader আর replica — ভাগ আর কপি একসাথে। আগে হিসাব, তারপর sharding.
- Shard key-এর চার শর্ত: অনেক ভিন্ন মান, সমান ছড়ানো, ঘন query এক shard-এ, বৃদ্ধিতে টেকা। Query আগে, key পরে।
- Range: range scan সস্তা, ক্রমিক key-এ hot spot; hash: সমান, range scan নেই; compound key দুটো মেলায়; directory নমনীয় কিন্তু বাড়তি অংশ; geo latency আর আইনের জন্য।
- Rebalancing-এ hash mod N নয়: node-এর চেয়ে অনেক বেশি fixed partition (৪ → ৫ node-এ প্রায় ২০% সরে), dynamic split, বা consistent hashing; map-এ সংস্করণ নম্বর, সত্যের উৎস coordination service.
- Shard key ছাড়া query = scatter-gather (১৬ shard-এ p99 প্রায় ৫ গুণ); local বনাম global index; resharding = dual write + backfill + যাচাই + ধাপে ধাপে বদল; ID = UUID v7 বা Snowflake (৪,০৯৬/ms/worker, ≈ ৬৯.৭ বছর)।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৪ · Transaction আর isolation level — anomaly, locking আর MVCCপরবর্তী পর্বএকসাথে চলা লেনদেনের ভুল আর তার প্রতিরোধ।
- পর্ব ২২ · Replication — leader-follower, multi-leader, leaderless আর replication lagএকই মডিউলপ্রতিটি shard-এর কপিগুলো কীভাবে মেলে।
- পর্ব ১৬ · Consistent hashing — ring, virtual node আর rendezvous hashingভিত্তিHash থেকে node বাছাই আর কম নড়াচড়ার গণিত।
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- DBMS ও SQL · Distributed database — partitioning, sharding আর CAPগভীরে যানFragmentation, horizontal ও vertical partitioning আর distributed query.
- DSA · Hash table — hash function, collision আর load factorপাশের ট্র্যাকHash function আর birthday সমস্যার হিসাব।