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

Mock interview — web crawler, leaderboard, pastebin আর আরও নকশা, পূর্ণ walkthrough

System Design Mock Interviews — Web Crawler, Real-Time Leaderboard, Pastebin, Search Autocomplete & Distributed Job Scheduler

Taught in Bangla, this page runs five timed mock interviews of about 45 minutes as interviewer-candidate dialogues: a web crawler with frontier, politeness and dedup, a real-time leaderboard with sorted sets, sharding and approximate rank, a pastebin, search autocomplete with prefix tables and edge cache, and a distributed job scheduler with leases and fencing, plus commentary, simulators, pushback handling and evaluation criteria.

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

  • ৪৫ মিনিটের একটি system design ইন্টারভিউ ঘড়ি ধরে আট ধাপে চালানো — কোন মিনিটে কী বলবেন, কী বাদ দেবেন
  • Web crawler-এর frontier, host-ভিত্তিক ভদ্রতা, robots.txt আর URL ও লেখার deduplication
  • Leaderboard-এ sorted set, sharding-এর দুই পথ আর বিপুল ব্যবহারকারীর জন্য আনুমানিক rank
  • Pastebin-এ object storage আর metadata-র ভাগ, মেয়াদের হিসাব; autocomplete-এর prefix table, হালনাগাদের pipeline আর edge cache
  • Job scheduler-এ টেকসই সূচি, partition-এর lease, fencing আর idempotent job; ইন্টারভিউয়ারের আপত্তি সামলানো আর মূল্যায়নের মাপকাঠি

১ · Mock interview কীভাবে ব্যবহার করবেন — ৪৫ মিনিটের ছক, একই ছাঁচ আর রিভিশনের মানচিত্র

System design ইন্টারভিউর প্রশ্ন সাধারণত এক লাইনের: “একটি web crawler নকশা করুন।” বাকি ৪৫ মিনিট আপনার। এ ধরনের প্রশ্ন প্রায়ই আসে, তবে ধরন আর সময় প্রতিষ্ঠান, পদ আর ইন্টারভিউয়ারভেদে বদলায়। এই পাতার পাঁচটি কথোপকথন বানানো — কোনো প্রতিষ্ঠানের আসল প্রশ্ন, প্রক্রিয়া বা মূল্যায়ন-পদ্ধতি নয়। প্রতিটি পণ্য কাল্পনিক, আর প্রতিটি সংখ্যা “ধরা যাক”।

ক্রিকেটের নেট প্র্যাকটিসের মতো। ম্যাচের আগে ব্যাটসম্যান নেটে বোলারদের সামনে দাঁড়ান — আসল চাপ নেই, কিন্তু বল আসল। কোচ পাশে দাঁড়িয়ে বলেন কোন শটটি ঠিক ছিল, কোনটিতে পা নড়েনি। Mock interview-ও তাই: প্রশ্ন আর আপত্তি আসল, ঘড়ি আসল, আর প্রতিটি ধাপের পরের “মন্তব্য” সেই কোচ।

৪৫ মিনিটের ছক

সময় ভাগ পর্ব ০৬-এর ছক থেকে; এটি নিয়ম নয়, শুরু করার নিরাপদ কাঠামো। প্রতিটি walkthrough-এর শিরোনামে এই সময়ের ছাপ দেখবেন।

মিনিটধাপশেষে হাতে যা থাকবে
০০–০৫১. পরিধি ও requirementFR, NFR আর “বাইরে রাখা” তালিকা
০৫–০৯২. হিসাবpeak QPS, storage, bandwidth — মুখে বলা অনুমানসহ
০৯–১৫৩–৪. API ও data model২–৪টি endpoint, মূল টেবিল, key আর access pattern
১৫–২৫৫. High-level designবাক্স আর তীরের ছবি, একটি অনুরোধের পুরো পথ
২৫–৩৮৬. Deep diveএক-দুটি কঠিন অংশের সমাধান, সংখ্যাসহ
৩৮–৪২৭. Bottleneck, failure ও trade-offকী ভাঙে, কী সামলায়, কী দাম দিলাম
৪২–৪৫৮. Wrap-upকী মাপবেন, ১০ গুণে কী বদলাবেন, সারাংশ

প্রতিটি walkthrough-এর ছাঁচ আর রিভিশনের মানচিত্র

প্রতিটি অংশ শুরু হয় ইন্টারভিউয়ারের প্রশ্ন দিয়ে, তারপর সাতটি সময়-খণ্ডে কথোপকথন, একটি ছবি, একটি simulator, আর শেষে “মূল্যায়নের চোখে” — কী ভালো হলো, কী আরও ভালো হতে পারত — আর দুটি ফলো-আপ প্রশ্ন। কোনো অংশে আটকে গেলে পাশের পর্বটি আবার পড়ুন; সংখ্যা আর নিয়ম এখানে সেগুলো থেকেই নেওয়া। পাঁচটি নকশার কোনোটিই পর্ব ৫৪–৬১-এর case study-র পুনরাবৃত্তি নয়; যেখানে কোনো অংশ আগেই আছে, সেখানে শুধু লিংক।

অংশনকশামূল ধারণারিভিশন
২Web crawlerfrontier, ভদ্রতা, dedup, partition০৩, ১৬, ২৬, ৪৫
৩Real-time leaderboardsorted set, sharding, scatter-gather, আনুমানিক rank২৩, ৩০, ৩৩
৪Pastebinobject store + metadata, মেয়াদ, CDN১৭, ২৬, ৫৪
৫Search autocompleteprefix table, top-k, হালনাগাদ, edge cache২৭, ২৮, ৩৬
৬Distributed job schedulerটেকসই সূচি, lease, fencing, at-least-once৩৪, ৩৭, ৪৩
পড়ার নিয়ম

আগে নিজে ১৫ মিনিট, তারপর পড়া। শুধু ধাপ ১–৫ খাতায় করুন: পরিধি, তিনটি সংখ্যা, দুটি endpoint, একটি টেবিল, একটি ছবি। তারপর walkthrough-এর সাথে মিলিয়ে দেখুন কোন প্রশ্ন আপনি করেননি, কোন সংখ্যা বাদ পড়েছে। পুরো ৪৫ মিনিটের মহড়া পরে, অংশ ৭-এর মূল্যায়ন-ছক হাতে।

২ · Mock ১ — web crawler: frontier, ভদ্রতা আর deduplication

রিভিশন: পর্ব ০৩ · হিসাব, পর্ব ১৬ · consistent hashing, পর্ব ২৬ · object storage, পর্ব ৪৫ · Bloom filter

ইন্টারভিউয়ার: “সন্ধানী” নামের একটি কাল্পনিক সার্চ স্টার্টআপের জন্য একটি web crawler নকশা করুন।

০০–০৫ · ধাপ ১ — পরিধি

প্রার্থী: কয়েকটি প্রশ্ন আগে। Crawler কীসের জন্য — সার্চ index, নাকি নির্দিষ্ট কিছু সাইটের দাম পর্যবেক্ষণ? শুধু HTML, নাকি ছবি আর PDF-ও? কত পাতা, কত ঘনঘন আবার নামাতে হবে?

ইন্টারভিউয়ার: সার্চ index-এর জন্য, শুধু HTML. ধরুন ১০০ কোটি পাতা, প্রতিটি মাসে একবার নতুন করে।

প্রার্থী: তাহলে FR: seed URL থেকে পাতা নামানো, পাতার লিংক থেকে নতুন URL খোঁজা, আর পাতা সংরক্ষণ — index বানানো অন্য দলের কাজ। NFR চারটি: ভদ্রতা — কোনো সাইটে অন্যায্য চাপ নয়, robots.txt মানা; scale — মাসে ১০০ কোটি; সহনশীলতা — ভাঙা HTML, ধীর সার্ভার, অসীম লিংকের ফাঁদ; আর একই পাতা বারবার নয়. বাইরে রাখছি JavaScript চালিয়ে পাতা আঁকা, login-এর পেছনের পাতা আর ranking.

মন্তব্য: ভদ্রতা প্রথম NFR — crawler-এর সবচেয়ে বড় ব্যবহারিক আর নৈতিক শর্ত। “বাইরে রাখা” তালিকা পরের ৪০ মিনিট বাঁচায়।

০৫–০৯ · ধাপ ২ — হিসাব

প্রার্থী: ধরা যাক ১ মাস = ৩০ দিন, ১ দিন = ৮৬,৪০০ সেকেন্ড। ১০০ কোটি ÷ (৩০ × ৮৬,৪০০) ≈ ৩৮৬ পাতা/সেকেন্ড; peak দ্বিগুণ ধরলে প্রায় ৭৭২। গড় HTML ১০০ KB ধরলে গড়ে প্রায় ৩৮.৬ MB/s, অর্থাৎ প্রায় ৩০৯ Mbit/s, peak-এ প্রায় ৬১৭ Mbit/s. Storage: মাসে কাঁচা ১০০ TB; ৪ গুণ সংকোচন ধরলে ২৫ TB, erasure coding-এ (১.৫ গুণ) প্রায় ৩৭.৫ TB — তিন কপি রাখলে ৭৫ TB.

ইন্টারভিউয়ার: কোন URL দেখা হয়ে গেছে, তা মনে রাখতে কত memory?

প্রার্থী: URL গড়ে ১০০ byte হলে শুধু string-ই প্রায় ১০০ GB. ০.১% false positive-এর Bloom filter-এ প্রতি key-এ প্রায় ১৪.৪ bit — মোট প্রায় ১.৮ GB, hash ১০টি (পর্ব ৪৫-এর সূত্র)। একটি মেশিনের RAM-এ আঁটে।

মন্তব্য: প্রতিটি অনুমান মুখে বলা, MB/s থেকে Mbit/s-এ একক বদল স্পষ্ট। “১.৮ GB” সংখ্যাটিই পরে নকশা ঠিক করে — দেখা-URL-এর filter আলাদা বিশাল ডেটাবেস নয়।

০৯–১৫ · ধাপ ৩–৪ — API ও data model

প্রার্থী: বাইরের API নেই বললেই চলে; ভেতরের দুটি endpoint আর একটি event যথেষ্ট:

POST /v1/seeds                      {"urls": ["https://news.example/"]}
PUT  /v1/hosts/news.example/policy  {"min_gap_s": 2, "paused": false}
event page_fetched  {url_hash, url, status, fetched_at, content_hash, object_key}

Host-এর policy বদলানোর পথ লাগে, কারণ কোনো সাইটের মালিক “ধীরে চালান” অনুরোধ করলে কোড না বদলেই মানা যায়। Data চার জায়গায়: (১) URL frontier — URL, অগ্রাধিকার, host, কখন নামানো যাবে; (২) host টেবিল — robots.txt-এর cache আর কখন আনা, দুটি অনুরোধের ন্যূনতম বিরতি, error-এর গণনা; (৩) পাতার metadata — key হলো normalise করা URL-এর hash; সাথে শেষবার কখন নামানো, লেখার hash, SimHash আর object key; (৪) লেখা নিজে object storage-এ, key = লেখার hash, তাই একই লেখা একবারই থাকে।

মন্তব্য: access pattern স্পষ্ট — host ধরে “পরের URL কোনটি”, URL-এর hash ধরে “দেখেছি কি”, লেখার hash ধরে “এই লেখা আগে আছে কি”। Schema-র খুঁটিনাটিতে সময় নষ্ট হয়নি।

১৫–২৫ · ধাপ ৫ — high-level design

Web crawler-এর high-level design

Frontier → politeness scheduler → fetchers → parser → content store, with a dedup loop back

Web crawler-এর high-level design URL frontier Politeness scheduler Fetcher ×N Parser robots.txt DNS Content store URL filter অগ্রাধিকারের সারি host-প্রতি সারি + “কখন অনুমতি” cache নামানো, timeout cache লিংক বের করা · লেখার hash object storage normalise → Bloom নতুন URL
ছবি ৬৬.১ — কঠিন তীর মূল পথ; ভাঙা তীর নতুন URL-এর ফেরার চক্র। বাক্সের সংখ্যা আর আকার load test দেখে ঠিক হবে।

প্রার্থী: একটি URL-কে পুরো পথ হাঁটিয়ে দেখাই — নিচের stepper-এ।

news.example-এর একটি পাতা (কাল্পনিক)
কোথায়
যা দেখা হলো
সিদ্ধান্ত
  1. Frontier থেকে বের হলো। https://news.example/bd/ — দেশের খবরের পাতা, ঘনঘন বদলায়, তাই অগ্রাধিকার উঁচু। Frontier এটিকে সরাসরি নামাতে দেয় না; পাঠায় তার host-এর নিজের সারিতে।
  2. robots.txt দেখা। এই host-এর robots.txt ৩ ঘণ্টা আগে আনা, cache-এ আছে। RFC 9309 অনুযায়ী cache সাধারণত ২৪ ঘণ্টার বেশি পুরোনো রাখা উচিত নয়। শুধু /admin/ নিষিদ্ধ, তাই /bd/ চলবে।
  3. ভদ্রতার অপেক্ষা। এই host-এ দুটি অনুরোধের মাঝে অন্তত ২ সেকেন্ড; আগেরটি গেছে 10:00:00-এ, তাই পরের অনুমতি 10:00:02-এ। এখন 10:00:01 — fetcher বসে থাকে না, ততক্ষণ অন্য host-এর একটি URL নেয়।
  4. নামানো। Host-এর IP আসে নিজেদের DNS cache থেকে — প্রতি পাতায় নতুন DNS খোঁজ অনেক সময় খায়। উত্তর 200 OK, ১২০ KB, ৩৫০ ms. লেখা যায় object storage-এ, আর একটি page_fetched event বের হয়।
  5. লেখার hash. হুবহু একই লেখার hash আগে নেই; প্রায়-একই পাতা খুঁজতে SimHash — নিকটতম পরিচিত পাতা ৯ bit দূরে, যা ধরা যাক ৩ bit-এর সীমার বাইরে। তাই এটি নতুন পাতা।
  6. লিংক বের করা আর normalise. পাতায় একটি লিংক: HTTP://News.Example:80/khela/../bd/?utm_source=fb&id=7#top. URL normalization-এ scheme আর host ছোট হাতে, ডিফল্ট port 80 বাদ, ../ মিলিয়ে path /bd/, #top বাদ, আর নীতি অনুযায়ী utm_ parameter বাদ: http://news.example/bd/?id=7.
  7. দেখা হয়েছে কি? URL filter প্রতিটি নতুন লিংক Bloom filter-এ দেখে। এই লিংকে উত্তর “হয়তো দেখা” — বাদ; ০.১% ক্ষেত্রে এটি ভুল হতে পারে। আরেকটি লিংকে উত্তর “নিশ্চিত নতুন” — Bloom filter-এ “না” কখনো ভুল নয় — তাই সেটি অগ্রাধিকারসহ frontier-এ। চক্র পূর্ণ।

মন্তব্য: ছবির প্রতিটি বাক্সের পেছনে একটি NFR — ভদ্রতার জন্য scheduler, dedup-এর জন্য filter আর লেখার hash, scale-এর জন্য বহু fetcher. একটি URL হাঁটিয়ে দেখানো ছবিকে জীবন্ত করে।

২৫–৩৮ · ধাপ ৬ — deep dive: ভদ্রতা, dedup আর ভাগ

ইন্টারভিউয়ার: Politeness scheduler-এর ভেতরটা বলুন। সাধারণ একটি সারি কেন যথেষ্ট নয়?

প্রার্থী: একটি পাতার লিংকের বেশিরভাগই একই host-এর। সাধারণ FIFO সারি তাই একই সাইটে পরপর আঘাত করে। প্রচলিত সমাধান দুই স্তরের সারি — Mercator crawler (Heydon ও Najork, ১৯৯৯) আর তার পরের লেখাগুলোর ধারায়: সামনের সারিগুলো অগ্রাধিকার ধরে, পেছনের সারিগুলো host ধরে — প্রতিটি পেছনের সারিতে একটিই host. সাথে একটি heap, যেখানে প্রতিটি host-এর “পরের অনুমতির সময়”; fetcher সবসময় heap-এর মাথার host-টি নেয়। নিচের simulator-এ দুটি পথ পাশাপাশি:

// Polite crawling: plain FIFO vs "pick the URL whose host is allowed soonest". 2 fetchers, 0.4 s/fetch.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rnd = mulberry32(66);
const gap = { "news.test": 2, "shop.test": 1, "blog.test": 0.5 };  // min seconds between hits (policy)
const hosts = Object.keys(gap), urls = [], count = {};
for (let i = 0; i < 12; i++) { const h = hosts[Math.floor(rnd() * 3)]; urls.push({ h, p: "/p" + i });
  count[h] = (count[h] || 0) + 1; }
console.log("URLs per host:", JSON.stringify(count));
function crawl(polite) {
  const free = [0, 0], last = {}, queue = urls.slice(), log = [];
  let early = 0;
  const allowed = (u, t) => Math.max(t, (last[u.h] ?? -99) + gap[u.h]);
  while (queue.length) {
    const f = free.indexOf(Math.min(...free));                 // fetcher that is free first
    let i = 0;
    if (polite) queue.forEach((u, j) => { if (allowed(u, free[f]) < allowed(queue[i], free[f])) i = j; });
    const u = queue.splice(i, 1)[0];
    const start = polite ? allowed(u, free[f]) : free[f];
    if (start < allowed(u, start) - 1e-9) early++;             // hit the host too soon
    last[u.h] = start; free[f] = start + 0.4;
    log.push(start.toFixed(1) + " " + u.h + u.p);
  }
  console.log((polite ? "polite" : "fifo  ") + " | too-early hits: " + early +
    " | all done at " + Math.max(...free).toFixed(1) + " s");
  return log.slice(0, 5).join(", ");
}
const a = crawl(false), b = crawl(true);
console.log("fifo   starts: " + a);
console.log("polite starts: " + b);

আউটপুট:

URLs per host: {"news.test":8,"blog.test":2,"shop.test":2}
fifo   | too-early hits: 9 | all done at 2.4 s
polite | too-early hits: 0 | all done at 14.4 s
fifo   starts: 0.0 news.test/p0, 0.0 news.test/p1, 0.4 news.test/p2, 0.4 blog.test/p3, 0.8 shop.test/p4
polite starts: 0.0 news.test/p0, 0.0 blog.test/p3, 0.4 shop.test/p4, 0.5 blog.test/p5, 1.4 shop.test/p8

FIFO ২.৪ সেকেন্ডে শেষ, কিন্তু ১২টি অনুরোধের ৯টি সীমার আগেই — শুরুতেই news.test-এ একই মুহূর্তে দুটি। ভদ্র পথে সীমা ভাঙে না, কিন্তু শেষ হয় ১৪.৪ সেকেন্ডে, আর সময়টা ঠিক করে সবচেয়ে বড় host: ৮টি URL, মাঝে ২ সেকেন্ড, তাই (৮ − ১) × ২ + ০.৪ = ১৪.৪. শিক্ষা দুটি: ভদ্রতার দাম সময়, আর fetcher-দের ব্যস্ত রাখতে একসাথে অনেক host লাগে। Peak-এ সেকেন্ডে প্রায় ৭৭২টি পাতা নামাতে, প্রতিটি host-এ সেকেন্ডে বড়জোর একটি অনুরোধ হলে, অন্তত প্রায় ৭৭২টি host একসাথে সক্রিয় থাকতে হবে — বাস্তবে আরও অনেক বেশি, কারণ বহু host-এর বিরতি এক সেকেন্ডের বেশি। gap-এ news.test ১ করে দেখুন সময় কত কমে।

ইন্টারভিউয়ার: ধরুন একটি বড় সাইটে ৫০ লাখ পাতা, আর সেকেন্ডে একটির বেশি অনুরোধ চলবে না। মাসে একবার সব পাতা হবে?

প্রার্থী: না — ৫০ লাখ সেকেন্ড প্রায় ৫৮ দিন। নিয়ম ভাঙা কোনো সমাধান নয়। বরং: ঘনঘন বদলানো আর গুরুত্বপূর্ণ পাতা আগে (অগ্রাধিকার); conditional GET — If-Modified-Since বা ETag পাঠালে না-বদলানো পাতায় শুধু 304, bandwidth বাঁচে, যদিও অনুরোধ একটিই থাকে; সাইটের sitemap থেকে বদলের সময় জানা; আর সার্ভার দ্রুত সাড়া দিলে সীমার মধ্যে থেকে ধীরে হার বাড়ানো, ধীর হলে কমানো। মাসিক লক্ষ্য তাই সব host-এর জন্য এক নয় — নীতির সিদ্ধান্ত।

মন্তব্য: চাপের প্রশ্নে সংখ্যা দিয়ে “না” বলা, তারপর বিকল্প — ইন্টারভিউয়ার ঠিক এটাই দেখেন।

ইন্টারভিউয়ার: Bloom filter ভুল করে “দেখা” বললে নতুন পাতা কখনো নামবে না। মেনে নেবেন?

প্রার্থী: হাজারে একটি নতুন URL বাদ পড়া সার্চ index-এর জন্য সাধারণত চলে, আর হার মাপা যায়। জরুরি domain-এর জন্য filter “হয়তো” বললে key-value store-এ নির্ভুল যাচাই; আর মাসে মাসে নতুন filter বানালে একই URL চিরকাল আটকে থাকে না। লেখার dedup আলাদা স্তর: হুবহু একই লেখা ধরতে লেখার hash (৬৪-bit fingerprint-এ ১০০ কোটি পাতায় প্রায় ৮ GB), প্রায়-একই পাতা ধরতে SimHash.

ইন্টারভিউয়ার: বহু মেশিনে ভাগ করবেন কীভাবে?

প্রার্থী: Host-এর hash ধরে — একটি host-এর সব URL একই crawler node-এ, তাই ভদ্রতার সীমা এক জায়গাতেই মানা যায়, কোনো বিতরণকৃত lock লাগে না। Node যোগ-বিয়োগে কম host সরাতে পর্ব ১৬-এর consistent hashing. নতুন URL অন্য node-এর host-এর হলে সেই node-এর frontier-এ পাঠাই। Frontier নিজে টেকসই — disk-এ বা একটি log-এ — যাতে node মরলে সারি হারায় না।

৩৮–৪২ · ধাপ ৭ — bottleneck, failure ও trade-off

  • লিংকের ফাঁদ: অসীম ক্যালেন্ডার বা session-id-ওয়ালা URL. প্রতিকার: host-প্রতি পাতার বাজেট, path-এর গভীরতা আর URL দৈর্ঘ্যের সীমা।
  • Fetcher মরল: সারির URL আবার আসবে — at-least-once; লেখা যায় hash-নামে, তাই দুবার নামানো ক্ষতিহীন।
  • সাইট ধীর বা 5xx: host-প্রতি backoff; robots.txt-ই 5xx দিলে RFC 9309-এর নিয়মে সাময়িকভাবে পুরো সাইট নিষিদ্ধ ধরা।
  • DNS bottleneck: নিজস্ব caching resolver, host-এর IP-ও cache-এ।
  • Trade-off: তাজা বনাম ভদ্র; BFS বনাম অগ্রাধিকার; নির্ভুল dedup-এর memory বনাম Bloom-এর ভুলের হার।

৪২–৪৫ · ধাপ ৮ — wrap-up

প্রার্থী: যা মাপব: পাতা/সেকেন্ড, host-প্রতি error-এর হার, robots.txt বা ভদ্রতার সীমা ভাঙার সংখ্যা — লক্ষ্য শূন্য — duplicate-এর অনুপাত, frontier-এর আকার, আর পাতার বয়সের p50 ও p99. দশ গুণ হলে: আরও node, আর বদলের হার দেখে কোন পাতা কত ঘনঘন আবার নামবে, তার আলাদা সূচি। সময় পেলে পরের কাজ: JavaScript-নির্ভর পাতা আঁকা।

মূল্যায়নের চোখে

শক্তিশালী: ভদ্রতাকে NFR থেকে নকশা পর্যন্ত টেনে আনা; Bloom-এর আকার সূত্র থেকে; host-ভিত্তিক ভাগের যুক্তি; চাপের প্রশ্নে সংখ্যা দিয়ে উত্তর। আরও ভালো হতে পারত: frontier-এর টেকসই রূপ (কোন storage, কীভাবে recovery) আরও স্পষ্ট করা, আর আবার নামানোর সূচির ছোট একটি সূত্র দেওয়া।

প্র ০১ফলো-আপ: crawler-এর কাজ URL-এর hash ধরে ভাগ না করে host-এর hash ধরে কেন? কোন ক্ষেত্রে এটি সমস্যা? (মাঝারি)

উত্তর: ভদ্রতার সীমা host-ভিত্তিক। URL-এর hash-এ ভাগ করলে একটি host-এর URL সব node-এ ছড়ায়; তখন “এই host-এ শেষ অনুরোধ কখন” জানতে সব node-কে একটি ভাগ করা counter বা lock দেখতে হয় — ধীর, জটিল। Host ধরে ভাগে সিদ্ধান্ত এক জায়গায়, স্থানীয় heap-এ। DNS আর robots.txt-এর cache-ও তখন স্থানীয়।

সমস্যা: খুব বড় একটি host-এর সব কাজ একটি node-এ — hot key-এর মতো অসমতা। তবে ভদ্রতার নিয়মেই সেই host-এ হার কম, তাই node-এর CPU-তে চাপ সাধারণত বেশি নয়। চাপ আসে parsing-এ — সেটি আলাদা worker pool-এ সরানো যায়।

প্র ০২ফলো-আপ: প্রায়-একই পাতা (near-duplicate) চিনতে SimHash কীভাবে কাজ করে? (কঠিন)

উত্তর: সাধারণ hash-এ একটি অক্ষর বদলালেই পুরো hash বদলে যায় — তাই শুধু হুবহু মিল ধরে। SimHash (Charikar, ২০০২) লেখার প্রতিটি শব্দ বা feature-এর hash নেয়, প্রতিটি bit-অবস্থানে ওজনসহ +১ বা −১ যোগ করে, শেষে যোগফল ধনাত্মক হলে সেই bit 1. দুটি পাতা প্রায় একই হলে বেশিরভাগ feature এক, তাই fingerprint-এর বেশিরভাগ bit-ও এক; Hamming distance ছোট। ২০০৭ সালের একটি প্রকাশিত গবেষণায় (Manku ও সহকর্মীরা, WWW সম্মেলন) web crawling-এর জন্য ৬৪-bit fingerprint-এ ৩ bit পর্যন্ত পার্থক্যকে প্রায়-একই ধরা হয়েছিল। দ্রুত খুঁজতে fingerprint-কে কয়েক খণ্ডে ভাগ করে খণ্ড ধরে index — দুটি fingerprint ৩ bit-এর কম দূরে হলে অন্তত একটি খণ্ড হুবহু মিলবে।

৩ · Mock ২ — real-time leaderboard: sorted set, sharding আর আনুমানিক rank

রিভিশন: পর্ব ০৪ · tail latency, পর্ব ২৩ · sharding, পর্ব ৩০ · distributed cache, পর্ব ৩৩ · log streaming, পর্ব ৩৪ · idempotency

ইন্টারভিউয়ার: “কুইজমেলা” নামের একটি কাল্পনিক মোবাইল কুইজ-গেমের জন্য একটি real-time leaderboard নকশা করুন।

০০–০৫ · ধাপ ১ — পরিধি

প্রার্থী: Leaderboard সবার একটিই, নাকি বন্ধুদের আলাদা? সময়ের জানালা কী — সর্বকালের, নাকি সাপ্তাহিক? স্কোর মানে সপ্তাহের মোট পয়েন্ট, নাকি একটি খেলার সেরা? “Real-time” মানে কত সেকেন্ড? আর লাখতম খেলোয়াড়কে কি হুবহু তাঁর অবস্থান বলতে হবে?

ইন্টারভিউয়ার: সবার একটি সাপ্তাহিক board, স্কোর = সপ্তাহের মোট পয়েন্ট। খেলা শেষের কয়েক সেকেন্ডের মধ্যে দেখা যাবে। নিবন্ধিত খেলোয়াড় ৫ কোটি, দিনে সক্রিয় ১ কোটি। শীর্ষ ১০ হাজারে হুবহু rank, বাকিদের “শীর্ষ ২%”-ধরনের আনুমানিক উত্তর চলবে।

প্রার্থী: FR: খেলা শেষে স্কোর যোগ; শীর্ষ ১০; নিজের স্কোর, rank আর আশেপাশের কয়েকজন। NFR: স্কোর থেকে board-এ দেখা কয়েক সেকেন্ডে; পড়ার p99 প্রায় ১০০ ms-এর নিচে; স্কোর কখনো হারাবে না বা দুবার যোগ হবে না। বাইরে: বন্ধুদের board, পুরস্কার, প্রতারণা ধরার বিস্তারিত — তবে স্কোর আসবে game server থেকে, ফোন থেকে সরাসরি নয়।

মন্তব্য: “হুবহু rank কতজনের লাগবে” প্রশ্নটিই পরে পুরো deep dive খুলে দেয়।

০৫–০৯ · ধাপ ২ — হিসাব

প্রার্থী: ধরা যাক সক্রিয় প্রত্যেকে দিনে ১০টি খেলা: ১০ কোটি স্কোর-হালনাগাদ ÷ ৮৬,৪০০ ≈ ১,১৫৭/সেকেন্ড; সন্ধ্যার peak ৫ গুণ ধরলে প্রায় ৫,৭৮৭। প্রত্যেকে দিনে ২০ বার board খোলেন: প্রায় ২,৩১৫ পড়া/সেকেন্ড, peak-এ প্রায় ১১,৫৭৪। Memory: ৫ কোটি সারি, sorted set-এ প্রতি সারি মোটামুটি ১০০ byte ধরলে (encoding আর সংস্করণভেদে আলাদা) প্রায় ৫ GB. প্রতিটি হালনাগাদ O(log N) — log₂(৫ কোটি) ≈ ২৬ ধাপ।

মন্তব্য: সংখ্যা বলছে ডেটা ছোট, অপারেশন সরল — কঠিন অংশ ক্রম রক্ষা আর rank-এর প্রশ্ন, storage নয়।

০৯–১৫ · ধাপ ৩–৪ — API ও data model

POST /v1/game-results        (game server only; Idempotency-Key: game-88213)
     {"game_id": "88213", "player_id": "p-4417", "points": 120}
GET  /v1/boards/2026-w44/top?limit=10
GET  /v1/boards/2026-w44/players/p-4417?around=5
     -> {"score": 9500, "rank": 3399, "exact": true, "around": [...]}

প্রার্থী: সত্যের উৎস একটি ডেটাবেস: game_results (key game_id, unique — একই ফল দুবার ঢোকে না) আর week_scores (week, player_id) → score. দ্রুত পড়ার জন্য memory-তে প্রতি সপ্তাহের একটি sorted set: key lb:2026-w44, member = player_id, score = পয়েন্ট — Redis-এর মতো store-এ ZADD, ZREVRANGE (শীর্ষ k), ZREVRANK আর ZCOUNT সবই প্রায় O(log N). সমান স্কোরে Redis member-এর নাম ধরে সাজায়; “আগে যিনি পৌঁছেছেন তিনি আগে” চাইলে score-এ সময় জুড়ে দিতে হয় — ফলো-আপ প্রশ্ন ০৪।

১৫–২৫ · ধাপ ৫ — high-level design

Leaderboard-এর high-level design

Game servers → score log → updater (DB first, then sorted sets) · Rank API reads the shards

Leaderboard-এর high-level design Game server খেলোয়াড়ের অ্যাপ Score log Rank API Updater Histogram ফল, game_id-সহ শীর্ষ ১০ · আমার rank player_id-এ partition scatter-gather আগে DB, পরে set আনুমানিক rank shard ০ shard ১ shard ২ shard ৩ Score DB — সত্যের উৎস
ছবি ৬৬.২ — চারটি sorted set shard, player_id-এর hash ধরে ভাগ। DB আর log থাকায় memory-র board হারালেও আবার বানানো যায়।

প্রার্থী: খেলা শেষে game server ফলটি একটি log-এ পাঠায় (পর্ব ৩৩), player_id-এ partition — একজনের হালনাগাদ ক্রমে থাকে। Updater প্রথমে DB-তে game_id ঢোকায়; আগেই থাকলে কিছুই করে না। নতুন হলে সপ্তাহের মোট বাড়ায়, তারপর sorted set-এ মোট মানটিই বসায়।

ইন্টারভিউয়ার: ZINCRBY দিয়ে সরাসরি ১২০ যোগ করলেন না কেন?

প্রার্থী: Log-এর consumer at-least-once (পর্ব ৩৪): ack-এর আগে crash হলে একই বার্তা আবার আসে। “১২০ যোগ” দুবার চললে স্কোর ২৪০ বাড়ে; “মোট ৯,৫০০ বসানো” দুবার চললেও ৯,৫০০ — idempotent. Dedup-এর আসল পাহারা DB-র unique game_id.

মন্তব্য: সবচেয়ে ভালো মুহূর্ত — একটি ছোট API-পছন্দকে delivery semantics-এর সাথে জোড়া।

২৫–৩৮ · ধাপ ৬ — deep dive: sharding-এর দুই পথ আর rank

প্রার্থী: ৫ GB একটি মেশিনের memory-তে আঁটে, তবু ভাগ করব — একটি node-এর failover-এ পুরো board বন্ধ, সর্বকালের board বাড়তেই থাকবে, আর একটি node-এর CPU-তে সব হালনাগাদ। দুটি পথ:

প্রশ্নplayer_id-এর hash ধরে ভাগস্কোরের পরিসর ধরে ভাগ
লেখাসমানভাবে ছড়ায়, একটি shardস্কোর সীমা পেরোলে এক shard থেকে মুছে অন্যটিতে — দুই জায়গায়, atomic নয়
শীর্ষ kপ্রতিটি shard-এর শীর্ষ k মিলিয়ে বাছাই (S × k প্রার্থী)শুধু সবচেয়ে উঁচু পরিসরের shard
আমার rankসব shard-এ “আমার চেয়ে বেশি কতজন” — scatter-gatherউঁচু পরিসরগুলোর গণনা (আগে রাখা) + নিজের shard-এ rank
ফাঁদপ্রতি rank-এ সব shard; latency = সবচেয়ে ধীর shardসাপ্তাহিক reset-এ সোমবার সবাই ০ — সবাই এক shard-এ; পরিসর বারবার নতুন করে ভাগ

সাপ্তাহিক board-এ তাই hash-এ ভাগই সরল। নিচের simulator-এ ২ লাখ কাল্পনিক খেলোয়াড়, চারটি shard: শীর্ষ ১০ মেলানো, হুবহু rank, আর ১০০ ঘরের histogram থেকে আনুমানিক rank.

// Leaderboard on 4 shards (by user-id hash): top-10 by merging, exact rank by scatter-gather,
// approximate rank from a 100-bucket score histogram. 200,000 fictional players.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rnd = mulberry32(2026), N = 200000, S = 4, MAX = 10000;
const shards = Array.from({ length: S }, () => []);
for (let id = 0; id < N; id++) {                       // skewed scores: most players are low
  const score = Math.floor(MAX * rnd() ** 3);
  shards[(Math.imul(id, 2654435761) >>> 0) % S].push({ id, score });
}
shards.forEach(s => s.sort((a, b) => b.score - a.score || a.id - b.id));   // like a sorted set
const best = shards.flatMap(s => s.slice(0, 10))       // each shard sends only its own top 10
  .sort((a, b) => b.score - a.score || a.id - b.id).slice(0, 10);
const all = shards.flat().sort((a, b) => b.score - a.score || a.id - b.id).slice(0, 10);
console.log("top-3:", best.slice(0, 3).map(p => p.id + ":" + p.score).join("  "),
  "| merged 40 candidates = global top-10:", best.every((p, i) => p.id === all[i].id));
const hist = new Array(100).fill(0);                  // bucket b covers scores [100b, 100b+99]
shards.flat().forEach(p => hist[Math.floor(p.score / 100)]++);
function approxRank(score) {                          // players in higher buckets + share of own
  const b = Math.floor(score / 100); let above = 0;
  for (let i = b + 1; i < 100; i++) above += hist[i];
  return Math.round(above + hist[b] * (100 * b + 99 - score) / 100) + 1;
}
console.log("score | per-shard higher counts | exact rank | approx | error");
for (const score of [9500, 7000, 3000, 500]) {
  const per = shards.map(s => s.filter(p => p.score > score).length);
  const exact = per.reduce((x, y) => x + y, 0) + 1, ap = approxRank(score);
  console.log(String(score).padStart(5), "|", per.join(" + ").padEnd(31), "|", String(exact).padStart(6),
    "|", String(ap).padStart(6), "|", ((ap - exact) / exact * 100).toFixed(2) + "%");
}

আউটপুট:

top-3: 76791:9999  94553:9999  139031:9999 | merged 40 candidates = global top-10: true
score | per-shard higher counts | exact rank | approx | error
 9500 | 856 + 862 + 812 + 868           |   3399 |   3401 | 0.06%
 7000 | 5584 + 5589 + 5508 + 5635       |  22317 |  22317 | 0.00%
 3000 | 16532 + 16638 + 16428 + 16557   |  66156 |  66158 | 0.00%
  500 | 31635 + 31515 + 31385 + 31662   | 126198 | 126201 | 0.00%

শীর্ষ ১০: প্রতিটি shard শুধু নিজের শীর্ষ ১০ পাঠাল, ৪০টি প্রার্থী থেকে বাছাই — পুরো ২ লাখের হিসাবের সাথে হুবহু মিলল (true). কারণ সহজ: global শীর্ষ ১০-এর কেউ তাঁর নিজের shard-এর শীর্ষ ১০-এর বাইরে থাকতে পারেন না। শীর্ষ তিনজনের স্কোর সমান (৯,৯৯৯) — সমতার নিয়ম লাগবেই। হুবহু rank: rank = নিজের চেয়ে বেশি স্কোরের মোট + ১; চারটি shard-এর গণনা যোগ করতে হয়। আনুমানিক: histogram-এ উঁচু ঘরগুলোর যোগ আর নিজের ঘরের ভেতরে সমান বণ্টন ধরে ভাগ — ত্রুটি সর্বোচ্চ ০.০৬%, অথচ কোনো shard-এ যেতেই হয় না; ১০০টি সংখ্যা যেকোনো API সার্ভারের memory-তে থাকে। বণ্টন খুব এবড়োখেবড়ো হলে ঘর বাড়াতে হবে।

Rank APIস্কোর ৯,৫০০
shard ০৮৫৬ জন
shard ১৮৬২ জন
shard ২৮১২ জন
shard ৩৮৬৮ জন
যোগ + ১rank ৩,৩৯৯
Histogram≈ ৩,৪০১
  1. অনুরোধ। একজন খেলোয়াড় নিজের অবস্থান চাইলেন। Rank API আগে তাঁর shard থেকে স্কোর আনে — ধরা যাক ৯,৫০০; এক shard, এক অপারেশন।
  2. Scatter. চারটি shard-কে একসাথে একই প্রশ্ন: “৯,৫০০-এর বেশি স্কোর কতজনের?” — ZCOUNT lb:2026-w44 (9500 +inf. বন্ধনী ( মানে ৯,৫০০ নিজে বাদ।
  3. উত্তর। ৮৫৬, ৮৬২, ৮১২ আর ৮৬৮. মোট সময় ঠিক করে সবচেয়ে ধীর shard — একটি shard GC-তে থামলে পুরো উত্তর দেরি, পর্ব ০৪-এর fan-out-এর tail latency.
  4. Gather. ৮৫৬ + ৮৬২ + ৮১২ + ৮৬৮ = ৩,৩৯৮; rank = ৩,৩৯৮ + ১ = ৩,৩৯৯ — শীর্ষ ১০ হাজারের ভেতরে, তাই হুবহু উত্তরই দেখানো হয়।
  5. আনুমানিক পথ। শীর্ষ ১০ হাজারের বাইরের কারও জন্য shard-এ না গিয়ে histogram: উত্তর ≈ ৩,৪০১, ত্রুটি ০.০৬%. পর্দায় “শীর্ষ ২%”-এর মতো রূপে দেখালে ভুলটুকু চোখেও পড়ে না।

৩৮–৪২ · ধাপ ৭ — bottleneck, failure ও trade-off

  • শীর্ষ ১০-এর ভিড়: peak-এ সেকেন্ডে প্রায় ১১,৫৭৪ পড়া, যার বড় অংশ একই শীর্ষ ১০. উত্তরটি ১ সেকেন্ডের জন্য API-র memory-তে রাখলে প্রতিটি API সার্ভার থেকে store-এ সেকেন্ডে মাত্র একটি অনুরোধ। দাম: সর্বোচ্চ ১ সেকেন্ড পুরোনো।
  • Shard-এর node মরল: replica-য় failover (পর্ব ৩০); দুটিই গেলে DB থেকে আবার বানানো — সেই সময়টুকু rank “হালনাগাদ হচ্ছে” দেখানো।
  • Updater পিছিয়ে: log-এর consumer lag মাপা; lag বাড়লে updater বাড়ানো — partition যত, ততজন পর্যন্ত।
  • সপ্তাহ বদল: নতুন key, পুরোনোটি কয়েক দিন শুধু-পড়া, তারপর TTL-এ মুছে যায়।

৪২–৪৫ · ধাপ ৮ — wrap-up

প্রার্থী: মাপব খেলা শেষ থেকে board-এ দেখা পর্যন্ত সময়ের p99, Rank API-র p99, consumer lag, shard-এর memory, আর রাতে একটি reconciliation — DB-র মোট আর board-এর মোট মেলে কি না। দশ গুণে: আরও shard, আর শীর্ষ ১০ হাজারের বাইরে সবার জন্য histogram.

মূল্যায়নের চোখে

শক্তিশালী: হুবহু বনাম আনুমানিক — প্রশ্নটি শুরুতেই; ZINCRBY-র ফাঁদ নিজে ধরা; দুই sharding পথের তুলনা আর সাপ্তাহিক reset-এর যুক্তি; সংখ্যা মেলানো simulator. আরও ভালো হতে পারত: “আশেপাশের ৫ জন” কীভাবে বের হবে (একটি shard-এ নয়, সব shard-এর সীমানা থেকে) — তা বলা হয়নি।

প্র ০৩ফলো-আপ: শুধু বন্ধুদের leaderboard লাগলে নকশা কীভাবে বদলাবে? (মাঝারি)

উত্তর: প্রতিজনের বন্ধুদের জন্য আলাদা sorted set রাখলে একটি স্কোর বদলালে তাঁর সব বন্ধুর set-এ লিখতে হয় — fan-out on write, বন্ধু বেশি হলে দামি (পর্ব ৫৬-এর সেই টানাপোড়েন)। বন্ধু সাধারণত কয়েকশো, তাই পড়ার সময় হিসাবই সরল: বন্ধুর তালিকা এনে মূল board থেকে সবার স্কোর একবারে (ZMSCORE-এর মতো batch অপারেশনে, shard ধরে ভাগ করে) এনে memory-তে সাজানো — fan-out on read. কয়েকশো সংখ্যা সাজাতে মাইক্রোসেকেন্ড লাগে; উত্তর কয়েক সেকেন্ড cache-এ রাখা যায়।

প্র ০৪ফলো-আপ: সমান স্কোরে “আগে যিনি পৌঁছেছেন তিনি আগে” — sorted set-এ কীভাবে? (কঠিন)

উত্তর: Score-এ দুটি তথ্য জুড়ে দেওয়া: score = পয়েন্ট × 1010 + (1010 − ১ − সপ্তাহের শুরু থেকে সেকেন্ড)। আগে পৌঁছালে দ্বিতীয় অংশ বড়, তাই উঁচুতে। সাবধানতা: Redis-এর sorted set-এর score একটি double, যা ২৫৩ (প্রায় ৯ × ১০১৫) পর্যন্ত পূর্ণসংখ্যা হুবহু রাখে। তাই এখানে পয়েন্ট প্রায় ৯ লাখের নিচে থাকতে হবে; সপ্তাহে ৬,০৪,৮০০ সেকেন্ড, ১০১০-এর ঘরে সহজেই আঁটে। বিকল্প: rank আনার পর সমান স্কোরের ছোট দলটি DB-র সময় দেখে আবার সাজানো।

৪ · Mock ৩ — pastebin: object store, metadata আর মেয়াদ

রিভিশন: পর্ব ০৮ · REST ও idempotency, পর্ব ১৭ · CDN, পর্ব ২৬ · object storage, পর্ব ৫৪ · ID তৈরি

ইন্টারভিউয়ার: “লেখাখাতা” নামের একটি কাল্পনিক pastebin নকশা করুন — মানুষ একটি লেখা বা কোড রাখবেন, একটি লিংক পাবেন।

০০–০৫ · ধাপ ১ — পরিধি

প্রার্থী: লেখা কি পরে বদলানো যাবে? মেয়াদ কে ঠিক করেন? লিংক কি সবার জন্য খোলা, নাকি শুধু যাঁর কাছে লিংক আছে? সর্বোচ্চ আকার কত?

ইন্টারভিউয়ার: বদলানো যাবে না, নতুন paste বানাতে হবে। মেয়াদ: ১ দিন, ৩০ দিন, ১ বছর বা কখনো না। বেশিরভাগ “unlisted” — লিংক যাঁর কাছে, তিনিই দেখবেন। সর্বোচ্চ ধরুন ১ MB.

প্রার্থী: FR: paste রাখা, লিংকে পড়া, মালিকের মুছে ফেলা, মেয়াদ শেষে অদৃশ্য। NFR: পড়া দ্রুত; মেয়াদের আগে কিছুই হারাবে না; unlisted লিংক অনুমান করা যাবে না। বাইরে: খোঁজা, মন্তব্য, syntax highlighting — সেটি ব্রাউজারেই।

মন্তব্য: “বদলানো যাবে না” — এই এক উত্তরে cache-এর অর্ধেক সমস্যা মিটে যায়, কারণ লেখা immutable.

০৫–০৯ · ধাপ ২ — হিসাব

প্রার্থী: ধরা যাক দিনে ১০ লাখ নতুন paste: প্রায় ১১.৬ লেখা/সেকেন্ড, peak ৩ গুণে প্রায় ৩৫. পড়া লেখার ১০ গুণ: প্রায় ১১৬/সেকেন্ড, peak-এ প্রায় ৩৪৭. গড় আকার ১০ KB: দিনে ১০ GB, মেয়াদ না থাকলে বছরে ৩.৬৫ TB. Peak-এ পড়ার bandwidth মাত্র প্রায় ৩.৫ MB/s, অর্থাৎ প্রায় ২৮ Mbit/s.

মন্তব্য: সংখ্যা বলছে এটি ভিড়ের সমস্যা নয় — storage আর মেয়াদের সমস্যা। Deep dive কোথায় হবে, এখানেই ঠিক হয়ে গেল।

০৯–১৫ · ধাপ ৩–৪ — API ও data model

POST   /v1/pastes   Idempotency-Key: 6f1c...
       {"content": "...", "expires_in": "30d", "visibility": "unlisted"}
  -> 201 {"id": "Qm7xK2pA", "expires_at": "2026-11-27T10:00Z", "delete_token": "..."}
GET    /v1/pastes/Qm7xK2pA          -> 200 text/plain  |  404 (not found or expired)
DELETE /v1/pastes/Qm7xK2pA          (header: delete token)  -> 204

প্রার্থী: Metadata একটি relational টেবিলে: pastes(id PK, object_key, size, content_hash, visibility, created_at, expires_at, deleted), expires_at-এ index. লেখা নিজে object storage-এ। ID ছোট আর অনন্য রাখার কৌশল — counter আর base62, আগে বানানো key, birthday-র অঙ্ক — পর্ব ৫৪-এ আছে, আবার বলছি না। শুধু একটি বাড়তি শর্ত: unlisted লেখা গোপন, তাই ID পরপর নয়, random, আর একটু লম্বা — যেমন ৮ অক্ষরের base62, প্রায় ৪৭.৬ bit. মেয়াদ শেষে 404 দিই, 410 নয় — কোনো লিংক কখনো ছিল কি না, তাও ফাঁস না করতে।

ইন্টারভিউয়ার: লেখা ডেটাবেসেই রাখলেন না কেন?

প্রার্থী: ১ MB পর্যন্ত লেখা সারিতে রাখলে টেবিল, backup আর replication ফুলে ওঠে; object storage সস্তা, টেকসই, আর CDN-এর origin হতে পারে (পর্ব ২৬)। খুব ছোট লেখা, ধরা যাক কয়েক KB-র কম, সারিতেই রাখা যায় — একটি network hop কম। এটি trade-off, নিয়ম নয়।

১৫–২৫ · ধাপ ৫ — high-level design

Pastebin-এর high-level design

CDN → API · metadata DB + object storage · expiry sweeper and lifecycle rule

Pastebin-এর high-level design ব্যবহারকারী CDN — immutable লেখার কপি LB → Paste API ×N ID generator Metadata DB Object storage লেখা · পড়া · মোছা max-age ≤ বাকি মেয়াদ · মুছলে purge stateless · মেয়াদ যাচাই পর্ব ৫৪ expires_at-এ index + expiry sweeper key: exp/দিন/id lifecycle rule
ছবি ৬৬.৩ — লেখার পথে আগে object, তারপর metadata; ভাঙা তীর sweeper-এর মোছার কাজ।

প্রার্থী: লেখার পথ: API একটি ID নেয়, আগে object লেখে, তারপর metadata-র সারি। মাঝপথে crash হলে শুধু একটি অনাথ object থাকে — sweeper পরে মোছে; উল্টো ক্রমে হলে metadata থাকত কিন্তু লেখা থাকত না, পাঠক error পেতেন। পড়ার পথ: CDN-এ না পেলে API metadata দেখে — নেই, মোছা, বা expires_at পেরোনো হলে সঙ্গে সঙ্গে 404, মোছার কাজ বাকি থাকলেও। নইলে object থেকে লেখা।

২৫–৩৮ · ধাপ ৬ — deep dive: মেয়াদ আর storage-এর অঙ্ক

প্রার্থী: কত storage লাগবে, তা নির্ভর করে মেয়াদের মিশ্রণে। পর্ব ০৪-এর Little-এর সূত্র এখানেও খাটে: জমে থাকা পরিমাণ = আসার হার × গড় আয়ু। ধরা যাক ৪০% লেখা ১ দিনের, ৩০% ৩০ দিনের, ২০% ১ বছরের, ১০% চিরকালের:

// Pastebin storage with expiry: content alive on a given day vs Little's law (stored = inflow x lifetime).
// Assumed: 10 GB of new paste content per day; expiry mix chosen by users.
const PER_DAY_GB = 10;
const mix = [ { name: "1 day",   share: 0.4, days: 1 },
              { name: "30 days", share: 0.3, days: 30 },
              { name: "1 year",  share: 0.2, days: 365 },
              { name: "never",   share: 0.1, days: Infinity } ];
const stored = (day) => mix.reduce((gb, m) =>        // content alive at the end of `day`
  gb + PER_DAY_GB * m.share * Math.min(day, m.days), 0);
console.log("day    stored GB   of which 'never'");
for (const d of [1, 30, 365, 730, 1825])
  console.log(String(d).padStart(4), stored(d).toFixed(0).padStart(11),
    (PER_DAY_GB * 0.1 * d).toFixed(0).padStart(12));
const little = mix.filter(m => m.days !== Infinity)
  .reduce((gb, m) => gb + PER_DAY_GB * m.share * m.days, 0);
console.log("Little's law, expiring part only: " + little.toFixed(0) + " GB (flat after 1 year)");
console.log("'never' grows " + (PER_DAY_GB * 0.1 * 365).toFixed(0) + " GB per year, forever");
const raw = PER_DAY_GB * 1825;
console.log("5 years with no expiry at all: " + raw.toFixed(0) + " GB = " +
  (raw / stored(1825)).toFixed(1) + "x the expiring design");

আউটপুট:

day    stored GB   of which 'never'
   1          10            1
  30         184           30
 365        1189          365
 730        1554          730
1825        2649         1825
Little's law, expiring part only: 824 GB (flat after 1 year)
'never' grows 365 GB per year, forever
5 years with no expiry at all: 18250 GB = 6.9x the expiring design

মেয়াদওয়ালা অংশ এক বছরে স্থির হয়ে যায়: ১০ × (০.৪ × ১ + ০.৩ × ৩০ + ০.২ × ৩৬৫) = ৮২৪ GB — তারপর রোজ যত আসে, তত চলে যায়। অথচ মাত্র ১০% “কখনো না”-ই ৫ বছরে ১,৮২৫ GB, মোট ২,৬৪৯-এর প্রায় ৬৯%. শিক্ষা: storage-এর ভবিষ্যৎ ঠিক করে ছোট্ট “চিরকাল” অংশটি — তাই পণ্যের সিদ্ধান্ত (চিরকালের বিকল্প শুধু লগইন করা ব্যবহারকারীর জন্য, বা আকারের সীমা ছোট) storage-এর নকশার চেয়েও বড়। কোনো মেয়াদ না থাকলে ৫ বছরে ১৮,২৫০ GB, প্রায় ৬.৯ গুণ। share বদলে নিজে দেখুন।

ইন্টারভিউয়ার: মেয়াদ শেষে মুছবেন কীভাবে? প্রতিদিন লাখো সারি।

প্রার্থী: তিন স্তরে। এক, পড়ার সময় যাচাই — দেখানোর সঠিকতা কখনো মোছার কাজের ওপর নির্ভর করে না। দুই, sweeper — expires_at-এর index ধরে ছোট ছোট batch-এ, ধরা যাক ১,০০০ সারি করে, metadata চিহ্নিত আর object মোছে; বড় একটি DELETE টেবিলে lock আর replication lag বাড়ায়। তিন, object-এর key-তে মেয়াদের দিন — exp/2026-11-27/Qm7xK2pA — তাহলে object storage-এর lifecycle rule পুরো prefix মুছে দেয়; নিয়মগুলো সাধারণত দিনের হিসাবে চলে, আর মোছা ঘটে কিছুটা দেরিতে, পেছনে পেছনে (লেখার সময়ে, সেবাভেদে আলাদা)। Metadata-ও দিন ধরে partition করলে পুরোনো partition একবারে ফেলে দেওয়া যায়।

ইন্টারভিউয়ার: কেউ ভুলে পাসওয়ার্ড paste করে সাথে সাথে মুছলেন। CDN-এ তো কপি রয়ে গেল?

প্রার্থী: হ্যাঁ, তাই unlisted লেখায় CDN-এর max-age ছোট — কয়েক মিনিট — আর মোছার সাথে সাথে purge (পর্ব ১৭)। Public লেখায় max-age = বাকি মেয়াদের চেয়ে কম। দাম: origin-এ একটু বেশি পড়া — কিন্তু peak মাত্র ৩৪৭/সেকেন্ড, সহজেই সামলানো যায়।

ইন্টারভিউয়ার: API-কে লেখা বইতে হবে কেন? Presigned URL দিয়ে ব্রাউজারকে সরাসরি object storage-এ পাঠালেই তো হয়।

প্রার্থী: হয়, আর বড় লেখায় সেটি ভালো — API-র bandwidth বাঁচে (পর্ব ২৬-এর presigned URL দেখুন)। কিন্তু তিনটি দাম। এক, URL-টির নিজের মেয়াদ থাকে; paste মুছলেও আগে দেওয়া URL সেই মেয়াদ পর্যন্ত কাজ করে, তাই URL-এর মেয়াদ কয়েক মিনিট রাখতে হয়। দুই, পড়ার সময় মেয়াদ যাচাই তখনো API-তেই — redirect দেওয়ার আগে। তিন, একটি বাড়তি round trip. গড় ১০ KB-র লেখায় আমি API থেকেই দেব; ১ MB-র কাছাকাছি লেখায় redirect.

৩৮–৪২ · ধাপ ৭ — bottleneck, failure ও trade-off

  • অপব্যবহার: IP আর ব্যবহারকারী ধরে rate limit (পর্ব ৪৯), আকারের সীমা; ক্ষতিকর লেখা যাচাই বাইরে, কিন্তু রিপোর্টের পথ থাকবে।
  • DB-র primary মরল: failover-এর কয়েক সেকেন্ড নতুন paste বন্ধ, পড়া replica থেকে চলে।
  • হঠাৎ জনপ্রিয় paste: লেখা immutable বলে CDN-ই সামলায়।
  • Trade-off: একই লেখা একবার রাখা (content hash) storage বাঁচায়, কিন্তু মেয়াদ আর মোছার হিসাব জটিল — ফলো-আপ ০৬।

৪২–৪৫ · ধাপ ৮ — wrap-up

প্রার্থী: মাপব লেখা আর পড়ার p99, 404-এর হার, storage-এর বৃদ্ধি বনাম পূর্বাভাস, আর sweeper-এর পিছিয়ে থাকা — মেয়াদ পেরোনো কিন্তু এখনো না-মোছা সারির সংখ্যা। দশ গুণেও সংখ্যা ছোট; metadata-কে ID-র hash ধরে ভাগ করার পথ খোলা।

মূল্যায়নের চোখে

শক্তিশালী: হিসাব দেখে বোঝা যে ভিড় নয়, storage আসল প্রশ্ন; Little-এর সূত্রে স্থির অবস্থা; লেখার ক্রম আর অনাথ object; পড়ার সময় মেয়াদ যাচাই; গোপনীয়তার pushback-এ CDN-এর দাম স্বীকার। আরও ভালো হতে পারত: ৫৪-এর ID-কৌশল থেকে একটি নির্দিষ্ট পছন্দ আর তার কারণ এক বাক্যে বলা।

প্র ০৫ফলো-আপ: লেখার পথে metadata আগে না object আগে? মাঝপথে crash হলে প্রতিটি ক্রমে কী হয়? (মাঝারি)

উত্তর: Object আগে, metadata পরে। Crash object-এর পরে হলে ক্ষতি শুধু একটি অনাথ object — কেউ দেখতে পায় না, sweeper পরে মুছে দেয়। উল্টো ক্রমে metadata আছে কিন্তু লেখা নেই — লিংক ভাঙা, পাঠক error পান, ঠিক করতে আলাদা মেরামত লাগে। Client-কে 201 যায় metadata লেখার পরেই; client timeout-এ আবার পাঠালে একই idempotency key দেখে আগের ID ফেরত (পর্ব ০৮)। দুই জায়গায় লেখা মানেই একটি distributed সমস্যা; এখানে ক্রম বেছে নিয়েই ক্ষতি নিরীহ দিকে সরানো হলো।

প্র ০৬ফলো-আপ: একই লেখা বহুজন paste করলে content hash দিয়ে একবার রাখবেন? (কঠিন)

উত্তর: লাভ আছে — জনপ্রিয় config বা error log বারবার আসে। কিন্তু তখন একটি object-এর মালিক অনেক paste, প্রত্যেকের মেয়াদ আলাদা। মুছতে হবে শেষ জনের মেয়াদ গেলে — একটি reference count লাগে, আর গণনা ও মোছার মাঝে race হলে জীবিত paste-এর লেখা মুছে যেতে পারে। নিরাপদ পথ: object-এর মেয়াদ = সবচেয়ে দূরের মেয়াদ, আর মোছার আগে metadata-য় আবার যাচাই। আরেকটি সূক্ষ্ম ঝুঁকি গোপনীয়তার: “এই লেখা আগেই আছে” — সাড়ার সময় বা আচরণ থেকে তা বোঝা গেলে অন্যের গোপন লেখার অস্তিত্ব আঁচ করা যায়। ১০ KB গড়ের সেবায় এই জটিলতা সাধারণত লাভের চেয়ে বেশি — তাই শুরুতে না।

৫ · Mock ৪ — search autocomplete: prefix table, হালনাগাদের pipeline আর edge cache

রিভিশন: পর্ব ১৭ · CDN, পর্ব ২৭ · autocomplete ও trie, পর্ব ২৮ · caching, পর্ব ৩৬ · batch ও stream

ইন্টারভিউয়ার: “বাজারঘর” নামের একটি কাল্পনিক ই-কমার্স সাইটের সার্চ বক্সের autocomplete (typeahead) নকশা করুন।

০০–০৫ · ধাপ ১ — পরিধি

প্রার্থী: পরামর্শ কী — মানুষের আগের খোঁজা প্রশ্ন, নাকি সরাসরি পণ্য? প্রত্যেকের জন্য আলাদা, নাকি সবার এক? বাংলা আর Banglish দুটোই? নতুন জনপ্রিয় খোঁজ কত দ্রুত পরামর্শে আসবে?

ইন্টারভিউয়ার: সবার একই, আগের খোঁজা প্রশ্ন থেকে সেরা ৫টি। বাংলা আর Banglish দুটোই। ঈদের আগে হঠাৎ জনপ্রিয় খোঁজ এক ঘণ্টার মধ্যে আসা চাই।

প্রার্থী: FR: prefix দিলে সেরা ৫টি পরামর্শ; আপত্তিকর আর ব্যক্তিগত তথ্যের মতো লেখা বাদ। NFR: প্রতিটি অক্ষরে উত্তর — মোট প্রায় ১০০ ms-এর latency budget-এ সার্ভারের ভাগ কয়েক দশক ms; এই সেবা বসে গেলে শুধু পরামর্শ বন্ধ, খোঁজা চলবে; নতুন প্রবণতা এক ঘণ্টায়, বাকি সব দিনে একবার। বাইরে: বানান-ভুল শোধরানো (পর্ব ২৭-এর fuzzy অংশ), ব্যক্তিগত পরামর্শ, পণ্যের ফল।

মন্তব্য: “সেবা বসে গেলে কী” প্রশ্নটি NFR-এই — এখানে graceful degradation সহজ আর স্বাভাবিক।

০৫–০৯ · ধাপ ২ — হিসাব

প্রার্থী: ধরা যাক দিনে ১ কোটি সক্রিয় ব্যবহারকারী, প্রত্যেকে ৫ বার খোঁজেন, প্রতি খোঁজে debouncing-এর পরে গড়ে ৪টি অনুরোধ: দিনে ২০ কোটি, প্রায় ২,৩১৫/সেকেন্ড, সন্ধ্যার peak ৪ গুণে প্রায় ৯,২৫৯. Data: ছাঁকার পরে ধরা যাক ১ কোটি আলাদা প্রশ্ন; ১০ অক্ষর পর্যন্ত prefix রাখলে সারি বড়জোর ১০ কোটি — prefix ভাগাভাগিতে আসলে অনেক কম। সারিপ্রতি প্রায় ১০০ byte ধরলে ঊর্ধ্বসীমা ১০ GB.

মন্তব্য: “ঊর্ধ্বসীমা” শব্দটি সৎ — নিখুঁত সংখ্যা না জেনেও নকশার সিদ্ধান্ত নেওয়া যায়: memory-তে আঁটে, ভাগ করার তাড়া নেই।

০৯–১৫ · ধাপ ৩–৪ — API ও data model

GET /v1/suggest?q=ei&lang=bn
-> 200  Cache-Control: public, max-age=300
   {"q": "ei", "v": 42, "items": ["eid panjabi", "eid offer", "eid saree"]}

প্রার্থী: উত্তর cache-যোগ্য GET, তাই CDN-এ রাখা যায়; v হলো prefix table-এর সংস্করণ। Data দুই জায়গায়: query log থেকে ঘণ্টা আর দিন ধরে গণনা — (query, hour) → count; আর পরিবেশনের জন্য একটি prefix table: prefix → সেরা ৫টি (প্রশ্ন, ওজন)। পর্ব ২৭-এ দেখেছেন প্রতিটি node-এ top-k রাখা trie; prefix table একই ধারণার সমতল রূপ — key-value-র একটিই খোঁজ, ভাগ আর replica করা সহজ, কিন্তু একই লেখা বহু prefix-এ বারবার থাকে বলে memory বেশি।

১৫–২৫ · ধাপ ৫ — high-level design

Autocomplete-এর high-level design

Online: client → CDN edge → suggest servers · Offline: query log → aggregate → build snapshot → swap

Autocomplete-এর high-level design ব্রাউজার CDN edge Suggest server ×N Query log Aggregator Builder Snapshot debounce · পুরোনো বাতিল ছোট prefix · TTL ৫ মিনিট memory-তে prefix table v42 · blocklist খোঁজা প্রশ্ন ঘণ্টা/দিনে গণনা ওজন কমানো · ছাঁকা prefix → সেরা ৫ object storage · v43 load + swap
ছবি ৬৬.৪ — বাঁয়ে পরিবেশনের পথ, ডানে হালনাগাদের পথ; দুটি পথের যোগাযোগ শুধু snapshot আর log দিয়ে।

প্রার্থী: পরিবেশনের পথে কোনো ডেটাবেস নেই: প্রতিটি suggest server পুরো table memory-তে রাখে — ১০ GB-র ঊর্ধ্বসীমা আঁটে — আর একটি hash lookup-এ উত্তর দেয়। হালনাগাদ আলাদা: log থেকে গণনা, ওজন কমানো আর ছাঁকা, নতুন snapshot, তারপর সার্ভারগুলো load করে এক মুহূর্তে পুরোনোটি বদলে দেয়।

২৫–৩৮ · ধাপ ৬ — deep dive: হালনাগাদ, সময়ের ওজন আর edge

ইন্টারভিউয়ার: শুধু মোট গণনায় সাজালে কী সমস্যা?

প্রার্থী: পুরোনো জনপ্রিয় প্রশ্ন জায়গা ছাড়ে না, ঈদের মতো নতুন প্রবণতা ঢুকতেই পারে না। সমাধান time decay: প্রতিটি গণনার ওজন তার বয়স অনুযায়ী কমে — half-life ১ দিন মানে গতকালের খোঁজ আজকের অর্ধেক ওজন পায়। নিচে সাত দিনের কাল্পনিক গণনা থেকে দুইভাবে prefix table:

// Typeahead build job: 7 days of query counts -> time-decayed score -> prefix table (prefix -> top 3).
// Fictional shop search. counts[d] = searches on day d (index 6 = today). Half-life = 1 day.
const LOG = {
  "mobile cover":    [900, 880, 910, 890, 900, 870, 880],
  "mobile charger":  [700, 720, 690, 710, 700, 690, 700],
  "mango":           [300, 320, 310, 300, 330, 310, 320],
  "eid panjabi":     [ 50,  60,  80, 150, 400, 900, 1600],
  "electric kettle": [600, 610, 590, 600, 620, 600, 610],
  "earphone":        [800, 790, 810, 800, 780, 800, 790],
  "mosquito net":    [100, 120, 150, 300, 500, 700, 900],
};
const HALF_LIFE = 1, MAX_PREFIX = 10, K = 3, raw = (c) => c.reduce((a, b) => a + b, 0);
const decayed = (c) => c.reduce((a, n, d) => a + n * 0.5 ** ((6 - d) / HALF_LIFE), 0);
function build(scoreOf) {                            // offline: every prefix gets its own top-K list
  const table = new Map();
  for (const [q, c] of Object.entries(LOG))
    for (let L = 1; L <= Math.min(q.length, MAX_PREFIX); L++) {
      const p = q.slice(0, L);
      table.set(p, [...(table.get(p) || []), [q, Math.round(scoreOf(c))]]);
    }
  for (const list of table.values()) { list.sort((a, b) => b[1] - a[1]); list.length = Math.min(list.length, K); }
  return table;
}
const show = (t, p) => t.get(p).map(([q, s]) => q + " (" + s + ")").join(" | ");
const byRaw = build(raw), byDecay = build(decayed);
for (const p of ["m", "e"]) {
  console.log(`"${p}" raw   -> ${show(byRaw, p)}`);
  console.log(`"${p}" decay -> ${show(byDecay, p)}`);
}
console.log("prefix rows stored: " + byDecay.size + " (one lookup per keystroke)");

আউটপুট:

"m" raw   -> mobile cover (6230) | mobile charger (4910) | mosquito net (2770)
"m" decay -> mobile cover (1750) | mosquito net (1427) | mobile charger (1385)
"e" raw   -> earphone (5570) | electric kettle (4230) | eid panjabi (3240)
"e" decay -> eid panjabi (2176) | earphone (1573) | electric kettle (1205)
prefix rows stored: 50 (one lookup per keystroke)

মোট গণনায় “eid panjabi” তৃতীয়, কারণ সপ্তাহের শুরুতে খোঁজ কম ছিল। ওজন কমালে আজকের ১,৬০০ পুরো ওজন পায়, গতকালের ৯০০ অর্ধেক, তার আগের ৪০০ এক-চতুর্থাংশ — মোট প্রায় ২,১৭৬, তাই প্রথম। “mosquito net”-ও বাড়ছে, “mobile charger”-কে টপকাল। ৭টি প্রশ্নে prefix-এর সারি ৫০টি; প্রতিটি অক্ষরে একটিই খোঁজ। আউটপুট ছোট রাখতে K = 3; আসল সেবায় ৫. HALF_LIFE ৭ করে দেখুন — প্রবণতা কত ধীরে ওঠে।

ইন্টারভিউয়ার: “এক ঘণ্টার মধ্যে প্রবণতা” — দৈনিক batch দিয়ে হবে?

প্রার্থী: দুই স্তরে (পর্ব ৩৬)। দিনে একবার batch পুরো table বানায়। পাশে একটি stream কাজ গত এক ঘণ্টার গণনা থেকে ছোট একটি “trending” তালিকা বানায় — কয়েক হাজার প্রশ্ন, প্রতি ১০ মিনিটে। Suggest server উত্তর দেওয়ার সময় দুটি তালিকা মিশিয়ে সেরা ৫টি নেয়। এতে পুরো table ঘণ্টায় ঘণ্টায় বানাতে হয় না।

ইন্টারভিউয়ার: ঘটনাচক্রে একটি আপত্তিকর বা ক্ষতিকর প্রশ্ন হঠাৎ trending. এখন?

প্রার্থী: Builder-এ ছাঁকনি থাকে, কিন্তু নতুন শব্দ সে চেনে না। তাই পরিবেশনের সময় একটি blocklist — কয়েক মিনিটে সব সার্ভারে ছড়ায়, snapshot-এর অপেক্ষা ছাড়াই। খুব কম মানুষের খোঁজা প্রশ্ন কখনো পরামর্শে নয়, যাতে একজনের ব্যক্তিগত খোঁজ অন্যের পর্দায় না আসে।

ইন্টারভিউয়ার: প্রতিটি অক্ষরে একটি অনুরোধ — অপচয় নয়?

প্রার্থী: চারটি কৌশলে অনেকটা কমে। Debounce — ধরা যাক ১৫০ ms না থামলে অনুরোধ নয়। পুরোনো অনুরোধ বাতিল — “ei”-র উত্তর “eid” লেখার পরে এলে আর দেখানো হয় না। ব্রাউজারে আগের উত্তর মনে রাখা — backspace চাপলে নতুন অনুরোধ লাগে না। আর একটি সূক্ষ্ম কৌশল: “ei”-র উত্তরে ৫টির কম পরামর্শ এলে “eid”-এর উত্তর তার ভেতরেই আছে — ব্রাউজার নিজেই ছেঁকে নেয়। ধাপ ২-এর “প্রতি খোঁজে ৪টি অনুরোধ” এই কৌশলগুলো ধরেই।

প্রার্থী (edge): এক-দুই অক্ষরের prefix হাতে গোনা, কিন্তু অনুরোধের বড় অংশ সেখানেই, কারণ প্রত্যেকে প্রথম অক্ষর দিয়েই শুরু করেন। তাই এদের উত্তর CDN-এ ৫ মিনিটের TTL-এ; cache key-তে prefix, ভাষা আর table-এর সংস্করণ। লম্বা prefix সার্ভারেই যায়।

৩৮–৪২ · ধাপ ৭ — bottleneck, failure ও trade-off

  • খারাপ snapshot — খালি বা অর্ধেক: বদলের আগে যাচাই (আকার, কয়েকটি পরিচিত prefix-এর উত্তর), আগের সংস্করণ রেখে দেওয়া, দরকারে ফেরত।
  • বদলের সময় memory দ্বিগুণ: পুরোনো আর নতুন table একসাথে; ১০ GB-র table-এ ক্ষণিকের জন্য ২০ GB. Table বড় হলে prefix-এর hash ধরে ভাগ — প্রথম অক্ষর ধরে নয়, তাতে “m”-এর shard গরম।
  • Suggest server বসে গেলে: বক্সে কিছু দেখায় না, খোঁজা চলে — graceful degradation.
  • Trade-off: তাজা বনাম স্থির; trie-র কম memory বনাম table-এর সরলতা; edge-এর TTL বনাম blocklist কত দ্রুত কার্যকর হয় — blocklist-এর জন্য CDN purge লাগতে পারে।

৪২–৪৫ · ধাপ ৮ — wrap-up

প্রার্থী: মাপব সার্ভারের p99, edge-এর hit ratio, snapshot-এর বয়স, পরামর্শ বেছে নেওয়ার হার, আর blocklist কতবার কাজে লাগল। দশ গুণে: prefix-এর hash-এ ভাগ আর অঞ্চলভিত্তিক replica.

মূল্যায়নের চোখে

শক্তিশালী: পরিবেশন আর হালনাগাদ — দুটি পথ আলাদা; time decay সংখ্যাসহ; batch আর stream-এর মিশ্রণ; ক্ষতিকর প্রবণতার প্রতিরক্ষা; ২৭-এর trie-র সাথে সৎ তুলনা। আরও ভালো হতে পারত: বাংলা লেখার normalization (একই শব্দের ভিন্ন Unicode রূপ) — পর্ব ২৭-এর একটি বাক্য টেনে আনা যেত।

প্র ০৭ফলো-আপ: trie নাকি prefix table — কখন কোনটি বেছে নেবেন? (মাঝারি)

উত্তর: Trie একই শুরু একবারই রাখে, তাই memory কম; প্রতিটি node-এ top-k রাখলে খোঁজ prefix-এর দৈর্ঘ্যমাত্র (পর্ব ২৭)। কিন্তু এটি একটি নিজস্ব data structure — নিজের সার্ভারে লিখতে, serialise করতে আর ভাগ করতে হয়। Prefix table যেকোনো key-value store বা hash map-এ বসে, একটিই lookup, ভাগ করা সহজ, কিন্তু প্রতিটি prefix-এর তালিকা আলাদা বলে লেখা বারবার থাকে। মোটা দাগে: table memory-তে স্বচ্ছন্দে আঁটলে আর দল ছোট হলে prefix table; প্রশ্ন বহুগুণ, memory টানাটানি হলে trie বা সংকুচিত automaton — যেমন Lucene-এর FST.

প্র ০৮ফলো-আপ: প্রত্যেকের নিজের সাম্প্রতিক খোঁজ পরামর্শে মেশাতে হলে? (মাঝারি)

উত্তর: Global table বদলানোর দরকার নেই। প্রতিজনের শেষ ২০–৫০টি খোঁজ একটি ছোট key-value store-এ (TTL-সহ), আর পরিবেশনের সময় মেলানো: global সেরা ৫ + ব্যবহারকারীর যে খোঁজগুলো prefix দিয়ে শুরু — ওজন দিয়ে মিশিয়ে সেরা ৫. দাম দুটি: CDN-এ আর cache করা যায় না (উত্তর ব্যক্তিভেদে আলাদা), তাই সার্ভারে চাপ বাড়ে; আর গোপনীয়তা — এই তথ্য মুছে ফেলার সুযোগ দিতে হয়, শেয়ার করা যন্ত্রে অন্যের খোঁজ দেখানো চলবে না। একটি মাঝামাঝি পথ: ব্যক্তিগত অংশ ব্রাউজারেই রাখা, সার্ভার শুধু global অংশ দেয়।

৬ · Mock ৫ — distributed job scheduler: টেকসই সূচি, lease আর fencing

রিভিশন: পর্ব ২৫ · outbox, পর্ব ৩৪ · at-least-once ও idempotency, পর্ব ৩৭ · job, cron ও lease, পর্ব ৪৩ · lease ও fencing token

ইন্টারভিউয়ার: একটি কাল্পনিক SaaS প্ল্যাটফর্মের “সময়সূচি” সেবা নকশা করুন — গ্রাহকেরা নির্দিষ্ট সময়ে বা নিয়মিত বিরতিতে কাজ চালাতে চান।

০০–০৫ · ধাপ ১ — পরিধি

প্রার্থী: একবারের কাজ, নাকি cron-এর মতো নিয়মিত — নাকি দুটোই? “চালানো” মানে কী — আমরা কোড চালাব, নাকি গ্রাহকের সারিতে একটি বার্তা পাঠাব? সময়ের নির্ভুলতা কত? আর কাজ দুবার চললে চলবে?

ইন্টারভিউয়ার: দুটোই। চালানো মানে গ্রাহকের সারিতে বার্তা — কোড গ্রাহকের। বেশিরভাগ কাজ নির্ধারিত সময়ের ১–২ সেকেন্ডের মধ্যে চাই। মোট ১ কোটি কাজ। দুবার চলা — আপনিই বলুন।

প্রার্থী: বলব at-least-once: কোনো run বাদ পড়বে না, কিন্তু বিরল ক্ষেত্রে দুবার যেতে পারে — প্রতিটি বার্তায় একটি অনন্য run key থাকবে, যাতে গ্রাহকের কাজ idempotent হতে পারে। “ঠিক একবার” প্রতিশ্রুতি বাইরের side effect-এ দেওয়া যায় না (পর্ব ৩৪)। FR: কাজ বানানো, বদলানো, থামানো, run-এর ইতিহাস। NFR: টেকসই — কিছুই বাদ নয়; সময়মতো — p99 দেরি ২ সেকেন্ডের কাছাকাছি; কোনো single point of failure নয়; এক গ্রাহকের ভিড়ে অন্যের ক্ষতি নয়। বাইরে: বহু ধাপের workflow — সেটি পর্ব ৩৭-এর workflow engine-এর কাজ।

মন্তব্য: ইন্টারভিউয়ার সিদ্ধান্ত প্রার্থীর হাতে দিলেন — প্রার্থী সংজ্ঞা, কারণ আর দায়ের ভাগ (run key) একসাথে দিলেন।

০৫–০৯ · ধাপ ২ — হিসাব

প্রার্থী: ধরা যাক কাজগুলো গড়ে ঘণ্টায় একবার: ১ কোটি ÷ ৩,৬০০ ≈ ২,৭৭৮ run/সেকেন্ড। কিন্তু মানুষ গোল সময় ভালোবাসেন — ধরা যাক ২০% ঠিক ঘণ্টার শুরুতে: একই সেকেন্ডে ২০ লাখ। পাঠানোর ক্ষমতা সেকেন্ডে ২০,০০০ হলে সারি খালি হতে ১০০ সেকেন্ড — শেষেরগুলো ১০০ সেকেন্ড দেরি। দিনে run ২৪ কোটি; ইতিহাসের সারিপ্রতি ২০০ byte-এ দিনে ৪৮ GB, ৩০ দিন রাখলে প্রায় ১.৪৪ TB. কাজের টেবিল নিজে ছোট: ৫০০ byte করে প্রায় ৫ GB.

মন্তব্য: গড় নয়, ঘণ্টার শুরুর ঢেউই আসল সমস্যা — পর্ব ৩৭-এর cron-এর thundering herd এখানে বড় আকারে।

০৯–১৫ · ধাপ ৩–৪ — API ও data model

POST /v1/jobs   Idempotency-Key: 1b9e...
     {"schedule": "0 * * * *", "window_s": 300,
      "target": {"queue": "t42.reports"}, "payload": {"report": "hourly"}}
  -> 201 {"job_id": "j-7f3a", "next_run_at": "2026-10-28T11:00:00Z"}
GET  /v1/jobs/j-7f3a/runs?limit=20

প্রার্থী: window_s গ্রাহককে বলতে দেয়, “ঘণ্টার শুরুর ৫ মিনিটের মধ্যে যেকোনো সময় চলবে” — তখন job_id-এর hash দিয়ে ছড়াই: ২০ লাখ ÷ ৩০০ ≈ ৬,৬৬৭/সেকেন্ড, ক্ষমতার অনেক নিচে। টেবিল তিনটি: jobs(job_id, tenant, schedule, next_run_at, partition, paused) — partition = hash(job_id) mod ২৫৬, index (partition, next_run_at); runs(run_key UNIQUE, epoch, status) — run_key = j-7f3a@2026-10-28T11:00Z; আর partition-এর মালিকানা একটি coordination service-এ (etcd-এর মতো): partition → মালিক, lease, epoch.

১৫–২৫ · ধাপ ৫ — high-level design

Distributed job scheduler-এর high-level design

Durable schedule store · partition leases with epochs · fire = run row + outbox in one transaction

Distributed job scheduler-এর high-level design Job API Schedule store Coordination Node A Node B Node C Run টেবিল + outbox · এক transaction কাজ বানানো · বদলানো ২৫৬ partition · next_run_at partition → মালিক lease · epoch (etcd-এর মতো) p0–p85 · heap p86–p170 p171–p255 run_key UNIQUE · epoch যাচাই গ্রাহকের সারি worker · idempotent
ছবি ৬৬.৫ — প্রতিটি partition-এর একজন মালিক, lease দিয়ে; fire মানে একটি transaction-এ run-এর সারি, পরের সময় আর outbox-এর বার্তা।

প্রার্থী: প্রতিটি scheduler node নিজের partition-গুলোর পরের ৩০ সেকেন্ডের কাজ index থেকে পড়ে memory-র একটি heap-এ রাখে (পর্ব ৩৭-এর timer দেখুন)। সময় হলে একটি transaction-এ তিনটি কাজ: run-এর সারি ঢোকানো (unique run_key), next_run_at এগোনো, আর outbox-এ বার্তা (পর্ব ২৫) — একটি relay পরে সারিতে পাঠায়। সাথে একটি শর্ত: এই partition-এর epoch এখনো আমার কি না।

২৫–৩৮ · ধাপ ৬ — deep dive: মালিকানা, থেমে থাকা node আর fencing

ইন্টারভিউয়ার: একজন leader সব চালালেই তো সহজ?

প্রার্থী: সহজ, আর সেকেন্ডে কয়েক হাজার run-এ হয়তো যথেষ্ট। কিন্তু ঘণ্টার শুরুর ঢেউ একটি মেশিনেই আসে, আর leader বদলের সময় সব কাজ থেমে যায়। Partition ভাগ করলে প্রতিটি node একটি অংশ সামলায়, আর একটি node গেলে শুধু তার অংশ কয়েক সেকেন্ড পিছায়। বিপদ সেই একই — থেমে থাকা node, যে জানে না যে তার lease গেছে। নিচের simulator-এ ৬টি partition, তিনটি node, আর B ১৮ সেকেন্ডের একটি GC বিরতিতে:

// Partitioned scheduler: 6 partitions owned by nodes A, B, C through leases (TTL 10 s, renewed every
// 3 s). Node B freezes (long GC pause) for t = 7..24. Each partition has one job due every 4 s.
function sim(fencing) {
  const P = 6, TTL = 10, nodes = ["A", "B", "C"], frozen = (n, t) => n === "B" && t >= 7 && t < 25;
  const lease = [], local = { A: {}, B: {}, C: {} }, st = { fenced: 0, dup: 0, late: 0 };
  const store = { epoch: Array(P).fill(1), next: Array(P).fill(0), runs: {} };  // shared, durable
  for (let p = 0; p < P; p++) { lease.push({ owner: nodes[p % 3], epoch: 1, until: TTL });
    local[nodes[p % 3]][p] = { epoch: 1, next: 0 }; }
  for (let t = 0; t <= 40; t++) for (const n of nodes) {
    if (frozen(n, t)) continue;
    lease.forEach((l, p) => {
      if (l.owner === n && t % 3 === 0) l.until = t + TTL;                       // renew own lease
      if (l.owner !== n && t >= l.until) {                                       // take over expired
        Object.assign(l, { owner: n, epoch: l.epoch + 1, until: t + TTL });
        local[n][p] = { epoch: l.epoch, next: store.next[p] }; }
    });
    for (const p in local[n]) { const me = local[n][p];  // local belief {epoch, next}: fire what is due
      while (me.next <= t) {
        if (fencing && me.epoch < store.epoch[p]) { st.fenced++; delete local[n][p]; break; }
        store.epoch[p] = Math.max(store.epoch[p], me.epoch);
        const key = p + "@" + me.next;
        if (store.runs[key]) st.dup++;                     // run key already taken
        else { store.runs[key] = n; st.late = Math.max(st.late, t - me.next); }
        me.next += 4; store.next[p] = Math.max(store.next[p], me.next);
      } }
  }
  console.log((fencing ? "fencing ON " : "fencing OFF") + " | runs " + Object.keys(store.runs).length +
    " | stale rejected " + st.fenced + " | repeats caught by run key " + st.dup + " | worst late " + st.late);
}
sim(false); sim(true);

আউটপুট:

fencing OFF | runs 66 | stale rejected 0 | repeats caught by run key 18 | worst late 8
fencing ON  | runs 66 | stale rejected 2 | repeats caught by run key 0 | worst late 8

দুই ক্ষেত্রেই ৬৬টি run (৬ partition × ০ থেকে ৪০ পর্যন্ত ১১টি সময়) — কোনোটি বাদ পড়েনি। Fencing ছাড়া: B জেগে উঠে নিজের পুরোনো তালিকা থেকে ৮, ১২, ১৬, ২০, ২৪ আবার চালাতে চায়, তারপর ২৮ থেকে ৪০ পর্যন্ত A-র পাশাপাশি — দুই partition মিলিয়ে ১৮টি পুনরাবৃত্তি; শুধু run_key-এর unique শর্ত সেগুলো আটকাল। বার্তা পাঠানো যদি সেই শর্তের বাইরে হতো, ১৮টি কাজ দুবার চলত। Fencing-সহ: B-র প্রথম চেষ্টাতেই store দেখল epoch ১ < ২ — প্রত্যাখ্যান, B partition ছেড়ে দিল; দুটি partition-এ দুটি প্রত্যাখ্যান, ব্যস। সবচেয়ে বেশি দেরি ৮ সেকেন্ড: t = ৮-এর run চলেছে t = ১৬-এ, যখন B-র lease ফুরাল (শেষ নবায়ন t = ৬, TTL ১০)।

Partition ১ · lease TTL ১০ s · নবায়ন প্রতি ৩ s · কাজ প্রতি ৪ s
ঘড়ি
Lease-এর মালিক · epoch
B যা ভাবে
Store-এ epoch
ঘটনা
  1. t = ৬: সব ঠিক। Partition ১-এর মালিক B, epoch ১. B নবায়ন করল, lease-এর মেয়াদ এখন t = ১৬ পর্যন্ত। Run ০ আর ৪ B-ই চালিয়েছে।
  2. t = ৭ থেকে: B থেমে গেল। দীর্ঘ GC বিরতি — process বেঁচে, কিন্তু কিছুই চলছে না, নবায়নও না। Run ৮ আর ১২-এর সময় পেরোল, কেউ চালাল না; lease তখনো B-র নামে, তাই অন্য কেউ নিতে পারে না।
  3. t = ১৬: lease ফুরাল, A নিল। Coordination service A-কে দিল epoch ২. A store থেকে partition-এর cursor পড়ল — পরের run ৮.
  4. t = ১৬: বকেয়া run. Misfire নীতি “সব চালানো” (পর্ব ৩৭): A চালাল ৮, ১২ আর ১৬ — দেরি ৮, ৪ আর ০ সেকেন্ড। এটাই simulator-এর “worst late 8”. Store-এ এখন epoch ২.
  5. t = ২৫: B জেগে উঠল। সে জানে না সময় পেরিয়েছে; তার memory-তে লেখা “মালিক আমি, epoch ১, পরের run ৮”। চালাতে গেলে transaction-এর শর্ত দেখে store-এ epoch ২ — ১ < ২, প্রত্যাখ্যান। B বুঝল lease গেছে, partition ছেড়ে দিল। এটাই fencing token-এর কাজ (পর্ব ৪৩)।
  6. t = ২৮: স্বাভাবিক। A run ২৮ চালাল সময়মতো। কোনো run বাদ নেই, কোনোটি দুবার নয়; দাম শুধু ৮ সেকেন্ডের দেরি, যা lease-এর TTL থেকেই আসে।

ইন্টারভিউয়ার: ৮ সেকেন্ডের দেরি তো আপনার ২ সেকেন্ডের লক্ষ্যের বাইরে। TTL ১ সেকেন্ড করবেন?

প্রার্থী: TTL ছোট করলে failover দ্রুত, কিন্তু ছোট network সমস্যা বা ছোট GC বিরতিতেই মালিক বদল — বারবার মালিক বদলে নিজেই অস্থিরতা (পর্ব ৪৩-এর TTL-এর trade-off দেখুন)। আমি বলব: স্বাভাবিক সময়ে p99 দেরি ২ সেকেন্ডের কাছে, আর node ব্যর্থ হলে ওই partition-এর কাজে TTL-এর সমান দেরি — SLO-তে এটি আলাদা করে লেখা থাকবে। TTL ধরা যাক ৫ সেকেন্ড, নবায়ন প্রতি ১–২ সেকেন্ডে।

মন্তব্য: সংখ্যার দ্বন্দ্ব স্বীকার করে SLO-র ভাষায় সমাধান — এটি পরিণত উত্তর।

৩৮–৪২ · ধাপ ৭ — bottleneck, failure ও trade-off

  • Schedule store বন্ধ: কোনো run চলে না, কিন্তু কিছু হারায়ও না; ফিরলে misfire নীতি মেনে বকেয়া — গ্রাহকভেদে “সব চালানো” বা “শুধু শেষটি”।
  • ঘড়ির অমিল: lease-এর মেয়াদ coordination service-এর হিসাবে; node নিজের ঘড়িতে নয়, ডেটাবেসের সময়ে “কোন কাজ পাকা” দেখে।
  • এক গ্রাহকের ঢেউ: গ্রাহক-প্রতি সীমা আর আলাদা সারি — noisy neighbour ঠেকাতে।
  • Worker crash: সারি আবার দেয়; run_key-এ গ্রাহকের কাজ idempotent. বারবার ব্যর্থ হলে dead letter queue (পর্ব ৩১)।

৪২–৪৫ · ধাপ ৮ — wrap-up

প্রার্থী: মাপব দেরির p50 আর p99 (চলার সময় − নির্ধারিত সময়), বাদ পড়া run — লক্ষ্য শূন্য — run_key-এ আটকানো পুনরাবৃত্তি, মালিক বদলের সংখ্যা, outbox-এর lag. দশ গুণে: partition ২৫৬-ই থাকে, শুধু node বাড়ে — partition-এর সংখ্যা আগেই বড় রাখার সুবিধা।

মূল্যায়নের চোখে

শক্তিশালী: at-least-once-এর সংজ্ঞা আর দায়ের ভাগ; ঘণ্টার শুরুর ঢেউয়ের হিসাব আর window_s; fire-কে একটি transaction-এ বাঁধা; fencing আর run_key — দুই স্তরের পাহারা; TTL-এর দ্বন্দ্ব SLO-তে। আরও ভালো হতে পারত: কাজ বদলানো বা মোছার সাথে memory-র heap কীভাবে হালনাগাদ হয় (version নম্বর), তা বলা হয়নি।

প্র ০৯ফলো-আপ: run_key-এর unique শর্ত থাকলে fencing-এর দরকার কী? (কঠিন)

উত্তর: Run_key আটকায় একই run-এর দ্বিতীয় সারি — কিন্তু শুধু সেই লেখাটিকে। থেমে থাকা পুরোনো মালিক আরও অনেক কিছু লিখতে পারে: next_run_at পেছনে সরিয়ে দেওয়া, কাজের অবস্থা বদলানো, বা unique শর্তের বাইরে কোনো side effect. Fencing পুরো partition-এর সব লেখাকে একটি প্রশ্নে বাঁধে: “লেখক কি এখনো বৈধ মালিক?” — epoch যাচাই transaction-এর শর্তে। আবার শুধু fencing-ও যথেষ্ট নয়: নতুন মালিক নিজেই crash করে একই run আবার চালাতে পারে — তখন run_key বাঁচায়। তাই দুটি স্তর একে অপরের পরিপূরক।

প্র ১০ফলো-আপ: গ্রাহকের কাজ idempotent নয় — যেমন প্রতি run-এ একটি SMS. তখন কী? (মাঝারি)

উত্তর: Scheduler-এর at-least-once বদলায় না; idempotency আনতে হয় কাজের দিকে। প্রতিটি বার্তায় run_key আসে — worker SMS পাঠানোর আগে নিজের ডেটাবেসে run_key-এর unique সারি ঢোকায়; আগেই থাকলে কিছু না করে ack (idempotent consumer নামে পরিচিত ধাঁচ)। তবু “সারি ঢোকানো” আর “SMS যাওয়া”-র মাঝে crash হলে ফাঁক থাকে; SMS সেবা নিজে idempotency key নিলে সেটিই সবচেয়ে মজবুত (পর্ব ৩৪)। দুবার যাওয়া একেবারেই চলবে না এমন কাজে at-most-once বেছে নেওয়া যায় — ack আগে, কাজ পরে — দাম: মাঝে মাঝে একটি run হারাতে পারে। কোনটি সহনীয়, তা ব্যবসার সিদ্ধান্ত।

৭ · Pushback সামলানো আর কী দেখে মূল্যায়ন হতে পারে — একটি সাধারণ ছক

পাঁচটি walkthrough-এই ইন্টারভিউয়ার আপত্তি তুলেছেন — “সাইট ছোট, সময়ে হবে?”, “ZINCRBY কেন নয়?”, “CDN-এ তো কপি রয়ে গেল”, “৮ সেকেন্ড তো লক্ষ্যের বাইরে”। এগুলো আক্রমণ নয়; আপনার যুক্তি চাপের মুখে কেমন দাঁড়ায়, তা দেখার উপায়। প্রায় সব আপত্তি ছয় ধরনের একটি:

আপত্তির ধরনযেমনকীভাবে সামলাবেন
“এটা কি বাড়াবাড়ি?”৫ GB-র board ভাগ কেন?বাক্সের পেছনের requirement বা সংখ্যা বলুন; না থাকলে সরল রূপটি মেনে নিন
“দশ গুণ হলে?”১০ গুণ pasteপ্রথমে কোথায় ভাঙবে, সংখ্যা দিয়ে; তারপর একটিই বদল
“এটি ভাঙলে?”Suggest server বসে গেলকীভাবে টের পাবেন, ব্যবহারকারী কী দেখবেন, কীভাবে সারবে
“অনুমান বদলান”১ req/s-এ ৫০ লাখ পাতামুখে আবার হিসাব; যে সিদ্ধান্তগুলো সেই সংখ্যার ওপর দাঁড়িয়ে, সেগুলো হালনাগাদ
“X কেন, Y নয়?”Trie না table?Trade-off-এর নাম, আর কোন অবস্থায় Y জিতত
যা জানেন নাকোনো পণ্যের নির্দিষ্ট সীমা“নিশ্চিত নই” বলুন, মূল নীতি থেকে যুক্তি দিন, কীভাবে যাচাই করবেন বলুন

দুটি ভুল প্রতিক্রিয়া: সাথে সাথে নিজের নকশা ছেড়ে দেওয়া (“ঠিক বলেছেন, সব বদলে দিই”), আর জেদ ধরে থাকা। মাঝের পথ: “ভালো প্রশ্ন — এই সংখ্যায় আমার পছন্দ টেকে, কারণ…; তবে যদি … হয়, তাহলে আমি … বদলাতাম।”

কী দেখে মূল্যায়ন হতে পারে

নিচের ছকটি সাধারণ — কোনো প্রতিষ্ঠানের আসল মূল্যায়ন-পদ্ধতি নয়। প্রতিষ্ঠান আর পদভেদে জোর আলাদা হয়; অভিজ্ঞ পদের জন্য সাধারণত trade-off, failure আর আলোচনা নিজে চালানোর ওপর বেশি জোর থাকে। নিজের মহড়ার পরে প্রতিটি সারিতে ১ থেকে ৪ নম্বর দিন।

মাপকাঠিদুর্বল সংকেতশক্তিশালী সংকেত
পরিধি ও প্রশ্নপ্রশ্ন ছাড়াই বাক্স আঁকা শুরু৩–৫টি ধারালো প্রশ্ন, স্পষ্ট “বাইরে রাখা”
হিসাবসংখ্যা নেই, বা একক গুলিয়ে ফেলামুখে বলা অনুমান, মাত্রা ঠিক, সংখ্যা থেকে সিদ্ধান্ত
API ও data modelপুরো schema লিখতে ১৫ মিনিটমূল endpoint, key আর access pattern — ৬ মিনিটে
High-level designকারণহীন বাক্স, প্রযুক্তির নামের তালিকাপ্রতিটি বাক্সের পেছনে কারণ, একটি অনুরোধের পুরো পথ
Deep diveসব জায়গায় অল্প অল্পসবচেয়ে কঠিন এক-দুটি অংশে গভীরতা, সংখ্যা বা simulation-সহ
Failure ও trade-offশুধু সুবিধাকী ভাঙে, কী দাম দিলাম, কোন অবস্থায় সিদ্ধান্ত বদলাত
যোগাযোগ ও সময়চুপচাপ আঁকা, বা ২০ মিনিট একটি বাক্সেমুখে ভাবা, মাঝে মাঝে সারাংশ, আপত্তি শান্তভাবে নেওয়া, ঘড়ি মেনে চলা

৮ · AI-এর যুগে — AI-কে mock interviewer বানানো, যাচাইসহ

একা অনুশীলনে AI ভালো সঙ্গী — প্রশ্ন করতে, আপত্তি তুলতে, আপনার হিসাব যাচাই করতে পারে। কিন্তু নিজে নকশা চাইলে AI প্রায়ই চেনা ফাঁদে পড়ে, আর ইন্টারভিউর ঘরে AI থাকবে না। তাই AI-কে উত্তরদাতা নয়, প্রশ্নকর্তা আর পরীক্ষক বানান।

AI প্রায়ই যা করেএই পাতার উদাহরণেযাচাইয়ের উপায়
সব প্রশ্নে “Kafka, Redis আর microservices”দিনে ১০ লাখ paste-এ message queue আর পাঁচটি সার্ভিসপ্রতিটি বাক্সের পেছনের সংখ্যা জিজ্ঞেস করুন
হিসাবে ভুলমাস আর বছর গুলিয়ে ১২ গুণ ভুল, MB আর Mbit গুলিয়ে ৮ গুণ ভুলনিজে বা স্ক্রিপ্টে আবার হিসাব, একক মিলিয়ে
Failure আর consistency বাদscheduler-এ থেমে থাকা node বা crawler-এ robots.txt-এর কথা নেইপ্রতিটি অংশ বন্ধ ধরে failure-mode review
ACID আর CAP-এর consistency মেশানো“leaderboard eventually consistent, তাই ACID নেই”ACID-এর C নিয়ম রক্ষা, CAP-এর C সব কপিতে সর্বশেষ মান (পর্ব ৪০)
পণ্যের সীমা বা দাম বানানোsorted set-এর সেকেন্ডপ্রতি অপারেশন, lifecycle rule কত দ্রুত চলেঅফিশিয়াল documentation, তারিখসহ; সম্ভব হলে ছোট load test
Over-engineering২ লাখ খেলোয়াড়ের board-এ তিন অঞ্চলে replicaআজকের সংখ্যায় সবচেয়ে সরল নকশা আগে

নিচের prompt AI-কে কড়া, কিন্তু ন্যায্য ইন্টারভিউয়ার বানায় — সে উত্তর বলে দেবে না, আপত্তি তুলবে, আর শেষে অংশ ৭-এর ছকে নম্বর দেবে:

তুমি একজন অভিজ্ঞ system design ইন্টারভিউয়ার। আমার সাথে ৪৫ মিনিটের একটি mock interview নাও। বিষয়: [যেমন: একটি কাল্পনিক অনলাইন নিলামের সেবা]। নিয়ম: ১. নিজে নকশা বলে দিও না। আমি প্রশ্ন করলে শুধু requirement বা সংখ্যা দাও। ২. আট ধাপে চলো (পরিধি, হিসাব, API ও data model, high-level design, deep dive, failure ও trade-off, wrap-up); প্রতিটি ধাপের সময় শেষ হলে আমাকে মনে করিয়ে দাও। ৩. প্রতিটি ধাপে অন্তত একটি pushback দাও: "দশ গুণ হলে?", "এই অংশ ভাঙলে?", "এটা কি বাড়াবাড়ি?" ৪. আমার প্রতিটি হিসাব ধাপে ধাপে আবার করো, একক মিলিয়ে; ভুল হলে কোথায়, তা দেখাও। ৫. কোনো পণ্যের সীমা বা দাম বললে উৎস দাও; নিশ্চিত না হলে স্পষ্ট লেখো "নিশ্চিত নই"। ৬. শেষে সাতটি মাপকাঠিতে (পরিধি, হিসাব, API ও data model, high-level design, deep dive, failure ও trade-off, যোগাযোগ ও সময়) ১–৪ নম্বর দাও, প্রতিটির কারণসহ, আর পরের মহড়ার জন্য দুটি পরামর্শ।

AI-এর সাথে মহড়ার পরে — চেকলিস্ট

৯ · নিজে অনুশীলন আর কোর্সের সমাপ্তি

কীভাবে অনুশীলন করবেন

  • একা: ঘড়ি চালু করে ফাঁকা কাগজে, জোরে কথা বলে — নিজের গলা রেকর্ড করুন। শেষে রেকর্ড শুনে অংশ ৭-এর ছকে নম্বর দিন। “চুপচাপ আঁকা” অংশগুলো রেকর্ডেই ধরা পড়ে।
  • জোড়ায়: একজন বন্ধু ইন্টারভিউয়ার, হাতে এই পাতার walkthrough আর pushback-এর তালিকা; পরের দিন ভূমিকা বদল।
  • আবার একই প্রশ্ন: এক সপ্তাহ পরে একই নকশা আবার — এবার অন্য একটি অংশে deep dive.

নিজে অনুশীলনের ১০টি প্রশ্ন

সবগুলো কাল্পনিক বা সাধারণ; প্রতিটি ৪৫ মিনিটে, আট ধাপে করুন।

  1. একটি লাইভ ক্রিকেট স্কোর আর ধারাভাষ্যের সেবা
  2. প্রোগ্রামিং প্রতিযোগিতার জন্য একটি online code judge
  3. বিজ্ঞাপনের click গণনা আর aggregation সেবা
  4. Metric সংগ্রহ আর alerting-এর একটি সেবা
  5. একসাথে কয়েকজনের সম্পাদনার document editor
  6. শেষ মুহূর্তের ভিড়সহ একটি অনলাইন নিলাম
  7. ফাইল sync আর শেয়ারের সেবা
  8. কাছের ফার্মেসি বা দোকান খোঁজার সেবা
  9. একটি ই-কমার্স সাইটের দাম কমার সতর্কবার্তা
  10. নিজের একটি distributed message queue

কোর্সের সমাপ্তি — এখন আপনি যা পারেন

অভিনন্দন — ৬৬টি পর্বের শেষ পাতায় পৌঁছেছেন। লক্ষ করুন, পাঁচটি নকশাতেই পুরো কোর্স ফিরে ফিরে এসেছে: হিসাব আর latency (মডিউল ১), API (২), sharding আর consistent hashing (৩), storage আর object store (৪), cache আর CDN (৫), log আর at-least-once (৬), lease, fencing আর Bloom filter (৭), rate limit আর observability (৮), আর case study-র অভ্যাস (৯)। এখন আপনি:

অনুশীলন চালিয়ে যেতে: সপ্তাহে একটি mock — ওপরের ১০টি প্রশ্ন দিয়ে শুরু; ধারণার প্রশ্নের জন্য পর্ব ৬৫, হিসাবের জন্য পর্ব ৬২, দ্রুত রিভিশনে পর্ব ৬৩ আর লিখিত-মৌখিকের জন্য পর্ব ৬৪. প্রকাশিত engineering blog আর postmortem পড়ুন — কোন সিদ্ধান্ত কেন, আর কী ভেঙেছিল। সার্টিফিকেট: ড্যাশবোর্ড থেকে মডিউল টেস্ট আর কোর্সের সমাপনী পরীক্ষা দিন; শর্ত পূরণ হলে সেখান থেকেই কোর্সের সার্টিফিকেট পাবেন।

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

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

ক্রমএকটি ভদ্র web crawler-এ একটি URL-এর যাত্রা — সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Frontier থেকে অগ্রাধিকার ধরে URL বের হয়ে তার host-এর সারিতে যায়
  2. robots.txt-এর cache দেখে নিশ্চিত হওয়া যে পথটি নিষিদ্ধ নয়
  3. Host-এর পরের অনুমতির সময় পর্যন্ত অপেক্ষা, তারপর পাতা নামানো
  4. লেখার hash আর SimHash দিয়ে দেখা পাতাটি নতুন কি না
  5. লিংক বের করে normalise, তারপর Bloom filter-এ “দেখা হয়েছে কি” যাচাই করে নতুনগুলো frontier-এ

(সহজ) একটি crawler-কে ৩০ দিনে ১০০ কোটি পাতা নামাতে হবে (১ দিন = ৮৬,৪০০ সেকেন্ড)। গড়ে প্রতি সেকেন্ডে প্রায় কতটি পাতা?

  • প্রায় ৩৮.৬
  • প্রায় ৩৮৬
  • প্রায় ৩,৮৫৮
  • প্রায় ১১,৫৭৪

(মাঝারি) ১০০ কোটি URL-এর একটি Bloom filter, লক্ষ্য false positive ০.১%. সূত্র m/n = −ln p ÷ (ln 2)2 ধরলে মোট memory প্রায় কত?

  • প্রায় ১.২ GB
  • প্রায় ১০০ GB
  • প্রায় ১৪.৪ GB
  • প্রায় ১.৮ GB

(মাঝারি) একটি host-এ দুটি অনুরোধের মাঝে অন্তত ২ সেকেন্ড রাখতে হবে। সেই host-এর ৮টি URL, প্রতিটি নামাতে ০.৪ সেকেন্ড, প্রথমটি t = ০-তে শুরু, fetcher যথেষ্ট। শেষ URL-টি নামানো সবচেয়ে আগে কখন শেষ হয়?

  • ১৪.৪ সেকেন্ডে
  • ১৬.৪ সেকেন্ডে
  • ৩.২ সেকেন্ডে
  • ১৯.২ সেকেন্ডে

(মাঝারি) একটি leaderboard ৪টি shard-এ, খেলোয়াড়ের id-এর hash ধরে ভাগ। পুরো board-এর শীর্ষ ১০ হুবহু পেতে ন্যূনতম কী আনলেই যথেষ্ট?

  • যেকোনো একটি shard-এর শীর্ষ ১০
  • প্রতিটি shard-এর শীর্ষ ৩, মোট ১২ জন
  • প্রতিটি shard-এর শীর্ষ ১০, মোট ৪০ জন থেকে বাছাই
  • সব shard-এর সব খেলোয়াড়

(কঠিন) Hash-এ ভাগ করা ৪টি shard-এ “৯,৫০০-এর বেশি স্কোর কতজনের” প্রশ্নের উত্তর এলো ৮৫৬, ৮৬২, ৮১২ আর ৮৬৮. ৯,৫০০ স্কোরের খেলোয়াড়ের rank (১ = শীর্ষ) কত?

  • ৩,৩৯৮
  • ৩,৩৯৯
  • ৮৬৯
  • ৩,৪০১

(মাঝারি) একটি pastebin-এ দিনে ১০ GB নতুন লেখা আসে: ৪০% ১ দিনের মেয়াদে, ৩০% ৩০ দিনের, ২০% ৩৬৫ দিনের, ১০% কখনো না মোছার। Little-এর সূত্রে, শুধু মেয়াদওয়ালা অংশ দীর্ঘমেয়াদে প্রায় কত GB-তে স্থির হয়?

  • প্রায় ৮২৪ GB
  • প্রায় ৩,৬৫০ GB
  • প্রায় ১,১৮৯ GB
  • প্রায় ১০ GB

(মাঝারি) একটি pastebin লেখা রাখে object storage-এ, আর id, মেয়াদ ও object key রাখে একটি relational ডেটাবেসে। লেখার পথে কোন ক্রমটি নিরাপদ?

  • আগে metadata, পরে object
  • দুটি একসাথে, সমান্তরালে
  • সব লেখা ডেটাবেসেই, object storage বাদ
  • আগে object, পরে metadata

(মাঝারি) Autocomplete-এর ওজনে half-life ১ দিন। একটি প্রশ্ন আজ ১,৬০০ বার, গতকাল ৯০০ বার, পরশু ৪০০ বার খোঁজা হয়েছে, তার আগে কখনো নয়। ওজন কমানো স্কোর কত?

  • ২,৯০০
  • ১,৬০০
  • ২,১৫০
  • ২,২৫০

(কঠিন) একটি scheduler-এ partition-এর lease-এর TTL ১০ সেকেন্ড। মালিক node শেষবার নবায়ন করেছে t = ৬-এ, তারপর t = ৭-এ থেমে গেছে। t = ৮-এর একটি run অন্য node সবচেয়ে আগে কখন চালাতে পারে, আর দেরি কত?

  • t = ৮-এ, দেরি ০
  • t = ১৬-এ, দেরি ৮ সেকেন্ড
  • t = ১৭-এ, দেরি ৯ সেকেন্ড
  • t = ১৮-এ, দেরি ১০ সেকেন্ড

(মাঝারি) থেমে থাকা একটি scheduler node জেগে উঠে epoch ১ নিয়ে একটি run চালাতে চাইল; store-এ ওই partition-এর epoch এখন ২. ঠিক নকশায় কী হওয়া উচিত?

  • Node নিজের ঘড়ি দেখে lease যাচাই করে চালিয়ে দেয়
  • দুজনেই চালায়, গ্রাহকের কাজ idempotent বলে চলে
  • Store epoch ১ মেনে নিয়ে ২-কে বাতিল করে
  • Store লেখা প্রত্যাখ্যান করে, node partition ছেড়ে দেয়

(সহজ) ইন্টারভিউয়ার বললেন, “আপনার নকশা কি বাড়াবাড়ি নয়?” সবচেয়ে ভালো জবাব কোনটি?

  • প্রতিটি অংশের পেছনের সংখ্যা বা requirement বলা, কারণ না থাকলে সেই অংশ সরল করা
  • সঙ্গে সঙ্গে সব মেনে নিয়ে নতুন নকশা শুরু করা
  • “না, এটাই ঠিক” বলে এগিয়ে যাওয়া
  • “বড় প্রতিষ্ঠানগুলো এভাবেই করে” বলা

(সহজ) প্রায় ৪৫ মিনিটের একটি system design ইন্টারভিউয়ের প্রচলিত ছকে কোন ধাপে সবচেয়ে বেশি সময় যায়?

  • পরিধি ও requirement
  • হিসাব
  • Deep dive
  • Wrap-up

শব্দকোষ

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

মনে রাখুন

  1. ঘড়ি আর ছাঁচ: ০০–০৫ পরিধি, ০৫–০৯ হিসাব, ০৯–১৫ API ও data model, ১৫–২৫ ছবি, ২৫–৩৮ deep dive, ৩৮–৪২ failure ও trade-off, ৪২–৪৫ wrap-up — আগে প্রস্থ, পরে গভীরতা।
  2. Crawler: host ধরে সারি আর “পরের অনুমতি”র heap, robots.txt মানা, host-এর hash ধরে ভাগ; URL normalise + Bloom (১০০ কোটিতে ০.১%-এ প্রায় ১.৮ GB), লেখার hash আর SimHash.
  3. Leaderboard আর pastebin: hash-এ ভাগ করা sorted set — শীর্ষ k = প্রতিটি shard-এর শীর্ষ k মিলিয়ে, rank = সব shard-এর গণনা + ১, দূরের জন্য histogram; মোট মান বসানো idempotent. Pastebin-এ আগে object, পরে metadata; মেয়াদ পড়ার সময় যাচাই; storage = আসার হার × আয়ু।
  4. Autocomplete আর scheduler: পরিবেশন আর হালনাগাদ আলাদা — snapshot, time decay, trending stream, edge-এ ছোট prefix. Scheduler-এ partition-এর lease, epoch দিয়ে fencing, run_key unique, fire = এক transaction; at-least-once, তাই কাজ idempotent.
  5. আপত্তি আর মূল্যায়ন: আপত্তি এলে সংখ্যা দিয়ে জবাব, আর কোন অবস্থায় সিদ্ধান্ত বদলাত তা বলা; সাতটি মাপকাঠিতে নিজের মহড়ায় নম্বর দিন। AI প্রশ্নকর্তা, উত্তরদাতা নয় — তার প্রতিটি সংখ্যা আবার হিসাব করুন।

এরপর: এটিই কোর্সের শেষ পর্ব — অভিনন্দন! ৬৬টি পর্বে আপনি requirement আর হিসাব থেকে শুরু করে API, scaling, storage, cache, messaging, distributed systems-এর তত্ত্ব, reliability আর পূর্ণ case study পর্যন্ত পুরো পথটি হেঁটেছেন। কোর্স হোম-এ ফিরে পুরো রোডম্যাপ দেখুন, ড্যাশবোর্ডে মডিউল টেস্ট আর কোর্সের সমাপনী পরীক্ষা দিন, আর ওপরের ১০টি প্রশ্নের যেকোনো একটি ঘড়ি ধরে একজন বন্ধুকে ইন্টারভিউয়ার বানিয়ে করে দেখুন।

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

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