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

Case study: URL shortener — ID তৈরি, redirect আর analytics

Case Study: URL Shortener — ID Generation, Redirects & Click Analytics

Taught in Bangla, this lesson is the first case study, following the eight-step design framework for a URL shortener: estimating redirect QPS, storage and cache size, the API and data model, four ID-generation strategies with birthday-bound collision math, 301, 302, 307 and 308 redirects, caching hot links at the edge, a click analytics pipeline with dedup and unique visitors, abuse protection and multi-region trade-offs.

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

  • একটি case study-কে আট ধাপে সাজানো — requirement থেকে interview wrap-up পর্যন্ত
  • URL shortener-এর QPS, storage, cache-এর আকার আর code-এর দৈর্ঘ্য নিজে হিসাব করা
  • চার রকম ID কৌশলের collision, অনুমেয়তা আর ব্যর্থতার trade-off, birthday-র অঙ্কসহ
  • 301, 302, 307 আর 308-এর পার্থক্য, analytics-এর দাম, cache, hot link আর edge-এ redirect
  • Click analytics-এর pipeline, dedup, unique visitor, অপব্যবহার থেকে সুরক্ষা আর বহু region

১ · সমস্যা আর পরিধি — “ছোটলিংক” কী করবে, কী করবে না

মডিউল ১ থেকে ৮-এ আপনি আলাদা আলাদা যন্ত্রাংশ চিনেছেন: load balancer, cache, sharding, queue, consensus, rate limiter. মডিউল ৯-এ সেই যন্ত্রাংশ দিয়ে পুরো গাড়ি বানানো হবে। প্রতিটি case study একই কাঠামো মানবে, যা পর্ব ৬-এ শিখেছেন: পরিধি, হিসাব, API, data model, high-level design, deep dive, ভাঙন আর wrap-up. কোনো ধারণা নতুন করে শেখানো হবে না; প্রতিটির এক লাইনের প্রয়োগ আর মূল পর্বের link থাকবে।

মডিউল ৯ — আটটি case study, একই আট ধাপ

পর্বCase studyমূলত যে পর্বগুলো খাটবে
৫৪URL shortener — ID তৈরি, redirect আর analytics (এই পর্ব)৩, ১৭, ২৩, ২৮–৩০, ৩৩–৩৬, ৩৯, ৪৫, ৪৯
৫৫Chat আর messaging system — delivery, ordering আর presence১০, ৩৪, ৩৯, ৪৪
৫৬News feed — fan-out on write বনাম read আর ranking২৮–৩০, ৩২, ৩৬
৫৭Notification system — push, SMS, email আর নির্ভরযোগ্য delivery৩১, ৩৪, ৪৮, ৪৯
৫৮Ride-sharing — location update, geohash আর matching১০, ২৩, ৩০
৫৯Video streaming — upload, transcoding আর adaptive streaming১৭, ২৬, ৩৭
৬০Ticket booking আর payment — concurrency, idempotency আর ledger৮, ২৪, ২৫, ৪৩
৬১ক্যাপস্টোন — একটি distributed key-value store নকশা আর simulator১৬, ২২, ৪১, ৪৪

URL shortener দিয়ে শুরু করার কারণ আছে। এর মূল কাজ এক লাইনে বলা যায়: লম্বা একটি ঠিকানার বদলে ছোট একটি code দেওয়া, আর কেউ ছোট code-এ এলে তাঁকে আসল ঠিকানায় পাঠানো (redirect). সরল বলেই এখানে প্রতিটি trade-off পরিষ্কার দেখা যায়। আমাদের কাল্পনিক সেবার নাম “ছোটলিংক”, domain s.example.

ব্যাংকের কাউন্টারে টোকেন নেওয়ার কথা ভাবুন। আপনার লম্বা পরিচয় (নাম, হিসাব নম্বর, কাজের ধরন) একটি ছোট সংখ্যায় বাঁধা পড়ে: “টোকেন ১০৪”। ডাক পড়লে ওই ছোট সংখ্যা দেখেই কর্মী আপনার পুরো কাজ খুঁজে পান। কিন্তু টোকেন ক্রমিক বলে পাশের মানুষও জানেন পরেরটি ১০৫ — এই “অনুমেয়তা” URL shortener-এও একটি বড় প্রশ্ন, অংশ ৬-এ দেখবেন।

Functional requirement — কী করবে

  • ছোট link তৈরি: লম্বা URL দিলে ছোট code. ইচ্ছা হলে নিজের পছন্দের নাম (custom alias), যেমন s.example/eid-offer, আর মেয়াদ (expiry).
  • Redirect: s.example/4c92-তে গেলে আসল ঠিকানায় পাঠানো; মেয়াদ শেষ, মুছে ফেলা বা নিষিদ্ধ link-এ পরিষ্কার উত্তর।
  • Click analytics: link-এর মালিক দেখবেন মিনিট বা দিন ধরে click-এর সংখ্যা, আনুমানিক কতজন আলাদা মানুষ, কোন দেশ বা ডিভাইস থেকে।
  • ব্যবস্থাপনা: নিজের link-এর তালিকা, মুছে ফেলা, অপব্যবহারের রিপোর্ট।

Non-functional requirement — কতটা ভালোভাবে

  • Redirect দ্রুত: ধরা যাক সার্ভারের ভেতরে p99 কয়েক ms থেকে কয়েক দশ ms; মানুষ link চাপার পর অপেক্ষা টের পাবেন না (পর্ব ৪)।
  • Redirect সবসময় চালু: SMS, পোস্টার বা QR code-এ ছাপা link বছর পরেও কাজ করবে; ধরা যাক availability-র লক্ষ্য ৯৯.৯৯% (পর্ব ৫)।
  • ভুল ঠিকানায় কখনো নয়: একটি code কখনো দুটি URL-এ যাবে না। এটি correctness-এর শর্ত, গতির চেয়েও জরুরি।
  • Analytics একটু দেরিতে চলবে: কয়েক মিনিট দেরি আর সামান্য আনুমানিক ভুল গ্রহণযোগ্য।

পরিধির বাইরে

Link-এর ভেতরের পাতা দেখানো বা host করা, পুরো marketing dashboard, অর্থ লেনদেন, আর link-এর destination পরে বদলানোর সুবিধা (অনুশীলন ২-এ এটি যোগ করবেন)।

ইন্টারভিউয়ার যে প্রশ্নগুলো শুনতে চান

প্রশ্নকেন জরুরিধরে নেওয়া উত্তর
দিনে কত নতুন link, কতবার খোলা হয়?Read:write অনুপাতই নকশার কেন্দ্র ঠিক করে১ কোটি নতুন, read:write ১০০:১
Code কত ছোট হতে হবে?দৈর্ঘ্য ঠিক করে ID-র জায়গা আর collision৭ অক্ষর বা কম
Link কতদিন থাকবে?Storage আর মুছে ফেলার কাজডিফল্টে চিরকাল, ইচ্ছা হলে মেয়াদ
প্রতিটি click গোনা জরুরি, নাকি আনুমানিক চলবে?301 বনাম 302, আর analytics-এর খরচClick গোনা জরুরি, unique visitor আনুমানিক
Code কি গোপন থাকার কথা?অনুমেয় ID চলবে কি নানা, তবে পরপর code গুনে সব link বের করা যেন সহজ না হয়

লম্বা link ছোট করার ধারণা নতুন নয়। TinyURL-এর মতো সেবা ২০০২ সালের দিকেই চালু হয়েছিল। পরে SMS আর microblog-এর অক্ষরসীমা এর চাহিদা বাড়ায়: একটি সাধারণ GSM SMS-এ ১৬০টি অক্ষর ধরে, কিন্তু বাংলা লেখা Unicode-এ গেলে এক অংশে মাত্র ৭০টি। বাংলাদেশে OTP, ব্যাংকের বার্তা আর প্রচারের SMS-এ ছোট link তাই এখনো কাজের — প্রতিটি অক্ষর বাঁচালে বার্তার অংশ কমে।

২ · হিসাব — চাপ পড়ার পথে, লেখায় নয়

পর্ব ৩-এ এই সেবারই সংখ্যা ধাপে ধাপে বের করেছিলেন। এখানে একই অনুমান রাখা হলো, শুধু নকশার জন্য তিনটি প্রশ্ন যোগ হলো: cache কত বড়, code কত অক্ষরের, আর কোন পথে কত ভার। সব input কাল্পনিক, “ধরা যাক”। নিচের estimator-এ যেকোনো একটি মান বদলে আবার চালান — কোন ফল কতটা নড়ে, সেটাই আসল শিক্ষা।

// URL shortener estimator. Every input is an assumption (ধরা যাক) - change one and re-run.
const A = {
  newPerDay: 1e7,      // new short links per day
  readsPerWrite: 100,  // read:write = 100:1
  peakFactor: 3,       // busiest hour vs the daily average
  recordBytes: 500,    // code + long URL + owner + times + flags + index share
  redirectBytes: 500,  // one 302 response with headers
  years: 5, copies: 3, // keep 5 years, 3 replicas
  hotDays: 30,         // most clicks go to links created in the last 30 days
  hotShare: 0.2,       // cache the top 20% of those links
  headroom: 100,       // random codes: keep the code space >= 100x the links
};
const wAvg = A.newPerDay / 86400, rAvg = wAvg * A.readsPerWrite;
const n0 = x => Math.round(x).toLocaleString("en-US");
console.log("write QPS  avg " + n0(wAvg) + "   peak " + n0(wAvg * A.peakFactor));
console.log("read  QPS  avg " + n0(rAvg) + "   peak " + n0(rAvg * A.peakFactor));
const egress = rAvg * A.peakFactor * A.redirectBytes * 8 / 1e6;
console.log("peak redirect egress " + egress.toFixed(0) + " Mbit/s");
const links = A.newPerDay * 365 * A.years, tb = links * A.recordBytes / 1e12;
console.log("links after " + A.years + " years " + links.toExponential(3));
console.log("storage " + tb.toFixed(2) + " TB raw, " + (tb * A.copies).toFixed(2) + " TB with " + A.copies + " copies");
const hot = A.newPerDay * A.hotDays * A.hotShare;
console.log("cache: top " + A.hotShare * 100 + "% of " + A.hotDays + " days of links = " +
  hot.toExponential(1) + " links = " + (hot * A.recordBytes / 1e9).toFixed(0) + " GB");
for (const [how, need] of [["counter", links], ["random + headroom", links * A.headroom]]) {
  let L = 1; while (62 ** L < need) L++;
  console.log(how.padEnd(18) + "needs " + L + " chars: 62^" + L + " = " + (62 ** L).toExponential(3) +
    ", 5-year fill " + (100 * links / 62 ** L).toFixed(2) + "%");
}
console.log("62^6 = " + n0(62 ** 6) + ", 62^7 = " + n0(62 ** 7));

আউটপুট:

write QPS  avg 116   peak 347
read  QPS  avg 11,574   peak 34,722
peak redirect egress 139 Mbit/s
links after 5 years 1.825e+10
storage 9.13 TB raw, 27.38 TB with 3 copies
cache: top 20% of 30 days of links = 6.0e+7 links = 30 GB
counter           needs 6 chars: 62^6 = 5.680e+10, 5-year fill 32.13%
random + headroom needs 7 chars: 62^7 = 3.522e+12, 5-year fill 0.52%
62^6 = 56,800,235,584, 62^7 = 3,521,614,606,208
  • লেখা নগণ্য, পড়া ভারী: peak-এ সেকেন্ডে ৩৪৭টি লেখা একটি সাধারণ database-ই সামলায়; ৩৪,৭২২টি redirect-ই নকশার আকার ঠিক করবে। QPS-এর এই অসমতা থেকেই সিদ্ধান্ত: পড়ার পথে cache আর edge, লেখার পথে সরলতা।
  • Network হালকা: peak-এ ১৩৯ Mbit/s — একটি সার্ভারের network card-ই যথেষ্ট।
  • Storage মাঝারি: ৫ বছরে ১,৮২৫ কোটি record, কাঁচা ৯.১৩ TB, তিন কপিতে ২৭.৩৮ TB. এক মেশিনে রাখা কষ্টকর, তাই কয়েকটি shard (পর্ব ২৩)।
  • Cache: ধরা যাক বেশিরভাগ click যায় গত ৩০ দিনের link-এ; তাদের জনপ্রিয় ২০% মানে ৬ কোটি link, প্রায় ৩০ GB — দু-তিনটি cache সার্ভারের RAM-এ আঁটে।
  • Code-এর দৈর্ঘ্য: counter থেকে code বানালে প্রতিটি সংখ্যা একবারই আসে, তাই ৬ অক্ষরেই (৬২৬ ≈ ৫,৬৮০ কোটি) ৫ বছর চলে, ৩২% ভরে। Random code-এ collision কম রাখতে জায়গা link-এর অন্তত ১০০ গুণ রাখা হলো, তাই ৭ অক্ষর: ৬২৭ = ৩৫,২১,৬১,৪৬,০৬,২০৮, অর্থাৎ প্রায় ৩.৫২ লাখ কোটি (৩.৫২ trillion), ৫ বছরে ভরে মাত্র ০.৫২%.
হিসাবের উপসংহার

এই সেবা read-heavy: প্রতি লেখায় একশো পড়া। তাই নকশার সব বুদ্ধি খরচ হবে redirect-এর পথে — cache, CDN, hot link — আর লেখার পথে মূল প্রশ্ন একটিই: অনন্য, ছোট code কীভাবে বানাবেন। Storage আর network দুটিই সামলানো যায়।

৩ · API — তিনটি endpoint আর একটি redirect

API-র নকশা পর্ব ৮-এর নিয়ম মানে: resource-এর নাম বিশেষ্য, ঠিক status code, আর POST-এ idempotency key. মোবাইল network-এ উত্তর হারিয়ে গেলে app একই অনুরোধ আবার পাঠায়; key না থাকলে একই link-এর দুটি code তৈরি হতো।

POST /api/v1/links HTTP/1.1
Host: api.s.example
Authorization: Bearer <token>
Idempotency-Key: k-77
Content-Type: application/json

{ "long_url": "https://shop.example/p/1", "alias": null, "expires_in_days": 30 }

HTTP/1.1 201 Created
Content-Type: application/json

{ "code": "4c92", "short_url": "https://s.example/4c92",
  "expires_at": "2026-11-27T00:00:00Z" }
GET /4c92 HTTP/1.1
Host: s.example

HTTP/1.1 302 Found
Location: https://shop.example/p/1
Cache-Control: private, max-age=0

# alias already taken          -> 409 Conflict
# expired or deleted link      -> 404 Not Found (or 410 Gone)
# blocked as malicious         -> a warning page, never a redirect

GET /api/v1/links/4c92/stats?from=2026-10-28&granularity=minute
-> 200 { "clicks": [...], "unique_visitors_approx": 13 }
  • Alias-এর নিয়ম: অক্ষরের তালিকা আর দৈর্ঘ্যের সীমা, আর api, login, admin-এর মতো সংরক্ষিত শব্দ বাদ। নইলে কেউ নিজের link-কে সেবার নিজের পাতার মতো দেখাতে পারেন।
  • বড়-ছোট হাতের অক্ষর: base62 case-sensitive, তাই 4c92 আর 4C92 আলাদা। মানুষ হাতে টাইপ করবেন বা মুখে বলবেন এমন code-এ 0/O আর l/1/I-এর মতো বিভ্রান্তিকর অক্ষর বাদ দেওয়া base58-এর মতো বর্ণমালা বিবেচনা করা যায় — দাম, প্রতি অক্ষরে একটু কম জায়গা।

৪ · Data model — একটি key, একটি খোঁজ

প্রথমে খোঁজার ধরন, তারপর টেবিল (পর্ব ৬-এর ধাপ ৪)। প্রতি সেকেন্ডে হাজারবার যে খোঁজ চলবে তা একটিই: “code থেকে URL”. বাকিগুলো বিরল: মালিকের নিজের link-এর তালিকা, আর পরিসংখ্যান।

Link

code (primary key), long_url, owner_id, created_at, expires_at, status (active, blocked, deleted). প্রায় ৫০০ byte.

KV store · partition key = code

Owner-এর তালিকা

owner_id → code-এর আলাদা index, “আমার link” পাতার জন্য। একটু দেরিতে হালনাগাদ হলেও চলে।

Global secondary index

Idempotency record

idem_key → code, প্রায় ২৪ ঘণ্টার TTL. একই key আবার এলে আগের উত্তর।

TTL-সহ ছোট টেবিল

Click event

প্রতিটি click একটি event; মূল store-এ নয়, আলাদা analytics store-এ, সময় ধরে সাজানো।

Column store · অংশ ৮
  • Store বাছাই: খোঁজ একটিই, join নেই, transaction নেই — key-value store বা wide-column store স্বাভাবিক পছন্দ (পর্ব ২০)। Shard-এ ভাগ করা SQL database-ও ঠিকঠাক চলবে; সিদ্ধান্তের কারণ খোঁজার ধরন, ফ্যাশন নয়।
  • Partition key: code, hash partitioning-এ (পর্ব ২৩)। Counter-এর code পরপর আসে, তাই range partitioning করলে সব নতুন লেখা একটি shard-এ জমত; hash সেটি ছড়িয়ে দেয়।
  • Owner index: partition key code হলে “owner 7-এর সব link” প্রশ্ন সব shard ঘুরবে। তাই আলাদা global index, যা অল্প দেরিতে মেলে (পর্ব ২১)।
  • Alias-এর অনন্যতা: “আগে দেখি আছে কি না, তারপর লিখি” দুই ধাপে করলে দুজন একসাথে একই alias পেতে পারেন। সমাধান একটি conditional write: “না থাকলে তবেই লেখা”, যা store নিজেই atomic-ভাবে করে।

নিচের SQLite demo একটি মেশিনে এই টেবিল আর দুটি নিয়ম দেখায়: alias-এর সংঘাত, আর redirect-এর সময় মেয়াদ ও status যাচাই। SQLite distributed নয়; এটি শুধু নিয়মগুলো দেখায়, shard বা replica নয়।

-- Single-node SQLite demo of the link table (production shards it by code).
CREATE TABLE links (
  code       TEXT PRIMARY KEY,       -- short code or custom alias
  long_url   TEXT NOT NULL,
  owner_id   INTEGER NOT NULL,
  created_at TEXT NOT NULL,
  expires_at TEXT,                   -- NULL = never expires
  status     TEXT NOT NULL DEFAULT 'active'   -- active | blocked | deleted
) WITHOUT ROWID;

INSERT INTO links VALUES ('4c92', 'https://shop.example/p/1', 7, '2026-10-01', NULL, 'active');
INSERT INTO links VALUES ('eid-offer', 'https://shop.example/eid', 7, '2026-10-01', '2026-10-20', 'active');
INSERT INTO links VALUES ('x9Lp2', 'https://bad.example/win', 9, '2026-10-02', NULL, 'blocked');

-- user 9 asks for the same alias: insert only if the code is free
INSERT INTO links VALUES ('eid-offer', 'https://other.example/', 9, '2026-10-28', NULL, 'active')
  ON CONFLICT(code) DO NOTHING;
SELECT changes() AS rows_inserted;

-- redirect decision at "now" = 2026-10-28
SELECT code,
  CASE WHEN status <> 'active' THEN 'warning page'
       WHEN expires_at IS NOT NULL AND expires_at <= '2026-10-28' THEN '404 expired'
       ELSE '302 ' || long_url END AS answer
FROM links ORDER BY code;

প্রথম তিনটি INSERT-এ “১টি সারি বদলেছে”, কিন্তু user 9-এর alias-এর অনুরোধে “০টি সারি বদলেছে” আর rows_inserted = 0 — কোনো error নেই, পুরোনো সারিও অক্ষত। API এই শূন্য দেখেই 409 Conflict দেয়। শেষ query-র তিনটি সারি: 4c92 → 302 https://shop.example/p/1, eid-offer → 404 expired (মেয়াদ ২০ অক্টোবরে শেষ), আর x9Lp2 → warning page (নিষিদ্ধ)। Distributed KV store-এ একই কাজ করে “না থাকলে তবেই লেখা” ধরনের conditional put; নিয়ম একই, শুধু প্রয়োগ হয় code-এর shard-এ।

৫ · High-level design — দুটি পথ, একটি ছবি

পুরো নকশা তিনটি আলাদা পথে ভাগ হয়: নীল পথে redirect (পড়া), সবুজ পথে নতুন link (লেখা), আর কমলা ড্যাশ পথে click-এর খবর। তিনটিকে আলাদা রাখাই মূল সিদ্ধান্ত — একটির ভিড় বা বিভ্রাট যেন অন্যটিকে না ডোবায়।

ছোটলিংক (কাল্পনিক) — পুরো নকশা

Client → edge/CDN → redirect service → cache → KV store; write path via ID service; click events → stream → analytics store

URL shortener-এর নকশা: redirect, নতুন link আর click analytics-এর তিন পথ ব্যবহারকারী browser · app · SMS আর QR-এর link miss GET /4c92 POST Edge · CDN কাছের PoP · TLS API gateway পরিচয় · rate limit Redirect service ×N stateless · 302 দেয় Link service URL যাচাই · alias Cache hot link · TTL ID service counter-এর block KV store · partition key = code shard-এ ভাগ · প্রতিটি ×3 replica click event Click stream · log partition = code Analytics consumer dedup · মিনিটের window Analytics store column store + HyperLogLog → dashboard redirect নতুন link click event
ছবি ৫৪.১ — Redirect-এর পথ সবচেয়ে ছোট আর সবচেয়ে বেশি ব্যস্ত; লেখার পথে বাড়তি যাচাই চলে, কারণ সেখানে সময় আছে। Click-এর খবর redirect-এর উত্তর দেওয়ার পরে, আলাদা পথে যায়।
  • Redirect service stateless: কোনো সার্ভার নিজের কাছে কিছু মনে রাখে না, তাই ভিড়ে শুধু সংখ্যা বাড়ান (পর্ব ১৩), সামনে load balancer ভাগ করে দেয় (পর্ব ১৪)।
  • সত্যের উৎস একটিই: KV store. Cache আর edge শুধু কপি রাখে; নিষিদ্ধ করার খবর সেখানে দেরিতে পৌঁছায় (অংশ ৭)।

লেখার পথ — একটি নতুন link

নিচে “পরে” চাপুন। একটি নতুন link-এর অনুরোধ পাঁচটি স্তর পেরিয়ে কীভাবে code পায়, প্রতিটি ধাপে কী যাচাই হয়, দেখুন।

  1. ক্লায়েন্টPOSTIdempotency-Key
  2. Gatewayauth · limitসামনের দরজা
  3. Link serviceURL checkযাচাই
  4. ID4c92block থেকে
  5. KV storePUT if absentসত্যের উৎস
অনুরোধমোবাইল app থেকে
POST /api/v1/links
Idempotency-Key: k-77
{ "long_url": "https://shop.example/p/1",
  "expires_in_days": 30 }
Gatewayতিনটি যাচাই
token      user 7        OK
rate limit 18/20 tokens  OK   (পর্ব ৪৯)
idem k-77  আগে দেখা নেই  নতুন কাজ
Link serviceURL-টি নিরাপদ তো?
normalize  https://shop.example/p/1
blocklist  নেই                 OK
scheme     https only          OK
alias      চাওয়া হয়নি -> ID লাগবে
IDনিজের block থেকে, network ছাড়া
block      [1,000,000 .. 1,000,999]
next id    1,000,000
base62     4c92        (অংশ ৬-এর stepper)
KV storeশর্তসাপেক্ষ লেখা
PUT links/4c92 IF NOT EXISTS   OK
{ long_url, owner 7, expires 2026-11-27, active }
replica-তে কপি   (পর্ব ২২)
উত্তরআর পরের কাজ
201 Created  { "short_url": "https://s.example/4c92" }
idem k-77 -> 4c92   (২৪ ঘণ্টা রাখা)
event link_created  -> stream, outbox দিয়ে
  1. অনুরোধ। App একটি POST পাঠায়, সাথে নিজের বানানো Idempotency-Key k-77. Network কেটে গিয়ে app আবার পাঠালেও একই key যাবে।
  2. Gateway. পরিচয় যাচাই, user 7-এর বালতিতে token আছে কি না (পর্ব ৪৯), আর k-77 আগে দেখা হয়েছে কি না। আগে দেখা হলে এখানেই পুরোনো উত্তর ফেরত যেত।
  3. যাচাই। URL-কে একটি নির্দিষ্ট রূপে আনা (normalize), শুধু http বা https scheme, আর ক্ষতিকর ঠিকানার তালিকায় মিলিয়ে দেখা। লেখার পথে সময় আছে, তাই যাচাই এখানে।
  4. ID. Link service-এর হাতে আগে থেকে ১,০০০টি সংখ্যার একটি block আছে; পরেরটি ১০,০০,০০০, base62-এ 4c92. ID service-কে প্রতিবার ডাকতে হয়নি।
  5. শর্তসাপেক্ষ লেখা। “Code না থাকলে তবেই লেখা” — counter-এর code-এ সংঘাত হওয়ার কথা নয়, তবু শর্তটি বাগ বা ভুল config থেকে বাঁচায়। Alias হলে এই শর্তই 409 ঠিক করে।
  6. উত্তর। 201 আর ছোট link. Idempotency record রাখা হলো, আর link_created event outbox দিয়ে stream-এ (পর্ব ২৫)। Cache-এ এখনই লেখা হয়নি — প্রথম click-এ ভরবে।

পড়ার পথ — প্রথমবার miss, তারপর hit

এবার একই link দুজন মানুষ দুই সেকেন্ডের ব্যবধানে খুলছেন। Cache কীভাবে দ্বিতীয়জনকে KV store-এ যাওয়া থেকে বাঁচায়, সারি ধরে দেখুন (latency-গুলো মোটামুটি মাত্রা, মাপা নয়)।

code 4c92, cache-aside, TTL ১ ঘণ্টা
সময়
কোথায়
Cache-এ 4c92
KV store-এ পড়া (মোট)
উত্তর
এই ধাপের সময় (প্রায়)
  1. Edge. DNS ব্যবহারকারীকে কাছের PoP-এ পাঠায় (পর্ব ১৭)। এখানে TLS শেষ হয়; এই নকশায় edge 302 নিজে রাখে না, তাই অনুরোধ redirect service-এ যায়।
  2. Cache miss. Redirect service cache-এ 4c92 খোঁজে — নেই, কারণ link সবে তৈরি। একটি network round trip, প্রায় ১ ms.
  3. KV store. Partition key 4c92 দিয়ে ঠিক একটি shard-এ সরাসরি পড়া। Record পাওয়া গেল, status active, মেয়াদ ২০২৬-১১-২৭ পর্যন্ত।
  4. Cache ভরা আর উত্তর। Cache-aside নিয়মে (পর্ব ২৮) record cache-এ, TTL ১ ঘণ্টা। ব্যবহারকারী পেলেন 302; click-এর খবর memory-র buffer-এ গেল, উত্তরের পরে stream-এ যাবে।
  5. Cache hit. দুই সেকেন্ড পরে আরেকজন একই link খুললেন। Cache-এ পাওয়া গেল; KV store-এ কোনো পড়া নেই, উত্তর প্রায় ১ ms-এ।
  6. সারাংশ। দুটি click-এ KV store-এ পড়া একটিই। Hit rate h হলে KV store-এর ভার (1 − h) × QPS: peak-এর ৩৪,৭২২টি redirect-এ h = ৮০% মানে সেকেন্ডে প্রায় ৬,৯৪৪টি পড়া। কত বড় cache-এ কত h, অংশ ৭-এর simulator বলবে।

৬ · Deep dive ১ — ID তৈরি: ছোট, অনন্য, আর সহজে অনুমেয় নয়

প্রথম কঠিন প্রশ্ন: কোটি কোটি link-এর প্রতিটির জন্য এমন একটি ছোট code, যা আর কারও নয়। কেন্দ্রীয় counter ছাড়া unique ID-র কৌশলগুলো পর্ব ২৩ আর সময়ক্রমের ID পর্ব ৩৯-এ দেখেছেন। এখানে শুধু প্রশ্ন: URL shortener-এর শর্তে কোনটি টেকে?

কৌশলকীভাবেসুবিধাদাম
Hash আর কেটে ছোট করাURL-এর hash, তার প্রথম প্রায় ৪১-৪২ bit base62-এ ৭ অক্ষরকোনো সমন্বয় লাগে না; একই URL-এ একই code চাইলে স্বাভাবিকCollision হবেই — প্রতিবার যাচাই, মিললে salt বদলে আবার; কেটে ছোট করায় hash-এর শক্তি অনেকটাই হারায়
Counter + base62একটি বাড়তে থাকা সংখ্যা, base62-এ লেখা; সার্ভার block ধরে নেয়Collision নেই; শুরুতে খুব ছোট code; সরলপরপর code, তাই অনুমেয়; counter-এর store-কে নির্ভরযোগ্য রাখতে হয় (ticket server)
আগে বানানো key (KGS)Random code আগেই বানিয়ে, অনন্যতা যাচাই করে, অব্যবহৃত তালিকায় রাখা; সার্ভার একগোছা তুলে নেয়Code এলোমেলো, লেখার সময় collision যাচাই নেইআরেকটি সার্ভিস আর তালিকা; একই key দুজনকে না দেওয়ার দায়; তুলে নিয়ে crash হলে কিছু key নষ্ট
Snowflake-ধাঁচের IDসময় + machine id + sequence, ৬৪ bitসমন্বয় ছাড়া অনন্য, সময়ক্রমে সাজানো৬৪ bit base62-এ ১১ অক্ষর — “ছোট” link-এর জন্য লম্বা; ঘড়ি পেছালে সমস্যা

দৈর্ঘ্যের অঙ্ক আর birthday-র ফাঁদ

L অক্ষরের base62 code-এ মোট সম্ভাবনা M = ৬২L: ৬ অক্ষরে ৫৬,৮০,০২,৩৫,৫৮৪ আর ৭ অক্ষরে ৩৫,২১,৬১,৪৬,০৬,২০৮। Counter হলে M পর্যন্ত কোনো collision নেই। কিন্তু code এলোমেলো হলে (hash বা random) জায়গা ফাঁকা থাকলেও collision আসে অনেক আগে — একে বলে birthday paradox: ২৩ জনের একটি দলেই দুজনের জন্মদিন একই হওয়ার সম্ভাবনা ৫০%-এর বেশি।

Birthday bound — ছোট একটি যুক্তি
  1. n-টি code এলোমেলোভাবে M-টি ঘরে পড়ছে। যেকোনো একজোড়া একই ঘরে পড়ার সম্ভাবনা 1/M.
  2. জোড়ার সংখ্যা n(n − 1)/2 ≈ n²/2. তাই গড় collision ≈ n²/(2M).
  3. জোড়াগুলো প্রায় স্বাধীন ধরলে, একটিও collision না হওয়ার সম্ভাবনা ≈ e−n²/(2M). ৫০% সম্ভাবনা আসে n ≈ ১.১৮√M-এ।

৬ অক্ষরে √M = ৬২৩ = ২,৩৮,৩২৮, তাই মাত্র প্রায় ২,৮০,৬১০টি link-এই ৫০% সম্ভাবনায় প্রথম collision; ৭ অক্ষরে প্রায় ২২,০৯,৫২৪টিতে। কোটি link-এর সেবায় collision তাই “হয়তো” নয়, নিশ্চিত — প্রশ্ন শুধু কতগুলো।

এবার নিজে মাপুন। নিচের কোড ১০ লাখ কাল্পনিক URL-কে একটি সরল hash দিয়ে ৫৩ bit সংখ্যা বানায়, তারপর ৫, ৬ বা ৭ অক্ষরে কেটে দেখে কতগুলো code আগেই নেওয়া। ৫ অক্ষরের ছোট জায়গাটি রাখা হয়েছে যাতে ফোনেও অল্প কাজে বড় সংখ্যায় সূত্র মেলানো যায়; ১ কোটির হিসাব শেষ লাইনে শুধু সূত্র থেকে। Hash-টি cyrb53 — দ্রুত, কিন্তু cryptographic নয়; বাস্তবে MD5 বা SHA-256-এর প্রথম কয়েক bit নিলেও হিসাব একই। চালাতে প্রায় এক সেকেন্ড লাগে।

// Hash-then-truncate vs a counter: how many short codes collide?
// cyrb53 = a simple, well-known 53-bit string hash (fast, NOT cryptographic).
function cyrb53(s) {
  let h1 = 0xdeadbeef, h2 = 0x41c6ce57;
  for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i);
    h1 = Math.imul(h1 ^ c, 2654435761); h2 = Math.imul(h2 ^ c, 1597334677); }
  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
  return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
function repeats(codes) {             // codes already taken by an earlier link (bucket by low 20 bits)
  const B = 1 << 20, start = new Uint32Array(B + 1), out = new Float64Array(codes.length);
  for (const x of codes) start[x % B + 1]++;
  for (let b = 0; b < B; b++) start[b + 1] += start[b];
  const pos = start.slice(0, B);
  for (const x of codes) out[pos[x % B]++] = x;
  let r = 0;
  for (let b = 0; b < B; b++) for (let i = start[b] + 1; i < start[b + 1]; i++)
    for (let j = start[b]; j < i; j++) if (out[i] === out[j]) { r++; break; }
  return r;
}
const ABC = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const base62 = n => { let s = ""; do { s = ABC[n % 62] + s; n = Math.floor(n / 62); } while (n > 0); return s; };
console.log("chars    links  space 62^L   collisions  birthday n^2/2M  P(>=1)");
const N = 1e6, h = new Float64Array(N);
for (let i = 0; i < N; i++) h[i] = cyrb53("https://shop.example/item/" + i);   // 1 million long URLs
for (const [L, n] of [[5, 1e5], [5, 1e6], [6, 1e6], [7, 1e6]]) {
  const M = 62 ** L, got = repeats(h.subarray(0, n).map(x => x % M)), exp = n * n / (2 * M);
  console.log(String(L).padStart(5), String(n / 1e6 + "M").padStart(8), M.toExponential(2).padStart(11),
    String(got).padStart(12), exp.toFixed(1).padStart(16), (1 - Math.exp(-exp)).toFixed(4).padStart(7));
}
console.log("counter + base62: 0 collisions; id 1,000,000 ->", base62(1e6), "; id 10,000,000 ->", base62(1e7),
  "; id 56,800,235,583 ->", base62(62 ** 6 - 1));
const est = (L, n) => (n * n / (2 * 62 ** L)).toFixed(1);
console.log("formula only, 10M links: 6 chars ~" + est(6, 1e7) + ", 7 chars ~" + est(7, 1e7) + " collisions");

আউটপুট:

chars    links  space 62^L   collisions  birthday n^2/2M  P(>=1)
    5     0.1M     9.16e+8            6              5.5  0.9957
    5       1M     9.16e+8          545            545.8  1.0000
    6       1M    5.68e+10            6              8.8  0.9998
    7       1M    3.52e+12            0              0.1  0.1324
counter + base62: 0 collisions; id 1,000,000 -> 4c92 ; id 10,000,000 -> FXsk ; id 56,800,235,583 -> ZZZZZZ
formula only, 10M links: 6 chars ~880.3, 7 chars ~14.2 collisions
  • সূত্র আর মাপ মেলে: ৫ অক্ষরে (প্রায় ৯২ কোটির জায়গা) ১০ লাখ link-এ ৫৪৫টি collision, সূত্র বলে ৫৪৫.৮; মাত্র ১ লাখেই ৬টি, সূত্রে ৫.৫। ৬ অক্ষরে ১০ লাখে ৬টি, সূত্রে ৮.৮। ছোট সংখ্যায় এমন ওঠানামা Poisson বণ্টনের স্বভাব — URL-এর ধরন বদলে চালালে ছোট সারিগুলো কয়েক ঘর নড়ে, বড়টি প্রায় স্থির থাকে।
  • ৭ অক্ষরেও শূন্য নয়: ১০ লাখে এবার শূন্য, কিন্তু অন্তত একটির সম্ভাবনা ১৩.২৪%. শেষ লাইনের সূত্র বলে ১ কোটি link-এ ৭ অক্ষরেও গড়ে প্রায় ১৪.২টি — জায়গার মাত্র ০.০০০৩% ভরলেও। যাচাই ছাড়া লিখলে সেই ব্যবহারকারীরা অন্য কারও পাতায় যেতেন — “ভুল ঠিকানায় কখনো নয়” শর্তটিই ভাঙত।
  • Counter: collision শূন্য, আর code অনেক ছোট — ১ কোটিতম link-ও মাত্র ৪ অক্ষর (FXsk), ৬ অক্ষর শেষ হয় ৫,৬৮০ কোটিতে (ZZZZZZ). দাম অনুমেয়তা, নিচে দেখবেন।

Counter-এর block আর base62 — ধাপে ধাপে

প্রতিটি link-এর জন্য কেন্দ্রীয় counter-কে ডাকলে সেটিই single point of failure আর bottleneck. তাই প্রতিটি সার্ভার একবারে ১,০০০টি সংখ্যার একটি block নেয়। Counter থাকে একটি নির্ভরযোগ্য, consensus-ভিত্তিক store-এ (পর্ব ৪২), যেমন etcd বা ZooKeeper.

বর্ণমালা 0–9 (0–9), a–z (10–35), A–Z (36–61)
কাজ
ভাগফল · ভাগশেষ
নতুন অক্ষর
Code (ডান থেকে গড়া)
সার্ভার A-র block
কেন্দ্রীয় counter
  1. Block নেওয়া। সার্ভার A কেন্দ্রীয় counter থেকে একটি atomic কাজে পেল ১০,০০,০০০ থেকে ১০,০০,৯৯৯; counter এগিয়ে ১০,০১,০০০। পরের ১,০০০টি link-এ A আর কাউকে ডাকবে না।
  2. প্রথম ভাগ। ১০,০০,০০০ ÷ ৬২ = ১৬,১২৯, ভাগশেষ ২ → অক্ষর 2.
  3. দ্বিতীয় ভাগ। ১৬,১২৯ ÷ ৬২ = ২৬০, ভাগশেষ ৯ → 9. Code এখন 92.
  4. তৃতীয় ভাগ। ২৬০ ÷ ৬২ = ৪, ভাগশেষ ১২ → বর্ণমালার ১২ নম্বর অক্ষর c (a = ১০, b = ১১)। Code c92.
  5. শেষ ভাগ। ৪ ÷ ৬২ = ০, ভাগশেষ ৪ → 4. ভাগফল শূন্য, থামুন। Code 4c92 — ঠিক id_collisions.js-এর ফলের মতো।
  6. যাচাই আর crash. ৪×২,৩৮,৩২৮ + ১২×৩,৮৪৪ + ৯×৬২ + ২ = ৯,৫৩,৩১২ + ৪৬,১২৮ + ৫৫৮ + ২ = ১০,০০,০০০। ধরা যাক A ৪১৮টি link বানিয়ে crash করল: বাকি ৫৮২টি সংখ্যা আর কেউ পাবে না। ফাঁক থাকে, কিন্তু collision হয় না — এই দামটুকু মেনে নেওয়াই block-এর মূল চুক্তি।

অনুমেয়তা — code গুনে গুনে link খুঁজে বের করা

Counter-এর code পরপর: 4c92-এর পরে 4c93. তাহলে যে কেউ code গুনে গুনে সব link-এর destination দেখে ফেলতে পারেন — একে বলে enumeration. বহু মানুষ ছোট link-কে “গোপন” ভেবে ব্যক্তিগত ফাইল বা ঠিকানা শেয়ার করেন, তাই এটি privacy-র ঝুঁকি। সুরক্ষার উপায়:

  • Counter-কে এলোমেলো দেখানো: encode করার আগে একটি গোপন key-সহ এক-এক মিল (keyed permutation, যেমন ছোট একটি Feistel network) দিয়ে সংখ্যাটি ঘুরিয়ে দেওয়া। অনন্যতা থাকে, পরপর সংখ্যা আর পরপর code হয় না। এটি আড়াল, নিরাপত্তা নয়।
  • 404-এর হারে সীমা: একই উৎস থেকে অনেক অস্তিত্বহীন code খোঁজা হলে rate limit (পর্ব ৪৯) আর alert.
  • ছোট code কখনো গোপন চাবি নয়: ব্যক্তিগত শেয়ারে login-ভিত্তিক অনুমতি, অথবা অনেক লম্বা random token — ২২ অক্ষরের base62-এ প্রায় ১৩১ bit — যা গুনে শেষ করা কার্যত অসম্ভব (পর্ব ৫২)।

আরও গভীরে: ২০১৬ সালে Georgiev ও Shmatikov-এর গবেষণা “Gone in Six Characters: Short URLs Considered Harmful for Cloud Services” দেখায় যে ছোট code-এর জায়গা ঘুরে দেখে cloud storage-এর শেয়ার করা ফাইল আর map-এর দিকনির্দেশের link বের করা যায়; প্রতিবেদন অনুযায়ী এরপর কিছু সেবা code লম্বা করে বা শেয়ারের ধরন বদলায়। Twitter ২০১০ সালে তাদের Snowflake ID-র নকশা প্রকাশ্যে বর্ণনা করে, যা সময়ক্রমের ID-র চেনা নমুনা। আর Feistel-ভিত্তিক format-preserving encryption একটি সংখ্যাকে একই আকারের আরেকটি সংখ্যায় উল্টানো যায় এমনভাবে বদলায় — counter লুকানোর পরিচিত কৌশল।

৭ · Deep dive ২ — Redirect-এর পথ: 301 না 302, cache আর edge

চারটি redirect code — কোনটি কখন

HTTP-তে redirect-এর চারটি প্রধান code, এখন RFC 9110-এ (২০২২) একসাথে বর্ণিত; 308 যোগ হয়েছিল RFC 7538-এ (২০১৫)। বিস্তারিত Computer Networks-এর HTTP পর্বে।

Codeঅর্থMethod বদলাতে পারে?ডিফল্টে cache হয়?Shortener-এ ফল
301স্থায়ীভাবে সরে গেছেহ্যাঁ, পুরোনো client POST-কে GET করেহ্যাঁ, browser প্রায়ই দীর্ঘদিন মনে রাখেপরের click সার্ভারে আসে না — analytics আর block করার ক্ষমতা হারায়
302সাময়িকভাবে অন্যখানেহ্যাঁনা, header না বললেপ্রতিটি click আসে; সবচেয়ে প্রচলিত পছন্দ
307সাময়িক, method একইনানা, header না বললেGET-এর ক্ষেত্রে 302-এর মতোই
308স্থায়ী, method একইনাহ্যাঁ301-এর মতো সমস্যা

মূল trade-off একটি: 301 সার্ভারের ভার কমায়, কিন্তু নিয়ন্ত্রণ কেড়ে নেয়। Browser একবার 301 মনে রাখলে link নিষিদ্ধ করলেও সেই browser পুরোনো destination-এ যেতেই থাকবে, আর click গোনা হবে না। আমাদের requirement-এ click গোনা জরুরি, আর ক্ষতিকর link দ্রুত বন্ধ করা দরকার — তাই 302 আর Cache-Control: private, max-age=0.

KV store-এর সামনে cache

  • Cache-aside, TTL-সহ: miss-এ store থেকে এনে ভরা (পর্ব ২৮)। Link মুছলে বা নিষিদ্ধ হলে key-টি সরাসরি মুছে ফেলা, আর TTL হলো বাড়তি নিরাপত্তা (পর্ব ২৯)।
  • অস্তিত্বহীন code: কেউ লাখো ভুল code খুঁজলে প্রতিটিই store-এ যায় (cache penetration). ছোট TTL-এর negative caching, বা সব বৈধ code-এর একটি Bloom filter (পর্ব ৪৫) সামনে রাখা যায়।

কত বড় cache-এ কত লাভ? Link-এর জনপ্রিয়তা সাধারণত Zipf-এর মতো: অল্প link বিপুল click টানে, বাকিরা প্রায় কিছুই না। নিচের simulator ১০,০০০টি link-এর ওপর seeded Zipf-ধাঁচের ১০ লাখ redirect চালায় (গড়ে প্রতি link-এ ১০০টি পড়া, ঠিক ১০০:১-এর মতো), আর নানা আকারের LRU cache-এর hit rate মাপে।

// Seeded Zipf-like redirect traffic through an LRU cache: hit rate and database QPS.
function rng(seed) {                                    // mulberry32: seeded, repeatable
  return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const LINKS = 10000, REQS = 1000000, S = 0.9, PEAK_READS = 34722;   // assumptions
const cdf = new Float64Array(LINKS); let sum = 0;
for (let k = 0; k < LINKS; k++) { sum += 1 / (k + 1) ** S; cdf[k] = sum; }  // rank k+1 popularity
function pick(u) {                                      // binary search in the CDF
  let lo = 0, hi = LINKS - 1, x = u * sum;
  while (lo < hi) { const mid = (lo + hi) >> 1; if (cdf[mid] < x) lo = mid + 1; else hi = mid; }
  return lo;
}
const rand = rng(54), trace = new Int32Array(REQS);
for (let i = 0; i < REQS; i++) trace[i] = pick(rand());
console.log("share of clicks on the top 1% / 20% of links: " +
  (100 * cdf[LINKS / 100 - 1] / sum).toFixed(1) + "% / " + (100 * cdf[LINKS / 5 - 1] / sum).toFixed(1) + "%");
console.log("cache size (links)   hit rate   DB reads/s at peak");
for (const pct of [1, 5, 10, 20, 50]) {
  const cap = LINKS * pct / 100, lru = new Map(); let hits = 0, counted = 0;
  for (let i = 0; i < REQS; i++) {
    const key = trace[i], warm = i >= REQS / 10;           // skip the first 10% (cold start)
    if (lru.has(key)) { lru.delete(key); if (warm) hits++; }
    else if (lru.size >= cap) lru.delete(lru.keys().next().value);  // evict least recent
    lru.set(key, 1); if (warm) counted++;
  }
  const hr = hits / counted;
  console.log((pct + "% = " + cap).padStart(16) + (100 * hr).toFixed(1).padStart(12) + "%" +
    Math.round(PEAK_READS * (1 - hr)).toLocaleString("en-US").padStart(18));
}

আউটপুট:

share of clicks on the top 1% / 20% of links: 41.0% / 76.2%
cache size (links)   hit rate   DB reads/s at peak
        1% = 100        26.2%            25,639
        5% = 500        45.6%            18,893
      10% = 1000        55.7%            15,376
      20% = 2000        67.2%            11,398
      50% = 5000        84.9%             5,255
  • অল্প link, বেশি click: সবচেয়ে জনপ্রিয় ১% link পায় ৪১% click, আর জনপ্রিয় ২০% পায় ৭৬.২% click.
  • LRU আদর্শের চেয়ে কম পায়: ২০% আকারের LRU-র hit rate ৬৭.২%, যদিও জনপ্রিয় ২০%-এর ভাগ ৭৬.২%. কারণ একবার-দুবার খোলা অজনপ্রিয় link-ও এসে জায়গা নেয় আর জনপ্রিয়দের সরায়। এই ফাঁক কমাতে admission-ভিত্তিক policy (যেমন W-TinyLFU, পর্ব ২৯)।
  • DB-র ভার: ২০% cache-এ peak-এ KV store-এ যায় সেকেন্ডে ১১,৩৯৮টি পড়া, ৫০%-এ ৫,২৫৫টি। Cache-এর আকার দ্বিগুণ করলে লাভ ক্রমে কমে — বাঁকটি কোথায় চ্যাপ্টা হয়, তা দেখেই আকার ঠিক করুন।
  • Exponent-ই সব: S = 0.9 একটি অনুমান। S = 1.1 করে চালালে ২০% cache-এর hit rate ৮৪.৯% আর DB-তে ৫,২৪০/s হয়। আসল মান নিজের access log থেকে মাপতে হয়।

Hot link — একটি পোস্ট ভাইরাল হলে

ঈদের আগে বাসের টিকিটের একটি লিংক লাখো মানুষের কাছে একসাথে গেল। তখন সব অনুরোধ একই key-এ, তাই একটি cache node-এ — একটি hot key. প্রতিকার পর্ব ৩০ থেকে: redirect service-এর নিজের memory-তে কয়েক সেকেন্ডের ছোট local cache, hot key-এর কয়েকটি কপি আলাদা node-এ, আর miss-এ একসাথে আসা অনুরোধগুলোকে একটিতে মেলানো (request coalescing), যাতে store-এ একটিই পড়া যায়।

Edge-এ redirect

আরও এগোলে redirect-এর উত্তর CDN-এর edge-এ রাখা যায় (পর্ব ১৭): edge 302-কে ধরা যাক ৬০ সেকেন্ডের s-maxage দিয়ে রাখে, অথবা edge function একটি replicate করা KV-র কপি থেকে নিজেই উত্তর দেয়। লাভ কম latency আর origin-এ কম ভার। দাম দুটি। এক, click গোনা তখন edge-এর log থেকে করতে হয়। দুই, link নিষিদ্ধ করলে edge-এর কপি TTL শেষ হওয়া বা purge পর্যন্ত চলতে থাকে — তাই TTL-ই “নিষিদ্ধ link সর্বোচ্চ কতক্ষণ চলবে” প্রশ্নের উত্তর।

৮ · Deep dive ৩ — Click analytics: redirect না থামিয়ে গোনা

মূল নিয়ম: redirect কখনো analytics-এর জন্য অপেক্ষা করবে না। প্রতিটি click-এ database-এ UPDATE clicks = clicks + 1 করলে ভাইরাল link-এর সারিটি একটি hot row হয়ে lock-এ আটকাবে, আর analytics-এর store ধীর হলে redirect-ও ধীর হবে। তাই click একটি event হয়ে আলাদা পথে যায়:

  1. Event: {event_id, code, ts, visitor, country, device}. Visitor হলো IP আর browser-এর তথ্যের hash, প্রতিদিন বদলানো salt-সহ, যাতে কাঁচা IP জমা না থাকে (PII, পর্ব ৫২)।
  2. Log-এ লেখা: redirect service ছোট batch-এ Kafka-ধাঁচের একটি log-এ পাঠায়; partition key code, যাতে একটি link-এর সব event একই partition-এ ক্রমে থাকে (পর্ব ৩৩)। Log বিকল হলে event হারানো মেনে নেওয়া যায়, redirect-এর ব্যর্থতা নয়।
  3. Stream processing: consumer group event পড়ে, event time ধরে এক মিনিটের tumbling window-এ গোনে, আর watermark পেরোলে window বন্ধ করে (পর্ব ৩৬)।
  4. Analytics store: মিনিট, ঘণ্টা আর দিনের সারাংশ একটি column store-এ (পর্ব ১৯), যেখানে “গত ৭ দিনে দেশ ধরে click” দ্রুত চলে।

At-least-once আর dedup

Log থেকে পড়ার সাধারণ নিশ্চয়তা at-least-once: consumer কাজ করে offset commit করার আগে crash করলে একই event আবার আসে (পর্ব ৩৪)। Counter-এ যোগ idempotent নয়, তাই event id দিয়ে dedup লাগে। নিচে একটি event-এর জীবন দেখুন।

event e-901 · code 4c92 · 10:00:07 · visitor v-3f2a
ঘটনা
Log offset
Dedup store-এ e-901
(4c92, 10:00) click
HLL (4c92, আজ) unique
Committed offset
  1. Append. Redirect service event-টি partition-এ পাঠাল, offset ৫০১২। Consumer শেষবার ৫০১১ পর্যন্ত “পড়েছি” বলেছে।
  2. কাজ। Consumer দেখল e-901 আগে দেখা হয়নি। মিনিটের সংখ্যা ১৭ থেকে ১৮, আর dedup-এর চিহ্ন — দুটোই একই transaction-এ লেখা (inbox pattern). নতুন visitor, তাই HyperLogLog-এর অনুমান ১৩।
  3. Crash. Offset commit-এর ঠিক আগে consumer বন্ধ হলো। লেখা হয়ে গেছে, কিন্তু log জানে না।
  4. আবার পড়া। নতুন consumer ৫০১২ থেকে শুরু করে আবার e-901 পেল। Dedup store বলল “আগেই গোনা”, তাই skip; সংখ্যা ১৮-ই থাকল। HyperLogLog-এ একই visitor আবার যোগ করলেও অনুমান বদলায় না — set-এর মতো এটি স্বভাবতই idempotent.
  5. Window বন্ধ। ১০:০০–১০:০১-এর window-এর শেষ সময়, আর ধরা যাক ২ মিনিটের দেরির ছাড়, watermark পেরোলে (১০:০৩) সারি (4c92, 10:00) = ১৮ চূড়ান্ত হয়ে analytics store-এ গেল। Dedup-এর চিহ্ন শুধু এই ছাড়ের চেয়ে একটু বেশি সময় রাখলেই চলে।

Unique visitor — নির্ভুল না আনুমানিক

প্রতিটি link আর দিনের জন্য সব visitor-এর একটি set রাখলে জনপ্রিয় link-এ লাখো entry, প্রতিটি কয়েক দশ byte. HyperLogLog (পর্ব ৪৫) কয়েক KB-তে প্রায় ১%-এর মতো ভুলে একই উত্তর দেয় — Redis-এর বাস্তবায়নে documentation অনুযায়ী প্রতি key প্রায় ১২ KB আর standard error প্রায় ০.৮১%. আরও দুটি সুবিধা: মিনিটের HLL মিলিয়ে ঘণ্টা বা দিনের HLL বানানো যায় (union), আর একই visitor দুবার এলেও ফল বদলায় না। ছোট link-এ, যেখানে visitor কম, নির্ভুল set-ই সস্তা।

নিচের ছোট stream-এ দুটোই দেখুন: ৬০টি আসল click-এর ১৫%-এর মতো দুবার এসেছে, যেমন consumer retry-তে হয়। Dedup ছাড়া আর dedup-সহ মিনিটের সংখ্যা, আর প্রতি link-এর নির্ভুল unique visitor:

// Click analytics on a small seeded stream: clicks per link per minute, with and without dedup,
// plus exact unique visitors. The stream is at-least-once, so some events arrive twice.
function rng(seed) {                                    // mulberry32: seeded, repeatable
  return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const rand = rng(2026), codes = ["eid24", "job7Q", "x9Lp2"], events = [];
for (let id = 1; id <= 60; id++) {
  const code = codes[Math.floor(rand() ** 2 * 3)];      // eid24 is the most popular
  const ev = { id, code, minute: "10:0" + Math.floor(rand() * 3), visitor: "v" + Math.floor(rand() * 15) };
  events.push(ev);
  if (rand() < 0.15) events.push({ ...ev });             // redelivered after a consumer retry
}
const raw = {}, clean = {}, seen = new Set(), uniq = {};
for (const e of events) {
  const k = e.code + " " + e.minute;
  raw[k] = (raw[k] || 0) + 1;
  if (seen.has(e.id)) continue;                          // dedup on event id
  seen.add(e.id);
  clean[k] = (clean[k] || 0) + 1;
  (uniq[e.code] = uniq[e.code] || new Set()).add(e.visitor);
}
console.log("events received " + events.length + ", distinct " + seen.size);
console.log("link  minute  raw  dedup");
for (const k of Object.keys(raw).sort())
  console.log(k.replace(" ", "  ") + String(raw[k]).padStart(5) + String(clean[k]).padStart(7));
for (const c of codes) {
  const clicks = events.filter(e => e.code === c).length;
  console.log(c + ": unique visitors " + uniq[c].size + " (raw clicks " + clicks + ")");
}

আউটপুট:

events received 71, distinct 60
link  minute  raw  dedup
eid24  10:00   19     17
eid24  10:01   16     12
eid24  10:02   11      8
job7Q  10:00    4      3
job7Q  10:01    4      4
job7Q  10:02    7      6
x9Lp2  10:00    3      3
x9Lp2  10:01    3      3
x9Lp2  10:02    4      4
eid24: unique visitors 13 (raw clicks 46)
job7Q: unique visitors 9 (raw clicks 15)
x9Lp2: unique visitors 8 (raw clicks 10)
  • দুবার আসা event সংখ্যা ফোলায়: ৭১টি event পৌঁছেছে, আসল ৬০টি। eid24-এর ১০:০১-এ dedup ছাড়া ১৬, আসলে ১২ — এক মিনিটেই ৩৩% বেশি। ছোট সংখ্যায় এই ভুল এলোমেলো, তাই “গড়ে ১৫% বাদ দিই” কৌশলেও চলে না।
  • Click আর মানুষ আলাদা: eid24-এ ৪৬টি কাঁচা click, অথচ আলাদা visitor মাত্র ১৩ জন। Unique-এর হিসাব dedup ছাড়াও ঠিক আসত, কারণ set-এ একই visitor দুবার যোগ হয় না — stepper-এর HLL-এর মতো।

ভাইরাল link-এর সব event একটি partition-এ গেলে সেটি গরম হয়; redirect service নিজের memory-তে প্রতি সেকেন্ডের গণনা জমিয়ে একটি event পাঠালে (pre-aggregation) ভার অনেক কমে।

৯ · ভাঙন, trade-off আর বিস্তার

কোনটি মারা গেলে কী হয়

যা ভাঙলফলনকশার উত্তর
একটি cache nodeতার key-গুলোর সব পড়া হঠাৎ KV store-এReplica-সহ cache, ধাপে ধাপে warm-up, store-এর সামনে load shedding (পর্ব ১৮)
KV store-এর একটি replica বা leaderকিছু সেকেন্ড ধরে সেই shard-এ লেখা আটকে, পড়া replica থেকে চলেReplication আর failover (পর্ব ২২); redirect-এর পড়া একটু পুরোনো হলেও চলে
ID service বা counter-এর storeনতুন block পাওয়া যায় নাহাতের block শেষ না হওয়া পর্যন্ত লেখা চলে; block বড় রাখলে বিভ্রাট সহ্য করার সময় বাড়ে
URL যাচাইয়ের সেবানতুন link-এর নিরাপত্তা যাচাই হয় নাFail closed: link তৈরি হয় “যাচাই বাকি” অবস্থায়, redirect-এর আগে সতর্কতার পাতা
Click streamEvent পাঠানো যায় নাMemory-র সীমিত buffer, ভরে গেলে event ফেলে দেওয়া; redirect কখনো আটকায় না
পুরো একটি regionসেখানকার সব ব্যবহারকারীঅন্য region-এ DNS failover, সেখানে data-র কপি (পর্ব ৫১)

১০ গুণ বড় হলে

দিনে ১০ কোটি নতুন link মানে peak-এ সেকেন্ডে প্রায় ৩,৪৭,২২২টি redirect আর ৫ বছরে তিন কপিতে প্রায় ২৭৪ TB. Write তখনও সেকেন্ডে প্রায় ৩,৫০০ — সামলানো যায়, তবে ID-র block বড় করতে হয়। প্রথম চাপ পড়বে cache-এর RAM আর hot key-এ, তাই edge-এ redirect রাখার যুক্তি শক্ত হয়। Analytics store-ই সম্ভবত সবচেয়ে বড় খরচ হয়ে দাঁড়াবে, কারণ কাঁচা click event আর মিনিটের সারাংশ দ্রুত জমে; তাই পুরোনো ডেটা ঘণ্টা বা দিনের সারাংশে নামানো আর retention-এর নিয়ম লাগে।

অপব্যবহার থেকে সুরক্ষা (প্রতিরক্ষার দিক থেকে)

  • ক্ষতিকর URL-এর যাচাই: তৈরির সময় পরিচিত phishing আর malware ঠিকানার তালিকার সাথে মেলানো (URL reputation), কিছুদিন পরপর আবার যাচাই, আর ব্যবহারকারীর রিপোর্টে দ্রুত পর্যালোচনা। সন্দেহজনক link-এ সরাসরি redirect নয়, সতর্কতার পাতা।
  • Rate limiting: link তৈরিতে account আর IP ধরে সীমা, নাম-পরিচয়হীন ব্যবহারে কড়া সীমা; অস্তিত্বহীন code-এর খোঁজেও সীমা (পর্ব ৪৯)। বিপুল ঢলে edge-এর DDoS ছাঁকনি।
  • নিষিদ্ধ করার গতি: নিষিদ্ধ link-এর key cache থেকে মুছে ফেলা আর CDN-এ purge; edge আর browser-এ যত লম্বা TTL, link তত বেশি সময় চলবে — এজন্যই 302.

মেয়াদ শেষে পরিষ্কার

দুই স্তরে কাজ। পড়ার সময়ই মেয়াদ যাচাই (lazy) — অংশ ৪-এর SQL-এর মতো, তাই মেয়াদোত্তীর্ণ link কখনো redirect হয় না। আর রাতে কম ভিড়ের সময় একটি background job ধীরে ধীরে সেগুলো মুছে ফেলে (পর্ব ৩৭); কিছু KV store-এ নিজস্ব TTL-ও আছে (পণ্যভেদে আলাদা)। Code আবার ব্যবহার করবেন না: পুরোনো SMS বা পোস্টারের QR code তখন নতুন কারও পাতায় নিয়ে যাবে। ৭ অক্ষরের জায়গায় পুনর্ব্যবহারের দরকারও নেই।

একাধিক region-এ পড়া

প্রতিটি region-এ KV store-এর কপি, asynchronous replication-এ (পর্ব ২২)। ফাঁদ: ঢাকার কাছের region-এ link তৈরি হলো, আর কয়েক মুহূর্ত পরে অন্য region-এর কেউ খুললেন — replication lag-এর কারণে সেখানে এখনো নেই, ফল 404. প্রতিকার: local miss হলে link-এর মূল region-কে জিজ্ঞেস করা (code-এর ভেতরে region-এর চিহ্ন রাখলে সহজ), আর 404-কে খুব অল্প সময়ের জন্যই negative cache করা। লেখার দিক আর alias-এর বিশ্বজোড়া অনন্যতা ভাবনার প্রশ্ন ৫-এ।

কী মাপবেন, কিসে alert

  • Redirect-এর সফলতার হার আর p99 latency — এটিই মূল SLO (পর্ব ৫০)।
  • Cache hit ratio হঠাৎ কমা; 404-এর হার হঠাৎ বাড়া (ভুল deploy, বা কেউ code গুনছে)।
  • Analytics consumer-এর lag; URL যাচাইয়ের অপেক্ষমাণ তালিকা; ID block শেষ হওয়ার গতি।
ইন্টারভিউর ৬০ সেকেন্ডের সারাংশ

“এটি read-heavy সেবা: দিনে ১ কোটি নতুন link আর ১০০:১ পড়া ধরে peak-এ সেকেন্ডে প্রায় ৩৫ হাজার redirect, আর লেখা প্রায় ৩৫০। Data model একটিই key-তে: code থেকে URL, KV store-এ code ধরে hash-shard, তিন কপি। Code আসে counter-এর block থেকে, base62-এ, একটি গোপন permutation দিয়ে এলোমেলো করা — collision নেই, আর গুনে বের করা কঠিন; সব লেখা conditional. Redirect stateless সার্ভিসে, সামনে cache আর ইচ্ছা হলে edge; analytics আর block করার ক্ষমতার জন্য 302. Click event async stream-এ, event id দিয়ে dedup, unique visitor HyperLogLog-এ। ঝুঁকি: hot link, cache node হারানো, ক্ষতিকর URL আর region-এর lag — প্রতিটির উত্তর আছে। ১০ গুণে প্রথমে edge আর cache বাড়াব।”

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই case-এ দুর্বল আর শক্ত উত্তরের পার্থক্য):

  • Code কীভাবে বানাবেন? — দুর্বল: “MD5-এর প্রথম ৭ অক্ষর।” শক্ত: birthday-র অঙ্ক দেখিয়ে collision-এর হিসাব, conditional write, আর counter বনাম random-এর অনুমেয়তার trade-off.
  • 301 না 302? — দুর্বল: “301, কারণ স্থায়ী।” শক্ত: 301 cache হয়ে analytics আর block করার ক্ষমতা হারায়; requirement অনুযায়ী 302 বা সীমিত max-age-সহ 301.
  • Scale কীভাবে? — দুর্বল: “Kafka, Redis আর microservices.” শক্ত: আগে সংখ্যা, তারপর বলা চাপ কোথায় (পড়ায়), cache-এর আকার আর hit rate-এর অনুমান।
  • Analytics? — দুর্বল: প্রতিটি click-এ database-এ counter বাড়ানো। শক্ত: async event, at-least-once আর dedup, HyperLogLog, redirect-এর পথ থেকে আলাদা।
  • আর কী ভাববেন? — শক্ত উত্তরে নিজে থেকেই আসে অপব্যবহার, মেয়াদ, code পুনর্ব্যবহার না করা আর বহু region-এর lag.

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

AI-কে URL shortener চাইলে কয়েক সেকেন্ডে পরিচ্ছন্ন, চেনা ছাঁচের একটি উত্তর আসে। ভুলগুলো লুকায় ছাঁচের ফাঁকে:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
“MD5-এর প্রথম ৬-৭ অক্ষর নিন, collision প্রায় হয় না”Birthday-র অঙ্কে ৭ অক্ষরেও কোটি link-এ গড়ে ১৪টির মতোn²/(2M) নিজে হিসাব করুন; অংশ ৬-এর simulator চালান
৬২৭-কে “৩.৫ billion” বলা, বা QPS-এ শূন্যের ভুলআসলে ৩.৫২ trillion; এক শূন্যের ভুলে নকশা ১০ গুণ ভুল আকারেরপ্রতিটি সংখ্যা estimator-এ আবার চালান
সেকেন্ডে কয়েকশো লেখায় Kafka, microservices আর বহু databaseঅপ্রয়োজনীয় জটিলতা; চাপ পড়ার পথে, লেখায় নয়জিজ্ঞেস করুন: কোন সংখ্যা এই বাক্সটিকে দরকারি করে?
সবসময় 301, “SEO-র জন্য ভালো”Analytics আর নিষিদ্ধ করার ক্ষমতা হারায়Requirement-এর তালিকার সাথে মেলান
প্রতিটি click-এ একই সারিতে counter বাড়ানোভাইরাল link-এ hot row, redirect ধীরFailure-mode পর্যালোচনা: এক link-এ সেকেন্ডে ১০ হাজার click হলে কী?
“Consistency-র জন্য CP database” — ACID আর CAP-এর consistency গুলিয়ে ফেলাদুটি আলাদা ধারণা (পর্ব ৪০); redirect-এ সামান্য পুরোনো পড়া চলে, alias-এ চলে নাপ্রতিটি পথে আলাদা করে জিজ্ঞেস করুন কোন নিশ্চয়তা লাগবে
পণ্যের সীমা বা দাম বানিয়ে বলা (“Redis এক node-এ ১০ লাখ ops/s”)সংস্করণ, hardware আর কাজভেদে আলাদাঅফিসিয়াল documentation; নিজের load test

AI-কে সংখ্যা, শর্ত আর ব্যর্থতার প্রশ্ন দিয়ে বাঁধুন, আর প্রতিটি অনুমান আলাদা লিখতে বলুন:

তুমি একজন অভিজ্ঞ backend প্রকৌশলী। একটি কাল্পনিক URL shortener নকশা করো। ধরা যাক দিনে ১ কোটি নতুন link, read:write ১০০:১, peak factor ৩, প্রতিটি record ৫০০ byte, ৫ বছর রাখতে হবে, ৩টি কপি। প্রতিটি click গোনা জরুরি, আর ক্ষতিকর link ৫ মিনিটের মধ্যে সবখানে বন্ধ হতে হবে। ১. QPS, storage আর cache-এর হিসাব দাও, প্রতিটি সূত্র আর অনুমান আলাদা লিখে। ২. Code-এর দৈর্ঘ্য আর তৈরির কৌশল বেছে নাও; random হলে birthday bound দিয়ে collision-এর সংখ্যা দেখাও, আর অনুমেয়তার ঝুঁকি কীভাবে কমাবে বলো। ৩. 301, 302, 307 না 308 — কেন? Browser আর CDN-এর cache-এর TTL কত রাখবে যাতে ৫ মিনিটের শর্ত মেলে? ৪. Click analytics-এর pipeline দাও; at-least-once হলে dedup কীভাবে? ৫. Cache node, KV store-এর leader, ID service আর একটি পুরো region বিকল হলে কী হবে? ৬. কোনো পণ্যের সীমা বা দাম নিশ্চিত না হলে বলো "নিশ্চিত নই"। কোনো বাক্স যোগ করলে কোন সংখ্যা সেটিকে দরকারি করে, তা লেখো।

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

AI দেয় চেনা ছাঁচ, সংখ্যা আর ব্যর্থতার পথ মেলানোর দায় আপনার।
অঙ্ক নিজে করুন, simulator চালান, প্রতিটি বাক্সকে প্রশ্ন করুন।

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

এগুলো ইন্টারভিউয়ারের সম্ভাব্য পরের প্রশ্নের ধরন। নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১301 তো “সঠিক” code মনে হয় — link তো আর বদলাবে না। তবু অনেক shortener 302 বা 307 দেয় কেন? কখন 301 ঠিক?

HTTP-র ভাষায় 301 বলে “এই resource স্থায়ীভাবে অন্য ঠিকানায়”, আর 302 বলে “এখনকার জন্য অন্যখানে”। শব্দের অর্থে ছোট link-এর destination স্থায়ী, তাই 301-ই ঠিক মনে হয়। সমস্যা আসে cache-এর নিয়মে। RFC 9110 অনুযায়ী 301 আর 308 ডিফল্টে cache করা যায়; আর অনেক browser এমন redirect দীর্ঘদিন মনে রাখে, প্রায়ই ব্যবহারকারী নিজে cache না মোছা পর্যন্ত। ফল: দ্বিতীয়বার একই link চাপলে browser আপনার সার্ভারকে জিজ্ঞেসই করে না, সরাসরি destination-এ যায়।

এতে তিনটি জিনিস হারায়। এক, click গোনা — দ্বিতীয় থেকে শুরু করে প্রতিটি click অদৃশ্য। দুই, নিয়ন্ত্রণ — link-টি পরে phishing প্রমাণিত হলে আপনি নিষিদ্ধ করলেন, কিন্তু যাঁদের browser 301 মনে রেখেছে, তাঁরা পুরোনো ঠিকানায় যেতেই থাকবেন। তিন, destination বদলের সুযোগ, যদি কখনো সেই সুবিধা দিতে চান। এজন্যই analytics-নির্ভর সেবা সাধারণত 302 বা 307 দেয়, প্রায়ই Cache-Control: private, max-age=0-সহ, যাতে প্রতিটি click সার্ভারে আসে।

307 আর 308 এসেছে একটি পুরোনো অস্পষ্টতা দূর করতে: ঐতিহাসিকভাবে অনেক client 301 আর 302-এ POST-কে GET বানিয়ে ফেলত। 307 আর 308 method বদলানো নিষেধ করে। Shortener-এ প্রায় সব অনুরোধই GET, তাই এখানে 302 আর 307-এর আচরণ কার্যত এক।

301 কখন ঠিক? যখন analytics লাগে না, link বদলাবে না, আর সার্ভারের ভার কমানোই লক্ষ্য — যেমন একটি প্রতিষ্ঠানের নিজের পুরোনো ঠিকানা থেকে নতুন ঠিকানায় স্থায়ী স্থানান্তর। মাঝামাঝি পথও আছে: 301-এর সাথে স্পষ্ট max-age, যেমন এক দিন, যাতে browser-এর স্মৃতির একটি সীমা থাকে। ইন্টারভিউতে উত্তরটি একটি code নয়, একটি যুক্তি: “আমাদের requirement-এ প্রতিটি click গোনা আর দ্রুত নিষিদ্ধ করা জরুরি, তাই 302; ভার কমাতে cache রাখব আমাদের নিজেদের স্তরে, যেখানে নিয়ন্ত্রণ আমাদের হাতে।”

মূল উপলব্ধি: Cache যত দূরে (browser), ভার তত কম কিন্তু নিয়ন্ত্রণও তত কম। কোন স্তরে cache রাখবেন, সেটিই আসল নকশার সিদ্ধান্ত।

প্র ০২ইন্টারভিউয়ার বললেন, “আপনার counter-ভিত্তিক code তো পরপর — কেউ সব link গুনে দেখে ফেলতে পারবে। এটি কি সত্যিকারের সমস্যা?”

হ্যাঁ, এবং এর প্রকাশ্য প্রমাণ আছে। ২০১৬ সালে Cornell Tech-এর গবেষক Martin Georgiev ও Vitaly Shmatikov “Gone in Six Characters: Short URLs Considered Harmful for Cloud Services” নামে একটি গবেষণা প্রকাশ করেন। তাঁরা দেখান, ছোট code-এর জায়গা এত ছোট যে তার বড় অংশ ঘুরে দেখা সম্ভব, আর তাতে cloud storage-এর “link থাকলেই খোলা যায়” ধরনের শেয়ার করা ফাইল, এমনকি map-এ বাড়ির দিকনির্দেশ বেরিয়ে আসে। প্রতিবেদন অনুযায়ী এরপর কিছু সেবা code লম্বা করে বা শেয়ারের ধরন বদলায়। লক্ষ্য করুন, কোনো system “ভাঙা” হয়নি; সমস্যা ছিল নকশার অনুমানে — ছোট code-কে গোপন চাবি ভাবা।

পরপর counter এই কাজ আরও সহজ করে, কারণ খুঁজতেই হয় না, শুধু গুনতে হয়। Random ৭ অক্ষরেও পুরোপুরি নিরাপদ নয়: ৩.৫২ লাখ কোটির জায়গায় কয়েকশো কোটি link থাকলে প্রতি কয়েকশো থেকে কয়েক হাজার এলোমেলো চেষ্টায় একটি মেলে। তাই সুরক্ষা কয়েক স্তরে:

  • নীতিতে: ছোট link জনসমক্ষে প্রকাশের জন্য; ব্যক্তিগত কিছু শেয়ার করতে হলে login-ভিত্তিক অনুমতি, অথবা প্রায় ১২৮ bit বা তার বেশি random token — ২২ অক্ষরের base62-এ প্রায় ১৩১ bit.
  • নকশায়: counter রাখলেও একটি গোপন key-সহ উল্টানো যায় এমন permutation দিয়ে সংখ্যা ঘুরিয়ে তারপর encode — পরপর link আর পরপর code হয় না, অনন্যতাও অটুট।
  • চালানোয়: অস্তিত্বহীন code-এর অনুরোধে rate limit, একটি উৎস থেকে 404-এর হার বাড়লে alert, আর অনুসন্ধানের ধরন দেখলে কড়া যাচাই।

দাম হিসাব করে বলাও শক্ত উত্তরের অংশ: permutation-এ code আর ছোট থেকে বড় হয় না, শুরু থেকেই সব code প্রায় পূর্ণ দৈর্ঘ্যের হয়; আর random token-এ link লম্বা হয়, যা SMS-এ অক্ষর খরচ বাড়ায়।

মূল উপলব্ধি: “অনুমান করা কঠিন” আর “গোপন” এক কথা নয়। নিরাপত্তা আসে অনুমতি থেকে; code এলোমেলো করা শুধু বাড়তি একটি স্তর।

প্র ০৩একটি phishing link ভাইরাল হয়ে গেছে — সেকেন্ডে হাজারো click. আপনি সেটি নিষিদ্ধ করলেন। কতক্ষণে সব ব্যবহারকারীর জন্য বন্ধ হবে, আর সেই সময় কীভাবে কমাবেন?

উত্তরটি নির্ভর করে link-এর কপি কোথায় কোথায় আছে আর প্রতিটির TTL কত তার ওপর। সত্যের উৎস KV store-এ status blocked লেখা মুহূর্তের কাজ। কিন্তু পথে তিনটি স্তর পুরোনো কপি রাখে: redirect service-এর local cache (ধরা যাক ৫ সেকেন্ড), ভাগ করা cache (১ ঘণ্টা TTL), আর edge (ধরা যাক ৬০ সেকেন্ড)। Browser-এ কিছু নেই, কারণ 302 আর max-age=0. শুধু TTL-এর ভরসায় থাকলে সবচেয়ে খারাপ ক্ষেত্রে এক ঘণ্টা — phishing-এর জন্য অনেক বেশি।

তাই নিষিদ্ধ করা একটি সক্রিয় কাজ: KV store-এ লেখার পর একটি “invalidate 4c92” event, যা ভাগ করা cache থেকে key মোছে, CDN-এ purge পাঠায় (পর্ব ১৭), আর সব redirect service-কে local কপি ফেলতে বলে (পর্ব ২৯)। তখন সময় মাপে purge-এর গতি, সাধারণত কয়েক সেকেন্ড; আর TTL থাকে শুধু event হারালে সর্বোচ্চ সীমা হিসেবে। একাধিক region থাকলে ক্রমও জরুরি: আগে store, তারপর cache; উল্টো হলে কোনো miss পুরোনো active মান আবার cache-এ ভরতে পারে (পর্ব ৩০-এর race).

ভাইরাল বলে আরেকটি সমস্যা: link-টি hot key. নিষিদ্ধ করার মুহূর্তে key মুছলে হাজারো অনুরোধ একসাথে store-এ যাবে। তাই key মোছার বদলে “blocked” মানটি দিয়েই key প্রতিস্থাপন করা ভালো, যাতে পরের অনুরোধগুলো cache থেকেই সতর্কতার পাতা পায়।

আর প্রতিরোধ: তৈরির সময় URL reputation যাচাই, নতুন বা নাম-পরিচয়হীন account-এর link-এ প্রথম কিছু ঘণ্টা কড়া নজর, হঠাৎ click বেড়ে যাওয়া নতুন link-কে আবার যাচাই, আর রিপোর্ট বোতাম। Alert-এর জন্য একটি SLI: “রিপোর্ট থেকে পুরো বন্ধ হওয়া পর্যন্ত সময়”।

মূল উপলব্ধি: প্রতিটি cache একটি প্রতিশ্রুতি ভাঙার জায়গা। “সর্বোচ্চ কত দেরি” প্রশ্নের উত্তর দিন TTL দিয়ে, আর গড় দেরি কমান সক্রিয় invalidation দিয়ে।

প্র ০৪Link-এর মালিক জানতে চান “কতজন আলাদা মানুষ দেখেছেন”। নির্ভুল উত্তর দেবেন, না আনুমানিক? Privacy আর bot-এর কী হবে?

প্রথম প্রশ্ন, “আলাদা মানুষ” মানে কী। আমাদের কাছে আছে IP, browser-এর তথ্য আর হয়তো একটি cookie — কোনোটিই মানুষ নয়। একই অফিসের শতজন একই IP-তে থাকতে পারেন (NAT, CGNAT), আর একজনই ফোন আর ল্যাপটপে দুটি “visitor”. তাই সংখ্যাটি শুরু থেকেই একটি অনুমান; ড্যাশবোর্ডে তা বলে দেওয়া সততার অংশ।

দ্বিতীয় প্রশ্ন খরচ। প্রতি link প্রতি দিনের জন্য সব visitor-এর একটি set রাখলে জনপ্রিয় link-এ লাখো entry, আর বহু link মিলিয়ে বিপুল memory. HyperLogLog (Flajolet ও সহকর্মীরা, ২০০৭) প্রতিটি hash-এর শুরুর শূন্যের দৈর্ঘ্য দেখে cardinality আন্দাজ করে, কয়েক KB-তে প্রায় ১%-এর মতো ভুলে (পর্ব ৪৫)। Google-এর HyperLogLog++ (২০১৩) ছোট সংখ্যায় এর নির্ভুলতা বাড়ায়। Redis-এর documentation অনুযায়ী এর বাস্তবায়নে প্রতি key প্রায় ১২ KB আর standard error প্রায় ০.৮১%. বড় সুবিধা দুটি: মিনিটের HLL মিলিয়ে দিনের, আর region-এর HLL মিলিয়ে বিশ্বের হিসাব বানানো যায়; আর একই visitor দুবার যোগ করলেও ফল বদলায় না, তাই at-least-once stream-এ dedup ছাড়াই টেকে।

তৃতীয় প্রশ্ন privacy. কাঁচা IP দীর্ঘদিন রাখা বহু দেশের আইনে স্পর্শকাতর ব্যক্তিগত তথ্য। নিরাপদ পথ: IP আর browser-এর তথ্যের সাথে প্রতিদিন বদলানো গোপন salt মিলিয়ে hash, শুধু সেই hash HLL-এ, আর salt দিন শেষে মুছে ফেলা — তখন দুই দিনের visitor মেলানোও যায় না, যা privacy-র জন্য ভালো, analytics-এর জন্য একটি সচেতন দাম (পর্ব ৫২)।

চতুর্থ প্রশ্ন bot. Messaging app link-এর preview বানাতে নিজেই link খোলে, search engine-এর crawler-ও খোলে। এগুলো user-agent আর আচরণ দেখে চিহ্নিত করে আলাদা কলামে রাখুন; মুছে ফেললে পরে নিয়ম বদলানো যায় না।

মূল উপলব্ধি: নির্ভুলতার আগে সংজ্ঞা। সংখ্যাটি কী মাপে, কতটা ভুল হতে পারে, আর কী তথ্য রাখা হয় না — তিনটিই নকশার অংশ।

প্র ০৫সেবাটি এখন তিন মহাদেশে। একজন ব্যবহারকারী link বানিয়ে সঙ্গে সঙ্গে বিদেশে বন্ধুকে পাঠালেন, বন্ধু পেলেন 404. কেন, আর পুরো নকশা কীভাবে বদলাবেন?

কারণ replication lag (পর্ব ২২)। প্রতিটি region-এ KV store-এর কপি, আর region-এর মধ্যে replication asynchronous — synchronous করলে প্রতিটি লেখায় মহাদেশ পেরোনো round trip, কয়েকশো ms. লেখা হলো এশিয়ার region-এ, বন্ধুর অনুরোধ গেল ইউরোপের region-এ, যেখানে record পৌঁছাতে ধরা যাক এক-দুই সেকেন্ড বাকি। এর ওপর ইউরোপের cache যদি “নেই” উত্তরটি negative cache করে, তাহলে কয়েক মিনিট ধরে 404 — সবচেয়ে খারাপ অংশ এটাই।

প্রতিকার তিনটি। এক, code-এর ভেতরে মূল region-এর চিহ্ন রাখা (যেমন counter-এর block region ধরে আলাদা পরিসরে), যাতে local miss-এ redirect service জানে কাকে জিজ্ঞেস করতে হবে; দূরের region থেকে একবার পড়া ধীর, কিন্তু 404-এর চেয়ে অনেক ভালো। দুই, link-এর বয়স কয়েক সেকেন্ড হলে negative cache না করা, বা খুব ছোট TTL. তিন, নির্মাতার নিজের জন্য read-your-writes: তাঁর app লেখা region-এই পড়বে।

লেখার দিকে প্রশ্ন আলাদা। Counter-এর code-এ সমন্বয় লাগে না, কারণ প্রতিটি region-এর নিজের পরিসর। কিন্তু custom alias বিশ্বজুড়ে এক — দুজন দুই মহাদেশে একসাথে eid-offer চাইলে একজনকেই পেতে হবে। তাই alias-এর লেখা যায় একটি মূল region-এ বা একটি consensus-ভিত্তিক global store-এ, শর্তসাপেক্ষে; alias তৈরি তাই একটু ধীর, যা বিরল কাজের জন্য গ্রহণযোগ্য। এখানে CAP-এর ভাষায় একটি স্পষ্ট বাছাই (পর্ব ৪০): redirect-এর পড়ায় availability, alias-এর অনন্যতায় consistency.

পুরো region বিকল হলে DNS অন্য region-এ পাঠায় (পর্ব ৫১)। শেষ কয়েক সেকেন্ডের লেখা না পৌঁছালে সেই link কিছুক্ষণ 404 দিতে পারে — এটিই RPO-র দাম, আগেই মেনে নেওয়া ও লেখা থাকা উচিত।

মূল উপলব্ধি: একই সেবার আলাদা কাজ আলাদা নিশ্চয়তা চায়। পড়া local আর দ্রুত, অনন্যতা কেন্দ্রীয় আর নিশ্চিত — এই ভাগটি স্পষ্ট বলাই senior উত্তরের চিহ্ন।

অনুশীলন

প্রথমটি কাগজ-কলমে হিসাব করুন, তারপর estimator-এ মান বদলে মেলান। চতুর্থটিতে আগে অনুমান, তারপর চালিয়ে যাচাই।

  1. নতুন সংখ্যায় হিসাব: একটি কাল্পনিক অফিস-ভিত্তিক shortener-এ ধরা যাক দিনে ২০ লাখ নতুন link, read:write ২০০:১, peak factor ৪, record ৪০০ byte, ৩ বছর, ৩ কপি। (ক) গড় আর peak-এর write ও read QPS (খ) ৩ বছরের link-সংখ্যা আর storage (গ) counter হলে কত অক্ষর, আর random code-এ জায়গা link-এর ১০০ গুণ রাখলে কত অক্ষর?
    • (ক) Write: ২০,০০,০০০ ÷ ৮৬,৪০০ ≈ ২৩.১৫/s, peak ≈ ৯২.৬/s. Read: × ২০০ ≈ ৪,৬৩০/s, peak ≈ ১৮,৫১৯/s.
    • (খ) ২০ লাখ × ৩৬৫ × ৩ = ২.১৯ × ১০৯, অর্থাৎ ২১৯ কোটি link. × ৪০০ byte ≈ ৮৭৬ GB কাঁচা, তিন কপিতে ≈ ২.৬৩ TB — কয়েকটি মেশিনেই আঁটে।
    • (গ) ৬২৫ = ৯১,৬১,৩২,৮৩২ — ২১৯ কোটির কম, তাই counter-এ ৬ অক্ষর (৬২৬-এর ৩.৮৬% ভরবে)। Random-এ লাগবে ২.১৯ × ১০১১-এর বেশি জায়গা; ৬২৬ ≈ ৫.৬৮ × ১০১০ যথেষ্ট নয়, তাই ৭ অক্ষর (৩ বছরে ০.০৬২% ভরবে)। Estimator-এর input বদলে চালালে একই সংখ্যা আসে।
  2. Requirement বদলালে নতুন নকশা: এখন link-এর মালিক destination বদলাতে পারবেন (যেমন পোস্টারে ছাপা QR code-এর পেছনের পাতা), আর বদল ৬০ সেকেন্ডের মধ্যে সবার কাছে কার্যকর হতে হবে। নকশার কোন কোন অংশ বদলাবে?
    • Redirect code: 301 বা 308 একেবারে বাদ, কারণ browser পুরোনো destination মনে রাখবে; 302 বা 307, max-age=0.
    • Cache আর edge: সব স্তরের TTL মিলিয়ে ৬০ সেকেন্ডের নিচে, অথবা বদলের সময় সক্রিয় invalidation আর CDN purge; ৬০ সেকেন্ডের শর্তটি তখন সর্বোচ্চ সীমা, গড় অনেক কম।
    • Data model: record-এ version, বদলের ইতিহাস একটি audit log-এ — কে, কখন, কোন ঠিকানা থেকে কোথায়।
    • নিরাপত্তা: প্রতিটি বদলে URL reputation আবার যাচাই, কারণ নিরীহ link বানিয়ে পরে ক্ষতিকর ঠিকানায় বদলানো অপব্যবহারের পরিচিত কৌশল; বদলের হারে rate limit.
  3. একটি নকশা সমালোচনা করুন: একটি দল লিখেছে: “code = MD5(long_url)-এর প্রথম ৬টি hex অক্ষর, collision যাচাই নেই; redirect 301, max-age এক বছর; প্রতিটি click-এ UPDATE links SET clicks = clicks + 1; custom alias-এ আগে SELECT, ফাঁকা হলে INSERT; সব একটি MySQL সার্ভারে।” অন্তত পাঁচটি সমস্যা খুঁজুন।
    • জায়গা খুব ছোট: ৬টি hex অক্ষরে মাত্র ১৬৬ = ১,৬৭,৭৭,২১৬টি code; birthday-র অঙ্কে প্রায় ৪,৮২৩টি link-এই ৫০% সম্ভাবনায় প্রথম collision. যাচাই না থাকায় এরপর কিছু ব্যবহারকারী অন্যের পাতায় যাবেন।
    • একই URL, একই code: দুজন মালিক একই URL দিলে একটি code — কার মেয়াদ, কার analytics, কে মুছতে পারবেন?
    • এক বছরের 301: click গোনা অচল, আর ক্ষতিকর link নিষিদ্ধ করলেও বহু browser এক বছর ধরে সেখানে যাবে।
    • প্রতিটি click-এ UPDATE: ভাইরাল link-এ hot row আর lock-এর অপেক্ষা, redirect ধীর; analytics redirect-এর পথে বসে আছে।
    • SELECT, তারপর INSERT: দুজন একসাথে একই alias পেতে পারেন; unique constraint বা conditional write লাগবে।
  4. আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে? আগে কাগজে, তারপর অংশ ২-এর estimator-এর জায়গায় বসিয়ে চালিয়ে যাচাই করুন।
    const ABC = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const base62 = n => { let s = ""; do { s = ABC[n % 62] + s; n = Math.floor(n / 62); } while (n > 0); return s; };
    console.log([0, 61, 62, 3843, 3844, 238328].map(base62).join(" "));
    const seen = new Set(); let raw = 0, clean = 0;
    for (const id of [5, 6, 5, 7, 6, 8, 5]) { raw++; if (seen.has(id)) continue; seen.add(id); clean++; }
    console.log(raw, clean);

    আউটপুট:

    0 Z 10 ZZ 100 1000
    7 4
    • ০ → 0; ৬১ বর্ণমালার শেষ অক্ষর Z; ৬২ = ১×৬২ + ০ → 10, ঠিক দশমিকে ১০-এর মতো।
    • ৩,৮৪৩ = ৬২² − ১ → দুই অক্ষরের সবচেয়ে বড় ZZ; ৩,৮৪৪ = ৬২² → 100; ২,৩৮,৩২৮ = ৬২³ → 1000.
    • দ্বিতীয় লাইন: ৭টি event এসেছে, কিন্তু আলাদা id মাত্র ৪টি (৫, ৬, ৭, ৮) — dedup-এর পর ৪।

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

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

ক্রমCache-এ না থাকা একটি ছোট link খোলার পর redirect-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🌐 DNS ব্যবহারকারীকে কাছের edge-এ পাঠায়
  2. 🔁 Edge অনুরোধটি একটি stateless redirect service-এ পাঠায়
  3. ⚡ Redirect service cache-এ code খোঁজে — miss
  4. 🗄️ KV store থেকে partition key দিয়ে record পড়া, মেয়াদ আর status যাচাই
  5. 📨 Cache ভরা, 302 উত্তর, তারপর click event stream-এ

a–z, A–Z আর 0–9 মিলিয়ে ৬২টি অক্ষরের ৭ অক্ষরের code-এ মোট কতগুলো আলাদা code সম্ভব?

  • প্রায় ৩.৫ billion (৩৫০ কোটি)
  • প্রায় ৩.৫২ trillion (৩.৫২ লাখ কোটি)
  • প্রায় ৫,৬৮০ কোটি
  • ৪৩৪

একটি shortener-এর requirement: প্রতিটি click গোনা হবে, আর ক্ষতিকর link দ্রুত বন্ধ করা যাবে। Redirect-এ কোন উত্তর বেশি মানানসই?

  • 301, কোনো Cache-Control ছাড়া
  • 308
  • 302, সাথে Cache-Control: private, max-age=0
  • 200, পাতার ভেতরে JavaScript redirect

৬ অক্ষরের random base62 code (মোট M ≈ ৫.৬৮ × ১০১০) দিয়ে ১০ লাখ link বানানো হলো। গড়ে প্রায় কতগুলো collision আশা করবেন?

  • প্রায় ৯টি
  • শূন্য, কারণ জায়গার খুব সামান্য অংশ ভরেছে
  • ০.০০০০১৮টি
  • প্রায় ৮৮০টি

Counter থেকে base62 code (4c92, 4c93, …) বানানোর সবচেয়ে বড় ঝুঁকি কোনটি?

  • অনেক collision
  • Code খুব লম্বা হয়ে যাওয়া
  • প্রতিটি link-এ কেন্দ্রীয় counter-এর ভিড়
  • Code অনুমেয়, গুনে গুনে link খুঁজে বের করা যায়

একটি সেবায় peak-এ সেকেন্ডে ৩৫,০০০টি redirect, আর cache hit rate ৮০%. KV store-এ সেকেন্ডে প্রায় কতগুলো পড়া যাবে?

  • ২৮,০০০
  • ৭,০০০
  • ৩৫,০০০
  • ৩,৫০০

At-least-once stream থেকে click গোনা হচ্ছে, তাই কিছু event দুবার আসে। কোন হিসাব dedup ছাড়াই ঠিক থাকে?

  • প্রতি মিনিটে মোট click-এর counter
  • প্রতি ঘণ্টার মোট click
  • HyperLogLog-এ unique visitor-এর অনুমান
  • দেশ ধরে click-এর সংখ্যা

শব্দকোষ

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

মনে রাখুন

  1. Case study মানে আগের পর্বের প্রয়োগ, আট ধাপে: পরিধি, হিসাব, API, data model, ছবি, deep dive, ভাঙন আর wrap-up.
  2. URL shortener read-heavy: peak-এ প্রায় ৩৪,৭০০ redirect বনাম ৩৫০ লেখা; ৫ বছরে ১,৮২৫ কোটি link, তিন কপিতে প্রায় ২৭ TB. চাপ পড়ার পথে।
  3. ID: counter + base62 collision-হীন কিন্তু অনুমেয়; random বা hash-এ গড় collision ≈ n²/(2M), ৬২৭ ≈ ৩.৫২ লাখ কোটি হলেও শূন্য নয় — তাই conditional write.
  4. Redirect: 302 analytics আর নিষিদ্ধ করার ক্ষমতা রাখে, 301 browser-এ cache হয়; সামনে cache, hot key-এর প্রতিকার আর ইচ্ছা হলে edge — প্রতিটি cache-এর TTL মানে সর্বোচ্চ দেরি।
  5. Analytics async stream-এ: event time-এর window, event id-তে dedup, unique visitor HyperLogLog-এ; সাথে অপব্যবহারের যাচাই, মেয়াদের পরিষ্কার আর region-এর lag-এর পরিকল্পনা।

পরের পর্বে: এখানে প্রতিটি অনুরোধ একা আর ক্রমের প্রশ্ন নেই। পর্ব ৫৫-এ chat আর messaging system — বার্তা পৌঁছানোর নিশ্চয়তা, ক্রম আর কে online, সেই খবর।

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

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