পাঠ ৪৩ · ৫১-এর মধ্যে · মডিউল ১১
Home / Courses / System Design / কেস স্টাডি: URL শর্টনার

কেস স্টাডি: URL শর্টনার ডিজাইন করা

Case study: designing a URL shortener
১১ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • একটি সিস্টেম ডিজাইন ইন্টারভিউয়ের পূর্ণাঙ্গ কাঠামো — রিকোয়ারমেন্ট থেকে ট্রেড-অফ পর্যন্ত
  • base62 কাউন্টার-এনকোডিং দিয়ে কীভাবে কোলিশন-ফ্রি ইউনিক শর্ট কোড তৈরি করা যায়
  • কীভাবে আগের ১৩+টি মডিউলের কৌশল (লোড ব্যালেন্সিং, ক্যাশিং, শার্ডিং) একটি বাস্তব সিস্টেমে একসাথে বসে
  • ডিস্ট্রিবিউটেড কাউন্টার ও রিডাইরেক্ট স্ট্যাটাস কোড বাছাইয়ের বাস্তব ট্রেড-অফ

১ · রিকোয়ারমেন্ট

যেকোনো কেস স্টাডির প্রথম ধাপ (L01-এর ফাংশনাল/নন-ফাংশনাল পার্থক্য মনে করুন) — সিস্টেমটি কী করবে এবং কতটা ভালোভাবে করবে তা স্পষ্ট করা।

ফাংশনাল
একটি লম্বা URL দিলে একটি ছোট, ইউনিক কোড তৈরি হবে। শর্ট কোডে ভিজিট করলে মূল URL-এ রিডাইরেক্ট হবে।
নন-ফাংশনাল
রিড:রাইট রেশিও ~১০০:১ (শর্টনিং কম হয়, রিডাইরেক্ট অনেক বেশি)। রিডাইরেক্ট লেটেন্সি খুব কম হতে হবে। কোড কখনো ডুপ্লিকেট হবে না।
স্কেল
উদাহরণ: প্রতি মাসে ১০ কোটি (100,000,000) নতুন URL শর্টেন হয়।

২ · ক্যাপাসিটি এস্টিমেশন (L02-এর পদ্ধতি)

L02-এ শেখা QPS হিসাবের পদ্ধতি এখানে সরাসরি প্রয়োগ করি। মাসে ১ সেকেন্ড = ৩০ × ৮৬,৪০০ = ২৫,৯২,০০০ সেকেন্ড ধরে —

হিসাব

গড় write QPS = ১০,০০,০০,০০০ / ২৫,৯২,০০০ ≈ ৩৮.৬। রিড:রাইট = ১০০:১ ধরে, গড় read QPS ≈ ৩৮.৬ × ১০০ ≈ ৩,৮৫৮। এই বিশাল রিড-হেভি প্রকৃতিই বলে দেয় কেন এই সিস্টেমে ক্যাশিং (L19) সবচেয়ে বেশি প্রভাব ফেলবে — মাত্র ~৩৯ রাইট/সেকেন্ডের জন্য জটিল রাইট-অপ্টিমাইজেশনের দরকার নেই, কিন্তু ~৩,৮৫৮ রিড/সেকেন্ড সরাসরি ডেটাবেসে পাঠালে তা অপ্রয়োজনীয়ভাবে ব্যয়বহুল।

৩ · শর্ট কোড জেনারেশন — গভীর বিশ্লেষণ

দুটি প্রধান পদ্ধতি আছে। অপশন A — হ্যাশ করে ট্রাংকেট: URL-এর MD5/SHA হ্যাশ নিয়ে প্রথম কয়েক অক্ষর নেওয়া — সহজ, কিন্তু দুটি ভিন্ন URL একই ট্রাংকেটেড কোড পেতে পারে (কোলিশন), তাই প্রতিবার একটি existence-check লাগবে (এবং কোলিশন হলে regenerate করতে হবে)। অপশন B — অটো-ইনক্রিমেন্টিং কাউন্টার: প্রতিটি নতুন URL একটি নতুন, সবসময়-বাড়তে-থাকা সংখ্যা পায় (1, 2, 3, ...), যা base62Base62 Encoding০-৯, a-z, A-Z — মোট ৬২টি চিহ্ন ব্যবহার করে একটি সংখ্যাকে ছোট স্ট্রিং-এ রূপান্তর — ঠিক যেমন base10 (দশমিক) বা base16 (হেক্স), শুধু ৬২টি প্রতীক দিয়ে।-এ এনকোড করা হয়। কাউন্টার-ভিত্তিক পদ্ধতিতে কোনো কোলিশন সম্ভবই না — প্রতিটি সংখ্যা ইউনিক, তাই এর এনকোডিংও ইউনিক। এই কোর্স কাউন্টার-ভিত্তিক পদ্ধতিকেই বেছে নেবে, কারণ এটি existence-check ছাড়াই নিশ্চিত ইউনিকনেস দেয়।

Python
CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"  # ৬২টি চিহ্ন
BASE = len(CHARS)

def encode_base62(num):
    if num == 0:
        return CHARS[0]
    digits = []
    while num > 0:
        num, rem = divmod(num, BASE)
        digits.append(CHARS[rem])
    return "".join(reversed(digits))

def decode_base62(code):
    num = 0
    for ch in code:
        num = num * BASE + CHARS.index(ch)
    return num

# --- রাউন্ড-ট্রিপ টেস্ট: কয়েকটি auto-increment ID এনকোড করে আবার ডিকোড করে মেলানো ---
test_ids = [0, 1, 61, 62, 12345, 999_999_999, 3_521_614_606_207]
for i in test_ids:
    code = encode_base62(i)
    back = decode_base62(code)
    status = "OK" if back == i else "MISMATCH"
    print(f"id={i:>15,} -> code='{code}' -> decoded={back:>15,}  [{status}]")

# --- ৭-অক্ষরের base62 কোডের সম্ভাব্য মোট সংখ্যা ---
capacity_7char = 62 ** 7
print(f"\n৭-অক্ষরের base62 কোডের সম্ভাব্য সংখ্যা: {capacity_7char:,}")

# --- ক্যাপাসিটি এস্টিমেশন (L02 পদ্ধতি) ---
urls_per_month = 100_000_000
seconds_per_month = 30 * 86_400
avg_write_qps = urls_per_month / seconds_per_month
read_write_ratio = 100
avg_read_qps = avg_write_qps * read_write_ratio
print(f"গড় write QPS: {avg_write_qps:.1f}")
print(f"গড় read QPS ({read_write_ratio}:1 রেশিওতে): {avg_read_qps:.1f}")

    
৭-অক্ষরের কোড ~৩.৫ ট্রিলিয়ন সম্ভাবনা দেয় — প্রতি মাসে ১০ কোটি URL হলেও, এই ক্যাপাসিটি হাজার হাজার বছরের গ্রোথের জন্য যথেষ্ট। বাস্তব সিস্টেমে সাধারণত মার্জিন রেখে ৬-৮ অক্ষর ব্যবহার করা হয়।

৪ · হাই-লেভেল আর্কিটেকচার

রিকোয়েস্টের পথ এই কোর্সের চেনা বিল্ডিং ব্লকগুলো দিয়েই তৈরি — প্রতিটি বক্স একটি নির্দিষ্ট আগের পাঠের প্রয়োগ।

ক্লায়েন্ট Client লোড ব্যালেন্সার (L11) Load Balancer স্টেটলেস অ্যাপ সার্ভার (L12) App Servers ক্যাশ (L19) hot redirects কাউন্টার সার্ভিস ID generator শার্ডেড DB (L14,L17) key-value store
শর্টনিং (write) কাউন্টার সার্ভিস থেকে ID পেয়ে DB-তে সেভ করে; রিডাইরেক্ট (read) আগে ক্যাশ চেক করে, মিস হলে DB থেকে পড়ে।

৫ · গভীর বিশ্লেষণ — রিডাইরেক্ট পাথ ও ক্যাশিং

যেহেতু রিড:রাইট ~১০০:১, রিডাইরেক্ট পাথের পারফরম্যান্সই সিস্টেমের সাফল্য ঠিক করে দেয়। L19-এর cache-aside প্যাটার্ন এখানে হুবহু প্রযোজ্য — একটি রিডাইরেক্ট রিকোয়েস্ট এলে প্রথমে ক্যাশ চেক হয় (hit হলে সরাসরি রিডাইরেক্ট, কোনো DB কল ছাড়াই); মিস হলে DB থেকে পড়ে ক্যাশে বসিয়ে দেওয়া হয়। যেহেতু কিছু URL ("hot" লিংক, যেমন ভাইরাল পোস্ট) বাকিদের চেয়ে বহুগুণ বেশি ক্লিক পায়, একটি ছোট ক্যাশও বেশিরভাগ ট্রাফিক সার্ভ করে ফেলতে পারে (Pareto-এর মতো একটি প্যাটার্ন — অল্প কিছু কী বেশিরভাগ ট্রাফিক বহন করে)।

লেখার পাশে, শর্ট কোড জেনারেশনের জন্য একটি কেন্দ্রীয় কাউন্টার সার্ভিস দরকার — কারণ একাধিক স্টেটলেস অ্যাপ সার্ভার (L12) সমান্তরালে কাউন্টার বাড়াতে চাইলে ডুপ্লিকেট ID এড়াতে কো-অর্ডিনেশন লাগবে। বাস্তব সিস্টেমে এটি সাধারণত হয় একটি ডেডিকেটেড অটো-ইনক্রিমেন্ট ডেটাবেস সিকোয়েন্স, অথবা প্রতিটি সার্ভারকে ID-এর একটি আলাদা রেঞ্জ (যেমন সার্ভার ১ পায় 1-1000, সার্ভার ২ পায় 1001-2000) আগে থেকে বরাদ্দ করে দেওয়া — যা শার্ডিং-এর (L17) মতো একই "পার্টিশন করে দাও, কোঅর্ডিনেশন কমাও" নীতি অনুসরণ করে।

৬ · ট্রেড-অফ

কাউন্টার বনাম হ্যাশ
কাউন্টার কোলিশন-ফ্রি কিন্তু একটি কেন্দ্রীয় কম্পোনেন্ট দরকার (যা রেঞ্জ-বরাদ্দ করে সমাধান হয়)। হ্যাশ+ট্রাংকেট কেন্দ্রীয় স্টেট এড়ায় কিন্তু প্রতি রাইটে একটি existence-check লাগে।
301 বনাম 302 রিডাইরেক্ট (L06)
301 (স্থায়ী) ব্রাউজার ক্যাশ করে ফেলে — সার্ভার লোড কমে কিন্তু প্রতিটি ক্লিকের অ্যানালিটিক্স ট্র্যাক করা কঠিন হয়। 302 (সাময়িক) প্রতিবার সার্ভারে হিট করে — লোড বেশি কিন্তু পূর্ণ ক্লিক-অ্যানালিটিক্স পাওয়া যায়।
কাস্টম অ্যালিয়াস
ইউজার-নির্বাচিত কাস্টম শর্ট কোড সাপোর্ট করলে অবশ্যই existence-check লাগবে (কাউন্টার অটোমেটিক ইউনিকনেসের গ্যারান্টি এখানে খাটে না) — এটি একটি বিশেষ কেস হিসেবে হ্যান্ডেল করতে হয়।
মূল কথা · Key takeaway

URL শর্টনার একটি "সহজ" মনে হওয়া সিস্টেম, কিন্তু এটি এই কোর্সের প্রায় প্রতিটি ফাউন্ডেশনাল কৌশল একত্রে দেখায় — ক্যাপাসিটি এস্টিমেশন (L02) রিড-হেভি প্রকৃতি প্রকাশ করে, যা ক্যাশিং (L19) কে অগ্রাধিকার দেয়; লোড ব্যালেন্সিং (L11) ও স্টেটলেস সার্ভার (L12) স্কেল করার ভিত্তি দেয়; শার্ডিং (L17)-এর নীতি কাউন্টার-বরাদ্দেও প্রযোজ্য হয়। এই প্যাটার্নটাই বাকি কেস স্টাডিগুলোতে বারবার ফিরে আসবে।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ কেন কাউন্টার-ভিত্তিক base62 এনকোডিং হ্যাশ+ট্রাংকেট পদ্ধতির চেয়ে "নিরাপদ" এখানে — কোনো কোলিশন-চেক ছাড়াই?

কারণ প্রতিটি অটো-ইনক্রিমেন্ট সংখ্যা সংজ্ঞা অনুযায়ীই ইউনিক (কখনো পুনরাবৃত্তি হয় না), এবং base62 এনকোডিং একটি এক-এক (bijective) ম্যাপিং — একটি সংখ্যা সবসময় একই কোডে এনকোড হয়, দুটি ভিন্ন সংখ্যা কখনো একই কোড দেয় না। তাই ইউনিকনেস স্বয়ংক্রিয়ভাবে গ্যারান্টিড, কোনো existence-check বা রিট্রাই লজিক ছাড়াই — যেখানে হ্যাশ+ট্রাংকেট পদ্ধতিতে দুটি ভিন্ন URL একই ছোট হ্যাশ-প্রিফিক্স পেতে পারে (কোলিশন), যা ধরতে প্রতি রাইটে একটি অতিরিক্ত DB লুকআপ লাগে।

প্র ০২ একাধিক স্টেটলেস অ্যাপ সার্ভার একসাথে চললে (L12), কেন একটি একক শেয়ার্ড কাউন্টার সরাসরি ব্যবহার করা সমস্যাজনক, এবং সমাধান কী?

যদি প্রতিটি সার্ভার সরাসরি "কাউন্টার + 1" পড়ে-লিখে, তাহলে দুটি সার্ভার একই মুহূর্তে একই মান পড়ে ফেলে একই ID বরাদ্দ করে ফেলতে পারে (race condition) — কোলিশন ফিরে আসে। বাস্তব সমাধান — প্রতিটি সার্ভারকে আগে থেকে ID-এর একটি ব্লক/রেঞ্জ বরাদ্দ করা (যেমন সার্ভার A: 1-1000, সার্ভার B: 1001-2000), যাতে প্রতিটি রিকোয়েস্টে কেন্দ্রীয় কাউন্টারে যেতে না হয় — এটি শার্ডিং-এর (L17) মতোই "প্রি-পার্টিশন করে কোঅর্ডিনেশন এড়াও" কৌশল।

প্র ০৩ রিডাইরেক্টের জন্য 301 বনাম 302 স্ট্যাটাস কোড বাছাই কীভাবে সরাসরি সিস্টেমের ক্যাপাসিটি রিকোয়ারমেন্টকে প্রভাবিত করে?

301 (Moved Permanently) ব্রাউজার স্থানীয়ভাবে ক্যাশ করে ফেলে — একই ইউজার একই লিংকে আবার ক্লিক করলে ব্রাউজার সরাসরি গন্তব্যে যায়, সার্ভারে দ্বিতীয়বার নাও যেতে পারে। এতে সার্ভারের কাছে আসা read QPS কমে যায়, কিন্তু প্রতিটি ক্লিক ট্র্যাক করার সুযোগও হারায়। 302 (Found) প্রতিবার সার্ভারে হিট করে — read QPS পুরোপুরি অনুমানমতোই থাকে এবং সম্পূর্ণ অ্যানালিটিক্স পাওয়া যায়, কিন্তু ক্যাশিং লেয়ার (L19)-এর উপর নির্ভরতা বাড়ে।

অনুশীলন

  1. হিসাব করুন: প্রতি মাসে ১০ কোটি নতুন URL শর্টেন হলে, ৫ বছরে মোট কতগুলো শর্ট কোড দরকার হবে? এই সংখ্যা কি ৫-অক্ষরের base62 (62⁵) ক্যাপাসিটির মধ্যে ধরবে? ৬-অক্ষরের (62⁶) ক্ষেত্রে কী হবে?

    ৫ বছর = ৬০ মাস। মোট প্রয়োজন = ৬০ × ১০,০০,০০,০০০ = ৬,০০,০০,০০,০০০ (৬ বিলিয়ন)। ৫-অক্ষরের ক্যাপাসিটি 62⁵ = ৯১,৬১,৩২,৮৩২ (~৯১.৬ কোটি) — এটি যথেষ্ট নয় (৬ বিলিয়নের চেয়ে অনেক কম)। ৬-অক্ষরের ক্যাপাসিটি 62⁶ = ৫৬,৮০,০২,৩৫,৫৮৪ (~৫৬.৮ বিলিয়ন) — এটি যথেষ্ট (৬ বিলিয়নের চেয়ে অনেক বেশি মার্জিন সহ)। তাই ন্যূনতম ৬ অক্ষর দরকার; ৭ অক্ষর বেছে নেওয়া হয় আরও বড় মার্জিনের জন্য।

  2. চিন্তা করুন: একটি ইউজার চায় তার শর্ট URL-টি "মেয়াদোত্তীর্ণ" (expire) হয়ে যাক ৩০ দিন পর। এই ফিচার যোগ করতে সিস্টেমের কোন কোন অংশে পরিবর্তন লাগবে (ক্যাশ ও DB উভয় দিক থেকে চিন্তা করুন)?

    DB রেকর্ডে একটি expiry timestamp যোগ করতে হবে, এবং রিডাইরেক্ট লজিকে expiry চেক করতে হবে (মেয়াদ শেষ হলে 404/410 রিটার্ন করা)। ক্যাশ লেয়ারেও (L19, L21) একটি TTL সেট করতে হবে যাতে মেয়াদোত্তীর্ণ এন্ট্রি ক্যাশ থেকে স্বয়ংক্রিয়ভাবে সরে যায় — নইলে ক্যাশ পুরনো/অবৈধ ডেটা সার্ভ করতে থাকবে DB-তে আপডেট হওয়ার পরেও। এটি L21-এর cache invalidation সমস্যার একটি বাস্তব উদাহরণ।

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

আগের পাঠ
SLA, SLO ও SLI — রিলায়েবিলিটি মেট্রিক্স