চ্যাট সিস্টেম ডিজাইন (Design a Chat System)
রিকোয়ারমেন্ট বা প্রয়োজনীয়তাগুলো বোঝা
আমরা হোয়াটসঅ্যাপ (WhatsApp), স্ল্যাক (Slack) বা ডিসকর্ডের (Discord) মতো একটি চ্যাট সিস্টেম ডিজাইন করতে যাচ্ছি। চলুন সঠিক প্রশ্নগুলো করার মাধ্যমে শুরু করি।
ফাংশনাল রিকোয়ারমেন্ট (ব্যবহারিক প্রয়োজনীয়তা):
- ১:১ মেসেজিং (1:1 messaging) — আনিকা রাফিকে একটি মেসেজ বা বার্তা পাঠাবে। রাফি সেটি (প্রায়) তাৎক্ষণিকভাবে দেখতে পাবে।
- গ্রুপ চ্যাট (Group chat) — একটি গ্রুপে সর্বোচ্চ ৫০০ জন সদস্য থাকতে পারবে।
- অনলাইন/অফলাইন স্ট্যাটাস (Online/offline status) — বর্তমানে কারা সক্রিয় বা অ্যাক্টিভ আছে তা দেখতে পাওয়া যাবে।
- রিড রিসিট (Read receipts) — আপনার মেসেজ কখন ডেলিভার (deliver) হয়েছে এবং কখন তা দেখা বা পড়া হয়েছে তা জানতে পারবেন।
- পুশ নোটিফিকেশন (Push notifications) — যেসব ব্যবহারকারী অফলাইনে আছে তাদের নোটিফাই বা অবহিত করা হবে।
- মেসেজ হিস্টোরি (Message history) — পুরনো মেসেজগুলো দেখতে স্ক্রল করে পেছনে যাওয়ার ব্যবস্থা থাকবে।
নন-ফাংশনাল রিকোয়ারমেন্ট (অ-ব্যবহারিক প্রয়োজনীয়তা):
- লো ল্যাটেন্সি (Low latency): মেসেজগুলো ২০০ মিলিসেকেন্ডের (< 200ms) মধ্যে পৌঁছাতে হবে (যেন রিয়েল-টাইম বা সাথে সাথে কথা বলার অনুভূতি হয়)।
- রিলায়্যাবিলিটি (Reliability): মেসেজ কখনোই হারিয়ে যাওয়া যাবে না। যদি রাফির ফোন বন্ধ থাকে, মেসেজটি তার জন্য অপেক্ষা করবে বা সার্ভারে স্টোর হয়ে থাকবে।
- অর্ডারিং (Ordering): একটি কথোপকথন বা কনভারসেশনের মেসেজগুলো সঠিক ক্রমানুসারে (order) উপস্থিত হতে হবে।
- স্কেল (Scale): প্রতিদিন প্রায় ৫০ মিলিয়ন (50M) সক্রিয় ব্যবহারকারী (Daily Active Users) এবং প্রতিদিন প্রায় ১ বিলিয়ন (1B) মেসেজ আদান-প্রদান হবে।
ওয়েবসকেট (WebSockets) বনাম পোলিং (Polling) বনাম লং পোলিং (Long Polling)
চ্যাটের জন্য রিয়েল-টাইম, বাইডিরেকশনাল বা দ্বিমুখী যোগাযোগ (bidirectional communication) প্রয়োজন। সার্ভারকে অবিলম্বে ক্লায়েন্টদের কাছে মেসেজ পুশ (push) করতে হবে — ক্লায়েন্টের জিজ্ঞাসা বা রিকোয়েস্ট করা পর্যন্ত অপেক্ষা করা যাবে না। চলুন অপশনগুলো বা বিকল্পগুলোর তুলনা করে দেখি:
শর্ট পোলিং (Short Polling): ক্লায়েন্ট প্রতি কয়েক সেকেন্ড পর পর সার্ভারকে জিজ্ঞাসা করে, "নতুন কোনো মেসেজ আছে কি?" এটি একটি সহজ পদ্ধতি হলেও অত্যন্ত অপচয়মূলক — ৯৯% রিকোয়েস্টের উত্তর আসে "না, নতুন কিছু নেই।" এটি অনেকটা একটি বাচ্চার মতো যে দীর্ঘ যাত্রায় প্রতি ৩০ সেকেন্ড পর পর জিজ্ঞাসা করে, "আমরা কি পৌঁছে গেছি?"
লং পোলিং (Long Polling): ক্লায়েন্ট সার্ভারকে জিজ্ঞাসা করে, "নতুন কোনো মেসেজ আছে কি?" কিন্তু সার্ভার কানেকশনটিকে ধরে রাখে বা খোলা রাখে যতক্ষণ না রিটার্ন করার মতো কিছু থাকে (অথবা কানেকশনটির টাইমআউট না হয়)। এটি শর্ট পোলিংয়ের চেয়ে বেশ ভালো — কারণ এতে খালি বা এম্পটি (empty) রেসপন্স কম আসে। কিন্তু প্রতিটি "পোল (poll)" সার্ভারের একটি কানেকশন দখল বা আবদ্ধ (tie up) করে রাখে।
ওয়েবসকেটস (WebSockets): এটি ক্লায়েন্ট এবং সার্ভারের মধ্যে একটি স্থায়ী বা পারসিস্টেন্ট (persistent), দ্বিমুখী কানেকশন। একবার কানেকশন স্থাপিত হলে, যেকোনো পক্ষ যেকোনো সময় ডেটা পাঠাতে পারে। চ্যাটের জন্য এটি হলো গোল্ড স্ট্যান্ডার্ড বা সর্বোত্তম পদ্ধতি। এতে ল্যাটিনসি কম হয়, ওভারহেড (overhead) কম হয় এবং কোনো অপচয়মূলক রিকোয়েস্ট থাকে না।
এর ট্রেড-অফ (Trade-off) বা অসুবিধা: ওয়েবসকেট কানেকশনগুলো মূলত স্টেটফুল (stateful) — অর্থাৎ আপনাকে জানতে হবে কোন ব্যবহারকারী কোন সার্ভারের সাথে যুক্ত আছে। সাধারণ স্টেটলেস HTTP-এর তুলনায় এটি অতিরিক্ত বা এক্সট্রা জটিলতা তৈরি করে। আমরা দেখব এটি কীভাবে পরিচালনা করা যায়।
একটি সাধারণ (Simple) ওয়েবসকেট চ্যাট সার্ভার
হাই-লেভেল আর্কিটেকচার (High-Level Architecture)
চলুন কম্পোনেন্টগুলো বা অংশগুলো সাজাই:
চ্যাট সার্ভার (WebSocket): এগুলো স্থায়ী বা পারসিস্টেন্ট ওয়েবসকেট (WebSocket) কানেকশনগুলো হ্যান্ডেল করে। প্রতিটি সার্ভার হাজার হাজার সক্রিয় কানেকশন বা অ্যাক্টিভ কানেকশন পরিচালনা করে। যখন কোনো মেসেজ আসে, তখন চ্যাট সার্ভার খুঁজে বের করে যে প্রাপক বা রেসিপিয়েন্ট কোন সার্ভারের সাথে যুক্ত আছে।
কানেকশন রেজিস্ট্রি (Redis): এটি user_id → chat_server_id -এর ম্যাপিং স্টোর করে। যখন ব্যবহারকারী আনিকা সার্ভার ৩-এর সাথে সংযুক্ত হয়, তখন আমরা Redis-এ anika → server-3 লিখে বা স্টোর করে রাখি। যখন রাফি আনিকাকে মেসেজ পাঠায়, তখন আমরা আনিকার সার্ভারটি খুঁজে বের করি এবং সেখানে মেসেজ রাউট বা রুট (route) করি।
মেসেজ কিউ (Kafka): এটি মেসেজ পাঠানোকে প্রসেসিং থেকে ডিকাপল (decouple) বা আলাদা করে। যখন কোনো মেসেজ পাঠানো হয়, তখন এটি Kafka-এ পাবলিশ (publish) করা হয়। কনজ্যুমাররা স্বাধীনভাবে স্টোরেজ, ডেলিভারি এবং পুশ নোটিফিকেশনগুলো নিয়ন্ত্রণ করে।
মেসেজ স্টোরেজ (Message Storage): মেসেজগুলোকে অবশ্যই রিলায়েবল, ডিউরেবল বা টেকসই হতে হবে এবং যাতে আবার ফিরে পাওয়া যায় এমন হতে হবে। এখানে দুটি অপশন বা বিকল্প রয়েছে:
- ১:১ চ্যাটের জন্য:
(user_id, conversation_id, timestamp)দ্বারা কি (key) করা একটি কী-ভ্যালু (key-value) স্টোর। Cassandra এ কাজে বেশ মানানসই — এটি চমৎকার রাইট থ্রুপুট (write throughput) দেয় এবং টাইমস্ট্যাম্প দ্বারা চমৎকার রেঞ্জ কুয়েরি করতে পারে। - গ্রুপ চ্যাটের জন্য:
(group_id, timestamp)দ্বারা মেসেজগুলো স্টোর করুন। এর ফলে প্রতিটি সদস্য তাদের গ্রুপের জন্য মেসেজগুলো রিট্রিভ (retrieve) বা উদ্ধার করতে পারে বা পড়তে পারে।
পুশ নোটিফিকেশন সার্ভিস: যখন কোনো ব্যবহারকারী অফলাইনে থাকেন, তখন APNs (iOS) বা FCM (Android) এর মাধ্যমে তাদের পুশ নোটিফিকেশন পাঠান। এটি একটি আলাদা সেবা যা মেসেজ কিউ (message queue) থেকে ডেটা ব্যবহার করে।
মেসেজ অর্ডারিং এবং আইডি (Message Ordering and IDs)
মেসেজগুলো অবশ্যই সঠিক ক্রমানুসারে (order) প্রদর্শিত হতে হবে। কিন্তু একটি ডিস্ট্রিবিউটেড সিস্টেমের ক্ষেত্রে বিভিন্ন ডিভাইসের টাইমস্ট্যাম্পগুলো সিঙ্কে (sync) বা সমলয়ে নাও থাকতে পারে (অর্থাৎ ঘড়ির সময়ে পার্থক্য থাকতে পারে)। তাহলে আপনি কীভাবে সঠিক ক্রমানুসারের নিশ্চয়তা দেবেন?
সার্ভার-সাইড টাইমস্ট্যাম্প: এটি খুব সাধারণ পদ্ধতি। চ্যাট সার্ভার যখন মেসেজটি পায়, তখন এটি একটি টাইমস্ট্যাম্প অ্যাসাইন করে বা যুক্ত করে দেয়। এটি একটি মাত্র সার্ভারের জন্য ভালোভাবে কাজ করে কিন্তু একাধিক সার্ভার হলে এটি বেশ জটিল হয়ে যায় (কারণ তাদের ঘড়ির সময় কয়েক মিলিসেকেন্ড ভিন্ন হতে পারে)।
প্রতিটি কনভারসেশনের জন্য সিকোয়েন্স নাম্বার (Sequence numbers): প্রতিটি কথোপকথন বা কনভারসেশনে একটি স্বয়ংক্রিয়ভাবে ইনক্রিমেন্টিং (auto-incrementing) সিকোয়েন্স বা সিরিয়াল নম্বর থাকে। যেমন মেসেজ ১, ২, ৩, ৪... এটি একটি কনভারসেশনের মধ্যে নিখুঁত অর্ডারিং বা ক্রম গ্যারান্টি দেয়। দ্রুত এই নম্বর বাড়ানোর জন্য সর্বশেষ সিকোয়েন্স নম্বরটি Redis-এ স্টোর করে রাখা হয়।
স্নোফ্লেক আইডি (Snowflake IDs): টুইটারের (Twitter) স্নোফ্লেক আইডি গ্লোবালভাবে ইউনিক, টাইম-অর্ডার করা বা সময় ভিত্তিক সাজানো আইডি তৈরি করে। প্রতিটি আইডিতে time-stamp (41 bits) + machine_id (10 bits) + sequence (12 bits) এনকোড করা থাকে। এর আইডিগুলো সময়ের উপর ভিত্তি করে ক্রমানুসারে সাজানো যায় (sortable), সমস্ত মেশিনের মধ্যে ইউনিক বা অনন্য থাকে এবং অন্য সার্ভারের সমন্বয় ছাড়াই তৈরি করা যায়।
অনলাইন/অফলাইন স্ট্যাটাস (Online/Offline Status)
কীভাবে বুঝবেন কোনো ব্যক্তি "অনলাইন" আছেন কি না? এর জন্য দুটি পদ্ধতি রয়েছে:
হার্টবিট-বেসড (Heartbeat-based - স্পন্দন-ভিত্তিক): প্রতিটি কানেকটেড বা সংযুক্ত ক্লায়েন্ট প্রতি ৩০ সেকেন্ডে একটি করে "হার্টবিট" (a tiny ping বা ছোট একটি সিগন্যাল) পাঠায়। সার্ভার সর্বশেষ হার্টবিটের সময়টি রেকর্ড বা সংরক্ষণ করে। যদি সর্বশেষ হার্টবিট ৬০ সেকেন্ডের চেয়েও পুরনো হয়, তবে ব্যবহারকারীকে অফলাইন হিসেবে বিবেচনা করা হয়।
সর্বশেষ দেখার (last-seen) টাইমস্ট্যাম্পগুলো রেডিজ (Redis)-এ সংরক্ষণ করুন: user:42:last_seen → 1640000000। "অনলাইন" বা "৫ মিনিট আগে দেখা গেছে (Last seen 5 min ago)" দেখানোর জন্য এটি ব্যবহার করতে পারেন।
গ্রুপ চ্যাটের ক্ষেত্রে: কেউ অনলাইনে আসলে বা অফলাইনে গেলে ৫০০ সদস্যের সবাইকে একই সাথে অনলাইন স্ট্যাটাস দেখানো সিস্টেমের বেশ লোড সৃষ্টি করবে (এটা বেশ চ্যাটি বা অতিরিক্ত কথা বলার মতো হয়ে যাবে)। এর সমাধান: শুধুমাত্র যখন কোনও ব্যবহারকারী নির্দিষ্ট একটি চ্যাট ওপেন বা খুলবে, তখনই অনলাইন স্ট্যাটাস দেখান। ক্রমাগতভাবে পুশ (push) না করে, প্রয়োজন হলে বা অন-ডিমান্ড (on demand) অবস্থা থেকে স্ট্যাটাস ফেচ (fetch) করুন বা তুলে আনুন।
রিড রিসিট (Read Receipts - পড়ার রশিদ)
রিড রিসিট ("ডেলিভারড" ✓ এবং "পড়া হয়েছে" ✓✓) সিস্টেমে আরও একটি লেয়ার বা স্তর যুক্ত করে:
- সেন্ড (Sent - পাঠানো হয়েছে): সার্ভার বা আপনার মেসেজটি পেয়েছে (প্রেরকের কাছে এটি নিশ্চিত বা নিশ্চিতকরণ করা হয়েছে)।
- ডেলিভারড (Delivered - পৌঁছে দেওয়া হয়েছে): প্রাপকের বা যার কাছে পাঠাচ্ছেন তার ডিভাইসটি মেসেজ পেয়েছে (প্রাপকের ডিভাইস বা ক্লায়েন্ট নিশ্চিত করে এটি সার্ভারকে বলবে)।
- রিড (Read - পড়া হয়েছে): প্রাপক চ্যাট ওপেন করেছে (ব্যবহারকারী যখন কথোপকথন বা মেসেজটি দেখে তখন ক্লায়েন্ট বা তার ডিভাইস একটি "রিড (read)" ইভেন্ট সার্ভারকে পাঠায়)।
১:১ চ্যাটের ক্ষেত্রে এটি খুব সহজ — প্রতিটি মেসেজের জন্য একটি মাত্র নিশ্চিতকরণ বা অ্যাকনোলেজমেন্ট (acknowledgment)। তবে গ্রুপ চ্যাটের ক্ষেত্রে, কে মেসেজটি পড়েছে তা ট্র্যাক করা বেশ ব্যয়বহুল বা কঠিন। হোয়াটসঅ্যাপ এই সমস্যাটির সমাধান করেছে একটি চালাকির মাধ্যমে, আপনি যখন মেসেজটির উপর ট্যাপ করে ধরে রাখেন শুধুমাত্র তখনই রিড ইনফো (read info) বা বিস্তারিত তথ্য দেখায় — এটি রিয়েল-টাইমে সবার জন্য ট্র্যাক করার পরিবর্তে যখন প্রয়োজন বা অন-ডিমান্ড (on demand) হয়, তখনই তথ্যটি ফেচ (fetch) করে নিয়ে আসে।
চ্যাট সিস্টেম স্কেলিং করা
প্রতিদিন ৫০ (50M) মিলিয়ন অ্যাক্টিভ ইউজার ১ (1B) বিলিয়ন মেসেজ পাঠালে, সেকেন্ডে প্রায় ১২,০০০ মেসেজ পাঠানো হয় (12,000 messages/sec)। সর্বোচ্চ চাপের সময়ে (peak) এটি ৩ গুণ বেড়ে প্রায় ৩৬,০০০ মেসেজ প্রতি সেকেন্ডে পৌঁছাতে পারে। সিস্টেমের প্রতিটি অংশ কীভাবে স্কেল করা যায় তা নিচে দেওয়া হলো:
- চ্যাট সার্ভার: প্রতিটি সার্ভার প্রায় ১০,০০০ সমসাময়িক ওয়েবসকেট কানেকশন হ্যান্ডেল করে। ৫০ (50M) মিলিয়ন অ্যাক্টিভ ইউজারের (সম্ভবত এর মধ্যে ১০ মিলিয়ন একসাথে অনলাইনে থাকে) জন্য, আপনার প্রায় ১,০০০টি চ্যাট সার্ভার প্রয়োজন হবে। কনসিস্টেন্ট হ্যাশিং (Consistent hashing) ব্যবহার করে ব্যবহারকারীদের সার্ভারের সাথে অ্যাসাইন বা যুক্ত করুন।
- রেডিজ (Redis - কানেকশন রেজিস্ট্রি): এটি এমন একটি রেডিজ ক্লাস্টার যা ইউজার→সার্ভার (user→server) ম্যাপিং স্টোর করে। ১০ মিলিয়ন ছোট মানের এন্ট্রি খুব অনায়াসেই এর মেমোরিতে জায়গা করে নিতে পারে।
- কাফকা (Kafka): কনভারসেশন আইডি (conversation_id) ব্যবহার করে মেসেজ টপিকগুলোকে পার্টিশন বা ভাগ করুন, যাতে একটি কনভারসেশনের সমস্ত মেসেজ একই পার্টিশনে ক্রমানুসারে থাকে।
- ক্যাসান্ড্রা (Cassandra - মেসেজ স্টোরেজ): কনভারসেশন আইডি (conversation_id) দিয়ে পার্টিশন করুন এবং টাইমস্ট্যাম্প (timestamp) দিয়ে ক্লাস্টার করুন। এটি আপনার লেখার কাজকে (writes) অত্যন্ত দ্রুত করবে এবং পুরনো মেসেজ পড়ার বা হিস্টরি রিট্রিভাল অনেক ফাস্ট বা দ্রুত করে তুলবে।
Key Metrics
ছোট কুইজ
পড়া চালিয়ে যান