০০ যে প্রশ্নগুলোর উত্তর খুঁজবো
Chat app আমরা সবাই দিনে শতবার খুলি। একটা message লিখি, send চাপি, আর এক মুহূর্ত পরে পাশে একটা tick আসে। এত সহজ একটা কাজের পেছনে আসলে অনেকগুলো সমস্যা লুকিয়ে আছে। কোটি কোটি phone এর সাথে একসাথে যোগাযোগ রাখতে হয়। দুর্বল network এ message হারানো চলে না। আর ৪০ জনের group এ সবার screen এ একই order দেখাতে হয়।
এই সেশনে আমরা “আড্ডা” নামের একটা chat app শুরু থেকে design করবো, ঠিক WhatsApp এর মতো। স্টেজ ০৯ এর বাকি সেশনগুলোর মতো এখানেও একই ধাপ মেনে চলবো: requirements, সংখ্যার হিসাব, API, data model, high-level design, তারপর একটা একটা করে গভীরে যাওয়া। শেষে দেখবো কোথায় চাপ পড়ে, আর কোন trade-off মেনে নিতে হয়।
- ১ আড্ডা app কে কী কী করতে হবে? (functional আর non-functional requirements)
- ২ কত message, কত connection, কত storage? (back-of-envelope estimation)
- ৩ নতুন message আনতে HTTP polling কেন যথেষ্ট নয়? (long polling, WebSocket)
- ৪ লাখো WebSocket connection কোথায় খোলা থাকে, আর message ঠিক connection server এ যায় কীভাবে? (connection server, routing)
- ৫ এক tick, দুই tick আর নীল tick কীভাবে কাজ করে? (delivery receipt)
- ৬ Offline user এর message কোথায় অপেক্ষা করে, আর order ঠিক থাকে কীভাবে? (data model, message ordering)
- ৭ ৪০ জনের group এ একটা message সবার কাছে যায় কীভাবে? (group fan-out)
- ৮ Server কীভাবে জানে কে online, আর offline হলে কী হয়? (presence, push notification)
- ৯ Server কি message পড়তে পারে, আর পুরো design এর bottleneck কোথায়? (E2E encryption, trade-off)
০১ আড্ডা app কে কী কী করতে হবে?
শুক্রবার, রাত নয়টা। নেত্রকোনার এক গ্রামে নানু বারান্দায় বসে আছেন। মোবাইলের কোণায় সিগন্যালের একটা মাত্র দাগ, সেটাও একবার আসে, একবার যায়। অনেকক্ষণ ধরে তিনি একটা একটা অক্ষর টাইপ করলেন: “সবাই কেমন আছো?” তারপর পাঠিয়ে দিলেন পরিবারের ৪০ জনের group এ। Message এর পাশে একটা ছোট ঘড়ি দেখা দিলো। নানু চশমাটা ঠিক করে screen এর দিকে তাকিয়ে রইলেন। কখন একটা tick আসবে, কখন দুইটা, কখন নীল হবে।
ঐ group এ আছে ঢাকার রিমি, যে WiFi তে বসে পড়ছে। আছেন দুবাইয়ের মামা, যার phone এখন বন্ধ। আর আছে আরো ৩৬ জন, কেউ জেগে, কেউ ঘুমিয়ে। নানুর এক লাইনের এই message টা সবার কাছে পৌঁছানোই এই সেশনের পুরো গল্প।
যেকোনো design শুরু করার আগে ঠিক করে নিতে হয় system টা কী করবে, আর কতটা ভালোভাবে করবে। প্রথমটাকে বলে functional requirement, দ্বিতীয়টাকে non-functional requirement।
Functional requirements (কী করবে)
- ১:১ আর group chat: দুজনের মধ্যে chat, আর group chat। Group এ সর্বোচ্চ ৫১২ জন ধরে নিচ্ছি।
- Message পাঠানো আর পাওয়া: দুজনেই online থাকলে message প্রায় সাথে সাথে পৌঁছাবে।
- Tick: sent, delivered আর read, তিনটা অবস্থা প্রেরক দেখতে পাবে।
- Offline user: phone বন্ধ থাকলেও message হারাবে না। Phone চালু হলেই পৌঁছে যাবে, আর একটা notification যাবে।
- Presence: কে online আছে, আর কে শেষ কখন online ছিলো (last seen)।
- ছবি আর video: এগুলো আলাদা পথে upload হয়। Object storage আর CDN এর আলাপ সেশন ২৩ আর ০৬ এ আছে, এখানে শুধু এক লাইনে ছুঁয়ে যাবো।
Voice call, video call, status আর payment এই সেশনের বাইরে রাখছি।
Non-functional requirements (কতটা ভালোভাবে করবে)
- কম latency: দুজনেই online থাকলে message ৫০০ms এর মধ্যে পৌঁছাবে।
- Message হারাবে না: Server একবার “পেয়েছি” বললে সেই message আর কখনো হারাবে না।
- Order ঠিক থাকবে: একটা chat এর ভেতরে সবার screen এ message একই ক্রমে দেখাবে।
- দুর্বল network সহ্য করবে: 2G, বারবার connection কেটে যাওয়া, এগুলো স্বাভাবিক ধরে নিয়ে design করতে হবে। নানুর মতো user রাই এখানে আসল পরীক্ষা।
- High availability: কিছু server down হলেও app চলবে।
- Privacy: message এর লেখা শুধু প্রেরক আর প্রাপক পড়তে পারবে, server নয়।
Interview এ “WhatsApp design করুন” শুনেই box আঁকা শুরু করবেন না। আগে interviewer এর সাথে মিলিয়ে নিন কোন feature ভেতরে আর কোনটা বাইরে। Group এর সর্বোচ্চ সাইজ, history কতদিন থাকবে, encryption লাগবে কিনা, এই তিনটা উত্তর পুরো design বদলে দিতে পারে।
০২ কত message, কত connection, কত storage?
সেশন ২৩ এ আমরা back-of-envelope estimation দেখেছি: খাতার কোণায় মোটা দাগে হিসাব করে বোঝা, system টা কত বড়। এখানেও আগে সংখ্যাগুলো বের করে নিই। ধরুন আড্ডা এখন বেশ জনপ্রিয়, প্রতিদিন ৫ কোটি মানুষ app টা খোলে:
১. ধরে নিই DAU (প্রতিদিন app খোলে): 5 কোটি = 50M একজন দিনে message পাঠায়: 40টা ২. Message rate দিনে: 50M × 40 = 2,000M message (200 কোটি) গড়ে প্রতি second: 2,000M ÷ 86,400 ≈ 23,000 msg/s Peak (ঈদের রাত, গড়ের 5 গুণ): 23,000 × 5 ≈ 1,15,000 msg/s ৩. একসাথে খোলা connection Peak এ DAU এর 40% online: 50M × 0.4 = 20M (2 কোটি) connection একটা connection server ধরে ~1 লাখ: 20M ÷ 1,00,000 = 200 server Headroom সহ (50% বাড়তি): ~300 server প্রতি connection ~20 KB memory: 1,00,000 × 20 KB ≈ 2 GB RAM প্রতি server ৪. Heartbeat 2 কোটি connection, প্রতি 30s এ 1টা ping: 20M ÷ 30 ≈ 6,70,000 ping/s ৫. Storage (শুধু text, ছবি আর video আলাদা) একটা message ~200 bytes (body + metadata) দিনে: 2,000M × 200 B = 400 GB বছরে: 400 GB × 365 ≈ 146 TB 3টা replica সহ: 146 TB × 3 ≈ 440 TB ৬. Bandwidth Peak: 1,15,000 msg/s × ~1 KB (frame + encryption) ≈ 115 MB/s ≈ ~1 Gbps
এই হিসাব থেকে তিনটা জিনিস চোখে পড়ে।
প্রথমত, message এর সংখ্যা ভয় পাওয়ার মতো নয়। ঈদের রাতে peak এ সেকেন্ডে প্রায় ১ লাখ ১৫ হাজার message, আর bandwidth মাত্র ~1 Gbps। ভালোভাবে shard করা একটা database cluster এটা সামলাতে পারে।
দ্বিতীয়ত, আসল চ্যালেঞ্জ হলো একসাথে খোলা ২ কোটি connection। প্রতিটা online phone এর সাথে server কে সারাক্ষণ একটা লাইন খোলা রাখতে হয়, message আসুক বা না আসুক। এজন্যই প্রায় ৩০০টা server শুধু connection ধরে রাখার কাজে লাগে।
তৃতীয়ত, একটা চমকে দেওয়ার মতো সংখ্যা আছে। শুধু “আমি এখনো আছি” জানানোর heartbeat ই সেকেন্ডে প্রায় ৬ লাখ ৭০ হাজার, আসল message এর প্রায় ছয় গুণ। Heartbeat হলো নিয়মিত পাঠানো ছোট একটা signal, যেটা দিয়ে বোঝা যায় অন্য পাশ এখনো চালু আছে (সেশন ০২)। এই সংখ্যাটা মাথায় রাখুন, সেকশন ০৮ এ এটা কাজে লাগবে।
Chat app এর কঠিন অংশ message সংখ্যায় নয়। কঠিন অংশ হলো কোটি কোটি connection সারাক্ষণ খোলা রাখা, আর প্রতিটা message এর সাথে আসা tick আর heartbeat এর বাড়তি traffic সামলানো।
০৩ নতুন message আনতে HTTP polling কেন যথেষ্ট নয়?
এখন পর্যন্ত আমরা যত system দেখেছি, সবগুলো HTTP এর নিয়মে চলে। Client request পাঠায়, server উত্তর দেয়। এই নিয়মে server নিজে থেকে কিছু বলতে পারে না, client না জিজ্ঞেস করা পর্যন্ত চুপ থাকে। অথচ chat app এ রিমির phone জানেই না নানু কখন message পাঠাবেন।
Short polling: বারবার জিজ্ঞেস করা
সবচেয়ে সহজ উপায় হলো phone টা কয়েক সেকেন্ড পরপর server কে জিজ্ঞেস করবে: “নতুন কিছু আছে?” এর নাম polling (বা short polling)। হিসাবটা দেখুন। ২ কোটি online phone প্রতি ৫ সেকেন্ডে একবার জিজ্ঞেস করলে server এ সেকেন্ডে ৪০ লাখ request আসে। অথচ সেকেন্ডে আসল message মাত্র ২৩ হাজারের মতো। মানে প্রতি ১৭০টার বেশি request এর মধ্যে মাত্র একটায় কাজের কিছু থাকে, বাকিগুলোর উত্তর “নেই”।
ক্ষতিটা নানুর phone এও পড়ে। প্রতিটা খালি request এর সাথে কয়েকশো byte এর header যায়, battery খরচ হয়, আর 2G তে প্রতিটা request এ এক সেকেন্ডের মতো সময় লাগে। তার উপর message গড়ে আড়াই সেকেন্ড দেরিতে পৌঁছায়, কারণ পরের প্রশ্নের জন্য অপেক্ষা করতে হয়।
Long polling: উত্তর না আসা পর্যন্ত ধরে রাখা
একটু বুদ্ধিমান উপায় হলো long polling। Phone request পাঠায়, কিন্তু server সাথে সাথে “নেই” বলে না। নতুন message না আসা পর্যন্ত, বা ধরুন ৩০ সেকেন্ড পার না হওয়া পর্যন্ত, request টা ধরে রাখে। Message এলে তখনই উত্তর দেয়। Phone উত্তর পেয়েই আবার নতুন request পাঠায়।
এতে খালি উত্তর অনেক কমে যায়, message ও তাড়াতাড়ি পৌঁছায়। তবু সমস্যা থেকে যায়। প্রতিটা message এর পর নতুন একটা HTTP request বানাতে হয়। দুই request এর ফাঁকে message এলে সেটা আলাদা করে সামলাতে হয়। আর নানুর message পাঠানোর জন্য আলাদা আরেকটা request লাগে, কারণ ঐ ধরে রাখা request টা শুধু এক দিকে কাজ করে।
WebSocket: একবার খুলে রাখা লাইন
WebSocket হলো এমন একটা connection, যেটা একবার খুললে খোলাই থাকে, আর দুই পাশই যেকোনো সময় কথা বলতে পারে। শুরুটা হয় একটা সাধারণ HTTP request দিয়ে, যার header এ লেখা থাকে Upgrade: websocket। Server রাজি হলে 101 Switching Protocols উত্তর দেয়। তারপর থেকে সেই TCP connection টাই chat এর লাইন হয়ে যায়। প্রতিটা message (যাকে বলে frame) এর সাথে মাত্র কয়েক byte এর বাড়তি তথ্য যায়, পুরো HTTP header নয়।
| পদ্ধতি | Message কত দ্রুত আসে | খালি request | Server এর কাজ |
|---|---|---|---|
| Short polling | গড়ে কয়েক সেকেন্ড দেরি | অনেক বেশি | সেকেন্ডে লাখো খালি request |
| Long polling | প্রায় সাথে সাথে | কম | প্রতিটা message এ নতুন request |
| WebSocket | সাথে সাথে | নেই | connection খোলা রাখার memory |
Chat এ server কেই আগে কথা বলতে হয়: “রিমি, আপনার জন্য একটা message এসেছে।” HTTP এ server আগে কথা বলতে পারে না, WebSocket এ পারে। তাই chat, live score, online game এর মতো জায়গায় WebSocket ই স্বাভাবিক পছন্দ।
০৪ লাখো WebSocket connection কোথায় খোলা থাকে, আর message ঠিক connection server এ যায় কীভাবে?
Connection server: যে শুধু লাইন ধরে রাখে
যে server গুলো phone এর WebSocket connection খোলা রাখে, তাদের বলবো connection server। হিসাব অনুযায়ী একেকটা connection server প্রায় ১ লাখ connection ধরে, আর আমাদের লাগে প্রায় ৩০০টা। এদের কাজ খুব সীমিত রাখা হয়: phone থেকে frame নেওয়া, ভেতরের service এ দেওয়া, আর ভেতর থেকে আসা message phone এ push করা। Message কোথায় store হবে, কাকে যাবে, এসব সিদ্ধান্ত নেয় পেছনের Chat Service।
এখানে সেশন ০২ এর একটা শিক্ষা উল্টে যায়। সেখানে আমরা web server কে stateless বানিয়েছিলাম, যাতে যেকোনো request যেকোনো server এ যেতে পারে। কিন্তু WebSocket একটা নির্দিষ্ট machine এর সাথে বাঁধা থাকে। রিমির connection Conn-17 এ খোলা থাকলে রিমির কাছে message পাঠানোর একমাত্র পথ Conn-17। মানে connection server stateful: তার ভেতরে কোন phone আছে, এই তথ্যটাই তার state।
Load balancer এখানেও আছে, তবে তার কাজ একটু আলাদা। নতুন phone connect করতে এলে সে ঠিক করে কোন connection server এ পাঠাবে, সাধারণত যার connection সবচেয়ে কম তাকে। একবার connection বসে গেলে সেটা ঘণ্টার পর ঘণ্টা ঐ server এই থাকে।
Routing: রিমি কোন server এ আছে?
নানুর message Conn-3 এ এলো। রিমি আছে Conn-17 এ। Chat Service কীভাবে জানবে? এর জন্য একটা ছোট খাতা রাখা হয়, যার নাম connection registry। রিমির phone Conn-17 এ connect হলেই Conn-17 Redis এ লিখে রাখে: rimi → conn-17। রিমি disconnect হলে entry টা মুছে যায়।
Chat Service প্রথমে message টা store করে। তারপর registry তে রিমিকে খোঁজে, আর message টা সরাসরি Conn-17 এ পাঠায়। Server গুলোর মধ্যে এই পাঠানোটা সরাসরি call দিয়েও হতে পারে, আবার প্রতিটা connection server এর নামে একটা pub/sub topic দিয়েও হতে পারে। Pub/sub এ একজন একটা topic এ message ছাড়ে, আর সেই topic এ যারা subscribe করে আছে সবাই পায় (সেশন ১০)।
API: phone আর server কী বলাবলি করে
বেশিরভাগ কথা চলে খোলা WebSocket এর উপর দিয়ে, ছোট JSON frame এ। যেখানে সাথে সাথে push এর দরকার নেই, যেমন পুরোনো message আনা বা ছবি upload, সেখানে সাধারণ HTTP request ই যথেষ্ট:
# WebSocket frame: phone → server (নতুন message)
{ "type": "send", "client_msg_id": "n-7f3a",
"chat_id": "fam-40", "body": "<ciphertext>" }
# server → নানুর phone (store হয়েছে: এক tick)
{ "type": "ack", "client_msg_id": "n-7f3a", "seq": 1042 }
# server → রিমির phone (নতুন message এলো)
{ "type": "msg", "chat_id": "fam-40", "seq": 1042,
"from": "nanu", "body": "<ciphertext>" }
# রিমির phone → server (receipt: 1042 পর্যন্ত সব পেয়েছি / পড়েছি)
{ "type": "delivered", "chat_id": "fam-40", "up_to_seq": 1042 }
{ "type": "read", "chat_id": "fam-40", "up_to_seq": 1042 }
# সাধারণ HTTP (REST), যেখানে সাথে সাথে push এর দরকার নেই
GET /v1/chats/fam-40/messages?after_seq=1038
reconnect এর পরে বাকি message আনা
POST /v1/media
ছবি বা video upload, message এ যায় শুধু link
দুটো field আলাদা করে খেয়াল করুন। client_msg_id টা phone নিজে বানায়, server এ পৌঁছানোর আগেই। আর seq টা দেয় server, store করার সময়। এই দুটো নম্বর পরের তিনটা সেকশনের প্রায় সব সমস্যার সমাধান করবে।
০৫ এক tick, দুই tick আর নীল tick কীভাবে কাজ করে?
নানু এখনো screen এর দিকে তাকিয়ে আছেন। প্রতিটা tick তাকে আসলে একটা করে খবর দেয়। এই খবরগুলোকে বলে delivery receipt: প্রাপকের দিক থেকে প্রেরকের কাছে ফেরত আসা ছোট একটা message, যেটা জানায় message টা কতদূর পৌঁছালো।
| নানু যা দেখেন | নাম | মানে |
|---|---|---|
| ছোট ঘড়ি | pending | message এখনো phone থেকে বের হয়নি, বা server এর ack আসেনি |
| এক ধূসর tick | sent | server message store করেছে |
| দুই ধূসর tick | delivered | রিমির phone message পেয়েছে |
| দুই নীল tick | read | রিমি chat খুলে message দেখেছে |
পুরো যাত্রাটা ধাপে ধাপে দেখুন:
প্রথম tick আসে server এর ACK থেকে। ACK মানে acknowledgment, অর্থাৎ “পেয়েছি” জানানো (সেশন ০৮)। Server message টা Message Store এ লিখে ফেলার পরেই ack পাঠায়, তার আগে নয়। তাই এক tick দেখলে নানু নিশ্চিন্ত থাকতে পারেন: message টা আর হারাবে না।
দ্বিতীয় আর তৃতীয় tick আসে রিমির phone থেকে। Message হাতে পেয়েই রিমির phone একটা delivered receipt পাঠায়। রিমি chat টা খুললে যায় read receipt। Chat Service এই receipt গুলো উল্টো পথে নানুর connection server এ পাঠিয়ে দেয়।
একটা ছোট কৌশল traffic অনেক কমায়। প্রতিটা message এর জন্য আলাদা receipt না পাঠিয়ে phone বলে “1042 পর্যন্ত সব পড়েছি” (up_to_seq)। রিমি সকালে উঠে ৫০টা message একসাথে পড়লে ৫০টা নয়, একটাই read receipt যায়।
দুর্বল network এ ack হারালে কী হয়?
নানুর গ্রামের 2G তে message টা server এ পৌঁছে গেলো, store ও হলো। কিন্তু ফেরার পথে ack টা হারিয়ে গেলো। নানুর phone এর দিক থেকে দেখলে কোনো উত্তর আসেনি, তাই ঘড়ি ঘুরতেই থাকে। কয়েক সেকেন্ড পর phone message টা আবার পাঠায়।
এখানেই client_msg_id কাজে আসে। Phone দ্বিতীয়বারও একই ID পাঠায়। Server দেখে, n-7f3a ID এর message তো আগেই store হয়েছে। তাই নতুন copy না বানিয়ে আগের seq 1042 দিয়েই আবার ack পাঠায়। এটাই idempotency: একই কাজ দুইবার হলেও ফল একবারের মতো (সেশন ০৮)। Duplicate ঠেকানোর আরো গভীর কৌশল সেশন ১৮ এ দেখবো।
এক tick মানে server কথা দিচ্ছে message টা আর হারাবে না। তাই store শেষ হওয়ার আগে কখনো ack পাঠানো যাবে না। আগে ack পাঠিয়ে পরে store করতে গিয়ে server crash করলে নানু এক tick দেখবেন, অথচ message টা কারো কাছেই পৌঁছাবে না।
০৬ Offline user এর message কোথায় অপেক্ষা করে, আর order ঠিক থাকে কীভাবে?
দুবাইয়ে মামা রাতের shift এ কাজ করছেন, phone টা লকারে বন্ধ। নানুর message তার কাছে এখন পৌঁছানোর কোনো পথ নেই। তাহলে message টা কোথায় থাকবে?
Data model
প্রতিটা message server এ জমা থাকে, যতক্ষণ না প্রাপক সেটা নিয়ে যায়। এই কৌশলকে বলে store-and-forward: আগে জমা রাখা, প্রাপককে পাওয়া গেলে পৌঁছে দেওয়া, ঠিক ডাকঘরের মতো। আড্ডার data model এ তিনটা table:
messages partition key: chat_id sort key: seq chat_id "fam-40" seq 1042 এই chat এর ভেতরে ক্রমিক নম্বর client_msg_id "n-7f3a" retry এলে duplicate চেনার জন্য sender_id "nanu" body <ciphertext> created_at 2026-04-10 21:00:03 (server এর ঘড়ি) chat_members partition key: chat_id chat_id, user_id last_delivered_seq 1042 এই পর্যন্ত phone এ পৌঁছেছে last_read_seq 1040 এই পর্যন্ত পড়া হয়েছে user_chats (inbox) partition key: user_id user_id, chat_id, last_seq, updated_at কোন chat এ নতুন কিছু আছে
messages table এর partition key হলো chat_id। মানে একটা chat এর সব message একই shard এ থাকে (সেশন ০৫ এর shard key)। “fam-40 group এর শেষ ৫০টা message” আনার query টা তখন একটা shard থেকেই শেষ হয়, অনেকগুলো shard ঘুরতে হয় না। আর shard এর ভেতরে message গুলো seq অনুযায়ী সাজানো থাকে।
এই ধরনের data তে লেখার পরিমাণ অনেক, আর পড়া হয় মূলত “এই chat এর এই seq এর পরেরগুলো” ধরনের query দিয়ে। তাই বড় chat app গুলো সাধারণত Cassandra বা HBase এর মতো wide-column database ব্যবহার করে। এরা partition key ধরে data ভাগ করে, আর ভেতরে sort key অনুযায়ী সাজিয়ে রাখে।
মামার phone চালু হলে কী হয়
মামার phone জানে fam-40 group এ সে শেষ কোন seq পর্যন্ত পেয়েছিলো, ধরুন 1038। Connect হয়েই phone বলে “1038 এর পরেরগুলো দিন”। Server 1039 থেকে 1042 পাঠিয়ে দেয়। কিছু বাদ পড়ে না, কিছু দুইবারও আসে না। শেষে phone delivered receipt পাঠায়, আর নানুর screen এ মামার জন্যও দুই tick হয়।
Message ordering: কার ঘড়ি ঠিক?
এবার একটা মজার সমস্যা। নানুর phone এর ঘড়ি ৭ মিনিট পিছিয়ে চলে, কেউ কখনো ঠিক করে দেয়নি। মামার phone এর ঘড়ি আবার ১০ মিনিট পিছিয়ে। প্রত্যেকের phone এর সময় দিয়ে message সাজালে কী হয়, দেখুন:
মামার “ভালো আছি আম্মা” উত্তরটা নানুর প্রশ্নের আগে বসে গেছে। কেউ পড়লে ভাববে মামা প্রশ্ন আসার আগেই উত্তর দিয়েছেন। তাই phone এর ঘড়ির উপর ভরসা করা যায় না।
সমাধান হলো message ordering এর দায়িত্ব server কে দেওয়া। প্রতিটা chat এর জন্য server একটা counter রাখে। নতুন message এলে counter এক বাড়ে, আর সেই নম্বরটাই হয় message এর seq। একটা chat এর সব message একই shard এ যায়, তাই counter টাও এক জায়গায় থাকে। দুজন একসাথে পাঠালেও যার message আগে server এ পৌঁছায়, সে ছোট নম্বর পায়। ৪০ জনের phone একই নম্বর দেখে সাজায়, তাই সবার screen এ একই order।
খেয়াল করুন, আমরা শুধু একটা chat এর ভেতরের order নিশ্চিত করছি। নানুর group আর রিমির অফিসের group এর message এর মধ্যে কোনটা আগে, সেটা কেউ জানতে চায় না। পুরো পৃথিবীর জন্য একটা order রাখতে গেলে একটা counter এ সব message এর চাপ পড়তো, যেটা বিশাল bottleneck। Bottleneck মানে system এর সবচেয়ে সরু জায়গা, যেটা পুরো system এর গতি আটকে দেয় (সেশন ০১)।
আলাদা আলাদা machine এর ঘড়ি কখনো পুরোপুরি মেলে না। তাই order ঠিক করতে timestamp নয়, একটা জায়গা থেকে দেওয়া ক্রমিক নম্বর ব্যবহার করুন। Chat এর ক্ষেত্রে সেই জায়গা হলো chat টার নিজের shard।
০৭ ৪০ জনের group এ একটা message সবার কাছে যায় কীভাবে?
১:১ chat এ message যায় একজনের কাছে। কিন্তু নানুর group এ ৪০ জন, মানে message টা নানু বাদে ৩৯ জনের কাছে যেতে হবে। একটা জিনিস অনেক জায়গায় ছড়িয়ে দেওয়াকে বলে fan-out। সেশন ২৫ এ feed এর জন্য fan-out on write আর fan-out on read দেখেছি। Chat এ পথটা মোটামুটি পরিষ্কার: message store হয় একবার, কিন্তু delivery যায় প্রত্যেক member এর কাছে আলাদা করে।
Chat Service এর কাজ চার ধাপে:
- Store একবার: fam-40 এর shard এ seq 1042 হিসেবে message টা লেখে। ৩৯টা copy নয়, একটাই।
- Member list আনে:
chat_memberstable থেকে ৪০ জনের তালিকা। - Connection server ধরে ভাগ করে: registry দেখে বোঝে কে কোন connection server এ। ধরুন ১৫ জন Conn-17 এ, ১২ জন Conn-22 এ, ৮ জন Conn-41 এ, আর ৪ জন offline।
- প্রতিটা server এ একটা batch পাঠায়: ৩৯টা আলাদা call এর বদলে তিনটা call যায়। Offline ৪ জনের জন্য যায় push notification।
এবার হিসাবটা দেখুন। একটা message এর জন্য ৩৯টা delivery, তারপর ৩৯টা delivered receipt, পরে ৩৯টা read receipt। সব মিলিয়ে একটা “সবাই কেমন আছো?” থেকে প্রায় ১১৭টা event তৈরি হয়। Group যত বড়, এই গুণফল তত বড়। এজন্যই WhatsApp এর মতো app group এর সাইজে একটা সীমা রাখে।
Group এ tick এর নিয়মও একটু আলাদা। নানু দুই tick দেখবেন তখনই, যখন ৩৯ জনের সবার phone এ message পৌঁছাবে। নীল হবে যখন সবাই পড়বে। কে কে পড়েছে, সেটা message এর info screen এ আলাদা করে দেখা যায়। বড় group এ প্রতিটা receipt সাথে সাথে প্রেরকের কাছে না পাঠিয়ে কিছুক্ষণ জমিয়ে একসাথে পাঠানো হয়।
Group message নিজে একবারই store হয়। কিন্তু প্রতিটা member এর delivered আর read receipt আলাদা আলাদা event। তাই বড় group design করার সময় message এর চেয়ে receipt এর traffic নিয়ে বেশি ভাবতে হয়।
০৮ Server কীভাবে জানে কে online, আর offline হলে কী হয়?
Presence: online আর last seen
কে এখন online আছে, আর না থাকলে শেষ কখন ছিলো, এই তথ্যকে বলে presence। প্রথমে মনে হতে পারে, connection খোলা থাকলেই online। কিন্তু নানুর phone এর সিগন্যাল চলে গেলে TCP connection টা সাথে সাথে বন্ধ হয় না। Server কিছুক্ষণ বুঝতেই পারে না যে অন্য পাশে আর কেউ নেই।
তাই phone নিয়মিত একটা heartbeat পাঠায়, ধরুন প্রতি ৩০ সেকেন্ডে। সবচেয়ে সহজ design এ Registry তে নানুর entry টা রাখা হয় একটা TTL সহ। TTL (time to live) মানে একটা সময়সীমা, যেটা পার হলে key নিজে থেকেই মুছে যায় (সেশন ০৪)। প্রতিটা heartbeat এ TTL আবার ৬০ সেকেন্ডে ফিরে যায়। Heartbeat বন্ধ হলে ৬০ সেকেন্ড পরে key মুছে যায়, আর নানুকে offline দেখায়। শেষ heartbeat এর সময়টাই হয় তার last seen।
সেকশন ০২ এর সেই চমকে দেওয়া সংখ্যাটা এখানে কাজে লাগে। সেকেন্ডে ৬ লাখ ৭০ হাজার heartbeat এর প্রতিটা Redis এ লিখলে Redis এর উপর অকারণ চাপ পড়ে। তাই বড় scale এ heartbeat থামে connection server এই। Connection server নিজেই হিসাব রাখে কার heartbeat ঠিকমতো আসছে। Registry তে লেখে শুধু তখন, যখন কারো অবস্থা বদলায়: ৬০ সেকেন্ড heartbeat না এলে online থেকে offline, আবার এলে offline থেকে online।
গ্রামের দুর্বল সিগন্যালে আরেকটা ঝামেলা হয়। নানুর phone এক মিনিটে দশবার connect হয়, আবার কেটে যায়। প্রতিবার online/offline বদলালে রিমির screen এ লেখাটা মিটমিট করতে থাকে। তাই status বদলানোর আগে কয়েক সেকেন্ড অপেক্ষা করা হয়। অবস্থাটা স্থির হলে তবেই সবাইকে জানানো হয়।
আর presence এর খবর সবাইকে পাঠানোর দরকার নেই। নানুর contact list এ ২০০ জন থাকলেও তার online হওয়ার খবর শুধু তাদের কাছে যায়, যারা এই মুহূর্তে নানুর সাথে chat খুলে বসে আছে। এরা pub/sub এর মতো করে নানুর presence এ subscribe করে থাকে।
Push notification: ঘুমন্ত phone কে জাগানো
মামার phone বন্ধ, বা app টা background এ ঘুমিয়ে আছে। Phone এর operating system battery বাঁচাতে ঘুমন্ত app এর WebSocket কেটে দেয়। আড্ডার server তখন সরাসরি phone এ পৌঁছাতে পারে না।
এখানে কাজে আসে push notification। Android phone এ Google এর FCM, আর iPhone এ Apple এর APNs সারাক্ষণ phone এর সাথে নিজেদের একটা connection খোলা রাখে। আড্ডার Push Service তাদের বলে “মামার phone এ একটা notification পৌঁছে দিন”। Phone এ notification দেখা যায়, আর app জেগে উঠে WebSocket খোলে। তারপর আগের সেকশনের মতো seq দিয়ে বাকি message গুলো নিয়ে নেয়।
Notification এ message এর আসল লেখা থাকে না। থাকে শুধু “আড্ডা তে নতুন message”, বা encrypted একটা ছোট টুকরা। কারণটা পরের সেকশনে।
Presence সবসময় একটু পিছিয়ে থাকে। Heartbeat এর ফাঁক আর TTL এর কারণে কেউ চলে যাওয়ার পরেও এক মিনিট পর্যন্ত online দেখাতে পারে। Chat app এ এটা মেনে নেওয়া যায়, কারণ presence দেখানোর জিনিস, কোনো জরুরি সিদ্ধান্ত এর উপর নির্ভর করে না।
০৯ Server কি message পড়তে পারে, আর পুরো design এর bottleneck কোথায়?
E2E encryption: খামের ভেতরটা শুধু প্রাপক দেখে
নানুর message টা কয়েকটা server ঘুরে রিমির কাছে যায়। এই পথে কেউ কি message টা পড়তে পারে? E2E encryption (end-to-end encryption) থাকলে পারে না। এই পদ্ধতিতে message lock হয় প্রেরকের phone এ, আর খোলে শুধু প্রাপকের phone এ। মাঝের কোনো server, এমনকি আড্ডার নিজের server ও, লেখাটা পড়তে পারে না।
ধারণাটা সহজ। প্রত্যেকের phone এ দুটো key থাকে। Public key সবাইকে দেওয়া যায়, আড্ডার server ও একটা copy রাখে। Private key কখনো phone ছেড়ে যায় না। Public key দিয়ে lock করা জিনিস শুধু সেই জোড়ার private key দিয়েই খোলে। নানুর phone রিমির public key দিয়ে message lock করে পাঠায়। পথে message টা দেখতে অর্থহীন অক্ষরের মতো, যাকে বলে ciphertext। রিমির phone নিজের private key দিয়ে সেটা খোলে।
বাস্তবে WhatsApp এর Signal protocol আরো জটিল। প্রতিটা message এর জন্য নতুন key বানায়, আর group এর জন্য আলাদা ব্যবস্থা রাখে। কিন্তু system design এর জন্য মূল কথা এটুকুই: server শুধু খাম বহন করে, খামের ভেতরটা দেখে না। Server দেখে শুধু খামের বাইরের লেখা: কে পাঠালো, কাকে, কখন, কত বড়।
এর কিছু cost আছে। Server message এর ভেতরে search করতে পারে না। Spam বা ক্ষতিকর content নিজে থেকে ধরতে পারে না। নতুন phone নিলে পুরোনো message server থেকে পড়ে আনা যায় না, user কে নিজের encrypted backup রাখতে হয়।
Bottleneck আর trade-off
এখন পুরো design টা একসাথে দেখি:
এই design যা দেয়
- প্রায় সাথে সাথে delivery: খোলা WebSocket দিয়ে server নিজেই message push করে, polling এর অপেক্ষা নেই।
- Message হারায় না: ack পাওয়া প্রতিটা message Message Store এ থাকে, offline user পরে seq দিয়ে sync করে নেয়।
- Duplicate দেখায় না: client_msg_id দিয়ে server retry চিনে ফেলে।
- সবার কাছে একই order: প্রতিটা chat এ server একটা করে seq দেয়।
- Privacy: E2E encryption এর কারণে server message এর লেখা পড়তে পারে না।
Downside আর bottleneck গুলো
- Connection server stateful: একটা server down হলে বা deploy হলে প্রায় 1 লাখ phone একসাথে reconnect করতে চায়।
- Registry এর উপর ভরসা: Registry ভুল বা পুরোনো তথ্য দিলে message ভুল server এ যায়, তাই TTL আর retry লাগে।
- বড় group মানে hot chat: একটা chat এর সব message একটা shard এ থাকে, তাই অনেক সক্রিয় বড় group সেই shard এ চাপ ফেলে।
- Receipt আর presence এর traffic: আসল message এর চেয়ে tick, heartbeat আর typing signal এর সংখ্যা অনেক বেশি।
- E2E এর cost: server message search করতে পারে না, নতুন phone এ পুরোনো history আনা কঠিন।
সবচেয়ে বিপজ্জনক মুহূর্তটা আসে deploy এর সময়। একটা connection server restart হলে তার ১ লাখ phone একসাথে reconnect করতে ছোটে। সবাই একই সেকেন্ডে চেষ্টা করলে load balancer আর registry এর উপর হঠাৎ অনেক চাপ পড়ে। তাই phone গুলো reconnect এর আগে একটা এলোমেলো সময় অপেক্ষা করে, যাকে বলে jitter (সেশন ০৯)। আর deploy এর সময় server গুলো একটা একটা করে ধীরে ধীরে restart করা হয়।
ঈদের রাতের হিসাবও মাথায় রাখতে হয়। রাত বারোটা বাজার সাথে সাথে সবাই “ঈদ মোবারক” পাঠায়, message rate গড়ের পাঁচ গুণ হয়ে যায়। হঠাৎ এই ভিড় সামলাতে Chat Service আর Message Store এর মাঝে একটা queue রাখা যায় (সেশন ০৮)। তবে store এর ack না আসা পর্যন্ত এক tick দেওয়া যাবে না, তাই queue টাকেও durable হতে হবে, মানে disk এ লেখা থাকতে হবে।
১০ মগজে প্রেশার দিন
আগে নিজে ভাবুন। তারপর দেখুন সবাই সাধারণত কী উত্তর দেয়, কোথায় সেটা অসম্পূর্ণ, আর আসল কথাটা কী।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“Load balancer দিয়ে round-robin করলেই হয়। Message যেকোনো server এ পাঠিয়ে দিলে সেই server রিমির কাছে পৌঁছে দেবে।”
এই চিন্তা HTTP request এর জন্য ঠিক ছিলো। কিন্তু WebSocket এর একটা বৈশিষ্ট্য এখানে বাদ পড়ে গেছে।
যে জায়গাটা যোগ করা দরকার
WebSocket হলো একটা নির্দিষ্ট machine এর সাথে খোলা একটা TCP connection। রিমির connection Conn-17 এ থাকলে অন্য কোনো server রিমির কাছে কিছু পাঠাতে পারে না। তাই কোন user কোন server এ আছে, এই state টা কোথাও রাখতেই হবে। ভালো design এ এই state টাকে যতটা সম্ভব ছোট রাখা হয়। Connection server শুধু socket ধরে রাখে, আর কে কোথায় সেটা লেখা থাকে registry তে। বাকি সব logic থাকে stateless Chat Service এ। কোনো connection server crash করলে তার phone গুলো অন্য server এ reconnect করে, registry আপডেট হয়, আর seq দিয়ে বাদ পড়া message চলে আসে।
State পুরোপুরি বাদ দেওয়া যায় না, শুধু এক কোণায় আটকে রাখা যায়। Connection server এ শুধু socket, registry তে শুধু ঠিকানা, আর বাকি সব stateless।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“রেখে দেওয়াই ভালো। Storage এখন সস্তা, আর user নতুন phone এ পুরোনো message পেলে খুশি হয়।”
কথাটা আংশিক ঠিক। কিন্তু দুই পথেই কিছু পাওয়া যায়, কিছু হারাতে হয়।
যে জায়গাটা যোগ করা দরকার
এটা product এর সিদ্ধান্ত, আর দুটো পথই বাস্তবে চলে। WhatsApp server কে ডাকঘরের মতো ব্যবহার করে। Message deliver হলেই server থেকে মুছে যায়, আর না পৌঁছালে বড়জোর ৩০ দিন থাকে। এতে storage অনেক কমে, privacy বাড়ে। কিন্তু নতুন phone এ history আনতে user কে নিজের backup রাখতে হয়। অন্যদিকে Messenger বা Telegram এর মতো app সব history server এ রাখে। এতে যেকোনো device এ সব message পাওয়া যায়, কিন্তু আমাদের হিসাবে বছরে ~440 TB storage লাগে। Server এ data যত বেশিদিন থাকে, তার সুরক্ষার দায়ও তত বাড়ে।
“রাখবো, নাকি মুছবো” প্রশ্নের উত্তর দেওয়ার আগে জিজ্ঞেস করুন: multi-device sync লাগবে কিনা, আর E2E encryption আছে কিনা। এই দুটো উত্তরই storage এর design ঠিক করে দেয়।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“হ্যাঁ, TCP প্রতিটা packet এর ack নেয়, হারালে আবার পাঠায়, order ও ঠিক রাখে। তাই app এর আলাদা কিছু করার নেই।”
TCP সত্যিই এগুলো করে, তবে শুধু একটা connection যতক্ষণ চালু থাকে, ততক্ষণ।
যে জায়গাটা যোগ করা দরকার
নানুর 2G connection কেটে গেলে সেই connection এর ভেতরে পথে থাকা frame গুলোর খবর আর কেউ রাখে না। TCP জানে না message টা server এর app পর্যন্ত পৌঁছেছিলো কিনা। তাই reliability টা app এর নিজের স্তরে বানাতে হয়। Server store শেষ করে নিজের ack দেয়। Ack না এলে phone একই client_msg_id দিয়ে আবার পাঠায়, আর server ID দেখে duplicate বাদ দেয়। Reconnect এর পরে phone শেষ পাওয়া seq এর পর থেকে বাকিগুলো চেয়ে নেয়। এই তিনটা মিলে message হারায়ও না, দুইবারও দেখায় না।
TCP একটা connection এর ভেতরের নিশ্চয়তা দেয়, পুরো যাত্রার নয়। পুরো যাত্রার নিশ্চয়তা আসে app এর ack, retry, client_msg_id আর seq থেকে।
১১ যে টার্মগুলো শিখলাম
| টার্ম | মানে |
|---|---|
| Functional requirement | System কী কী করবে, যেমন message পাঠানো বা tick দেখানো। |
| Non-functional requirement | System কতটা ভালোভাবে করবে, যেমন latency, availability, privacy। |
| Polling (short polling) | Client কয়েক সেকেন্ড পরপর server কে জিজ্ঞেস করে নতুন কিছু আছে কিনা। |
| Long polling | Server request টা ধরে রাখে, নতুন data এলে বা timeout হলে তবেই উত্তর দেয়। |
| WebSocket | একবার খুলে রাখা connection, যেখানে client আর server দুজনেই যেকোনো সময় message পাঠাতে পারে। |
| Connection server | যে server phone এর WebSocket connection খোলা রাখে আর message আনা নেওয়া করে। Stateful। |
| Connection registry | কোন user কোন connection server এ আছে, তার তালিকা। সাধারণত Redis এ থাকে। |
| Delivery receipt | প্রাপকের দিক থেকে ফেরত আসা ছোট message, যেটা জানায় message delivered নাকি read হয়েছে। |
| client_msg_id | Phone এর বানানো message ID, যেটা দিয়ে server retry করা duplicate চিনে ফেলে। |
| Store-and-forward | Message আগে server এ জমা রাখা, প্রাপক reachable হলে পৌঁছে দেওয়া। |
| Message ordering | একটা chat এর message সবার screen এ একই ক্রমে দেখানো। |
| Sequence number (seq) | প্রতিটা chat এ server এর দেওয়া ক্রমিক নম্বর, যেটা দিয়ে order আর offline sync দুটোই হয়। |
| Fan-out (group) | একটা message group এর প্রত্যেক member এর কাছে আলাদা করে পৌঁছে দেওয়া। |
| Presence | কে এখন online আছে, আর না থাকলে শেষ কখন ছিলো (last seen)। |
| Push notification | FCM বা APNs এর মাধ্যমে ঘুমন্ত বা বন্ধ app কে জাগানোর notification। |
| E2E encryption | Message প্রেরকের phone এ lock হয়, খোলে শুধু প্রাপকের phone এ। মাঝের server পড়তে পারে না। |
| Public key / Private key | Public key দিয়ে lock করা যায়, খোলা যায় শুধু তার জোড়ার private key দিয়ে। Private key phone ছেড়ে যায় না। |
| Ciphertext | Encrypt করা লেখা, key ছাড়া যেটা অর্থহীন অক্ষরের মতো দেখায়। |
১২ শর্ট সামারি
- Chat app এর আসল চ্যালেঞ্জ message এর সংখ্যা নয়, বরং কোটি কোটি connection সারাক্ষণ খোলা রাখা। তাই polling বাদ দিয়ে WebSocket ব্যবহার করা হয়, আর stateful connection server গুলো শুধু socket ধরে রাখে।
- Chat Service message store করে seq দেয়, তারপর connection registry দেখে প্রাপকের connection server এ পাঠায়। Store শেষ হলে তবেই ack যায়, আর সেটাই প্রথম tick।
- Delivered আর read receipt উল্টো পথে ফিরে দ্বিতীয় আর নীল tick আনে।
client_msg_idretry এর duplicate ঠেকায়, আর per-chat seq দিয়ে order আর offline sync দুটোই ঠিক থাকে। - Group এ message একবার store হয়ে connection server ধরে batch এ যায়। Presence চলে heartbeat আর TTL দিয়ে, offline user কে জাগায় push notification, আর E2E encryption এর কারণে server শুধু খাম বহন করে, ভেতরের লেখা পড়ে না।
১৩ মনে রাখুন
- Server কে আগে কথা বলতে হলে WebSocket নিন: Polling এ সেকেন্ডে লাখো খালি request যায়, আর message তবুও দেরিতে আসে।
- Connection server পাতলা রাখুন: সে শুধু socket ধরে রাখবে, বাকি সব logic থাকবে stateless Chat Service এ।
- Store শেষ না হওয়া পর্যন্ত ack দেবেন না: এক tick মানে একটা প্রতিশ্রুতি যে message আর হারাবে না।
- Retry করলে একই client_msg_id পাঠান: দুর্বল network এ ack হারায়, আর server ID দেখেই duplicate চেনে।
- Order এর জন্য phone এর ঘড়ি নয়, server এর seq: প্রতিটা chat এ একটা counter থাকলে সবার screen এ একই order দেখায়।
- Group এ receipt এর traffic গুনুন: ৪০ জনের group এ একটা message থেকে ১১৭টা event তৈরি হয়।
- Presence এর প্রতিটা heartbeat database এ লিখবেন না: Connection server এই থামান, শুধু অবস্থা বদলালে registry আপডেট করুন।