এই পর্বে যা শিখবেন
- কখন ডেটাবেস ভাগ করা দরকার, scale-up আর scale-out-এর পার্থক্য, আর কখন ভাগ করা উচিত নয়
- horizontal ও vertical partitioning; range, hash, list আর directory ভাগ; ভালো shard key বাছাই — লাইভ SQL-এ shard গোনা
- consistent hashing আর virtual node — Python-এ গুনে দেখা কতগুলো key সরে; scatter–gather কোয়েরি আর global secondary index
- two-phase commit কীভাবে কাজ করে আর কোথায় আটকে যায়; saga আর compensation
- CAP, PACELC, consistency model (linearizable থেকে eventual), quorum আর consensus; NewSQL ব্যবস্থাগুলো কী প্রতিশ্রুতি দেয়
১ · একটি যন্ত্র কখন যথেষ্ট নয় — distribute করার তিন কারণ
এখন পর্যন্ত পুরো কোর্সে ডেটাবেস ছিল একটি যন্ত্রে। পর্ব ৫২-এ (Backup, replication আর high availability) সেই ডেটার কয়েকটি কপি বানিয়েছি, কিন্তু প্রতিটি কপিতে ছিল পুরো ডেটা, আর সব লেখা যেত একটি primary-তে। Distributed database-এ ডেটা নিজেই কয়েকটি যন্ত্রে ভাগ হয়ে থাকে, আর যন্ত্রগুলো network দিয়ে মিলে একটি ডেটাবেসের মতো আচরণ করে। কেন এত ঝামেলা? কারণ তিনটি:
আকার আর লেখার চাপ
ডেটা একটি যন্ত্রের ডিস্কে ধরে না, বা সেকেন্ডে এত লেখা আসে যে একটি primary পারে না। read replica পড়ার চাপ ভাগ করে, কিন্তু প্রতিটি লেখা তো সব কপিতেই করতে হয়।
দূরত্ব আর latency
ঢাকার সার্ভার থেকে অন্য মহাদেশের ব্যবহারকারীর কাছে প্রতিটি round trip-এ শতাধিক মিলিসেকেন্ড লাগতে পারে। ডেটা ব্যবহারকারীর কাছাকাছি রাখলে latency কমে।
ব্যর্থতা সহ্য করা
একটি যন্ত্র, র্যাক বা পুরো ডেটা সেন্টার বন্ধ হলেও বাকিরা কাজ চালিয়ে যাবে — ডেটার কিছু অংশ বা কপি অন্য জায়গায় থাকে বলে।
চাপ বাড়লে দুটি পথ। Scale-up (vertical scaling): আরও বড় যন্ত্র — বেশি CPU core, বেশি RAM, দ্রুত SSD. সরল, অ্যাপে কিছুই বদলাতে হয় না, কিন্তু একসময় আর বড় যন্ত্র নেই, আর দাম দ্রুত বাড়ে। Scale-out (horizontal scaling): আরও অনেক সাধারণ যন্ত্র, কাজ ভাগ করে। সীমা অনেক দূরে, কিন্তু এখন প্রতিটি কোয়েরি আর লেনদেনকে ভাবতে হয় “ডেটা কোথায়”.
একটি মুদি দোকানের বাকির খাতা ভাবুন। খাতা মোটা হলে দোকানি বড় খাতা কেনেন আর দ্রুত লেখেন — scale-up. কিন্তু গ্রাহক হাজার ছাড়ালে তিনি তিনজন কর্মচারী রাখেন: নামের প্রথম অক্ষর অ–ক একজনের খাতায়, খ–ত আরেকজনের, বাকিরা তৃতীয়জনের — scale-out. এখন “রহিমের বাকি কত” জানতে একজনকে জিজ্ঞেস করলেই হয়, কিন্তু “আজ মোট কত বাকি” জানতে তিনজনের কাছেই যেতে হয়। আর রহিম যদি করিমের বাকি শোধ করেন, দুটি খাতায় একসাথে লিখতে হবে — একটি খাতায় লেখার পর কর্মচারী অসুস্থ হলে?
একটি যন্ত্রে ব্যর্থতা মানে “সব চলছে” বা “কিছুই চলছে না”. distributed ব্যবস্থায় আসে আংশিক ব্যর্থতা: কিছু যন্ত্র চলছে, কিছু নেই, আর কিছু হয়তো চলছে কিন্তু network-এ পৌঁছানো যাচ্ছে না — বাইরে থেকে শেষ দুটি আলাদা করা যায় না। এই পর্বের প্রায় সব কৌশল এই একটি সমস্যার উত্তর।
২ · Partitioning — সারি ধরে ভাগ, column ধরে ভাগ
বড় টেবিলকে ছোট টুকরায় ভাগ করার নাম partitioning. ভাগের দুটি দিক:
| Horizontal | Vertical | |
|---|---|---|
| কী ভাগ হয় | সারি: প্রতিটি টুকরায় একই column, আলাদা সারি | column বা টেবিল: প্রতিটি টুকরায় আলাদা column, একই সারির অংশ |
| উদাহরণ | গ্রাহক ১–১০ লাখ এক যন্ত্রে, বাকিরা আরেকটিতে | পণ্যের নাম-দাম একটি টেবিলে, বড় বিবরণ আর ছবি আলাদা জায়গায় |
| লাভ | আকার আর লেখার চাপ যন্ত্রগুলোর মধ্যে ভাগ | প্রায়ই-পড়া ছোট column দ্রুত, বড় column আলাদা |
| দাম | যে কোয়েরি সব সারি চায়, তাকে সব টুকরায় যেতে হয় | পুরো সারি চাইলে join লাগে |
horizontal ভাগ যখন আলাদা যন্ত্রে থাকে, তার নাম sharding, আর প্রতিটি টুকরা একটি shard. একই যন্ত্রের ভেতরে ভাগ — পর্ব ৫৩-এর table partitioning — পুরোনো ডেটা মোছা আর partition pruning-এ সাহায্য করে, কিন্তু যন্ত্রের সীমা বাড়ায় না। vertical ভাগের বড় রূপ হলো কাজ ধরে ভাগ: অর্ডারের ডেটাবেস এক সার্ভারে, ব্যবহারকারীর প্রোফাইল আরেকটিতে — microservice-এর যুগে খুব প্রচলিত, আর প্রায়ই sharding-এর আগের ধাপ।
PostgreSQL-এ একই যন্ত্রের ভেতরে দুই ধরনের ভাগ এভাবে লেখা হয় (চালানো যাবে না, SQLite-এ এই সুবিধা নেই):
-- range: মাস ধরে (partition key primary key-এর অংশ হতে হয়)
CREATE TABLE orders (
order_id bigint, customer_id bigint NOT NULL,
order_date date NOT NULL, status text,
PRIMARY KEY (order_id, order_date)
) PARTITION BY RANGE (order_date);
CREATE TABLE orders_2025_09 PARTITION OF orders
FOR VALUES FROM ('2025-09-01') TO ('2025-10-01');
-- hash: customer_id ধরে ৪ ভাগ (PostgreSQL 11 থেকে)
CREATE TABLE carts (customer_id bigint, items jsonb) PARTITION BY HASH (customer_id);
CREATE TABLE carts_p0 PARTITION OF carts FOR VALUES WITH (MODULUS 4, REMAINDER 0);MySQL-এ PARTITION BY RANGE / LIST / HASH / KEY আছে। কয়েকটি যন্ত্রে ভাগ করতে লাগে বাড়তি স্তর — অ্যাপের নিজের কোড, Vitess বা Citus-এর মতো টুল (অংশ ৯), বা জন্ম থেকেই distributed কোনো ডেটাবেস।
৩ · কোন সারি কোন shard-এ — range, hash, list আর directory
প্রতিটি সারির জন্য একটি নিয়ম লাগে: এটি কোন shard-এ যাবে? নিয়মটি চলে একটি column বা column-এর দলের ওপর, যার নাম shard key (partition key). প্রচলিত চারটি নিয়ম:
Range partitioning: key-এর পরিসর ধরে — যেমন বোর্ড পরীক্ষার ফল নিতে রোল ১–৫০০০ এক কাউন্টারে, ৫০০১–১০০০০ পরেরটিতে। লাভ: পরিসরের কোয়েরি (“সেপ্টেম্বরের সব অর্ডার”) শুধু এক-দুটি shard-এ যায়, আর key-এর ক্রমে সাজানো থাকে। ঝুঁকি: ডেটা অসমান হতে পারে, আর সময় বা ক্রমিক id-র মতো ক্রমাগত বাড়তে থাকা key-তে সব নতুন লেখা শেষ shard-এ জমে — একটি hot spot.
Hash partitioning: key-এর hash বের করে তার মান ধরে, সবচেয়ে সরল রূপে hash(key) % N. লাভ: পাশাপাশি key-ও ছড়িয়ে যায়, তাই লেখা সমানভাবে ভাগ হয়। দাম: পরিসরের কোয়েরিকে সব shard-এ যেতে হয়, কারণ ক্রম হারিয়ে গেছে; আর N বদলালে প্রায় সব key সরে যায় (অংশ ৪)।
List partitioning: নির্দিষ্ট মানের তালিকা ধরে — যেমন Dhaka আর Gazipur-এর গ্রাহক এক shard-এ, Chattogram আর Cox's Bazar আরেকটিতে। দেশ বা অঞ্চলভেদে ডেটা রাখার নিয়ম মানতে কাজে লাগে। ঝুঁকি: একটি মান (ঢাকা) একাই অনেক বড় হতে পারে।
Directory-based sharding: একটি আলাদা lookup টেবিল প্রতিটি key (বা key-এর দল) কোন shard-এ, তা লিখে রাখে। লাভ: সবচেয়ে নমনীয় — একজন বড় গ্রাহককে একাই একটি shard দেওয়া যায়, ডেটা সরানো মানে শুধু তালিকা বদলানো। দাম: প্রতিটি অনুরোধে আগে তালিকা দেখা, আর তালিকাটি নিজেই একটি গুরুত্বপূর্ণ, সবসময় চালু থাকা সেবা হয়ে ওঠে।
এবার দোকানের নমুনা ডেটাবেসে হাতে-কলমে। এর টেবিল: customers (customer_id, name, city, phone, joined_on), categories, products (product_id, name, category_id, price, stock), orders (order_id, customer_id, order_date, status) আর order_items (order_id, product_id, quantity, unit_price). ধরুন তিনটি shard, আর নিয়ম shard = customer_id % 3:
SELECT customer_id, name, city, customer_id % 3 AS shard
FROM customers
ORDER BY shard, customer_id;
SELECT c.customer_id % 3 AS shard,
COUNT(DISTINCT c.customer_id) AS customers,
COUNT(o.order_id) AS orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY shard
ORDER BY shard;প্রথম ফলে আট সারি: shard 0-তে গ্রাহক 3 আর 6, shard 1-এ 1, 4, 7, shard 2-তে 2, 5, 8. দ্বিতীয় ফল: 0 | 2 | 2, 1 | 3 | 5, 2 | 3 | 3. গ্রাহক প্রায় সমান ভাগ হয়েছে, কিন্তু অর্ডার নয় — shard 1-এ ৫টি, কারণ Rahim (customer 1) একাই ৩টি অর্ডার দিয়েছেন। hash গ্রাহকদের ছড়ায়, কিন্তু একজন খুব সক্রিয় গ্রাহকের সব কাজ তাঁর shard-এই যায় (data skew). বাস্তবে একজন বড় বিক্রেতা বা জনপ্রিয় পণ্য একটি shard-কে অন্যদের চেয়ে অনেক ব্যস্ত করতে পারে। আর % এখানে সরলতার জন্য সরাসরি id-তে; বাস্তবে আগে hash নেওয়া হয়, যাতে id-র কোনো ধাঁচ ভাগকে বাঁকা না করে।
এবার range: মাস ধরে অর্ডার ভাগ। order_parts টেবিলে প্রতিটি ভাগের নাম (part) আর সীমা — lo থেকে শুরু, hi-এর ঠিক আগে শেষ (half-open, পর্ব ১৬)। নভেম্বরের ভাগটি আগেই বানানো, এখনো খালি:
SELECT p.part, p.lo, p.hi, COUNT(o.order_id) AS orders,
MAX(o.order_date) AS newest
FROM order_parts p
LEFT JOIN orders o ON o.order_date >= p.lo AND o.order_date < p.hi
GROUP BY p.part
ORDER BY p.part;
-- ১০ থেকে ২০ সেপ্টেম্বরের অর্ডার কোন ভাগে খুঁজতে হবে?
SELECT part FROM order_parts
WHERE lo < '2025-09-21' AND hi > '2025-09-10';প্রথম ফল: p2025_08 | 0, p2025_09 | 6 (newest 2025-09-20), p2025_10 | 4 (newest 2025-10-09), p2025_11 | 0. দ্বিতীয় ফলে শুধু p2025_09 — router বাকি তিনটি ভাগ ছোঁবেই না, যা range-এর বড় সুবিধা। কিন্তু প্রথম ফলের দিকে আবার তাকান: আজ যত নতুন অর্ডার আসবে, সব যাবে অক্টোবরের ভাগে। ভাগগুলো আলাদা যন্ত্রে হলে বাকি যন্ত্রগুলো প্রায় বসে থাকে, আর একটি যন্ত্র সব লেখা নেয় — ক্রমাগত বাড়তে থাকা key-র hot spot. পুরোনো ডেটা মুছতে বা archive করতে অবশ্য এই ধাঁচ চমৎকার: পুরো একটি ভাগ একবারে সরিয়ে ফেলা যায়।
shard key বাছাই — সবচেয়ে দামি সিদ্ধান্ত
shard key পরে বদলানো মানে প্রায় সব ডেটা নতুন জায়গায় সরানো, তাই এটি শুরুতেই ভাবতে হয়। দোকানের orders-এর দুই প্রার্থী তুলনা করুন:
| প্রশ্ন | customer_id (hash) | order_date (range) |
|---|---|---|
| লেখা সমান ভাগ হয়? | হ্যাঁ, অনেক আলাদা মান | না — আজকের সব লেখা এক ভাগে |
| “আমার অর্ডার” পাতা | এক shard-এ | সব মাসের ভাগে খুঁজতে হয় |
| “সেপ্টেম্বরের বিক্রি” | সব shard-এ (scatter) | এক ভাগে |
| সম্পর্কিত ডেটা একসাথে | গ্রাহক, তাঁর অর্ডার আর order_items এক জায়গায় | একজন গ্রাহকের অর্ডার নানা ভাগে ছড়ানো |
সাধারণ নিয়ম: (১) অনেক আলাদা মান আর সমান ছড়ানো; (২) সবচেয়ে বেশি চলা কোয়েরির WHERE-এ key থাকে, যাতে সেগুলো এক shard-এ যায়; (৩) একসাথে লাগা ডেটা একই key পায় — orders আর order_items দুটিই customer_id ধরে; (৪) key-এর মান কখনো বদলায় না, নইলে সারিটিকে অন্য যন্ত্রে সরাতে হয়। দোকানের লেনদেনের কাজে (OLTP) তাই customer_id সাধারণত ভালো; মাসভিত্তিক বিক্রির হিসাব চলে পর্ব ৫৫-এর data warehouse-এ। আর সময়-ক্রমের id (auto-increment বা timestamp) range-এর key হলে hot spot অনিবার্য — Google Spanner-এর ডকুমেন্টেশনও primary key-তে এমন ক্রমাগত বাড়া মান এড়াতে বলে।
৪ · Consistent hashing আর rebalancing
তিনটি shard আর পারছে না, চতুর্থটি যোগ করলেন। key % 3 থেকে key % 4: কতগুলো key-কে নতুন জায়গায় যেতে হবে? SQL-এ ১২,০০০টি key গুনে দেখি (recursive CTE, পর্ব ২৫):
WITH RECURSIVE k(id) AS (
SELECT 1 UNION ALL SELECT id + 1 FROM k WHERE id < 12000)
SELECT SUM(id % 3 <> id % 4) AS moved_3_to_4,
SUM(id % 4 <> id % 5) AS moved_4_to_5,
SUM(id % 3 <> id % 6) AS moved_3_to_6,
COUNT(*) AS keys
FROM k;ফল: 9000 | 9600 | 6000 | 12000. ৩ থেকে ৪-এ ৭৫% key সরে, ৪ থেকে ৫-এ ৮০% — সাধারণভাবে N থেকে N+1-এ প্রায় N/(N+1) ভাগ। অথচ নতুন যন্ত্রের ন্যায্য ভাগ মাত্র ১/(N+1) — বাকি সব সরানো অপচয়, আর ডেটা সরানোর সময় network আর ডিস্ক ব্যস্ত, cache ঠান্ডা। একটি ব্যতিক্রম: ৩ থেকে ৬ (দ্বিগুণ) করলে ঠিক অর্ধেক সরে — প্রতিটি shard নিজের অর্ধেক একটি নতুন যন্ত্রকে দেয়, আর দ্বিগুণ করায় এর চেয়ে কম সরানো সম্ভবও নয়। তাই অনেক ব্যবস্থা shard ভাগ করে দুই টুকরায় (split).
Consistent hashing (Karger ও সহকর্মীরা, ১৯৯৭; শুরুতে web cache-এর জন্য) এই অপচয় দূর করে। hash-এর সব মানকে একটি বৃত্ত ভাবুন — hash ring. প্রতিটি যন্ত্রকেও hash করে বৃত্তের একটি বিন্দুতে বসানো হয়। একটি key-এর মালিক হলো key-এর অবস্থান থেকে ঘড়ির কাঁটার দিকে প্রথম যে যন্ত্র পড়ে। নতুন যন্ত্র D বসলে শুধু D আর তার আগের যন্ত্রের মাঝের key-গুলো D-তে যায়; বাকি সবাই যেখানে ছিল সেখানেই থাকে।
একটি সমস্যা: প্রতিটি যন্ত্র বৃত্তে একটি মাত্র বিন্দু হলে ভাগ্যক্রমে কারও ভাগে বিশাল চাপ পড়তে পারে। সমাধান virtual node: প্রতিটি যন্ত্রকে বৃত্তের অনেকগুলো বিন্দুতে বসানো (যেমন A#0, A#1, …)। Amazon-এর Dynamo (২০০৭-এর গবেষণাপত্র) আর তার অনুপ্রেরণায় তৈরি Cassandra এই কৌশল ব্যবহার করে; শক্তিশালী যন্ত্রকে বেশি virtual node দিয়ে বেশি ভাগও দেওয়া যায়। নিচের প্রোগ্রাম ১০,০০০ গ্রাহক-key নিয়ে দুইভাবে হিসাব করে:
import bisect, hashlib
def h(s): # stable 32-bit hash (Python's built-in hash() changes per run)
return int(hashlib.md5(s.encode()).hexdigest()[:8], 16)
def build_ring(nodes, vnodes):
points = sorted((h(f"{n}#{v}"), n) for n in nodes for v in range(vnodes))
return [p for p, _ in points], [n for _, n in points]
def owner(ring, key): # first point clockwise from the key's position
pos, names = ring
return names[bisect.bisect(pos, h(key)) % len(pos)]
keys = [f"customer:{i}" for i in range(1, 10001)]
for v in (1, 100):
r3, r4 = build_ring("ABC", v), build_ring("ABCD", v)
before = {k: owner(r3, k) for k in keys}
after = {k: owner(r4, k) for k in keys}
load = [sum(1 for k in keys if before[k] == n) for n in "ABC"]
moved = [k for k in keys if before[k] != after[k]]
only_to_d = all(after[k] == "D" for k in moved)
print(f"vnodes={v:<3} load A,B,C = {load} add D: moved {len(moved)}"
f" ({len(moved) / 100:.1f}%), all moved keys went to D: {only_to_d}")
mod = sum(1 for k in keys if h(k) % 3 != h(k) % 4)
print(f"hash % N, 3 -> 4 shards: moved {mod} ({mod / 100:.1f}%)")আউটপুট:
vnodes=1 load A,B,C = [284, 5750, 3966] add D: moved 5615 (56.1%), all moved keys went to D: True vnodes=100 load A,B,C = [3180, 3447, 3373] add D: moved 2419 (24.2%), all moved keys went to D: True hash % N, 3 -> 4 shards: moved 7521 (75.2%)
এক বিন্দুতে ring ভয়ানক অসমান: A পেয়েছে মাত্র ২৮৪টি key, B ৫,৭৫০টি; আর D-ও ভাগ্যক্রমে একটি বড় ফাঁকে বসে ৫৬% নিয়েছে। ১০০টি virtual node-এ ভাগ প্রায় সমান (প্রতিটি ৩,৩৩৩-এর কাছে), আর D নেয় ২৪.২% — ন্যায্য ভাগ ২৫%-এর খুব কাছে। দুই ক্ষেত্রেই সরানো প্রতিটি key গেছে শুধু D-তে; পুরোনো যন্ত্রগুলোর মধ্যে কোনো অকারণ অদলবদল নেই। সাধারণ hash % N-এ সরেছে ৭৫.২% — SQL-এর হিসাবের সাথে মিলে যায়। md5 এখানে নিরাপত্তার জন্য নয়, শুধু প্রতিবার একই, ভালোভাবে ছড়ানো সংখ্যার জন্য।
Rebalancing — যন্ত্র যোগ বা বাদের পর ডেটা নতুন করে সাজানো — বাস্তবে আরও কয়েকভাবে হয়। এক, শুরুতেই যন্ত্রের চেয়ে অনেক বেশি “যৌক্তিক” ভাগ বানানো (যেমন ১,০২৪টি), আর নতুন যন্ত্র এলে কয়েকটি পুরো ভাগ তাকে দেওয়া — key-এর হিসাব কখনো বদলায় না, বদলায় শুধু ভাগ→যন্ত্রের তালিকা। দুই, range ভাগ বড় হলে মাঝখান থেকে দুই টুকরা করা (Bigtable/HBase-এর tablet বা region, MongoDB-র chunk এই ধাঁচের)। যেকোনো পথে সরানো চলে চালু অবস্থায়: আগে কপি, তারপর কপির সময়ের পরিবর্তন (CDC) ধরে ফেলা, শেষে router-এর তালিকা বদল।
৫ · কয়েক shard জুড়ে কোয়েরি — scatter–gather আর global index
অ্যাপ আর shard-গুলোর মাঝে থাকে একটি router (অ্যাপের লাইব্রেরি বা আলাদা proxy). কোয়েরির WHERE-এ shard key থাকলে router সেটি সরাসরি এক shard-এ পাঠায়। না থাকলে পথ একটাই — scatter–gather: কোয়েরি সব shard-এ ছড়িয়ে দেওয়া (scatter), প্রতিটির আংশিক ফল জোগাড় করে মেলানো (gather). এমন cross-shard query-র দাম দুটি: প্রতিটি shard কাজ করে, আর উত্তর আসে সবচেয়ে ধীর shard-এর গতিতে — shard যত বেশি, কোনো একটির হঠাৎ ধীর হওয়ার সম্ভাবনা তত বেশি (tail latency).
নিচের এডিটরে তিনটি shard টেবিল আগেই বানানো: orders_s0, orders_s1, orders_s2 — প্রতিটিতে customer_id % 3 অনুযায়ী অর্ডার, সাথে একটি বাড়তি column total (অর্ডারের মোট টাকা)। gather-এর অংশটি একটি view (পর্ব ২৪), আর ভেতরে UNION ALL (পর্ব ২২):
CREATE VIEW partial AS
SELECT 0 AS shard, COUNT(*) AS n, SUM(total) AS s, AVG(total) AS a FROM orders_s0
UNION ALL SELECT 1, COUNT(*), SUM(total), AVG(total) FROM orders_s1
UNION ALL SELECT 2, COUNT(*), SUM(total), AVG(total) FROM orders_s2;
SELECT shard, n, s, ROUND(a, 1) AS a FROM partial;
SELECT SUM(n) AS orders, SUM(s) AS revenue,
ROUND(SUM(s) / SUM(n), 1) AS true_avg,
ROUND(AVG(a), 1) AS avg_of_avgs
FROM partial;view তৈরিতে ✓, তারপর প্রতিটি shard-এর আংশিক ফল: 0 | 2 | 8000 | 4000, 1 | 5 | 43270 | 8654, 2 | 3 | 5550 | 1850. মিলিত ফল: 10 | 56820 | 5682 | 4834.7. COUNT আর SUM-কে নিরাপদে যোগ করা যায়, কিন্তু গড়ের গড় ভুল: ৪৮৩৪.৭ নয়, আসল গড় ৫৬৮২ — কারণ shard 1-এর ৫টি অর্ডার আর shard 0-এর ২টি অর্ডারকে সমান ওজন দেওয়া হয়েছে। তাই router প্রতিটি shard-এর কাছে AVG নয়, SUM আর COUNT চায়, আর শেষে নিজে ভাগ করে। একই কারণে COUNT(DISTINCT), median বা “top 10” মেলাতে বাড়তি সতর্কতা লাগে: একটি গ্রাহক যদি কয়েক shard-এ থাকতেন, প্রতিটি shard-এর top 10 মিলিয়েও আসল top 10 না-ও আসতে পারে।
Join আর global secondary index
দুটি টেবিলের join-এ সারিগুলো আলাদা যন্ত্রে থাকলে ডেটা network দিয়ে সরাতে হয় — সবচেয়ে দামি কাজ। এড়ানোর তিনটি কৌশল। Co-location: যে টেবিলগুলো একসাথে join হয়, সেগুলো একই key-তে ভাগ — customers, orders, order_items তিনটিই customer_id ধরে, তাই একজন গ্রাহকের join পুরোটা এক shard-এর ভেতরে। Reference table: ছোট, কম বদলানো টেবিল (categories) প্রতিটি shard-এ পুরো কপি। ডেটা সরানো: বড় বিশ্লেষণে দুই দিকের সারি join key ধরে নতুন করে ভাগ করে পাঠানো (shuffle) — data warehouse-এর কাজ।
আরেক সমস্যা: গ্রাহক shard হয়েছে customer_id ধরে, কিন্তু login-এ খোঁজা হয় ফোন নম্বর দিয়ে। প্রতিটি shard-এর নিজের index (local index) আছে, তবু কোন shard-এ খুঁজতে হবে জানা নেই। সমাধান global secondary index: phone থেকে customer_id আর shard-এর একটি আলাদা টেবিল, যা নিজেও phone ধরে ভাগ করা যায়। এডিটরে customers_s0..s2 আর phone_idx (phone, customer_id, shard) আগেই বানানো:
-- ধাপ ১: index থেকে shard জানা, ধাপ ২: শুধু সেই shard-এ যাওয়া
SELECT * FROM phone_idx WHERE phone = '01811000002';
SELECT customer_id, name, city FROM customers_s2 WHERE customer_id = 2;
-- index না থাকলে: তিনটি shard-কেই জিজ্ঞেস করতে হয়
SELECT 's0' AS asked, COUNT(*) AS found FROM customers_s0 WHERE phone = '01811000002'
UNION ALL
SELECT 's1', COUNT(*) FROM customers_s1 WHERE phone = '01811000002'
UNION ALL
SELECT 's2', COUNT(*) FROM customers_s2 WHERE phone = '01811000002';প্রথম ফল: 01811000002 | 2 | 2; দ্বিতীয়: 2 | Karima Begum | Chattogram — মোট দুটি ছোট কোয়েরি, ছোঁয়া হলো একটি shard. index ছাড়া ফল s0 | 0, s1 | 0, s2 | 1: তিনটি shard খাটল, কাজে লাগল একটি। আর customer 5-এর ফোন NULL, তাই index-এ তাঁর সারি নেই। দাম: Karima ফোন বদলালে লিখতে হবে দুই জায়গায় — তাঁর shard-এ আর index-এর সারিতে, যা অন্য যন্ত্রে থাকতে পারে। দুটি একসাথে হবে কীভাবে? হয় distributed transaction (পরের অংশ), নয়তো index কিছুক্ষণ পিছিয়ে থাকবে মেনে নেওয়া — যেমন Amazon DynamoDB-র global secondary index থেকে পড়া সবসময় eventually consistent.
৬ · কয়েক shard জুড়ে লেনদেন — two-phase commit আর saga
Rahim-এর wallet shard A-তে, Karima-র shard B-তে। Rahim ৫০০ টাকা পাঠালেন: A-তে −৫০০, B-তে +৫০০. দুটি commit হতে হবে একসাথে, নয়তো একটিও না — পর্ব ৪৪-এর atomicity, এবার দুই যন্ত্রে। এমন লেনদেনের নাম distributed transaction, আর প্রচলিত প্রোটোকল two-phase commit (2PC), যার ধারণা ১৯৭০-এর দশকের শেষের দিকে Jim Gray-সহ গবেষকদের লেখায় আসে। একটি coordinator সিদ্ধান্ত নেয়; প্রতিটি shard একজন participant.
| C | |
| A | |
| B | |
| lock |
- কাজ। দুই shard নিজের অংশ করে, কিন্তু commit করে না; পর্ব ৪৬-এর মতো lock ধরে রাখে।
- Phase 1 — prepare. coordinator প্রত্যেককে জিজ্ঞেস করে: “নিশ্চিতভাবে commit করা যাবে?”
- Vote. YES বলার আগে participant নিজের log-এ সব লিখে ডিস্কে fsync করে — যাতে crash-এর পরেও commit করতে পারে। YES দেওয়ার পর সে প্রতিশ্রুতিবদ্ধ: নিজে আর বাতিল করতে পারে না। কেউ NO বললে (যেমন ব্যালান্স কম) coordinator সবাইকে ABORT পাঠায়।
- সিদ্ধান্ত। সব YES পেয়ে coordinator নিজের log-এ COMMIT লেখে। এই record ডিস্কে লেখা শেষ হওয়ার মুহূর্তেই লেনদেনটি commit হয়ে গেল — বাকিটা শুধু খবর পৌঁছানো (phase 2).
- দুর্বলতা — blocking. খবর পাঠানোর আগেই coordinator মরল। A আর B জানে না সিদ্ধান্ত commit না abort; YES দিয়েছে বলে নিজে abort-ও পারে না। তারা in-doubt অবস্থায় lock ধরে বসে থাকে, আর Rahim-এর wallet-এর অন্য লেনদেনও আটকে যায়।
- পুনরুদ্ধার। coordinator ফিরে log পড়ে, দেখে COMMIT লেখা আছে, আবার পাঠায়। log-এ সিদ্ধান্ত না থাকলে সে নিরাপদে ABORT পাঠাত। সবাই ack দিলে END লিখে লেনদেনের হিসাব মুছে ফেলে।
2PC-এর খরচ স্পষ্ট: অন্তত দুটি round trip, প্রতিটি participant-এ বাড়তি fsync, আর পুরো সময় lock ধরা — একটি ধীর participant সবাইকে ধীর করে। blocking এড়াতে three-phase commit-এর প্রস্তাব আছে, কিন্তু তা network-এর সময় নিয়ে কড়া অনুমান করে, তাই প্রায় কোথাও ব্যবহৃত হয় না। বাস্তব সমাধান হলো coordinator-কে নিজেই replicated করা: Google Spanner-এ প্রতিটি participant একটি Paxos দল (অংশ ৮), তাই একটি যন্ত্র মরলেও সিদ্ধান্ত হারায় না। PostgreSQL আর MySQL-এ 2PC-এর participant হওয়ার কমান্ড আছে (XA standard বা নিজস্ব), coordinator-টি বাইরের কোনো transaction manager:
-- shard A-তে (max_prepared_transactions > 0 হতে হবে; default 0)
BEGIN;
UPDATE wallets SET balance = balance - 500 WHERE owner = 'Rahim';
PREPARE TRANSACTION 't42'; -- vote YES: ডিস্কে টেকসই, lock ধরা থাকে
-- coordinator-এর সিদ্ধান্তের পর, যেকোনো সংযোগ থেকে:
COMMIT PREPARED 't42'; -- অথবা ROLLBACK PREPARED 't42'
SELECT gid, prepared FROM pg_prepared_xacts; -- ঝুলে থাকা in-doubt লেনদেনMySQL-এ একই কাজ XA START, XA END, XA PREPARE, XA COMMIT দিয়ে। সতর্কতা: ভুলে যাওয়া prepared লেনদেন সার্ভার রিস্টার্টের পরেও lock ধরে রাখে, আর PostgreSQL-এ vacuum-কেও আটকায় — তাই এগুলোর monitoring জরুরি।
Saga — ছোট ছোট local transaction আর compensation
অনলাইন দোকানে অর্ডার একটি লম্বা কাজ: অর্ডার তৈরি, bKash থেকে টাকা কাটা, মজুদ আটকে রাখা, courier বুক করা। প্রতিটি হয়তো আলাদা সেবা, আলাদা ডেটাবেসে; courier কোম্পানির সিস্টেম তো বাইরের, সেটি 2PC-তে অংশই নেবে না। আর কয়েক মিনিট lock ধরে রাখা অসম্ভব। Hector Garcia-Molina আর Kenneth Salem ১৯৮৭ সালের একটি গবেষণাপত্রে এর বিকল্প দেন: saga — একটি লম্বা কাজকে কয়েকটি local transaction-এ ভাগ করা, প্রতিটি সঙ্গে সঙ্গে commit; আর প্রতিটির জন্য একটি compensating transaction, যা তার প্রভাব অর্থের দিক থেকে উল্টে দেয়। কোনো ধাপ ব্যর্থ হলে আগের সফল ধাপগুলোর compensation চলে উল্টো ক্রমে.
| অর্ডার 1011 | |
| টাকা | |
| stock (110) | |
| saga |
- T1. orders সেবা নিজের ডেটাবেসে অর্ডার লিখে সঙ্গে সঙ্গে commit করে। কোনো lock অন্য সেবার জন্য ধরা থাকে না।
- T2. টাকা কাটা হলো। এই মুহূর্তে অন্য কেউ দেখলে দেখবে টাকা কাটা কিন্তু মাল যায়নি — saga-য় মাঝের অবস্থা সবার কাছে দৃশ্যমান।
- T3. মজুদ ৫ থেকে ৪. এর মধ্যে অন্য একজন ক্রেতা হয়তো দেখলেন “আর মাত্র ৪টি”.
- T4 ব্যর্থ। ACID-এর rollback এখানে নেই — T1–T3 আগেই commit হয়ে গেছে। এগোনো যায় না, তাই saga উল্টো পথ ধরে।
- Compensation, উল্টো ক্রমে। আগে C3 (মজুদ ফেরত), তারপর C2 (refund). compensation মুছে ফেলা নয়, একটি নতুন কাজ: refund-এর নিজস্ব লেনদেন থাকে, ইতিহাসে দুটিই থাকে।
- শেষ। C1 অর্ডার বাতিল করে, গ্রাহককে জানানো হয়। ফল সংগত, কিন্তু মাঝে Sabbir-এর SMS-এ “৩৫০০ টাকা কাটা হয়েছে” গিয়েছিল — এমন কাজ (SMS, ইমেইল) উল্টানো যায় না, শুধু নতুন বার্তায় ঠিক করা যায়।
saga চালানোর দুটি ধাঁচ: orchestration — একটি কেন্দ্রীয় সেবা ধাপগুলো ডাকে আর অবস্থা লিখে রাখে; choreography — প্রতিটি সেবা event শুনে নিজের কাজ করে পরের event ছাড়ে। orchestrator-এর log দেখতে কেমন, আর ব্যর্থতার পর কোন compensation চলবে — SQL-এ। saga_steps টেবিলে প্রতিটি ধাপের saga_id, step_no, service (কোন সেবা), step (কাজ), compensation (উল্টানোর কাজ) আর status:
SELECT step_no, service, step, status
FROM saga_steps WHERE saga_id = 7 ORDER BY step_no;
-- ব্যর্থতার পর: সফল ধাপগুলোর compensation, উল্টো ক্রমে
SELECT step_no, service, compensation AS run_now
FROM saga_steps
WHERE saga_id = 7 AND status = 'done'
ORDER BY step_no DESC;প্রথম ফলে চার সারি, শেষটি 4 | shipping | book courier | failed. দ্বিতীয় ফল: 3 | inventory | release 1 x product 110, 2 | payments | refund 3500 tk, 1 | orders | cancel order 1011 — ঠিক stepper-এর ক্রম। ব্যর্থ ধাপের compensation লাগে না, কারণ তার কিছুই commit হয়নি; তাই status = 'done' ফিল্টার। এই log টেবিলটিই orchestrator-এর স্মৃতি: সে নিজে crash করলে ফিরে এসে এখান থেকে ঠিক কোথায় ছিল তা পড়ে। তাই প্রতিটি compensation এমন হতে হবে যে দুবার চললেও ক্ষতি নেই (idempotency key দিয়ে)। আর ধাপের কাজ আর “পরের সেবাকে খবর দাও” একই local transaction-এ লিখতে প্রচলিত কৌশল outbox pattern.
| 2PC | Saga | |
|---|---|---|
| Atomicity | হ্যাঁ, একসাথে commit | শেষে সংগত — সব হয়, নয়তো compensation |
| Isolation | হ্যাঁ, lock দিয়ে | না — মাঝের অবস্থা দেখা যায় |
| Lock-এর সময় | পুরো লেনদেন জুড়ে | প্রতিটি local transaction-এর ছোট সময় |
| উপযুক্ত | এক প্রতিষ্ঠানের ভেতরের ডেটাবেস, ছোট লেনদেন | লম্বা ব্যবসায়িক প্রক্রিয়া, বাইরের সেবা, microservice |
৭ · CAP theorem — নেটওয়ার্ক ভাগ হলে consistency না availability
২০০০ সালে PODC সম্মেলনের একটি বক্তৃতায় Eric Brewer একটি অনুমান (conjecture) দেন, আর ২০০২ সালে Seth Gilbert ও Nancy Lynch সেটিকে নির্দিষ্ট সংজ্ঞায় প্রমাণ করেন। তিনটি শব্দ, প্রমাণের অর্থে:
- C — consistency: এখানে মানে linearizability — যেন ডেটার একটিমাত্র কপি আছে, আর প্রতিটি পড়া সর্বশেষ সম্পন্ন লেখা দেখে। (ACID-এর C, অর্থাৎ নিয়ম না ভাঙা, সম্পূর্ণ আলাদা জিনিস।)
- A — availability: চালু থাকা প্রতিটি নোডে আসা প্রতিটি অনুরোধ একটি (ত্রুটি নয় এমন) উত্তর পায়।
- P — partition tolerance: নোডগুলোর মধ্যে বার্তা হারালেও — network partition — ব্যবস্থা কাজ চালিয়ে যায়।
যুক্তিটি সহজ। দুটি replica, মাঝের লিংক কাটা। একটিতে লেখা হলো, অন্যটিতে পড়ার অনুরোধ এল। দ্বিতীয়টি উত্তর দিলে পুরোনো মান দেবে (C ভাঙল); নতুন মান জানতে অপেক্ষা করলে বা ত্রুটি দিলে উত্তর নেই (A ভাঙল)। তৃতীয় পথ নেই, কারণ খবর পৌঁছানোর কোনো উপায়ই নেই। ধাপে ধাপে:
| D-র মান | |
| C-র মান | |
| কী পেলাম |
- স্বাভাবিক সময়। network ঠিক থাকলে দুটিই পাওয়া যায় — CAP তখন কিছুই নিষেধ করে না।
- Partition. লিংক কাটা, কিন্তু দুই শহরেই ক্রেতা আছেন। ধরুন majority (তিনটির দুটি নোড, অংশ ৮) ঢাকার দিকে।
- এক দিকে লেখা। ঢাকায় বিক্রি হলো, চট্টগ্রাম তা জানার কোনো উপায় নেই।
- CP পছন্দ। চট্টগ্রাম majority-তে নেই, তাই পড়া-লেখা প্রত্যাখ্যান করে (ত্রুটি বা অপেক্ষা)। কেউ ভুল তথ্য পায় না, কিন্তু চট্টগ্রামের ক্রেতার কাছে সেবা বন্ধ — পর্ব ৫২-এর fencing-এর মতো।
- AP পছন্দ। চট্টগ্রাম নিজের পুরোনো কপি থেকে উত্তর দেয়, আর বিক্রিও নেয়। সবাই উত্তর পেলেন, কিন্তু দুটি কপি এখন আলাদা ইতিহাস বহন করছে।
- মিলন। CP-তে কিছু মেলাতে হয় না। AP-তে দ্বন্দ্ব মেটাতে হয়: “শেষ লেখক জেতে” (last-writer-wins) একটি বিক্রি নিঃশব্দে হারায়; “কয়টি কমল” গুনে মেলালে (CRDT-ধাঁচের counter) ঠিক ৩. ৫টির মধ্যে ১টিই থাকলে AP-তে দুজনকে একই শেষ জিনিস বিক্রি হয়ে যেত।
CAP-এর প্রচলিত ভুল ব্যাখ্যা:
- “তিনটির যেকোনো দুটি বাছুন।” একাধিক যন্ত্রের ব্যবস্থায় partition আসবেই — ঘটবে কি না তা আপনার পছন্দ নয়। তাই “CA ব্যবস্থা” কথাটির মানে প্রায় নেই; বাছাই শুধু partition-এর সময় C না A.
- “আমাদের ডেটাবেস CP, তাই availability কম।” CAP-এর A খুব কড়া: প্রতিটি চালু নোডের উত্তর দেওয়া। majority-র দিকটি চালু রেখে বাস্তবে খুব উঁচু availability পাওয়া যায়; শুধু সংখ্যালঘু দিকটি থামে।
- “একবার বাছলে সব সময়ের জন্য।” বাছাই হয় partition-এর সময়ে, আর প্রায়ই কাজভেদে — কার্ট-এ AP, টাকা কাটায় CP. Brewer নিজেই ২০১২ সালের “CAP Twelve Years Later” প্রবন্ধে এই সূক্ষ্মতাগুলো ব্যাখ্যা করেছেন।
- “CAP-এর C = ACID-এর C.” না; একটি replica-দের কপির মিল, অন্যটি constraint আর invariant রক্ষা।
PACELC — partition না থাকলেও একটি বাছাই আছে
CAP শুধু partition-এর সময়ের কথা বলে, অথচ বেশিরভাগ সময় network ঠিকই থাকে। Daniel Abadi ২০১২ সালের একটি প্রবন্ধে বিষয়টি গুছিয়ে দেন — PACELC: Partition হলে A না C; Else (স্বাভাবিক সময়ে) Latency না Consistency. কারণ শক্ত consistency মানে প্রতিটি লেখায় (অনেক সময় পড়াতেও) অন্য replica-র সম্মতির জন্য অপেক্ষা — পর্ব ৫২-এর sync replication-এর round trip. Abadi-র নিজের উদাহরণে Dynamo আর Cassandra-র মতো ব্যবস্থা default সেটিংয়ে PA/EL (দ্রুত উত্তরকে প্রাধান্য), আর পুরোপুরি consistent ব্যবস্থাগুলো PC/EC. তবে অনেক ব্যবস্থায় এটি সেটিং দিয়ে বদলানো যায় — তাই কোনো ডেটাবেসকে একটি লেবেলে বাঁধার আগে তার কনফিগারেশন দেখুন।
৮ · Consistency model, quorum আর consensus
“consistent” কথাটি একটি মাত্রা, হ্যাঁ-না নয়। replica-রা কতটা একমত দেখাবে, তার প্রতিশ্রুতিকে বলে consistency model. শক্তিশালী থেকে দুর্বল, মোটামুটি এই ক্রমে:
| Model | প্রতিশ্রুতি |
|---|---|
| Linearizable (strong) | যেন একটিই কপি; একটি লেখা শেষ হওয়ার পরে শুরু হওয়া যেকোনো পড়া তা দেখবে। Herlihy ও Wing, ১৯৯০. |
| Sequential | সব কাজ একটি একক ক্রমে সাজানো যায়, যেখানে প্রতিটি client-এর নিজের ক্রম ঠিক থাকে — কিন্তু ঘড়ির আসল সময়ের ক্রম না-ও থাকতে পারে। Lamport, ১৯৭৯. |
| Causal | কারণ আর ফল সবাই একই ক্রমে দেখে (প্রশ্ন দেখার আগে উত্তর নয়); সম্পর্কহীন লেখা ভিন্ন ক্রমে দেখা যেতে পারে। |
| Read-your-writes | একটি session-এর গ্যারান্টি: নিজের লেখা নিজে অন্তত দেখবেন। |
| Eventual | নতুন লেখা থামলে একসময় সব replica একই মানে পৌঁছাবে; মাঝে যা খুশি দেখা যেতে পারে। |
দুই replica-র একটি ছোট গল্পে পার্থক্যগুলো দেখুন। R1 ঢাকায়, R2 চট্টগ্রামে, replication async; অর্ডার 1009-এর status শুরুতে pending:
| R2-তে status | |
| যে model ভাঙল (আর এর ওপরের সব) | |
| eventual |
- লেখা শেষ। R1 লেখাটি নিয়ে Rahim-কে সফল জানাল। R2-তে এখনো পৌঁছায়নি।
- শুধু linearizable ভাঙে। লেখা শেষ হওয়ার পরে শুরু হওয়া পড়া পুরোনো মান পেল — linearizability-তে নিষেধ। sequential-এ এটি চলে: Karima-র পড়াটিকে Rahim-এর লেখার আগে সাজিয়ে নিলেই দুজনের নিজস্ব ক্রম ঠিক থাকে।
- নিজের লেখা হারাল। Rahim-এর নিজের ক্রমে আগে লেখা, পরে পড়া — তবু পুরোনো মান। read-your-writes ভাঙল। causal আর sequential-ও ভাঙল, কারণ দুটিই একজন client-এর নিজের ক্রম রক্ষার শর্ত রাখে, আর সেটিই রক্ষা হয়নি।
- ফল আগে, কারণ পরে। “পেয়েছি” মন্তব্যটি shipped দেখার ফল; Sabbir ফল দেখলেন কিন্তু কারণ নয়। causal consistency এটিই নিষেধ করে।
- Eventual. সব replica শেষে একই মানে পৌঁছেছে — eventual consistency-র একমাত্র প্রতিশ্রুতি এটুকুই, আর পুরো সময়টায় এটি কখনো ভাঙেনি।
Quorum — N, W আর R
Dynamo-ধাঁচের ব্যবস্থায় (Cassandra, Riak) প্রতিটি ডেটা থাকে N টি replica-তে। লেখা সফল হয় W টি replica “পেয়েছি” বললে, পড়া জিজ্ঞেস করে R টি replica-কে আর সবচেয়ে নতুন সংস্করণটি নেয়। নিয়ম: W + R > N হলে প্রতিটি পড়ার দল প্রতিটি সফল লেখার দলের সাথে অন্তত একটি replica-তে মেলে। প্রমাণ: দুটি দলের মোট সদস্য W + R, অথচ আলাদা replica আছে মাত্র N টি; W + R > N হলে pigeonhole নীতিতে অন্তত একজন দুই দলেই আছে। সব সম্ভাব্য দল গুনে দেখি:
from itertools import combinations
N = 3 # replicas 0, 1, 2; all start with version 1
def check(W, R):
stale = total = 0
for wrote in combinations(range(N), W): # replicas that acked version 2
for asked in combinations(range(N), R): # replicas a later read contacts
total += 1
newest = max(2 if r in wrote else 1 for r in asked)
stale += (newest == 1) # read returned the old version
return stale, total
print("W R W+R>N stale reads writes ok with reads ok with")
for W, R in [(1, 1), (2, 1), (1, 2), (2, 2), (3, 1), (1, 3)]:
s, t = check(W, R)
print(f"{W} {R} {str(W + R > N):6} {s:>3} of {t:<6} {N - W} down {N - R} down")আউটপুট:
W R W+R>N stale reads writes ok with reads ok with 1 1 False 6 of 9 2 down 2 down 2 1 False 3 of 9 1 down 2 down 1 2 False 3 of 9 2 down 1 down 2 2 True 0 of 9 1 down 1 down 3 1 True 0 of 3 0 down 2 down 1 3 True 0 of 3 2 down 0 down
W + R ≤ N হলে কিছু জোড়ায় পড়া শুধু পুরোনো replica-তে পৌঁছায় — W = R = 1-এ ৯টির ৬টি। W + R > N হলেই stale শূন্য। শেষ দুই column দেখায় দামটি কোথায়: W = 3 মানে একটি replica বন্ধ থাকলেও লেখা অসম্ভব; R = 3 হলে পড়া। N = 3, W = 2, R = 2 তাই প্রচলিত আপস — দুটি দিকই একটি ব্যর্থতা সয়। W আর R কমালে latency কমে আর availability বাড়ে, কিন্তু consistency কমে — একটি ব্যবস্থার ভেতরেই PACELC-এর বাছাই। সতর্কতা: overlap থাকলেও শুধু এটুকু linearizability দেয় না — একসাথে দুটি লেখা, আধা-সফল লেখা, বা ব্যর্থ নোডের বদলে অন্য নোডে লেখা (“sloppy quorum”) থাকলে আরও কৌশল লাগে।
Consensus — এক পাতায়
পর্ব ৫২-এ দেখেছি, কে primary তা ঠিক করতে consensus লাগে। সাধারণ রূপে সমস্যাটি: কয়েকটি যন্ত্র, যাদের কিছু ব্যর্থ হতে পারে আর বার্তা দেরিতে আসতে পারে, একটি মানে একমত হবে আর সিদ্ধান্ত কখনো উল্টাবে না। Leslie Lamport-এর Paxos (১৯৯৮ সালে প্রকাশিত) এর ক্লাসিক সমাধান, কিন্তু বোঝা আর বানানো কঠিন বলে পরিচিত। Diego Ongaro ও John Ousterhout ২০১৪ সালে Raft দেন, বোঝার সুবিধাকে প্রধান লক্ষ্য করে। দুটিরই মূল কৌশল majority quorum: একজন নেতা বাছাই হয় (leader election), নেতা প্রতিটি কাজ একটি replicated log-এ যোগ করে, আর majority-র ডিস্কে পৌঁছালেই সেটি commit. ২N + ১ যন্ত্রে N টি ব্যর্থতা সয়। etcd (Raft) আর ZooKeeper (নিজস্ব Zab প্রোটোকল) এই সেবা দেয় — shard-এর তালিকা, নেতা কে, কনফিগারেশন রাখতে; আর distributed SQL ডেটাবেসগুলো প্রতিটি ডেটা-খণ্ডের লেখাতেই এটি চালায়।
৯ · NewSQL ও distributed SQL — আর কখন distribute করবেন না
২০১০-এর দশকে একটি ধারা আসে, যাকে NewSQL বা distributed SQL বলা হয়: SQL আর ACID লেনদেন রেখে নিজে থেকেই sharding, replication আর failover. প্রচলিত কয়েকটি, সংক্ষেপে (খুঁটিনাটি সংস্করণভেদে বদলায়):
| ব্যবস্থা | মূল ধারণা |
|---|---|
| Google Spanner | ২০১২ সালের গবেষণাপত্রে প্রকাশিত; Paxos দিয়ে replication, 2PC দিয়ে কয়েক খণ্ডের লেনদেন, আর TrueTime ঘড়ি দিয়ে বিশ্বজুড়ে সংগত ক্রম। Google Cloud-এ সেবা হিসেবে। |
| CockroachDB | Spanner-এর ধারণায় অনুপ্রাণিত, Raft; PostgreSQL-এর wire protocol-এর সাথে অনেকটা সামঞ্জস্যপূর্ণ; বিশেষ ঘড়ির বদলে hybrid logical clock. |
| TiDB | MySQL-এর সাথে সামঞ্জস্যপূর্ণ SQL স্তর, নিচে Raft-ভিত্তিক TiKV স্টোরেজ; PingCAP-এর তৈরি। |
| YugabyteDB | PostgreSQL-এর query স্তর পুনর্ব্যবহার করে, নিচে Raft-ভিত্তিক distributed স্টোরেজ। |
| Vitess | নতুন ডেটাবেস নয় — অনেকগুলো MySQL-এর সামনে sharding আর routing-এর স্তর; YouTube-এ জন্ম, এখন CNCF প্রকল্প। |
| Citus | PostgreSQL-এর extension; টেবিলকে একটি distribution column ধরে কয়েকটি worker-এ ভাগ করে, ছোট টেবিলকে reference table বানায়; ২০১৯ সালে Microsoft Citus Data কিনে নেয়। |
-- order_items-এও customer_id column থাকতে হবে, যাতে একই worker-এ থাকে
SELECT create_distributed_table('orders', 'customer_id');
SELECT create_distributed_table('order_items', 'customer_id', colocate_with => 'orders');
SELECT create_reference_table('categories'); -- প্রতিটি worker-এ পুরো কপিএগুলো sharding-এর অনেক কষ্ট লুকায়, কিন্তু পদার্থবিজ্ঞান নয়: কয়েক খণ্ড জোড়া লেনদেনে এখনো consensus আর 2PC-এর round trip, আর দূরের region-এ replica মানে ধীর commit. shard key-এর মতো সিদ্ধান্ত (primary key-এর গঠন, কোন টেবিল একসাথে) এখনো আপনার।
আরও গভীরে: TrueTime সময়কে একটি বিন্দু নয়, একটি অনিশ্চয়তার পরিসর [earliest, latest] হিসেবে দেয়, GPS আর atomic clock-এর সাহায্যে পরিসরটি কয়েক মিলিসেকেন্ডে রাখে। Spanner commit-এর timestamp বেছে সেই পরিসর পার না হওয়া পর্যন্ত অপেক্ষা করে (“commit wait”) — ফলে commit-এর timestamp-এর ক্রম আসল সময়ের ক্রমের সাথে মেলে, যাকে গবেষণাপত্র বলে external consistency. CockroachDB-র কাছে এমন ঘড়ি নেই; তাই সে যন্ত্রগুলোর ঘড়ির সর্বোচ্চ ফারাক ধরে নেয় (default ৫০০ ms) আর অনিশ্চিত পড়া আবার চালায়, আর ঘড়ি সীমা ছাড়ালে নোডটি নিজেকে বন্ধ করে।
কখন distribute করবেন না
সবচেয়ে গুরুত্বপূর্ণ কথাটি শেষে: বেশিরভাগ অ্যাপের sharding লাগে না। আজকের একটি সার্ভারে কয়েকশো CPU core আর কয়েক TB RAM পর্যন্ত পাওয়া যায়, আর ভালো index ও কোয়েরিসহ একটি PostgreSQL বা MySQL বিপুল কাজ সামলায়। sharding-এর আগে সাধারণত এই ক্রমে ভাবুন: ধীর কোয়েরি আর index ঠিক করা (পর্ব ৪১, ৫৩) → connection pool → পড়ার জন্য read replica (পর্ব ৫২) → cache (পর্ব ৫৮) → বড় যন্ত্র → পুরোনো ডেটা archive আর একই যন্ত্রে partitioning → কাজ ধরে আলাদা ডেটাবেস → তারপর sharding. প্রতিটি ধাপের পর আবার মাপুন। sharding-এর পর প্রতিটি নতুন feature-এ প্রশ্ন আসে “এটি কয়টি shard ছোঁবে?”, foreign key আর unique constraint shard পেরিয়ে কাজ করে না, আর ব্যর্থতার নতুন নতুন ধরন আসে। এই দাম তখনই দেবেন, যখন মাপা সংখ্যা বলছে আর কোনো পথ নেই।
১০ · AI-এর যুগে distributed ডিজাইন
“আমার অ্যাপ scale করতে চাই” লিখলে AI প্রায়ই সঙ্গে সঙ্গে sharding, Kafka আর microservice-এর বড় নকশা দেয় — যা দেখতে চমৎকার, কিন্তু অনেক সময় অপ্রয়োজনীয় বা ভুল। সাধারণ ভুল:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
| সংখ্যা না জেনেই sharding | অকারণ জটিলতা | ডেটার আকার, প্রতি সেকেন্ডে লেখা আর বর্তমান বাধা জিজ্ঞেস করুন |
| scatter–gather-এ AVG বা COUNT(DISTINCT) সরাসরি যোগ | চলে, কিন্তু ভুল সংখ্যা | shard-এর টেবিলে আর একক টেবিলে একই কোয়েরি চালিয়ে মিলান |
| “X ডেটাবেস CP/AP” — একটি লেবেল | ভুল প্রত্যাশা | নির্দিষ্ট সংস্করণ আর consistency সেটিংয়ের ডকুমেন্টেশন পড়ুন |
| saga-য় compensation নেই বা idempotent নয় | দুবার refund, আটকে থাকা টাকা | প্রতিটি ধাপে জিজ্ঞেস করুন: ব্যর্থ হলে, আর দুবার চললে কী হবে? |
| ক্রমিক id-কে range-এর shard key | সব লেখা এক shard-এ | লেখার ভাগ মাপুন, প্রতিটি shard-এর লেখা গুনুন |
AI-কে সংখ্যা আর কোয়েরির ধরন দিন, আর বিকল্প ও যাচাইয়ের পথ চান:
AI-এর নকশা মানার আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Sharding আর replication-এর পার্থক্য? — sharding ডেটা ভাগ করে (প্রতিটি যন্ত্রে আলাদা অংশ), replication একই ডেটার কপি রাখে; বাস্তবে প্রতিটি shard-ও replicated থাকে।
- Range আর hash partitioning-এর দাম-লাভ? — range: পরিসরের কোয়েরি দ্রুত, কিন্তু ক্রমিক key-তে hot spot; hash: লেখা সমান ভাগ, কিন্তু পরিসরের কোয়েরি সব shard-এ।
- CAP theorem ঠিকভাবে বলুন। — network partition-এর সময় একটি distributed ব্যবস্থা linearizable consistency আর পূর্ণ availability একসাথে দিতে পারে না।
- 2PC কেন blocking? — YES vote দেওয়া participant coordinator-এর সিদ্ধান্ত না জেনে commit বা abort কোনোটিই করতে পারে না, তাই coordinator মরলে lock ধরে অপেক্ষা করে।
- N = 3-এ W আর R কত হলে পড়া সবসময় সর্বশেষ সফল লেখা দেখবে? — W + R > N, যেমন W = 2, R = 2.
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১“CAP-এর তিনটির যেকোনো দুটি বাছুন” — কথাটি এত জনপ্রিয় হলো কেন, আর এটি ভুল কোথায়? Brewer নিজে ১২ বছর পরে কী বললেন?
২০০০ সালের বক্তৃতার প্রেক্ষাপট ছিল তখনকার বড় web সেবাগুলো: Brewer নিজে Inktomi নামের সার্চ কোম্পানির সহ-প্রতিষ্ঠাতা ছিলেন, আর দেখছিলেন, হাজারো যন্ত্রের সেবায় শক্ত consistency-র জন্য availability ছাড়লে ব্যবসা চলে না। ত্রিভুজের ছবি আর “তিনটির দুটি” স্লোগান সহজে মনে থাকে, তাই NoSQL-এর জোয়ারে (২০০৮-এর পরে) এটি বিপণনের ভাষা হয়ে যায় — “আমরা AP, তাই দ্রুত”.
সমস্যা হলো, তিনটি অক্ষর সমান নয়। partition একটি ঘটনা, বৈশিষ্ট্য নয় — যন্ত্র আর network থাকলে বার্তা হারাবেই। তাই প্রকৃত বাছাই দুটির মধ্যে, আর তা-ও শুধু partition-এর সময়। দ্বিতীয়ত, Gilbert ও Lynch-এর প্রমাণে C মানে linearizability আর A মানে প্রতিটি চালু নোডের উত্তর — দুটিই চরম সংজ্ঞা। বাস্তবের বেশিরভাগ ব্যবস্থা কোনোটিই পুরোপুরি দেয় না, আবার পুরোপুরি ছাড়েও না: snapshot isolation বা causal consistency C-এর মাঝামাঝি, আর majority-র দিক চালু থাকা A-এর মাঝামাঝি।
২০১২ সালে IEEE Computer-এ “CAP Twelve Years Later: How the ‘Rules’ Have Changed” প্রবন্ধে Brewer নিজেই লেখেন, “তিনটির দুটি” ধারণাটি বিভ্রান্তিকর। তাঁর পরামর্শ: partition বিরল, তাই স্বাভাবিক সময়ে দুটিই পাওয়ার চেষ্টা করুন; partition ধরা পড়লে একটি স্পষ্ট “partition mode”-এ যান, যেখানে কিছু কাজ সীমিত (যেমন টাকা তোলায় সীমা); আর partition শেষে মেলানো ও ক্ষতিপূরণের পরিকল্পনা রাখুন — ATM-এর উদাহরণে, network বন্ধ থাকলে ছোট অঙ্কের টাকা তুলতে দেওয়া, পরে হিসাব মেলানো। একই বছর Abadi PACELC দিয়ে দেখান, স্বাভাবিক সময়ের latency–consistency বাছাইটিই আসলে প্রতিদিনের নকশাকে বেশি প্রভাবিত করে।
মূল উপলব্ধি: CAP একটি অসম্ভবতার প্রমাণ, নকশার মেনু নয়। কাজের প্রশ্ন হলো: partition-এর সময় প্রতিটি কাজে কোনটি ছাড়ব, ছাড়লে কতটা, আর partition শেষে কীভাবে মেলাব।
প্র ০২Amazon-এর ২০০৭ সালের Dynamo গবেষণাপত্র কেন shopping cart-এর জন্য ইচ্ছা করে availability বেছেছিল? এর ফল আজকের ডেটাবেসে কোথায় দেখা যায়?
Dynamo-র গবেষণাপত্রের মূল যুক্তি ব্যবসায়িক: ক্রেতা কার্টে জিনিস যোগ করতে না পারলে বিক্রি হারায়, তাই “কার্টে যোগ করুন” সবসময় সফল হতে হবে — যন্ত্র মরুক বা network ভাগ হোক। তার জন্য তারা এই পর্বের প্রায় সব কৌশল একসাথে ব্যবহার করে: virtual node-সহ consistent hashing দিয়ে ডেটা ভাগ, প্রতিটি key N টি replica-তে, W আর R দিয়ে বদলানো যায় এমন quorum, ব্যর্থ নোডের বদলে অন্য নোডে সাময়িক লেখা (sloppy quorum ও hinted handoff), আর replica-দের মধ্যে পার্থক্য খুঁজতে Merkle tree.
দাম হলো দ্বন্দ্ব। partition-এর দুই দিকে একই কার্ট বদলালে দুটি সংস্করণ তৈরি হয়। Dynamo vector clock দিয়ে বোঝে কোন সংস্করণ কোনটির উত্তরসূরি, আর কোন দুটি পরস্পর স্বাধীন; স্বাধীন হলে দুটিই অ্যাপকে দেয়, আর কার্টের অ্যাপ দুটির জিনিস একত্র (union) করে। গবেষণাপত্র নিজেই স্বীকার করে এর একটি অদ্ভুত ফল: মুছে ফেলা জিনিস কখনো কখনো কার্টে আবার ফিরে আসে — union-এ মোছা হারিয়ে যায়। কার্টের জন্য এটি সহনীয়; ব্যাংকের ব্যালান্সে নয়।
এর প্রভাব বিশাল। Cassandra (Facebook-এ শুরু) Dynamo-র ভাগ আর replication-এর নকশা নেয়, Riak-ও তাই; “eventual consistency” আর “tunable consistency” শব্দ দুটি ব্যাপক প্রচলিত হয়। মজার ব্যাপার, Amazon-এর পরের DynamoDB সেবা নাম ধরে রাখলেও নকশায় আলাদা, আর তাতে প্রতিটি পড়ায় strongly consistent পড়া বেছে নেওয়া যায় (base table-এ) — অর্থাৎ বাছাইটি কাজভেদে, যা Brewer-এর ২০১২ সালের পরামর্শের সাথেও মেলে।
মূল উপলব্ধি: consistency বাছাই প্রযুক্তির নয়, ব্যবসার প্রশ্ন — কোন ভুল সহনীয় আর কোনটি নয়। কার্টে একটি ফিরে আসা জিনিস চলে, ব্যালান্সে একটি টাকাও নয়; তাই একই প্রতিষ্ঠানে দুই ধরনের ডেটার জন্য দুই রকম নকশা থাকাই স্বাভাবিক।
প্র ০৩distributed ডেটাবেসে ঘড়ি নিয়ে এত মাথাব্যথা কেন? Spanner atomic clock ব্যবহার করে, অথচ অন্যরা ছাড়াই চলে — দাম কোথায়?
একটি যন্ত্রে লেনদেনের ক্রম সহজ: একটি counter বা একটি log. অনেক যন্ত্রে প্রতিটির নিজের ঘড়ি, আর সাধারণ সার্ভারের ঘড়ি NTP দিয়ে মেলানো হলেও কয়েক মিলিসেকেন্ড থেকে অনেক বেশি পর্যন্ত সরে যেতে পারে। ধরুন ঢাকার যন্ত্রে লেনদেন X commit হলো, তারপর চট্টগ্রামের যন্ত্রে Y — কিন্তু চট্টগ্রামের ঘড়ি একটু পিছিয়ে, তাই Y-এর timestamp X-এর চেয়ে ছোট। এখন timestamp ধরে “অমুক মুহূর্তের snapshot” পড়লে Y দেখা যাবে কিন্তু X নয় — অথচ বাস্তবে X আগে ঘটেছে। Lamport ১৯৭৮ সালেই দেখিয়েছিলেন, ঘটনার ক্রম বোঝাতে logical clock দিয়ে কারণ-ফলের ক্রম রাখা যায়, কিন্তু বাইরের পৃথিবীর আসল সময়ের ক্রম তাতে আসে না।
Spanner-এর সমাধান হার্ডওয়্যারে। প্রতিটি ডেটা সেন্টারে GPS আর atomic clock-এর সার্ভার, আর TrueTime API সময়কে একটি অনিশ্চয়তার পরিসর হিসেবে দেয়। commit-এর পর Spanner পরিসরটি পার হওয়া পর্যন্ত অপেক্ষা করে, তাই পরের যেকোনো লেনদেন নিশ্চিতভাবে বড় timestamp পায়। দাম: প্রতিটি লেখায় কয়েক মিলিসেকেন্ডের অপেক্ষা, আর বিশেষ হার্ডওয়্যার — যা শুধু Google-এর মতো প্রতিষ্ঠানের হাতে।
CockroachDB বা YugabyteDB-র মতো ব্যবস্থা সাধারণ সার্ভারে চলে, তাই hybrid logical clock ব্যবহার করে (আসল সময় আর logical counter মিলিয়ে), আর ঘড়ির সর্বোচ্চ ফারাক একটি সীমা ধরে নেয়। কোনো পড়ার সময় সেই অনিশ্চিত জানালায় নতুন লেখা পেলে পড়াটি আবার চালায় — কখনো বাড়তি latency, কখনো লেনদেন retry. ঘড়ি সীমা ছাড়িয়ে গেলে প্রতিশ্রুতি ভাঙতে পারে, তাই নোড নিজেকে বন্ধ করে দেয়। ক্লাউড প্রদানকারীরাও এখন উন্নত সময়-মেলানোর সেবা দেয়, যা এই জানালা ছোট করে।
মূল উপলব্ধি: distributed ব্যবস্থায় “এখন” কথাটির কোনো সহজ মানে নেই। আসল সময়ের ক্রম দিতে হলে হয় বিশেষ ঘড়ি আর অপেক্ষা, নয়তো অনিশ্চয়তা মেনে retry — দাম দিতে হয়ই, প্রশ্ন শুধু কোন রূপে।
প্র ০৪sharding-এর পর id বানানোও সমস্যা হয়ে যায় কেন? auto-increment, UUID আর Snowflake-ধাঁচের id-র মধ্যে বাছবেন কীভাবে?
একটি ডেটাবেসে AUTO_INCREMENT বা sequence নিরাপদে ১, ২, ৩ দেয়, কারণ একটিই counter. দশটি shard-এ প্রতিটির নিজের counter থাকলে একই id দুই জায়গায় জন্মায়। একটি কেন্দ্রীয় id-সেবা বানালে প্রতিটি INSERT-এ বাড়তি round trip, আর সেটি নিজেই একটি single point of failure. প্রচলিত বিকল্প তিনটি।
এক, প্রতিটি shard-কে আলাদা ধাপ দেওয়া — MySQL-এর auto_ আর auto_ দিয়ে shard 1 পায় ১, ১১, ২১…, shard 2 পায় ২, ১২, ২২… সরল, কিন্তু shard-এর সংখ্যা বদলালে ঝামেলা। দুই, UUID: ১২৮ বিটের এলোমেলো মান, কারও সাথে সমন্বয় ছাড়াই অনন্য। কিন্তু সম্পূর্ণ এলোমেলো UUID (version 4) B+ tree index-এ (পর্ব ৪০) প্রতিটি INSERT-কে এলোমেলো পাতায় পাঠায়, ফলে cache কাজে লাগে না আর page split বাড়ে; সময়ের ক্রম মানা UUID version 7 (২০২৪ সালের RFC 9562-এ) এই সমস্যা কমায়।
তিন, Snowflake-ধাঁচ: Twitter ২০১০ সালের দিকে ৬৪ বিটের id প্রকাশ করে — ওপরের বিটে সময় (মিলিসেকেন্ড), মাঝে যন্ত্রের নম্বর, নিচে একটি ছোট counter. Instagram-এর প্রকৌশল ব্লগেও (২০১২) একই ধারণা, মাঝের বিটে যৌক্তিক shard-এর নম্বর রেখে — ফলে id দেখেই বোঝা যায় সারিটি কোন shard-এ, lookup ছাড়া। এমন id মোটামুটি সময়ের ক্রমে বাড়ে, তাই index-এ ভালো; কিন্তু সাবধান: range-ভিত্তিক shard key হিসেবে ব্যবহার করলে আবার সেই hot spot. তাই এমন id-কে primary key রেখে, shard ঠিক করা হয় hash বা মাঝের shard-বিট দিয়ে।
মূল উপলব্ধি: id-এর নকশা আর shard key-এর নকশা একসাথে ভাবতে হয়। “অনন্য” হওয়াই যথেষ্ট নয় — id-কে index-বান্ধব, সমন্বয়-মুক্ত আর লেখার চাপ ছড়ানোর উপযোগীও হতে হবে।
প্র ০৫একটি দ্রুত বাড়তে থাকা বাংলাদেশি ই-কমার্স স্টার্টআপের CTO বলছেন, “এখনই sharding করে রাখি, পরে ঝামেলা কম।” আপনি কী পরামর্শ দেবেন?
প্রথম প্রশ্ন সংখ্যার: ডেটা কত GB, প্রতি সেকেন্ডে কত লেখা, আর এখন সবচেয়ে বড় বাধা কোথায়? বেশিরভাগ স্টার্টআপে উত্তর হয় কয়েকশো GB আর কয়েকশো লেখা — যা একটি ভালোভাবে চালানো PostgreSQL বা MySQL অনায়াসে সামলায়। বিভিন্ন বড় প্রতিষ্ঠানের প্রকাশিত প্রকৌশল লেখায় দেখা যায়, বিশাল ব্যবহারকারী নিয়েও অনেকে বহু বছর একটি বড় primary আর কয়েকটি replica দিয়ে চলেছেন; Stack Overflow অল্প কয়েকটি শক্তিশালী SQL Server-এ চলার জন্য পরিচিত উদাহরণ।
আগাম sharding-এর দাম দৈনন্দিন। প্রতিটি কোয়েরিতে shard key ভাবতে হয়; foreign key আর unique constraint shard পেরিয়ে কাজ করে না, তাই অ্যাপে নিজে যাচাই করতে হয়; রিপোর্টের কোয়েরি scatter–gather; schema migration (পর্ব ৫০) চালাতে হয় সব shard-এ, কিছু ব্যর্থ হলে মাঝপথের অবস্থা; backup আর restore বহুগুণ জটিল, কারণ সব shard-এর “একই মুহূর্তের” কপি পাওয়াই কঠিন। ছোট দলের প্রকৌশল-সময় যায় ব্যবসার feature থেকে অবকাঠামোয়। সবচেয়ে বড় ঝুঁকি: শুরুতে ব্যবসা কোন দিকে যাবে জানা থাকে না, তাই আজকের shard key কাল ভুল প্রমাণ হতে পারে — আর ভুল shard key বদলানো নতুন করে শুরু করার মতো।
যুক্তিসংগত মাঝামাঝি পথ: ভবিষ্যতের দরজা খোলা রাখা, কিন্তু খরচ এখন না দেওয়া। প্রধান টেবিলগুলোতে একটি স্বাভাবিক ভাগের key রাখা (যেমন প্রতিটি অর্ডারে seller_id বা customer_id), এমন id বাছাই যা পরে shard পেরিয়েও অনন্য থাকবে, shard-বিরোধী অভ্যাস কমানো (অনেক টেবিল জুড়ে বিশাল join অনলাইন অনুরোধে), আর মাপার ব্যবস্থা — কোন টেবিল কত দ্রুত বাড়ছে। সময় এলে Citus বা Vitess-এর মতো টুল, বা distributed SQL — তখনকার সংখ্যা দেখে।
মূল উপলব্ধি: sharding একটি দামি ওষুধ, ভিটামিন নয়। মাপা সংখ্যা যখন বলছে একক যন্ত্রের সব পথ শেষ, তখনই এর দাম দেওয়া যুক্তিসংগত — আর ততদিন নকশাকে sharding-বান্ধব রাখাই বুদ্ধিমানের কাজ।
অনুশীলন
একটি কোয়েরি লেখা, একটি হাতে trace, একটি হিসাব, একটি নকশার সিদ্ধান্ত।
-
কোয়েরি লিখুন: অংশ ৫-এর তিনটি shard টেবিল
orders_s0,orders_s1,orders_s2(একই column, customer_id % 3 অনুযায়ী ভাগ) থেকে scatter–gather করে সবচেয়ে বেশি অর্ডার দেওয়া ৩ জন গ্রাহকের customer_id আর অর্ডারের সংখ্যা বের করুন। (অংশ ৫-এর প্রথম এডিটরে লিখে চালাতে পারেন।)SELECT customer_id, SUM(n) AS orders FROM (SELECT customer_id, COUNT(*) AS n FROM orders_s0 GROUP BY customer_id UNION ALL SELECT customer_id, COUNT(*) FROM orders_s1 GROUP BY customer_id UNION ALL SELECT customer_id, COUNT(*) FROM orders_s2 GROUP BY customer_id) GROUP BY customer_id ORDER BY orders DESC, customer_id LIMIT 3;ফল:
1 | 3,2 | 2,3 | 1. প্রতিটি shard নিজের আংশিক COUNT পাঠায়, router SUM করে সাজায়। এখানে আসলে প্রতিটি গ্রাহক একটি shard-এই থাকেন (shard key = customer_id), তাই প্রতিটি shard-এর top 3 মেলালেও সঠিক উত্তর আসত। group-এর key shard key না হলে (যেমন product_id ধরে top 3) প্রতিটি shard-এর top 3 মেলানো ভুল হতে পারে — তখন পূর্ণ আংশিক ফল লাগে। আর একই সংখ্যায় দুজন থাকলেcustomer_idদিয়ে tiebreak না দিলে ফল প্রতিবার আলাদা হতে পারে। -
হাতে trace করুন: একটি hash ring-এ অবস্থান ০ থেকে ৯৯. যন্ত্র A ২০-এ, B ৫০-এ, C ৮০-তে। key-এর অবস্থান: ১০, ২৫, ৪৫, ৫৫, ৭৯, ৯০. মালিক = key থেকে ঘড়ির কাঁটার দিকে (বড় সংখ্যার দিকে, ৯৯-এর পরে ০) প্রথম যে যন্ত্র, সমান হলে সেই যন্ত্র। (ক) প্রতিটি key-এর মালিক কে? (খ) D যন্ত্র ৩৫-এ যোগ হলে কোন key সরবে?
(ক) ১০ → A, ২৫ → B, ৪৫ → B, ৫৫ → C, ৭৯ → C, ৯০ → A (৯৯ পেরিয়ে ০, তারপর প্রথম যন্ত্র A ২০-এ)। A-র ভাগ ২টি, B ২টি, C ২টি। (খ) D-র ভাগ হলো তার আগের যন্ত্র A (২০) থেকে D (৩৫) পর্যন্ত, অর্থাৎ ২১–৩৫. শুধু key ২৫ সরে, B থেকে D-তে। বাকি পাঁচটি key যেখানে ছিল সেখানেই —
key % N-এ ৩ থেকে ৪ করলে গড়ে তিন-চতুর্থাংশ সরত। লক্ষ করুন, D শুধু B-র কাছ থেকে নিয়েছে; virtual node থাকলে D সব যন্ত্রের কাছ থেকে অল্প অল্প নিত, ফলে চাপ সমানভাবে কমত। -
হিসাব করুন: একটি key-value ব্যবস্থায় N = ৫ replica. (ক) W = ৩, R = ২ হলে প্রতিটি পড়া কি সর্বশেষ সফল লেখা দেখবে? (খ) R = ৩ করলে? (গ) W = ৩, R = ৩-এ লেখা আর পড়া প্রত্যেকে সর্বোচ্চ কয়টি replica বন্ধ সইতে পারে?
(ক) ৩ + ২ = ৫, যা N-এর চেয়ে বড় নয়। লেখা হয়তো replica ১, ২, ৩-এ গেছে, আর পড়া জিজ্ঞেস করল ৪ আর ৫-কে — কোনো মিল নেই, তাই পুরোনো মান আসতে পারে। (খ) ৩ + ৩ = ৬ > ৫: যেকোনো ৩টির দল আর যেকোনো ৩টির দল অন্তত একটিতে মেলে (৬টি আসন, ৫টি replica). (গ) লেখা: ৫ − ৩ = ২টি বন্ধ সয়; পড়া: ৫ − ৩ = ২টি। এটি majority quorum (⌊৫/২⌋ + ১ = ৩)-এর সাথে মেলে, যা consensus অ্যালগরিদমগুলোও ব্যবহার করে।
-
নকশার সিদ্ধান্ত: একটি মোবাইল wallet সেবায়
transactions(txn_id, wallet_id, amount, created_at)টেবিল দ্রুত বাড়ছে। সবচেয়ে বেশি চলা কোয়েরি “একটি wallet-এর শেষ ২০টি লেনদেন”. তিনটি প্রার্থী shard key: (ক) created_at-এর range, (খ) txn_id-এর hash, (গ) wallet_id-এর hash. কোনটি বাছবেন, আর কোন সমস্যা থেকে যাবে?(ক) সব নতুন লেখা সর্বশেষ সময়ের shard-এ — hot spot; আর একটি wallet-এর লেনদেন সব সময়ের shard-এ ছড়ানো। (খ) লেখা সমান ভাগ হয়, কিন্তু একটি wallet-এর ২০টি লেনদেন খুঁজতে সব shard-এ scatter–gather. (গ) সবচেয়ে উপযুক্ত: লেখা ছড়ানো (অনেক wallet), আর প্রধান কোয়েরি এক shard-এ, যেখানে
(wallet_id, created_at)-এর composite index (পর্ব ৪১) শেষ ২০টি দ্রুত দেয়। যা থেকে যায়: এক wallet থেকে আরেক wallet-এ টাকা পাঠানো প্রায়ই দুই shard ছোঁয় — 2PC বা saga লাগে; খুব বড় ব্যবসায়ী wallet একটি shard-কে গরম করতে পারে; আর “আজ মোট লেনদেন” কোয়েরি scatter–gather, যা data warehouse-এ (পর্ব ৫৫) সরানো ভালো।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমসফল একটি two-phase commit-এর ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- coordinator সব participant-কে PREPARE পাঠায়
- প্রতিটি participant নিজের log ডিস্কে লিখে YES vote দেয়
- coordinator নিজের log-এ COMMIT সিদ্ধান্ত লেখে
- coordinator সব participant-কে COMMIT পাঠায়
- participant-রা commit করে lock ছাড়ে আর ack পাঠায়
CAP theorem-এর সবচেয়ে সঠিক বিবরণ কোনটি?
- যেকোনো distributed ডেটাবেস consistency, availability আর partition tolerance-এর যেকোনো দুটি বেছে নিতে পারে
- network partition-এর সময় একটি ব্যবস্থা linearizable consistency আর পূর্ণ availability একসাথে দিতে পারে না
- CAP-এর consistency আর ACID-এর consistency একই জিনিস
- partition না থাকলেও consistency আর availability-র একটি ছাড়তেই হয়
একটি quorum ব্যবস্থায় প্রতিটি ডেটার N = 3টি replica. W (লেখায় কয়টি ack) আর R (পড়ায় কয়টি replica) কোনটি হলে প্রতিটি পড়ার দল প্রতিটি সফল লেখার দলের সাথে অন্তত একটি replica-তে মিলবেই?
- W = 1, R = 1
- W = 2, R = 1
- W = 2, R = 2
- W = 1, R = 2
চারটি যন্ত্রের একটি consistent hashing ring (অনেক virtual node-সহ, ভাগ প্রায় সমান)-এ পঞ্চম যন্ত্র যোগ হলো। মোটামুটি কত ভাগ key-কে জায়গা বদলাতে হবে?
- প্রায় ১/৫ ভাগ (২০%)
- প্রায় ৪/৫ ভাগ (৮০%)
- প্রায় অর্ধেক
- কোনো key সরবে না
two-phase commit-এ একটি participant YES vote দেওয়ার পর coordinator বন্ধ হয়ে গেল, আর participant কোনো সিদ্ধান্ত পায়নি। participant-এর কী করা উচিত?
- অপেক্ষা না করে নিজে abort করা
- নিজে commit করা, কারণ সে YES দিয়েছে
- lock ছেড়ে দিয়ে পরে সিদ্ধান্তের অপেক্ষা
- lock ধরে রেখে coordinator-এর সিদ্ধান্ত জানা পর্যন্ত অপেক্ষা করা
একটি অর্ডারের saga-র ধাপ: T1 অর্ডার তৈরি, T2 টাকা কাটা, T3 মজুদ আটকানো, T4 courier বুক। T3 ব্যর্থ হলো (মজুদ নেই)। এখন কী চলবে?
- সব সেবার ডেটাবেসে একটি ROLLBACK
- প্রথমে টাকা ফেরত (T2-এর compensation), তারপর অর্ডার বাতিল (T1-এর compensation)
- T3 আর T4-এর compensation
- T4 চালিয়ে যাওয়া, পরে মজুদ এলে মিলিয়ে নেওয়া
orders টেবিল hash(customer_id) ধরে ৮টি shard-এ ভাগ করা। কোয়েরি: “১ থেকে ৭ সেপ্টেম্বরের সব অর্ডার”. router-কে কয়টি shard-এ যেতে হবে?
- একটি shard-এ, যেটিতে সেপ্টেম্বর আছে
- ৭টি shard-এ, প্রতিদিনের জন্য একটি
- ৮টি shard-এই (scatter–gather)
- এমন কোয়েরি চালানোই যাবে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- আগে একক যন্ত্রের সব পথ: index, কোয়েরি, replica, cache, বড় যন্ত্র — sharding আসে মাপা সংখ্যা বলার পরে, কারণ এর দাম প্রতিদিনের।
- shard key-ই সব: range-এ পরিসরের কোয়েরি দ্রুত কিন্তু ক্রমিক key-তে hot spot; hash-এ লেখা সমান কিন্তু পরিসর scatter–gather. প্রধান কোয়েরির WHERE-এ key, একসাথে লাগা ডেটা একই key-তে।
- Consistent hashing:
hash % N-এ N থেকে N+1-এ প্রায় N/(N+1) ভাগ সরে; ring আর virtual node-এ প্রায় ১/(N+1). gather-এ SUM আর COUNT যোগ হয়, গড়ের গড় নয়। - 2PC বনাম saga: 2PC = একসাথে commit, কিন্তু lock আর coordinator ব্যর্থতায় blocking; saga = local commit আর উল্টো ক্রমে compensation, কিন্তু isolation নেই — idempotent হওয়া চাই।
- CAP, PACELC, quorum: partition-এর সময় C না A; স্বাভাবিক সময়ে latency না C. W + R > N হলে পড়া-লেখার দল মেলে; consensus (Paxos, Raft) majority দিয়ে নেতা আর replicated log ঠিক রাখে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৫ · Data warehouse — OLAP, star schema আর ETLপরবর্তী পর্ববিশ্লেষণের জন্য আলাদা ডেটাবেস।
- পর্ব ৫২ · Backup, replication আর high availabilityফিরে দেখুনreplica, lag, failover আর quorum-এর ভিত্তি।
- পর্ব ৫৬ · NoSQL — key-value, document, wide-column আর graphসামনেযেখানে এই পর্বের কৌশলগুলো প্রথম দিন থেকে।
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- DSA · Hash tableসম্পর্কিত কোর্সhash function আর bucket — consistent hashing-এর ভিত্তি।
- কম্পিউটার ফান্ডামেন্টালস · Cloud computingসম্পর্কিত কোর্সregion, data center আর ক্লাউড সেবা।