কেস স্টাডি: একটি মোবাইল অ্যাপকে লাখো ব্যবহারকারীর জন্য স্কেল করা
এই পাঠে যা শিখবেন
- একটি মোবাইল অ্যাপ স্কেল করার সময় কোন ক্রমে আর্কিটেকচারাল সিদ্ধান্তগুলো নেওয়া হয়, এবং কেন
- কেন "কেবল ব্যাকএন্ড সার্ভার বাড়ালেই স্কেল হবে" ধারণাটি মোবাইল ক্লায়েন্টের জন্য যথেষ্ট নয়
- naive পোলিং বনাম পুশ+ব্যাচড সিঙ্কের ব্যাটারি/নেটওয়ার্ক খরচের একটি সত্যিকারের গণনাকৃত তুলনা
- এই কোর্সের আগের মডিউলগুলোর (M8-M11) ধারণাগুলো একটি বাস্তবসম্মত গল্পে কীভাবে একসাথে কাজ করে
১ · একটি অ্যাপের স্কেলিং যাত্রা — চারটি ধাপ
ধরা যাক আমরা "QuickNote" নামের একটি নোট-সিঙ্কিং অ্যাপ তৈরি করছি। শুরুতে এর ব্যবহারকারী মাত্র কয়েকশো — সময়ের সাথে সাথে তা লাখো ছাড়িয়ে যায়। প্রতিটি ধাপে একটি নির্দিষ্ট ব্যথার জায়গা (pain point) সমাধান করতে আর্কিটেকচার বদলাতে হয়েছে।
শুরুতে অ্যাপটি সরাসরি সার্ভারে প্রতিটি রিড পাঠাত। প্রথম সমাধান ছিল একটি লোকাল SQLite টেবিলে (M8/L33-এর ধাঁচে) ডেটা ক্যাশ করা — রিড দ্রুত হলো, কিন্তু অফলাইনে লেখা এখনও অসম্ভব ছিল।
ব্যবহারকারীরা দুর্বল সিগন্যালে নোট হারাতেন। সমাধান — লেখা সবসময় আগে লোকালি সেভ হয়, একটি পেন্ডিং-সিঙ্ক কিউতে যুক্ত হয় (M8/L36), পরে সিঙ্ক হয়।
২ · সংখ্যায় দেখা যাক: naive পোলিং বনাম পুশ + ব্যাচড সিঙ্ক
"কেন এই সিদ্ধান্তগুলো নেওয়া হলো" প্রশ্নের উত্তর নিছক প্রোজ দিয়ে না দিয়ে সংখ্যায় দেখা যাক। ধরা যাক QuickNote-এর এখন ২০ লাখ (২০,০০,০০০) সক্রিয় ব্যবহারকারী, প্রতিদিন গড়ে ১৬ ঘণ্টা সক্রিয়। প্রথম পদ্ধতিতে (ধাপ ১-২র পুরনো স্টাইল) অ্যাপ প্রতি ৩০ সেকেন্ডে সার্ভার পোল করত, নতুন ডেটা থাকুক বা না থাকুক। দ্বিতীয় পদ্ধতিতে (ধাপ ৩-৪) শুধু সত্যিকারের নতুন ইভেন্ট এলে পুশ ডেলিভারি হয়, আর বাকি সবকিছু দিনে মাত্র কয়েকবার ব্যাচড সিঙ্কে যায়।
# naive পোলিং বনাম পুশ + ব্যাচড সিঙ্ক -- ফ্লিট-ওয়াইড ব্যাটারি ও নেটওয়ার্ক খরচের সত্যিকারের গণনা
ACTIVE_USERS = 2_000_000
ACTIVE_HOURS_PER_DAY = 16
# ---------- পদ্ধতি ১: naive পোলিং (ধাপ ১-২ স্টাইল) ----------
POLL_INTERVAL_SECONDS = 30
polls_per_user_per_day = (ACTIVE_HOURS_PER_DAY * 3600) // POLL_INTERVAL_SECONDS
BATTERY_UNITS_PER_POLL = 2 # প্রতিটি পোল রেডিও ওয়েকআপ করে -- নতুন ডেটা থাকুক বা না থাকুক
NETWORK_KB_PER_POLL = 3 # হেডার + "কোনো নতুন ডেটা নেই" রেসপন্স সমেত
naive_battery_per_user = polls_per_user_per_day * BATTERY_UNITS_PER_POLL
naive_network_kb_per_user = polls_per_user_per_day * NETWORK_KB_PER_POLL
naive_battery_fleet = naive_battery_per_user * ACTIVE_USERS
naive_network_kb_fleet = naive_network_kb_per_user * ACTIVE_USERS
# ---------- পদ্ধতি ২: পুশ + ব্যাচড ব্যাকগ্রাউন্ড সিঙ্ক (ধাপ ৩-৪ স্টাইল) ----------
EVENTS_PER_USER_PER_DAY = 50 # ব্যবহারকারীর জন্য সত্যিকারের নতুন ডেটার গড় সংখ্যা (নতুন/আপডেটেড নোট)
PUSH_BATTERY_UNITS = 1 # OS-লেভেল পুশ ডেলিভারি, অ্যাপকে নিজে রেডিও পোল করতে হয় না
PUSH_NETWORK_KB = 1
BATCHED_SYNCS_PER_DAY = 6 # প্রতি ৪ ঘণ্টায় একবার -- অনুকূল শর্তে (Wi-Fi/চার্জিং) OS চালায়
BATCH_BATTERY_UNITS = 4 # একবারে অনেকগুলো অপারেশন, প্রতি সিঙ্কে খরচ বেশি কিন্তু সংখ্যায় কম
BATCH_NETWORK_KB = 8
optimized_battery_per_user = (EVENTS_PER_USER_PER_DAY * PUSH_BATTERY_UNITS
+ BATCHED_SYNCS_PER_DAY * BATCH_BATTERY_UNITS)
optimized_network_kb_per_user = (EVENTS_PER_USER_PER_DAY * PUSH_NETWORK_KB
+ BATCHED_SYNCS_PER_DAY * BATCH_NETWORK_KB)
optimized_battery_fleet = optimized_battery_per_user * ACTIVE_USERS
optimized_network_kb_fleet = optimized_network_kb_per_user * ACTIVE_USERS
# ---------- তুলনা ----------
print(f"প্রতি ব্যবহারকারী পোল সংখ্যা/দিন (naive): {polls_per_user_per_day:,}")
print()
print(f"{'':30s} {'naive পোলিং':>20s} {'পুশ + ব্যাচড সিঙ্ক':>22s}")
print("-" * 76)
print(f"{'ব্যাটারি ইউনিট/ব্যবহারকারী/দিন':30s} {naive_battery_per_user:>20,} {optimized_battery_per_user:>22,}")
print(f"{'নেটওয়ার্ক KB/ব্যবহারকারী/দিন':30s} {naive_network_kb_per_user:>20,} {optimized_network_kb_per_user:>22,}")
print(f"{'মোট ব্যাটারি ইউনিট (ফ্লিট)':30s} {naive_battery_fleet:>20,} {optimized_battery_fleet:>22,}")
print(f"{'মোট নেটওয়ার্ক KB (ফ্লিট)':30s} {naive_network_kb_fleet:>20,} {optimized_network_kb_fleet:>22,}")
battery_reduction_pct = (1 - optimized_battery_fleet / naive_battery_fleet) * 100
network_reduction_pct = (1 - optimized_network_kb_fleet / naive_network_kb_fleet) * 100
battery_multiplier = naive_battery_fleet / optimized_battery_fleet
network_multiplier = naive_network_kb_fleet / optimized_network_kb_fleet
print()
print(f"ব্যাটারি খরচ কমেছে: {battery_reduction_pct:.2f}% (naive প্রায় {battery_multiplier:.1f}x বেশি খরচ করে)")
print(f"নেটওয়ার্ক খরচ কমেছে: {network_reduction_pct:.2f}% (naive প্রায় {network_multiplier:.1f}x বেশি খরচ করে)")
assert optimized_battery_fleet < naive_battery_fleet, "অপ্টিমাইজড পদ্ধতির ব্যাটারি খরচ কম হওয়া উচিত ছিল"
assert optimized_network_kb_fleet < naive_network_kb_fleet, "অপ্টিমাইজড পদ্ধতির নেটওয়ার্ক খরচ কম হওয়া উচিত ছিল"
১,৯২০ বার রেডিও ওয়েকআপ
করায় (৯৯%+ ক্ষেত্রেই "কোনো নতুন ডেটা নেই" উত্তর নিয়ে ফেরত আসে), অথচ পুশ + ব্যাচড সিঙ্ক পদ্ধতিতে রেডিও তখনই
ওয়েকআপ হয় যখন সত্যিই নতুন কিছু আছে (দিনে গড়ে মাত্র ৫০ বার), বাকি সবকিছু দিনে মাত্র ৬ বার একসাথে ব্যাচ করে
পাঠানো হয়। ব্যাটারি ও নেটওয়ার্ক উভয় খরচেই এটি প্রায় ৯৮% হ্রাস আনে — লাখো ডিভাইসে গুণিত হলে এই পার্থক্যই
একটি অ্যাপকে "ব্যাটারি খেয়ে ফেলে" এমন বদনাম থেকে বাঁচায়।
৩ · ব্যাকএন্ডের দিকটা — এক বাক্যে
এই একই স্কেলিং যাত্রায় ব্যাকএন্ডকেও সমান্তরালভাবে বদলাতে হয়েছে — লোড ব্যালান্সিং, ডেটাবেস শার্ডিং, মেসেজ কিউ দিয়ে পুশ ডেলিভারি ফ্যান-আউট করা — কিন্তু সেই সম্পূর্ণ ডিস্ট্রিবিউটেড-সিস্টেমস আলোচনা System Design কোর্সের বিষয়; এই পাঠ ইচ্ছাকৃতভাবে শুধু মোবাইল ক্লায়েন্টের দৃষ্টিকোণেই থেকেছে।
মোবাইল অ্যাপ স্কেল করা মানে শুধু "সার্ভারে বেশি ইউজার সামলানো" নয় — প্রতিটি ডিভাইসের ব্যাটারি ও নেটওয়ার্ক খরচ লাখো গুণ বাড়ানো একটি বাস্তব সমস্যা। প্রতিটি আর্কিটেকচারাল পরিবর্তন (ক্যাশিং → অফলাইন-ফার্স্ট → পুশ → ব্যাচিং) একটি নির্দিষ্ট, পরিমাপযোগ্য সমস্যার সমাধান হিসেবে এসেছে, কোনো তাত্ত্বিক "সেরা প্র্যাকটিস" মেনে নয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
কোড সেলে PUSH_BATTERY_UNITS (১) BATTERY_UNITS_PER_POLL (২)-এর চেয়ে কম
ধরা হয়েছে কেন — বাস্তবে কি সত্যিই তাই?
একটি পুশ বার্তা এলে OS-এর নিজস্ব রেডিও/পুশ-সার্ভিস চ্যানেল ইতিমধ্যে কাজ করছিল (একটি একক দীর্ঘস্থায়ী কানেকশন হাজারো অ্যাপের জন্য শেয়ার্ড), তাই অ্যাপের নিজের আলাদা করে রেডিও ওয়েকআপ ও নেটওয়ার্ক হ্যান্ডশেক করতে হয় না — শুধু ডেলিভার হওয়া পেলোড প্রসেস করলেই হয়। বাস্তবেও এই কারণেই পুশ ডেলিভারি একটি অ্যাপের নিজস্ব পোলিং-এর চেয়ে সিস্টেম-লেভেলে কার্যকরভাবে সস্তা ধরা হয়।
প্র ০২
যদি EVENTS_PER_USER_PER_DAY অনেক বেশি হয়ে যায় (যেমন ১,৫০০, একটি খুব সক্রিয় চ্যাট
অ্যাপের মতো), তাহলে কি পুশ + ব্যাচড সিঙ্ক পদ্ধতি এখনও naive পোলিং-এর চেয়ে ভালো থাকবে?
হ্যাঁ, কারণ পোলিং-এর খরচ নির্দিষ্ট (দিনে ১,৯২০ বার, ইভেন্ট সংখ্যা নির্বিশেষে) — অথচ পুশ পদ্ধতির খরচ
সরাসরি প্রকৃত ইভেন্ট সংখ্যার সমানুপাতিক। EVENTS_PER_USER_PER_DAY ১,৯২০-এর কাছাকাছি বা
তার বেশি হয়ে গেলে পুশ পদ্ধতির সুবিধা কমতে থাকবে, কিন্তু ব্যাচড অংশটি (দিনে মাত্র ৬ বার) তখনও অপরিবর্তিত
থাকবে — কোড সেলে সংখ্যাটি বদলে সরাসরি Run করে দেখাই সবচেয়ে নির্ভরযোগ্য উপায় ঠিক কোন পয়েন্টে এই সুবিধা
কমতে শুরু করে তা যাচাই করার।
প্র ০৩ ধাপ ২ (অফলাইন-ফার্স্ট সিঙ্ক) ছাড়া সরাসরি ধাপ ১ থেকে ধাপ ৩-এ (পুশ-ভিত্তিক রিয়েল-টাইম) যাওয়া কেন সমস্যাজনক হতো?
পুশ শুধু "নতুন ডেটা আছে" এই সংকেত পাঠায় — এটি নিজে কোনো নির্ভরযোগ্য লোকাল-রাইট বা সিঙ্ক-কিউ ব্যবস্থা দেয় না। অফলাইন-ফার্স্ট সিঙ্ক (ধাপ ২) ছাড়া, ব্যবহারকারী অফলাইনে থাকা অবস্থায় একটি নোট লিখলে সেটি কোথাও নির্ভরযোগ্যভাবে সংরক্ষিত থাকত না — নেটওয়ার্ক ফিরে এলে পুশ শুধু অন্য ডিভাইসের পরিবর্তনের কথা জানাতে পারত, কিন্তু নিজের অসংরক্ষিত পরিবর্তনটি হারিয়ে যেতেই পারত। তাই ভিত্তি (লোকাল-প্রথম লেখা) আগে স্থাপন করা জরুরি ছিল, তার উপরেই রিয়েল-টাইম সংকেত স্তর যুক্ত হয়েছে।
অনুশীলন
-
চিন্তা করুন: কোড সেলে
BATCHED_SYNCS_PER_DAY৬ (প্রতি ৪ ঘণ্টায়) থেকে কমিয়ে ২ (প্রতি ১২ ঘণ্টায়) করা হলে ব্যাটারি খরচ কমবে, কিন্তু এর একটি বাস্তব খরচও (trade-off) আছে — সেটা কী বলে আপনার মনে হয়?ব্যাচড সিঙ্কের ফ্রিকোয়েন্সি কমালে ব্যাটারি বাঁচে ঠিকই, কিন্তু পুশ ছাড়া পাঠানো অন্য যেকোনো ডেটা (যেমন অ্যানালিটিক্স ইভেন্ট, ব্যাকগ্রাউন্ডে জমে থাকা মাইনর আপডেট) সার্ভারে পৌঁছাতে বেশি সময় নেবে — অর্থাৎ freshness (তথ্যের সাম্প্রতিকতা) বনাম ব্যাটারির মধ্যে একটি ট্রেড-অফ। এই কারণেই বাস্তব অ্যাপগুলো প্রায়ই ব্যাটারি চার্জিং অবস্থায় থাকলে ব্যাচ ফ্রিকোয়েন্সি বাড়িয়ে দেয় (যেমন L48-এ দেখা "GPS শুধু স্ক্রিন-অন অবস্থায়" নীতির মতো কন্ডিশনাল আচরণ)।
-
পরীক্ষা করুন: উপরের কোড সেলে
ACTIVE_USERS-কে ২০ লাখ থেকে বাড়িয়ে ২০ কোটি (200_000_000) করুন এবং Run চেপে দেখুনbattery_reduction_pctওnetwork_reduction_pct-এর মান বদলায় কি না।না, শতাংশ দুটি অপরিবর্তিত থাকবে (এখনও প্রায় ৯৮.০৭% ও ৯৮.৩০%) — কারণ
ACTIVE_USERSউভয় পদ্ধতির ফ্লিট-ওয়াইড হিসাবে একই গুণক হিসেবে যুক্ত হয়, তাই অনুপাত অপরিবর্তিত থাকে। যা বদলাবে তা হলোnaive_battery_fleetওoptimized_battery_fleet-এর পরম মান (absolute value) — উভয়ই ১০০ গুণ বেড়ে যাবে। এটাই দেখায় যে প্রতি-ব্যবহারকারী দক্ষতা (efficiency) স্কেল-স্বাধীন, কিন্তু মোট প্রভাব ব্যবহারকারী সংখ্যার সাথে সরলরৈখিকভাবে বাড়ে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরের পাঠ L56 ক্রস-প্ল্যাটফর্ম বনাম নেটিভ সিদ্ধান্ত কাঠামো — একটি worked example।
- আগের পাঠ L54 অ্যাপ স্টোর ও প্লে স্টোর রিলিজ প্রসেস।
- System Design কোর্স সহোদর কোর্স এই কেস স্টাডির ব্যাকএন্ড-স্কেলিং দিকটা (লোড ব্যালান্সিং, শার্ডিং, মেসেজ কিউ) বিস্তারিতভাবে সেই কোর্সে কভার করা হয়েছে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাপ লাইফসাইকেল, মোবাইল UI/UX, MVVM/MVI আর্কিটেকচার, নেটিভ বনাম ক্রস-প্ল্যাটফর্ম, নেভিগেশন, স্টেট ম্যানেজমেন্ট, লোকাল স্টোরেজ, ডিভাইস ফিচার ও ডিপ্লয়মেন্ট।