কেস স্টাডি: একটি ফুল-স্ট্যাক অ্যাপ্লিকেশন স্কেল করা
এই পাঠে যা শিখবেন
- একটি বাস্তবসম্মত স্কেলিং যাত্রার আখ্যান — মনোলিথ+SSR থেকে CDN-cached SSG ও সার্ভিস-স্প্লিট পর্যন্ত
- একটি সরলীকৃত গাণিতিক মডেল দিয়ে "কেন" প্রশ্নের সংখ্যাভিত্তিক উত্তর
- Python দিয়ে দুটি স্কেলিং সিদ্ধান্তের আগে/পরের রেসপন্স-টাইম হাতে-কলমে গণনা ও যাচাই
- এই ধরনের ব্যবহারিক আর্কিটেকচার সিদ্ধান্ত কোন মডিউলের ধারণার উপর ভিত্তি করে তৈরি (M9 রেন্ডারিং, M12 ডিপ্লয়মেন্ট)
১ · প্রাথমিক আর্কিটেকচার — মনোলিথ + SSR
TaskFlow একটি টাস্ক-ম্যানেজমেন্ট অ্যাপ, শুরুতে একটিমাত্র সার্ভারে চলে — প্রতিটি রিকোয়েস্টে সার্ভার-সাইড রেন্ডারিং (SSR) দিয়ে সম্পূর্ণ HTML তৈরি হয়ে ক্লায়েন্টে পাঠানো হয় (M9-এ শেখা SSR প্যাটার্ন)। প্রথম কয়েক মাস অল্প ব্যবহারকারীর জন্য এটি চমৎকার কাজ করে — কোড সহজ, ডিপ্লয়মেন্ট একক (M12-এর মনোলিথ ডিপ্লয়মেন্ট), এবং প্রতিটি ফিচার একই কোডবেসে যোগ করা সহজ।
২ · সমস্যা — ট্রাফিক বৃদ্ধি ও ধীর রেসপন্স
ব্যবহারকারী বাড়ার সাথে সাথে দুটি আলাদা সমস্যা দেখা দেয়। প্রথমত, বেশিরভাগ পেজ (যেমন ড্যাশবোর্ড, পাবলিক প্রোফাইল)
পড়ার জন্যই বেশি ব্যবহৃত হয়, কিন্তু ডেটা খুব ঘন ঘন বদলায় না — অথচ প্রতিটি রিকোয়েস্টেই সার্ভার নতুন করে পুরো HTML
রেন্ডার করছে, যা অপ্রয়োজনীয় কম্পিউট খরচ করছে। দ্বিতীয়ত, একটি নির্দিষ্ট endpoint —
/api/search — মোট ট্রাফিকের একটি বড় অংশ টানছে, এবং এটি একই সার্ভারের বাকি সব রুটের সাথে
রিসোর্স শেয়ার করছে, ফলে সার্চ ট্রাফিক বাড়লে অন্য সব ফিচারও ধীর হয়ে যাচ্ছে।
৩ · সিদ্ধান্ত ১ — read-heavy পেজ SSG + CDN cache-এ সরানো
যেসব পেজের ডেটা কম ঘন ঘন বদলায়, সেগুলো স্ট্যাটিক সাইট জেনারেশন (SSG)-এ রূপান্তরিত করা হয় — HTML একবার (build সময়ে বা প্রথম রিকোয়েস্টে) রেন্ডার হয়ে CDN-এ ক্যাশ হয়ে যায়, তারপরের প্রতিটি রিকোয়েস্ট সরাসরি সেই ক্যাশড কপি থেকে সার্ভ হয় — সার্ভারকে আর প্রতিবার নতুন করে রেন্ডার করতে হয় না।
৪ · সিদ্ধান্ত ২ — হট endpoint আলাদা সার্ভিসে ভাগ করা
/api/search-কে মূল মনোলিথ থেকে আলাদা করে তার নিজস্ব, স্বাধীনভাবে স্কেল করা যায় এমন সার্ভিসে
সরানো হয় (M12-এর মাইক্রোসার্ভিস ডিপ্লয়মেন্ট প্যাটার্নের প্রয়োগ) — এতে দুটো সুবিধা হয়: সার্চ ট্রাফিকের জন্য
আলাদাভাবে বেশি ক্যাপাসিটি বরাদ্দ করা যায়, এবং বাকি মনোলিথ থেকে সেই ভারী ট্রাফিক সরে যাওয়ায় বাকি সব ফিচারও
দ্রুত হয়ে যায়। এই মডিউল বৃহত্তর সিস্টেম-স্কেল ডিজাইনের বদলে শুধু ওয়েব-অ্যাপ্লিকেশন-ফ্রেমওয়ার্ক স্তরের এই
সিদ্ধান্তগুলোতে ফোকাস করে — বিস্তারিত ডিস্ট্রিবিউটেড-সিস্টেম স্কেলেবিলিটি প্যাটার্ন ../system-design/
কোর্সে গভীরভাবে আলোচিত হয়েছে।
৫ · কোড দিয়ে যাচাই — একটি সরলীকৃত রেসপন্স-টাইম মডেল
নিচের কোড সেলে একটি সরলীকৃত, ইলাস্ট্রেটিভ সূত্র ব্যবহার করা হয়েছে —
average_response_time = base_processing_time + (concurrent_requests / server_capacity) * queue_penalty
— যেখানে বেশি concurrent request একই সার্ভার-ক্যাপাসিটির তুলনায় বেশি হলে একটি "queue penalty" যোগ হয়। এটি
বাস্তব নেটওয়ার্ক/সার্ভার পারফরম্যান্সের নিখুঁত মডেল নয়, কিন্তু উপরের দুটি সিদ্ধান্তের প্রভাব সংখ্যায় concrete
করে তোলে।
def average_response_time(base_processing_time, concurrent_requests, server_capacity, queue_penalty):
"""
সরলীকৃত, ইলাস্ট্রেটিভ মডেল (বাস্তব সিস্টেমের নিখুঁত গাণিতিক প্রতিনিধিত্ব নয়) --
average_response_time = base_processing_time + (concurrent_requests / server_capacity) * queue_penalty
"""
return base_processing_time + (concurrent_requests / server_capacity) * queue_penalty
queue_penalty = 4 # ms -- ক্যাপাসিটির তুলনায় প্রতি এক্সট্রা রিকোয়েস্ট-রেশিওতে যোগ হওয়া অতিরিক্ত অপেক্ষার সময়
# ---------- ধাপ ০: প্রাথমিক অবস্থা -- একক মনোলিথ সার্ভার, প্রতি রিকোয়েস্টে SSR ----------
concurrent_requests = 500
base_processing_time_ssr = 120 # ms -- প্রতিটি রিকোয়েস্টে সার্ভার-সাইড রেন্ডারের খরচ
server_capacity_monolith = 100 # একটি মনোলিথ সার্ভার আরামে কতগুলো concurrent request সামলাতে পারে
before_1 = average_response_time(base_processing_time_ssr, concurrent_requests, server_capacity_monolith, queue_penalty)
print(f"ধাপ ০ (মনোলিথ + SSR, {concurrent_requests} concurrent request): {before_1:.1f}ms")
# ---------- সিদ্ধান্ত ১: read-heavy পেজ SSG + CDN cache ----------
base_processing_time_ssg = 5 # ms -- CDN থেকে আগে থেকে ক্যাশড HTML সার্ভ করা
server_capacity_cdn = 5000 # CDN edge অনেক বেশি concurrent request সামলাতে পারে
after_1 = average_response_time(base_processing_time_ssg, concurrent_requests, server_capacity_cdn, queue_penalty)
improvement_pct_1 = (1 - after_1 / before_1) * 100
print(f"সিদ্ধান্ত ১-এর পর (read-heavy পেজ SSG + CDN, একই {concurrent_requests} concurrent request): {after_1:.1f}ms")
print(f" উন্নতি: {before_1 - after_1:.1f}ms কমেছে ({improvement_pct_1:.1f}%)")
# ---------- ধাপ ২: ট্রাফিক আরও বাড়ল, /api/search বড় অংশ টানছে ----------
concurrent_requests_growth = 2000
before_2 = average_response_time(base_processing_time_ssr, concurrent_requests_growth, server_capacity_monolith, queue_penalty)
print(f"\nধাপ ২ (ট্রাফিক বৃদ্ধি, একই মনোলিথ সার্ভার, {concurrent_requests_growth} concurrent request): {before_2:.1f}ms")
# ---------- সিদ্ধান্ত ২: /api/search আলাদা, স্বাধীনভাবে স্কেলড সার্ভিসে ভাগ করা ----------
search_share = 0.6 # মোট ট্রাফিকের ৬০% /api/search-এ যায়
search_requests = int(concurrent_requests_growth * search_share)
rest_requests = concurrent_requests_growth - search_requests
server_capacity_search_service = 800 # আলাদা, শুধু সার্চের জন্য অপ্টিমাইজড ও স্বাধীনভাবে স্কেলড ক্যাপাসিটি
base_processing_time_search = 90 # ms
after_2_search = average_response_time(base_processing_time_search, search_requests, server_capacity_search_service, queue_penalty)
after_2_rest = average_response_time(base_processing_time_ssr, rest_requests, server_capacity_monolith, queue_penalty)
blended_after_2 = (search_requests * after_2_search + rest_requests * after_2_rest) / concurrent_requests_growth
print(f"\nসিদ্ধান্ত ২-এর পর:")
print(f" /api/search (নিজস্ব সার্ভিস, {search_requests} concurrent request): {after_2_search:.1f}ms")
print(f" বাকি মনোলিথ (অবশিষ্ট {rest_requests} concurrent request): {after_2_rest:.1f}ms")
print(f" ব্লেন্ডেড গড় রেসপন্স টাইম (উভয় সার্ভিস একসাথে বিবেচনা করে): {blended_after_2:.1f}ms")
print(f" (আগে, সব ট্রাফিক একই মনোলিথে থাকলে: {before_2:.1f}ms)")
/api/search-ই দ্রুত হয়নি (কারণ এটি এখন অনেক বড়
server_capacity_search_service নিয়ে একা কাজ করছে) — বাকি মনোলিথের রেসপন্স টাইমও ধাপ ২-এর
তুলনায় কমেছে, কারণ concurrent_requests-এর একটা বড় অংশ (search_requests) আর মূল সার্ভারের
ক্যাপাসিটির জন্য প্রতিযোগিতা করছে না। এটাই দেখায় একটি হট endpoint আলাদা করা শুধু সেই endpoint নয়, পুরো
সিস্টেমকেই উপকৃত করতে পারে।
স্কেলিং সিদ্ধান্ত সবসময় "সব একসাথে নতুন করে লেখা" নয় — TaskFlow-এর প্রতিটি সিদ্ধান্ত একটি নির্দিষ্ট, পরিমাপযোগ্য সমস্যার লক্ষ্যভিত্তিক সমাধান ছিল। SSG+CDN রিড-হেভি পেজের রিপিটেড রেন্ডারিং খরচ দূর করেছে, আর হট endpoint আলাদা করা তাকে স্বাধীনভাবে স্কেল করার সুযোগ দিয়েছে এবং বাকি সিস্টেমের চাপ কমিয়েছে। এই সরলীকৃত সংখ্যাগুলো বাস্তব বেঞ্চমার্ক নয়, কিন্তু "কেন এই সিদ্ধান্তগুলো নেওয়া হলো" প্রশ্নের একটি concrete, হাতে-কলমে যাচাইযোগ্য উত্তর দেয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
CDN + SSG-তে সরানোর পর base_processing_time ১২০ms থেকে ৫ms-এ নেমে আসে কেন?
কারণ HTML প্রতিটি রিকোয়েস্টে নতুন করে রেন্ডার হচ্ছে না — এটি আগে থেকেই একবার রেন্ডার হয়ে CDN-এ ক্যাশ হয়ে
আছে (M9-এর SSG প্যাটার্ন)। প্রতিটি নতুন রিকোয়েস্ট শুধু সেই ক্যাশড ফাইলটি সার্ভ করে, যা সার্ভার-সাইড রেন্ডার
করার চেয়ে অনেক সস্তা অপারেশন — তাই base_processing_time কমে যায়।
প্র ০২ হট endpoint আলাদা সার্ভিসে ভাগ করার পর শুধু সেই endpoint-ই না, বাকি মনোলিথ ট্রাফিকও দ্রুত হয় কেন?
কারণ সূত্রে concurrent_requests / server_capacity অনুপাতটিই queue penalty নির্ধারণ করে।
সার্চ ট্রাফিক আলাদা হয়ে যাওয়ায় মূল মনোলিথের concurrent_requests কমে যায় (২০০০ থেকে
rest_requests-এ), কিন্তু server_capacity_monolith একই থাকে — ফলে অনুপাতটি
ছোট হয়ে যায় এবং বাকি সব রুটের রেসপন্স টাইমও কমে।
প্র ০৩
এই সূত্র অনুযায়ী server_capacity বাড়ালে (উদাহরণ: হরাইজন্টাল স্কেলিং — আরও সার্ভার যোগ
করা) response time-এর উপর কী প্রভাব পড়বে?
সূত্রে server_capacity হর (denominator)-এ আছে, তাই এটি বাড়ালে
concurrent_requests / server_capacity অনুপাত ছোট হয়ে যায়, ফলে queue penalty অংশটি কমে
এবং মোট average_response_time-ও কমে। এটাই দেখায় কেন হরাইজন্টাল স্কেলিং (আরও সার্ভার/ইনস্ট্যান্স
যোগ করা) একটি প্রচলিত স্কেলিং কৌশল — যদিও বাস্তবে এর সাথে লোড-ব্যালেন্সিং ও কো-অর্ডিনেশন খরচও যুক্ত হয়,
যা এই সরলীকৃত মডেলে ধরা হয়নি।
অনুশীলন
-
চিন্তা করুন: যদি
search_share০.৬-এর বদলে ০.৯ হতো (অর্থাৎ মোট ট্রাফিকের ৯০% সার্চে যেত),rest_requestsও তার ফলে মনোলিথের রেসপন্স টাইমের উপর কী প্রভাব পড়ত বলে মনে হয়?search_share = 0.9হলেrest_requestsআরও ছোট হয়ে যেত (২০০০-এর মাত্র ১০%, অর্থাৎ ২০০) — ফলে বাকি মনোলিথেরconcurrent_requests / server_capacityঅনুপাত আরও ছোট হতো এবংafter_2_restআরও কমে যেত। তবেsearch_requestsবেড়ে ১৮০০ হতো, তাইafter_2_search-এর উপরও নজর দিতে হবে — এই ট্রেড-অফ সরাসরি কোড চালিয়ে যাচাই করাই সবচেয়ে নির্ভরযোগ্য। -
পরীক্ষা করুন: উপরের কোড সেলে
search_share-এর মান ০.৮-এ বদলে দিন এবং Run চেপে দেখুনblended_after_2-এর মান কীভাবে বদলায়।search_share = 0.8হলেsearch_requests = 1600ওrest_requests = 400হয়ে যাবে।after_2_searchসামান্য বাড়বে (কারণ একইserver_capacity_search_service-এ এখন বেশি রিকোয়েস্ট), কিন্তুafter_2_restআরও কমবে (কারণ মনোলিথে কম রিকোয়েস্ট বাকি থাকবে) — চূড়ান্তblended_after_2কীভাবে বদলায় তা কোড চালিয়ে সরাসরি সংখ্যায় দেখা সবচেয়ে স্পষ্ট।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ L58 · ক্যাপস্টোন চূড়ান্ত প্রকল্প — একটি সম্পূর্ণ ফুল-স্ট্যাক অ্যাপ্লিকেশন ডিজাইন ও সিমুলেট করা।
- আগের পাঠ L56 প্রজেক্টের জন্য সঠিক স্ট্যাক বাছাই করা — একটি ওজনযুক্ত সিদ্ধান্ত-ম্যাট্রিক্স।
- System Design কোর্স সহোদর কোর্স বৃহত্তর ডিস্ট্রিবিউটেড-সিস্টেম স্কেলেবিলিটি ও ক্যাপাসিটি-প্ল্যানিং সেই কোর্সে গভীরভাবে আলোচিত।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।