কেস স্টাডি: চ্যাট/মেসেজিং সিস্টেম ডিজাইন করা
এই পাঠে যা শিখবেন
- রিয়েল-টাইম চ্যাট সিস্টেমের ফাংশনাল ও নন-ফাংশনাল রিকোয়ারমেন্ট
- কেন ক্রস-সার্ভার মেসেজ রাউটিং-এর জন্য pub/sub একটি WebSocket-ভিত্তিক সিস্টেমে অপরিহার্য
- কনভার্সেশন-ভিত্তিক পার্টিশনিং কীভাবে চ্যাট হিস্টোরি স্টোরেজকে স্কেল করে
- অনলাইন বনাম অফলাইন রিসিভারের জন্য ডেলিভারি লজিক কীভাবে ভিন্ন হয়, তবু ডিউরেবিলিটি বজায় থাকে
১ · রিকোয়ারমেন্ট
রিয়েল-টাইম ১-এর-সাথে-১ ও গ্রুপ মেসেজিং, মেসেজ হিস্টোরি দেখা, ডেলিভারি/সিন স্ট্যাটাস।
খুব কম-লেটেন্সি ডেলিভারি, একটি কনভার্সেশনে মেসেজ অর্ডার বজায় থাকা, লক্ষ লক্ষ কনকারেন্ট কানেকশনে স্কেল করা।
২ · হাই-লেভেল ডিজাইন — WebSocket ও কানেকশন ম্যানেজমেন্ট
L08-এ শেখা WebSocketWebSocketএকটি একক, দীর্ঘস্থায়ী TCP কানেকশনের উপর ফুল-ডুপ্লেক্স যোগাযোগ — সার্ভার ক্লায়েন্টকে সরাসরি পুশ করতে পারে, বারবার পোলিং ছাড়াই। এখানে রিয়েল-টাইম ডেলিভারির ভিত্তি। প্রতিটি ইউজার তার ডিভাইস থেকে একটি অ্যাপ সার্ভারের সাথে একটি WebSocket কানেকশন খোলে এবং সেটা ধরে রাখে। কিন্তু একাধিক অ্যাপ সার্ভার ইনস্ট্যান্স থাকলে, দুই ইউজারের কানেকশন সাধারণত ভিন্ন সার্ভারে থাকে — তাই একটি কানেকশন-ম্যানেজমেন্ট লেয়ার দরকার যা ট্র্যাক রাখে কোন ইউজারের লাইভ কানেকশন কোন সার্ভার ইনস্ট্যান্সে আছে।
যখন Alice (সার্ভার ১-এ সংযুক্ত) Bob-কে (সার্ভার ২-এ সংযুক্ত) মেসেজ পাঠায়, সার্ভার ১ সরাসরি সার্ভার ২-এর মেমরিতে পৌঁছাতে পারে না। সমাধান — L22-এর pub/sub প্যাটার্ন — সার্ভার ১ বার্তাটি pub/sub-এ পাবলিশ করে; যে সার্ভার (সার্ভার ২) Bob-এর কানেকশন ধরে আছে সে সাবস্ক্রাইবার হিসেবে বার্তাটি পায় এবং নিজের WebSocket দিয়ে Bob-কে ফরোয়ার্ড করে।
৩ · স্টোরেজ — কনভার্সেশন-ভিত্তিক পার্টিশনিং
চ্যাট মেসেজের অ্যাক্সেস প্যাটার্ন খুবই নির্দিষ্ট — প্রায় সবসময় "এই কনভার্সেশনের সাম্প্রতিক মেসেজগুলো দাও"। এটি একটি ওয়াইড-কলাম/NoSQL স্টোরের (L14) জন্য আদর্শ — যেখানে ডেটা কনভার্সেশন আইডি দিয়ে শার্ড করা (L17) হয়, যাতে একটি কনভার্সেশনের সব মেসেজ একই শার্ডে/পার্টিশনে থাকে এবং একটি রেঞ্জ-কোয়েরি দিয়ে দ্রুত পড়া যায়।
৪ · অফলাইন রিসিভার — ডিউরেবিলিটি প্রথমে
একটি বার্তা সবসময় প্রথমে persist হয় (স্টোরেজে সেভ) — রিসিভার অনলাইন থাকুক বা না থাকুক। রিসিভার অনলাইন থাকলে pub/sub দিয়ে সাথে সাথে push হয় (উপরের ডায়াগ্রাম)। রিসিভার অফলাইন থাকলে, বার্তা শুধু persist হয়ে থাকে; রিসিভার পরে রিকানেক্ট করলে ক্লায়েন্ট তার "last-seen marker"-এর পর থেকে সব মেসেজ ফেচ করে নেয়। এই ক্রম (persist-প্রথমে, delivery-পরে) নিশ্চিত করে কোনো বার্তা হারায় না, চাই রিসিভার যতক্ষণই অফলাইন থাকুক।
# কোন ইউজারের লাইভ WebSocket কানেকশন কোন সার্ভার ইনস্ট্যান্স ধরে আছে
connection_registry = {
"alice": "server-1",
"bob": "server-2",
"carol": "server-3",
"dave": "server-1",
}
servers = ["server-1", "server-2", "server-3"]
message_store = [] # ডিউরেবল স্টোরেজ (L14) — সবসময় প্রথমে এখানে সেভ হয়
def send_message(from_user, to_user, text):
# ১) durability প্রথমে — ডেলিভারি হোক বা না হোক, বার্তা persist হয়
message_store.append((from_user, to_user, text))
if to_user not in connection_registry:
print(f"[persist-only] '{to_user}' অফলাইন -> বার্তা সংরক্ষিত, রিকানেক্টে ফেচ হবে")
return
target_server = connection_registry[to_user]
# pub/sub সিমুলেশন: সব সার্ভারে পাবলিশ হয়, শুধু কানেকশন-ধারী সার্ভার ফরোয়ার্ড করে
for server in servers:
if server == target_server:
print(f"[{server}] DELIVER -> '{to_user}': \"{text}\" (from {from_user})")
# --- সিমুলেশন: বিভিন্ন সার্ভার জুড়ে সংযুক্ত ইউজারদের মধ্যে বার্তা ---
send_message("alice", "bob", "hey Bob, free tonight?")
send_message("bob", "carol", "Carol, check the report pls")
send_message("carol", "dave", "Dave, meeting moved to 5pm")
send_message("dave", "erin", "Erin, are you there?") # erin এখনো রেজিস্ট্রিতে নেই (অফলাইন)
print(f"\nমোট persisted মেসেজ: {len(message_store)}")
৫ · ট্রেড-অফ
একটি অতিরিক্ত কম্পোনেন্ট ও নেটওয়ার্ক-হপ যোগ হয়, কিন্তু এটাই একমাত্র উপায় সার্ভারগুলোকে একে অপরের থেকে ডিকাপল রেখে ক্রস-সার্ভার ডেলিভারি সম্ভব করার।
একই কনভার্সেশনের মেসেজ একই পার্টিশনে (L17) রাখলে ক্রম বজায় রাখা সহজ হয় — ভিন্ন পার্টিশন জুড়ে গ্লোবাল অর্ডার নিশ্চিত করা অনেক ব্যয়বহুল।
একটি ইউজারের WebSocket কানেকশন একটি নির্দিষ্ট সার্ভারে "স্টিকি" থাকে (L12-এর স্টেটফুল আলোচনা) — সেই সার্ভার ডাউন হলে ইউজারকে পুনরায় কানেক্ট করে নতুন সার্ভারে রেজিস্ট্রি আপডেট করতে হয়।
একটি চ্যাট সিস্টেমের আসল কঠিন সমস্যা মেসেজ পাঠানো নয় — এটি হলো "সঠিক সার্ভার" খুঁজে বের করা যেখানে রিসিভারের লাইভ কানেকশন আছে। pub/sub (L22) এই লুকআপ-ও-ফরোয়ার্ড সমস্যার সমাধান দেয়, আর ডিউরেবিলিটি-প্রথমে নীতি নিশ্চিত করে রিসিভার অনলাইন থাকুক বা না থাকুক, কোনো বার্তা হারায় না।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ কেন সেন্ডার সরাসরি রিসিভারের WebSocket-এ বার্তা পাঠাতে পারে না, এবং pub/sub কীভাবে এই সমস্যা সমাধান করে?
কারণ সেন্ডার ও রিসিভারের WebSocket কানেকশন প্রায়ই ভিন্ন ভিন্ন সার্ভার ইনস্ট্যান্সের মেমরিতে থাকে — একটি সার্ভার প্রসেস অন্য প্রসেসের মেমরিতে সরাসরি প্রবেশ করতে পারে না। Pub/sub (L22) একটি শেয়ার্ড মধ্যস্থতাকারী হিসেবে কাজ করে — যেকোনো সার্ভার সেখানে বার্তা পাবলিশ করতে পারে, আর যে সার্ভার আসলে রিসিভারের কানেকশন ধরে আছে সে সাবস্ক্রাইবার হিসেবে সেটা পেয়ে ফরোয়ার্ড করে দেয়।
প্র ০২ কেন চ্যাট মেসেজ কনভার্সেশন আইডি দিয়ে শার্ড করা হয়, সেন্ডার বা রিসিভারের ইউজার আইডি দিয়ে নয়?
সবচেয়ে সাধারণ কোয়েরি হলো "এই কনভার্সেশনের মেসেজগুলো দাও" — কনভার্সেশন আইডি দিয়ে শার্ড করলে একটি কনভার্সেশনের সব মেসেজ একই শার্ডে থাকে, তাই একটি সিঙ্গেল-শার্ড রেঞ্জ-কোয়েরিতেই পুরো হিস্টোরি পাওয়া যায়। ইউজার আইডি দিয়ে শার্ড করলে একটি গ্রুপ কনভার্সেশনের মেসেজ একাধিক ইউজারের শার্ডে ছড়িয়ে যেতে পারত, যা একটি মহার্ঘ ক্রস-শার্ড কোয়েরি (L17-এর আলোচনার সমস্যা) তৈরি করত।
প্র ০৩ একজন রিসিভার অফলাইন থাকা অবস্থায় বার্তা পাঠালে, কীভাবে নিশ্চিত হওয়া যায় বার্তাটি হারাবে না?
বার্তাটি pub/sub-এ পাঠানোর আগেই স্টোরেজে persist করা হয় — এই ক্রমটাই আসল চাবিকাঠি। রিসিভার অফলাইন থাকলে শুধু pub/sub ডেলিভারি স্কিপ হয় (কেউ সাবস্ক্রাইব করছে না), কিন্তু বার্তা ইতিমধ্যেই ডেটাবেসে সংরক্ষিত। রিসিভার পরে রিকানেক্ট করলে ক্লায়েন্ট তার শেষ-দেখা মার্কারের পর থেকে সব মেসেজ ফেচ করে নেয় — অর্থাৎ ডেলিভারি মেকানিজম (pub/sub) ব্যর্থ হলেও ডেটা কখনো হারায় না।
অনুশীলন
-
চিন্তা করুন: উপরের কোড সেলে "erin" যদি পরে "server-2"-তে কানেক্ট করে connection_registry-তে যোগ হয়, তাহলে তার আগের অফলাইন মেসেজ ("Dave, are you there?") সে কীভাবে পাবে? কোন কম্পোনেন্ট এই কাজ করবে — pub/sub না ডেটাবেস?
pub/sub নয়, কারণ pub/sub শুধু বর্তমানে-অনলাইন সাবস্ক্রাইবারদের কাছে বার্তা পৌঁছায় — অতীতের বার্তা এটি "মনে রাখে না"। এই কাজটি করবে message_store (ডেটাবেস) — Erin রিকানেক্ট করার পর ক্লায়েন্ট একটি "since last-seen" কোয়েরি চালাবে message_store-এ, যা তার নামে সব পেন্ডিং মেসেজ (এক্ষেত্রে Dave-এর বার্তা) ফেরত দেবে।
-
ডিজাইন করুন: একটি গ্রুপ চ্যাট (৫ জন সদস্য) এ একজন মেসেজ পাঠালে বাকি ৪ জনের কাছে পৌঁছাতে হবে। উপরের send_message ফাংশনটি কীভাবে বদলাবেন যাতে এটি একাধিক রিসিভার হ্যান্ডল করতে পারে?
এক জনের বদলে একটি to_users তালিকা (গ্রুপ মেম্বারশিপ থেকে) নিতে হবে, এবং একই persist-then-lookup লজিক প্রতিটি রিসিভারের জন্য আলাদাভাবে চালাতে হবে — প্রতিটি রিসিভারের জন্য connection_registry-তে তার সার্ভার খুঁজে বার্তা ফরোয়ার্ড করা (অনলাইন হলে) বা persist-only রাখা (অফলাইন হলে)। এটি L46-এর ফ্যান-আউট সমস্যার (একজনের একটি অ্যাকশন, বহু জনের কাছে পৌঁছানো) একটি ছোট সংস্করণ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫১টি পাঠ পরবর্তী কেস স্টাডি — একজনের পোস্ট লক্ষ লক্ষ ফলোয়ারের ফিডে কীভাবে পৌঁছায়।
- কেস স্টাডি: নিউজফিড/টাইমলাইন ডিজাইন করা পরবর্তী পাঠ ফ্যান-আউট-অন-রাইট বনাম ফ্যান-আউট-অন-রিড — "সেলিব্রিটি" সমস্যার সমাধান।
- কেস স্টাডি: রেট লিমিটার ডিজাইন করা আগের পাঠ শেয়ার্ড স্টেট নিয়ে আরও একটি কেস স্টাডি — এখানে রেট লিমিটিং প্রসঙ্গে।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics ও System Design — সব এক জায়গায়।