সরাসরি পর্বে যান
পর্ব ৫৫ · ৬৬-এর মধ্যে · মডিউল ৯

Case study: chat আর messaging system — delivery, ordering আর presence

Case Study: Chat & Messaging System — Delivery, Ordering & Presence

Taught in Bangla, this lesson designs a chat and messaging system: estimating messages, open connections, gateways and storage, a WebSocket message envelope with idempotent send, conversation logs partitioned by key with per-device cursors, connection registry, heartbeats and reconnect storms, client ids plus per-conversation sequence numbers for ordering, at-least-once delivery with dedup, group fan-out, presence, end-to-end encryption and multi-region concerns.

এই পর্বে যা শিখবেন

  • একটি chat অ্যাপের requirement, পরিধি আর ইন্টারভিউয়ারের প্রত্যাশিত প্রশ্ন — পর্ব ০৬-এর কাঠামোয়
  • বার্তা, সংযোগ, gateway আর storage-এর হিসাব, যা চালিয়ে মেলানো যায়
  • Client id, per-conversation sequence, at-least-once আর dedup দিয়ে ক্রম ও নিশ্চয়তা; sent, delivered আর read
  • Connection registry, heartbeat, reconnect storm আর per-device sync; group fan-out-এর খরচ
  • Presence-এর fan-out, debounce আর lazy presence; end-to-end encryption, multi-region আর spam-এর প্রভাব

১ · সমস্যা আর পরিধি — কী বানাচ্ছি, কী বানাচ্ছি না

রিনা ঢাকায় অফিসে, বাবা-মা রাজশাহীতে, ভাই তানভীর চট্টগ্রামের ট্রেনে। পরিবারের group-এ রিনা লিখলেন “আসছি”. তানভীরের ফোনে নেটওয়ার্ক এই আছে, এই নেই; ল্যাপটপটি বাসায় চালু। কয়েক সেকেন্ড পর রিনার পর্দায় দুটি টিক, বিকেলে নীল। এই সাধারণ অভিজ্ঞতার পেছনের সিস্টেমটিই এই পর্বের বিষয়। আমরা একটি chat অ্যাপ নকশা করব — নাম ধরা যাক “আলাপ” (কাল্পনিক); কোনো বাস্তব প্রতিষ্ঠানের নকশা এটি নয়।

Module ৯-এর প্রতিটি case study আগের ৫৩টি পর্বের টুকরো জোড়া দেয়। কোনো ধারণা আবার শেখানো হবে না; প্রতিটির এক বাক্যের প্রয়োগ আর সেই পর্বের লিংক থাকবে। ক্রমটি পর্ব ০৬-এর আট ধাপের: পরিধি, হিসাব, API, data model, ছবি, গভীরে নামা, ভাঙন আর গুছিয়ে শেষ।

পুরোনো দিনের ডাকঘরের কথা ভাবুন। চিঠি জমা দিলে রসিদ পান (sent). চিঠি প্রাপকের বাড়ির বাক্সে পড়লে ডাকপিয়ন খাতায় সই নেন (delivered). প্রাপক খাম খুললে আপনি জানেন না — chat-এ সেটিও জানা যায় (read). প্রাপক বাড়িতে না থাকলে চিঠি ডাকঘরে জমা থাকে, তিনি ফিরে এসে নিজের খাতার শেষ নম্বর দেখিয়ে বাকিগুলো নিয়ে যান। পুরো নকশাটি এই ডাকঘরের দ্রুত, কোটি-গুণ বড় সংস্করণ।

Functional requirement — অ্যাপটি কী করবে

  • 1:1 আর group chat — group-এ সর্বোচ্চ ৫০০ সদস্য (ধরা যাক)। লেখা, ছবি, ভয়েস নোট; বড় ফাইল যায় object storage-এ, বার্তায় থাকে শুধু তার pointer.
  • তিন অবস্থার টিক — sent (server জমা নিয়েছে), delivered (প্রাপকের অন্তত একটি ডিভাইসে পৌঁছেছে), read (প্রাপক খুলে দেখেছেন)।
  • Presence আর last seen — কে অনলাইন, কে শেষ কখন ছিলেন; গোপনীয়তার সেটিং মেনে।
  • Multi-device — একজনের ফোন, ল্যাপটপ, ট্যাব; সব ডিভাইসে একই কথোপকথন, একই ক্রমে।
  • Offline delivery — প্রাপক অফলাইন থাকলে বার্তা জমা থাকে, ফিরলে পৌঁছায়; ফোনে push notification যায়।

Non-functional requirement — কতটা ভালোভাবে

  • Durability: server যে বার্তায় “sent” দিয়েছে, তা কখনো হারাবে না। এটিই সবচেয়ে কড়া শর্ত (পর্ব ০২)।
  • ক্রম: একটি কথোপকথনের ভেতরে সব সদস্য আর সব ডিভাইস একই ক্রম দেখবে। দুটি আলাদা কথোপকথনের মধ্যে ক্রমের দাবি নেই।
  • Latency: দুজনই অনলাইন থাকলে বার্তা পৌঁছাবে মোটামুটি এক সেকেন্ডের কম সময়ে, p99 লক্ষ্য ধরা যাক ৫০০ ms (পর্ব ০৪)।
  • দুর্বল নেটওয়ার্ক: বাংলাদেশে অনেকেই মূলত মোবাইল ডেটায় ইন্টারনেট ব্যবহার করেন, আর লিফট, ট্রেন বা লোডশেডিংয়ে সংযোগ কেটে যাওয়া পরিচিত ঘটনা। কোনো পরিসংখ্যান না ধরে আমরা এটিকেই স্বাভাবিক অবস্থা ধরি: সংযোগ ভাঙবেই, বার্তা হারাবে না, ব্যাটারি আর ডেটা কম খরচ হবে।
  • Availability: বছরে কয়েক ঘণ্টার বেশি বন্ধ নয় (পর্ব ০৫); কোনো অংশ বসে গেলেও বার্তা পাঠানো চালু থাকা চাই, presence-এর মতো বাড়তি সুবিধা আগে বাদ যাবে।

পরিধির বাইরে (এই পর্বে): voice আর video call (পর্ব ১০-এ WebRTC-এর পরিচয় আছে), status বা story, লাখো সদস্যের broadcast channel (আচরণে news feed-এর কাছাকাছি — পর্ব ৫৬), bot আর payment. Push notification-এর ভেতরের নকশা পর্ব ৫৭-এর বিষয়; এখানে আমরা শুধু তাকে ডাকি।

যে প্রশ্নগুলো ইন্টারভিউয়ার আশা করেন

প্রশ্নআমাদের ধরে নেওয়া উত্তরনকশায় প্রভাব
Group-এর সর্বোচ্চ আকার?৫০০Fan-out-এর খরচ সীমিত; per-member receipt সম্ভব
Server কি ইতিহাস রাখবে?হ্যাঁ, encrypted অবস্থায়, retention নীতিসহনতুন ডিভাইসে sync; storage-এর বড় হিসাব
End-to-end encryption?হ্যাঁServer-এ search আর বিষয়বস্তু-ভিত্তিক moderation নেই
প্রতি ব্যবহারকারীর কয়টি ডিভাইস?গড়ে ১.৪, সর্বোচ্চ কয়েকটিপ্রতিটি ডিভাইসের আলাদা cursor আর key
কোন অঞ্চলের ব্যবহারকারী?মূলত একটি দেশ, প্রবাসীরাওএক home region, দূরের জন্য কাছের edge

২ · হিসাব — বার্তা, সংযোগ, gateway আর storage

পর্ব ০৩-এর chat উদাহরণের ধারাবাহিকতায় একই ভিত্তি ধরি: ৫ কোটি DAU, প্রত্যেকে দিনে ৪০টি বার্তা, ২০০ byte, peak factor ৫। এবার যোগ হলো group, ডিভাইস, সংযোগ আর media. প্রতিটি সংখ্যা “ধরা যাক”; একটি বদলে আবার চালিয়ে দেখুন।

// Chat estimator. Every input is an assumption (ধরা যাক): change one and run again.
const DAU = 50e6;            // daily active users
const MSG_PER_USER = 40;     // messages each user sends per day
const PEAK = 5;              // peak hour vs average
const GROUP_SHARE = 0.3, GROUP_RECIP = 25;  // 30% go to groups; avg 25 other members
const DEVICES = 1.4;         // devices per recipient that must receive a copy
const ONLINE = 0.2;          // share of DAU connected at the peak moment
const PER_GW = 100000;       // our cap of connections per gateway (blast radius)
const ZONES = 3;             // must survive losing one zone
const BYTES = 200, REPL = 3; // text + metadata per message; copies in the store
const MEDIA_SHARE = 0.05, MEDIA_KB = 200;   // 5% carry a photo/voice note, avg 200 KB

const perDay = DAU * MSG_PER_USER, avg = perDay / 86400, peak = avg * PEAK;
const recipients = (1 - GROUP_SHARE) * 1 + GROUP_SHARE * GROUP_RECIP;
const deliveries = peak * recipients * DEVICES;     // copies pushed to devices per second
const receipts = peak * recipients * 2;             // delivered + read, per recipient user
const conns = DAU * ONLINE * DEVICES;
const gws = Math.ceil(conns / PER_GW), gwsSafe = Math.ceil(gws / (ZONES - 1)) * ZONES;
const textTB = perDay * BYTES * 365 * REPL / 1e12;
const mediaPB = perDay * MEDIA_SHARE * MEDIA_KB * 1e3 * 365 / 1e15;
const f = (x) => Math.round(x).toLocaleString("en-US");
console.log("messages/day        " + f(perDay));
console.log("avg msg/s           " + f(avg));
console.log("peak msg/s (writes) " + f(peak));
console.log("recipients/message  " + recipients.toFixed(1));
console.log("peak deliveries/s   " + f(deliveries));
console.log("peak receipts/s     " + f(receipts));
console.log("connections at peak " + f(conns));
console.log("gateways (cap " + f(PER_GW) + ") " + gws + ", zone-safe " + gwsSafe);
console.log("text, 1 year x" + REPL + "   " + textTB.toFixed(1) + " TB");
console.log("media, 1 year       " + mediaPB.toFixed(2) + " PB (object storage, before its own redundancy)");

আউটপুট:

messages/day        2,000,000,000
avg msg/s           23,148
peak msg/s (writes) 115,741
recipients/message  8.2
peak deliveries/s   1,328,704
peak receipts/s     1,898,148
connections at peak 14,000,000
gateways (cap 100,000) 140, zone-safe 210
text, 1 year x3   438.0 TB
media, 1 year       7.30 PB (object storage, before its own redundancy)

দিনে ২০০ কোটি বার্তা মানে গড়ে সেকেন্ডে ২৩,১৪৮, peak-এ ১,১৫,৭৪১টি লেখা। কিন্তু একটি বার্তা গড়ে ৮.২ জন প্রাপকের (০.৭ × ১ + ০.৩ × ২৫), আর প্রত্যেকের ১.৪টি ডিভাইস — তাই ডিভাইসে পৌঁছানো কপি সেকেন্ডে ১৩,২৮,৭০৪, লেখার প্রায় ১১.৫ গুণ। Receipt (delivered আর read) আরও বেশি: ১৮,৯৮,১৪৮ — বার্তার চেয়ে বার্তা-সংক্রান্ত খবর বেশি! খোলা সংযোগ ১.৪ কোটি; প্রতি gateway-তে ১ লাখের সীমায় ১৪০টি, আর তিনটি zone-এর একটি হারালেও টিকতে ২১০টি (পর্ব ১০-এর হিসাবের নিয়মে)। Storage-এ লেখা তিন কপিতে বছরে ৪৩৮ TB, আর মাত্র ৫% বার্তার media বছরে ৭.৩ PB — প্রায় ১৭ গুণ।

সংখ্যাগুলো থেকে চারটি নকশা-সিদ্ধান্ত সরাসরি বেরিয়ে আসে:

  1. লেখা-ভারী, append-only: বার্তা লেখা হয় একবার, বদলায় প্রায় কখনো না, পড়া হয় সাম্প্রতিকগুলোই। এমন কাজের জন্য LSM-tree-ভিত্তিক store মানানসই (পর্ব ১৯)।
  2. Gateway গোনা হয় সংযোগে: ১.৪ কোটি সংযোগ ধরে রাখাই মূল কাজ; বার্তার সংখ্যা তুলনায় ছোট।
  3. Receipt-কে সস্তা করতে হবে: প্রতিটি বার্তার আলাদা read receipt না পাঠিয়ে “৫৮ পর্যন্ত পড়েছি” — একটি সংখ্যা অনেক বার্তা ঢেকে দেয়। ১৮ লাখ সংখ্যাটি তাই ঊর্ধ্বসীমা।
  4. Media আলাদা পথে: ছবি chat service-এর ভেতর দিয়ে যাবে না; ফোন সরাসরি object storage-এ তোলে, বার্তায় যায় শুধু key (পর্ব ২৬), আর পড়া হয় CDN থেকে (পর্ব ১৭)।

৩ · API — WebSocket-এর বার্তা-খাম, REST-এর পথ আর idempotent send

অনলাইন থাকার সময় ফোন আর server-এর মাঝে একটি WebSocket সংযোগ খোলা থাকে; দুই দিক থেকেই যেকোনো সময় ছোট frame যায় (পর্ব ১০)। Frame-এর ভেতরের রূপ JSON না Protobuf, সেটি পর্ব ১২-এর প্রশ্ন; মোবাইল ডেটা বাঁচাতে binary রূপ লাভজনক। নিচে বোঝার জন্য JSON:

→ {"t":"send", "conv":"c42", "cid":"r-901", "body":"<ciphertext>", "media":null}
← {"t":"ack",  "cid":"r-901", "seq":1207, "ts":"2026-10-28T09:14:03Z"}
← {"t":"msg",  "conv":"c42", "seq":1207, "from":"u17/d1", "body":"<ciphertext>"}
→ {"t":"rcpt", "conv":"c42", "delivered_upto":1207}
→ {"t":"rcpt", "conv":"c42", "read_upto":1207}
→ {"t":"sync", "cursors":{"c42":1207, "c77":88}}
← {"t":"sync_page", "conv":"c42", "msgs":[...], "next_after":1215, "more":false}
→ {"t":"ping"}   ← {"t":"pong"}

সবচেয়ে গুরুত্বপূর্ণ field হলো cid — ফোনের নিজের বানানো অনন্য id. বার্তা পাঠানোর আগেই ফোন তাকে নিজের local outbox-এ লেখে, পর্দায় দেখায় ঘড়ির চিহ্ন। Ack না এলে একই cid দিয়ে আবার পাঠায়। Server (conv, sender_device, cid)-এ unique নিয়ম রাখে; একই cid আবার এলে নতুন বার্তা না বানিয়ে আগের seq-টাই ফেরত দেয়। এটি REST-এর idempotency key-এর হুবহু ভাবনা (পর্ব ০৮), শুধু WebSocket-এর ওপরে।

যে কাজগুলো স্থায়ী সংযোগে মানায় না, সেগুলো সাধারণ HTTPS-এ যায়:

POST /v1/media/uploads            {"size": 184320, "type": "image/jpeg"}
  → 201 {"media_key": "m/7f3a…", "upload_url": "<presigned URL, 15 min>"}
GET  /v1/conversations/c42/messages?before_seq=1150&limit=50
  → 200 {"msgs": [...], "next_before": 1100}

প্রথমটি ফোনকে একটি presigned URL দেয়, যাতে ছবি সরাসরি object storage-এ ওঠে। দ্বিতীয়টি পুরোনো ইতিহাস পাতা ধরে আনে — seq-ভিত্তিক keyset pagination, offset নয়, কারণ নতুন বার্তা ঢুকলেও পাতা সরে না।

৪ · Data model — conversation log, partition key আর ডিভাইসের cursor

প্রতিটি table-এর key ঠিক হয় তার প্রশ্ন দেখে (পর্ব ২০)। আমাদের প্রধান প্রশ্নগুলো: c42-এর ১২০৭-এর পরের বার্তাগুলো কী, আমার chat তালিকায় কী কী, আর এই ডিভাইস কোথা পর্যন্ত পেয়েছে।

EntityKeyStoreকেন
messages (conversation log)partition: (conv_id, bucket); ভেতরে seqWide-column, LSM-ভিত্তিকলেখা-ভারী, append, “seq-এর পরের ৫০টি” একটি range scan
conversations, membersconv_id; (conv_id, user_id)Relationalছোট, সদস্য যোগ-বাদে transaction আর নিয়ম দরকার
user_conversations (chat তালিকা)partition: user_id; ভেতরে শেষ সক্রিয়তাWide-columnএকজনের তালিকা এক জায়গায়
device_cursors(user_id, device_id, conv_id)Key-value / wide-columndelivered_upto, read_upto — শুধু দুটি সংখ্যা
registry, presencedevice_id → gateway; user_id → শেষ heartbeatIn-memory key-value, TTL-সহঘন ঘন বদলায়, হারালে আবার গড়া যায়
mediamedia_keyObject storage + CDNবড়, অপরিবর্তনীয় bytes

Message table-এর partition key হলো conversation; ফলে একটি কথোপকথনের সব বার্তা এক জায়গায়, seq-এর ক্রমে সাজানো (clustering key). খুব পুরোনো বা ব্যস্ত কথোপকথন সীমাহীন বড় না হতে bucket — একটি সময়ের জানালা — যোগ হয়েছে; এই পুরো যুক্তি আর CQL উদাহরণ পর্ব ২৩-এ আছে। বাকি থাকে প্রশ্নটি: seq দেবে কে?

মূল ধারণা

প্রতিটি কথোপকথনের একজন একক লেখক। c42-এর seq কেবল একটি জায়গা থেকে বাড়ে — সেই partition-এর leader, বা consistent hashing দিয়ে c42-এর মালিক বানানো chat service-এর একটি instance (পর্ব ১৬)। পুরো সিস্টেমের একটি global counter লাগে না, তাই bottleneck নেই; আবার এক কথোপকথনের ভেতরে total order মেলে (পর্ব ৩৯)। মালিক বদলের সময় দুজন একসাথে লেখক হয়ে না বসেন, সেজন্য lease আর fencing token (পর্ব ৪৩)।

৫০০ সদস্যের সীমা এখানেও কাজ করে: একটি group-এ সেকেন্ডে বড়জোর কয়েক ডজন বার্তা আসে, তাই এক লেখকেই কুলোয়। লাখো সদস্যের channel হলে এই হিসাব ভাঙত — সেজন্যই সেটি পরিধির বাইরে।

৫ · High-level design — ছবি আর একটি বার্তার পুরো পথ

“আলাপ” অ্যাপের high-level নকশা (কাল্পনিক)

Clients, gateways, chat service, stores, presence, push and media

Chat অ্যাপের architecture: client, gateway, chat service, store, presence, push আর media রিনার ফোন তানভীরের ফোন ল্যাপটপ Load balancer · TLS WebSocket gateway fleet Connection registry Presence service Chat service Push Message store Metadata DB Object storage + CDN A · app B · device 1 B · device 2 G1 … G210 · প্রতিটিতে ≤ ১ লাখ device → gateway · TTL heartbeat → TTL dedup · seq · fan-out পর্ব ৫৭ conversation log (conv, bucket) · seq member · group device cursor media bytes · বার্তায় শুধু key register heartbeat send / deliver lookup upload কঠিন তীর: বার্তা ও সংযোগ · ভাঙা তীর: presigned URL-এ media
ছবি ৫৫.১ — Gateway শুধু সংযোগ ধরে; সিদ্ধান্ত নেয় chat service. বার্তা একবার conversation log-এ লেখা হয়; registry বলে কে কোন gateway-তে; অফলাইনদের জন্য push. Media chat service ছোঁয় না।

লেখার পথ: ফোন → load balancer → একটি gateway → chat service (c42-এর মালিক) → dedup আর seq → message store-এ টেকসই লেখা (পর্ব ২২-এর replica-সহ) → তারপরেই ack. আগে ack দিয়ে পরে লেখা ব্যর্থ হলে “sent” বার্তা হারাত — durability-র শর্ত ভাঙত। পৌঁছানোর পথ: chat service সদস্যদের ডিভাইস খোঁজে connection registry-তে, তারপর সংশ্লিষ্ট gateway-গুলোকে পাঠায়; অফলাইন ডিভাইসের জন্য push. পড়ার পথ: ফোন নিজের cursor জানিয়ে বাকিটা চায়। এর সাধারণ রূপ পর্ব ১০-এর stepper-এ দেখেছেন; এবার দুই ডিভাইস আর তিন টিক যোগ করে দেখি। “পরে” চাপুন:

  1. রিনার ফোনপ্রেরক
  2. Chat servicec42-এর মালিক
  3. তানভীরের ফোনdevice 1
  4. তানভীরের ল্যাপটপdevice 2
অবস্থা
c42-এর শেষ seq
ফোনের delivered_upto
ল্যাপটপের delivered_upto
তানভীরের read_upto
রিনা দেখছেন
  1. Outbox থেকে পাঠানো। রিনার ফোন “আসছি” আগে নিজের local outbox-এ লিখল, cid r-901, পর্দায় ঘড়ি ⏱. তারপর WebSocket-এ G3 gateway হয়ে chat service-এ পাঠাল। নেটওয়ার্ক কাটলেও বার্তা ফোনেই নিরাপদ।
  2. টেকসই লেখা, তারপর ack. c42-এর মালিক দেখল r-901 নতুন, পরের seq দিল 1207, replica-সহ log-এ লিখল, তারপর ack ফেরত দিল। রিনার পর্দায় একটি টিক ✓ — sent.
  3. কোথায় পাঠাব? Registry বলছে তানভীরের ফোন G5-এ, ল্যাপটপ G8-এ। দুটি gateway-তেই frame পাঠানো হলো। এই মুহূর্তে কেউ এখনো পায়নি, তাই রিনা এখনো এক টিকে।
  4. প্রথম ডিভাইসে পৌঁছাল। ফোন বার্তা জমা করে delivered ack দিল: ফোনের delivered_upto 1207. প্রাপকের অন্তত একটি ডিভাইসে পৌঁছেছে, তাই রিনা পেলেন দুটি টিক ✓✓.
  5. দ্বিতীয় ডিভাইস। বাসায় চালু ল্যাপটপেও একটু পরে বার্তা পৌঁছাল; তার cursor-ও 1207. রিনার পর্দায় কিছু বদলায় না — delivered আগেই দেখানো হয়েছে। কিন্তু দুই ডিভাইসের cursor আলাদা থাকে, কারণ পরের sync প্রত্যেকের নিজের জায়গা থেকে।
  6. Read — ব্যবহারকারীর, ডিভাইসের নয়। বিকেলে বাসায় ফিরে তানভীর ল্যাপটপে chat খুললেন: “1207 পর্যন্ত পড়েছি”. Server read_upto রাখে ব্যবহারকারী-স্তরে, খবরটি রিনাকে পাঠায় (নীল ✓✓), আর তানভীরের ফোনকেও — ফোন notification আর unread badge মুছে দেয়। একটি সংখ্যাই 1207-এর আগের সব বার্তাকে পড়া বলে চিহ্নিত করে।

৬ · Deep dive ১ — লাখো সংযোগ: gateway, registry, heartbeat আর reconnect storm

Gateway পাতলা রাখা। প্রতিটি gateway শুধু TLS শেষ করে, পরিচয় যাচাই করে, heartbeat দেখে আর socket-এ লেখে। ব্যবসার কোনো যুক্তি এখানে নেই; ফলে gateway বদলানো, বাড়ানো বা মারা সহজ। যে state আছে — কোন socket কার — তা হারালে client আবার সংযোগ নিলেই আবার গড়ে ওঠে (পর্ব ১৫)। দীর্ঘজীবী সংযোগে load balancer-এর least-connections বা ভারসাম্যের সমস্যা আর deploy-এর সময় connection draining পর্ব ১০, পর্ব ১৪ আর পর্ব ৫৩-এ আছে।

Registry. সংযোগ খুললেই gateway লেখে “device d1 → G5”, TTL ধরা যাক ৯০ সেকেন্ড, আর প্রতিটি heartbeat-এ নবায়ন করে। Gateway নিঃশব্দে মরলে entry নিজে থেকেই মুছে যায়, chat service তখন ডিভাইসটিকে অফলাইন ধরে push পাঠায়। Registry হারালেও বিপদ কম: প্রতিটি gateway নিজের সংযোগের তালিকা আবার লিখে দেয়। এটি “cache, সত্যের উৎস নয়” — সত্য থাকে message store আর cursor-এ।

Heartbeat-এর ব্যবধান। মোবাইল অপারেটরের CGNAT অনেকক্ষণ নীরব সংযোগের mapping ভুলে যেতে পারে; তখন server ভাবে সংযোগ আছে, অথচ বার্তা পৌঁছায় না (Computer Networks · NAT). ঘন heartbeat এটি ঠেকায়, কিন্তু প্রতিবার ফোনের radio জাগে — tail energy-র খরচ (পর্ব ১০)। তাই ব্যবধান মাপা হয় বাস্তব নেটওয়ার্কে, আর অ্যাপ পেছনে গেলে স্থায়ী সংযোগ ছেড়ে platform push-এর ওপর ভরসা করা হয়।

এক gateway-তে কত? RAM-এ হয়তো কয়েক লাখ সংযোগ আঁটে, কিন্তু আমরা সীমা রেখেছি ১ লাখ। কারণ একটি gateway মরলে তার সব client একসাথে ফিরবে; সেই ভিড়ই তার blast radius. সরল রাখতে ধরা যাক একটি cell-এ ১০টি gateway; তার একটি, G0, যাতে ৫০ হাজার সংযোগ, হঠাৎ crash করল, আর load balancer তার client-দের ওই cell-এর বাকি ৯টিতে পাঠায়। প্রতিটি সেকেন্ডে বড়জোর ২,০০০টি নতুন handshake শেষ করতে পারে, আর প্রত্যাখ্যাত চেষ্টাও একটি handshake-এর ১০% CPU খায় — সব ধরা যাক। চারটি client-কৌশল তুলনা করি (পর্ব ৪৮-এর backoff আর jitter):

// Gateway G0 (50,000 connections) dies at t = 0. Its clients reconnect through the load
// balancer to the 9 surviving gateways. ধরা যাক: each gateway can finish 2,000 new
// handshakes per second; a refused attempt still burns 10% of a handshake's CPU.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const C = 50000, GW = 9, CAP = 2000, REFUSE = 0.1, CAP_S = 32;
function storm(name, firstDelay, backoff) {
  const rnd = mulberry32(48);
  let wait = Array.from({ length: C }, () => [firstDelay(rnd), 0]);   // [next try time, failures]
  let peak = 0, tries = 0, t = 0, done = 0, t99 = null;
  for (; wait.length && t < 300; t++) {
    const now = wait.filter(w => w[0] < t + 1), later = wait.filter(w => w[0] >= t + 1);
    const load = new Array(GW).fill(0), next = [];
    const byGw = Array.from({ length: GW }, () => []);
    for (const w of now) byGw[Math.floor(rnd() * GW)].push(w);   // load balancer: random survivor
    byGw.forEach((list, g) => { const A = list.length; load[g] = A; tries += A;
      const ok = A <= CAP ? A : Math.max(0, Math.floor((CAP - REFUSE * A) / (1 - REFUSE)));
      done += ok; for (const w of list.slice(ok)) next.push([t + 1 + backoff(rnd, w[1]), w[1] + 1]); });
    peak = Math.max(peak, ...load); wait = later.concat(next);
    if (t99 === null && done >= 0.99 * C) t99 = t + 1;
  }
  console.log(name.padEnd(28) + String(peak).padStart(6) + String(tries - C).padStart(9) +
    String(t99).padStart(7) + " s");
}
console.log("strategy (cap " + CAP + "/s/gw)    peak  refused  99% back");
storm("retry every 1 s", () => 0, () => 0);
storm("exp backoff, no jitter", () => 0, (r, k) => Math.min(CAP_S, 2 ** k) - 1);
storm("full jitter", () => 0, (r, k) => r() * Math.min(CAP_S, 2 ** k));
storm("spread 0-5 s + full jitter", (r) => r() * 5, (r, k) => r() * Math.min(CAP_S, 2 ** k));

আউটপুট:

strategy (cap 2000/s/gw)    peak  refused  99% back
retry every 1 s               5632    56760      4 s
exp backoff, no jitter        5632    56760      8 s
full jitter                   5632    55074      4 s
spread 0-5 s + full jitter    1160        0      5 s

প্রথম তিন সারির peak একই: সবচেয়ে ব্যস্ত বেঁচে থাকা gateway-তে প্রথম সেকেন্ডেই 5632টি চেষ্টা, সামর্থ্যের প্রায় ২.৮ গুণ। কারণ তিনটিতেই প্রথম চেষ্টা হয় t = 0-তে — full jitter শুধু retry-কে ছড়ায়, প্রথম ঢেউকে নয়। “Retry every 1 s” আর jitter-হীন exponential backoff-এ প্রত্যাখ্যান একই, 56760; দ্বিতীয়টি ঢেউগুলো একসাথে রাখে বলে ৯৯% ফিরতে লাগে ৮ সেকেন্ড। শেষ সারিতে প্রতিটি client প্রথম চেষ্টার আগেই ০–৫ সেকেন্ডের এলোমেলো অপেক্ষা করে: peak 1160, সামর্থ্যের নিচে, প্রত্যাখ্যান 0, আর সবাই ফেরে ৫ সেকেন্ডে।

ন্যূনতম ছড়ানোর জানালা হিসাব করা যায়: ৫০,০০০ ÷ (৯ × ২,০০০) ≈ ২.৮ সেকেন্ড। এর কম হলে প্রত্যাখ্যান হবেই (অনুশীলন ৪)। Gateway process crash করলে TCP সংযোগ সঙ্গে সঙ্গে ভাঙে, তাই সবাই একই মুহূর্তে টের পায়; যন্ত্রটি নিঃশব্দে হারালে heartbeat-এর timeout-এ টের পাওয়া ছড়িয়ে যায় — প্রকৃতিই কিছুটা jitter দেয়। বাকি প্রতিরক্ষা server-এর হাতে: প্রতি gateway-তে নতুন সংযোগের token bucket (পর্ব ৪৯), সস্তায় “পরে আসুন” বলা (পর্ব ১৮-এর load shedding), আর TLS session resumption-এ handshake-এর CPU কমানো। ফিরে আসা client পুরো ইতিহাস চায় না, শুধু cursor-এর পরেরটুকু — না হলে storm পৌঁছে যেত message store পর্যন্ত।

৭ · Deep dive ২ — বার্তার নিশ্চয়তা: id, seq, dedup, sync আর group fan-out

চারটি নিয়মে পুরো নিশ্চয়তা দাঁড়ায়, প্রতিটি আগের কোনো পর্বের প্রয়োগ:

  1. Client id → idempotent send: retry নিরাপদ, server-এ duplicate নেই (পর্ব ৩৪)।
  2. Server-এর seq → ক্রম: ফোনের ঘড়ি নয়, কথোপকথনের লেখক ক্রম ঠিক করে; ফোনের ঘড়ি ভুল হতে পারে (পর্ব ৩৯)।
  3. At-least-once + dedup: ack না পেলে server আবার পাঠায়; গ্রাহক seq দেখে দ্বিতীয় কপি ফেলে, ফাঁক থাকলে পরেরটি আটকে রাখে।
  4. প্রতি ডিভাইসের cursor: “কোথা পর্যন্ত পেয়েছি” — reconnect-এর পর সেখান থেকে sync.

নিচের simulator একটি group-এর: A আর B দুজনেই চারটি করে বার্তা পাঠান, ৩০% পাঠানো আর ৩০% ack হারায় (ধরা যাক)। প্রত্যেকের একটিই বার্তা পথে থাকে, তাই নিজের ক্রম ঠিক থাকে। তারপর server থেকে C-র কাছে কপিগুলো দেরিতে, দুবার বা উল্টো ক্রমে যায়:

// Group chat over flaky links. Senders retry with the SAME client id; the server dedups by id
// and assigns seq; receiver C gets copies late, twice or out of order, then dedups and reorders.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rnd = mulberry32(42), LOSS = 0.3, ACK_LOSS = 0.3;   // ধরা যাক: 30% of sends and acks lost
const outbox = { A: ["A1", "A2", "A3", "A4"], B: ["B1", "B2", "B3", "B4"] };  // client ids
const seqOf = new Map(), log = []; let attempts = 0, dupAtServer = 0;
for (let tick = 0; outbox.A.length + outbox.B.length > 0; tick++) {
  for (const who of ["A", "B"]) {
    const cid = outbox[who][0]; if (!cid) continue;   // one in flight per sender: keeps its order
    attempts++; if (rnd() < LOSS) continue;           // lost on the way: retry next tick
    if (seqOf.has(cid)) dupAtServer++;                // retry of a stored message: same seq back
    else { seqOf.set(cid, log.length + 1); log.push(cid); }
    if (rnd() >= ACK_LOSS) outbox[who].shift();       // ack arrived: next message
  }
}
console.log("sends " + attempts + ", duplicates absorbed by server " + dupAtServer);
console.log("server log: " + log.map((c, i) => (i + 1) + ":" + c).join(" "));
const copies = [];                                     // server -> C: at-least-once, any order
log.forEach((cid, i) => { copies.push([i + rnd() * 3, i + 1]);
  if (rnd() < 0.35) copies.push([i + 2 + rnd() * 3, i + 1]); });   // redelivered after lost ack
copies.sort((x, y) => x[0] - y[0]);
const naive = copies.map(([, s]) => log[s - 1]);
console.log("naive arrival: " + naive.join(" "));
const seen = new Set(), held = new Set(), shown = []; let next = 1, dropped = 0, maxHeld = 0;
for (const [, s] of copies) {
  if (seen.has(s)) { dropped++; continue; } seen.add(s); held.add(s);
  while (held.has(next)) { held.delete(next); shown.push(next + ":" + log[next - 1]); next++; }
  maxHeld = Math.max(maxHeld, held.size);
}
console.log("C shows:       " + shown.join(" "));
console.log("dropped duplicates " + dropped + ", max held waiting for a gap " + maxHeld);

আউটপুট:

sends 20, duplicates absorbed by server 5
server log: 1:A1 2:B1 3:B2 4:A2 5:A3 6:A4 7:B3 8:B4
naive arrival: B1 A1 A1 A2 B2 B1 A4 B2 B3 A3 A2 A4 B4
C shows:       1:A1 2:B1 3:B2 4:A2 5:A3 6:A4 7:B3 8:B4
dropped duplicates 5, max held waiting for a gap 2

৮টি বার্তার জন্য 20টি পাঠানো; 5টি ছিল এমন বার্তার retry, যা server আগেই জমা করেছিল — ack হারিয়েছিল। একই cid দেখে server নতুন seq দেয়নি, তাই log-এ ঠিক ৮টি সারি। Log-এ A আর B মিশে গেছে (B1, B2-এর পরে A2), কিন্তু প্রত্যেকের নিজের ক্রম অটুট। C-র কাছে কপি এসেছে 13টি: B1 আগে, A1 দুবার, আর A4 এসেছে A3-এর আগে। কপি আসামাত্র দেখালে C এই এলোমেলো ধারাই দেখতেন। Seq দেখে C 5টি duplicate ফেলেছে, ফাঁক ভরার অপেক্ষায় সর্বোচ্চ 2টি বার্তা আটকে রেখেছে, আর দেখিয়েছে server-এর হুবহু ক্রম। সব সদস্য, সব ডিভাইস — একই ধারা।

ফাঁক বেশিক্ষণ না ভরলে ডিভাইস নিজেই sync চায় — কপি আসার ভরসায় চিরকাল বসে থাকে না। অফলাইন থেকে ফিরে আসাও একই sync. তানভীরের ফোন তিন ঘণ্টা নেটওয়ার্কের বাইরে ছিল:

  1. তানভীরের ফোন৩ ঘণ্টা অফলাইন ছিল
  2. Gateway G2নতুন gateway
  3. Sync servicechat service-এর অংশ
  4. Message storeconversation log
ফোনের cursor
c42 (পরিবার)
c77 (অফিস)
c90 (বন্ধু)
ফোনে এলো
  1. Push ঘুম ভাঙায়। ফোন অফলাইন থাকার সময় c42-এ ৮টি আর c77-এ ২টি নতুন বার্তা জমা হয়েছে। Registry-তে ফোন নেই, তাই chat service push service-কে বলেছে (পর্ব ৫৭)। নেটওয়ার্ক ফিরতেই platform push এলো “নতুন বার্তা”. E2EE-তে push-এ বার্তার লেখা থাকে না বা encrypted থাকে।
  2. ফেরা আর cursor জানানো। অ্যাপ jitter-সহ সংযোগ নিল — এবার G2-তে, আগের gateway-তে নয়। G2 registry-তে লিখল “ফোন → G2”. ফোন পাঠাল নিজের তিনটি cursor: c42-এ 1207, c77-এ 88, c90-এ 15.
  3. কী বদলেছে? Sync service প্রতিটি কথোপকথনের শুধু শেষ seq দেখল: c42-এ 1215, c77-এ 90, c90-এ 15. তৃতীয়টি বদলায়নি, তার জন্য কিছু পড়তে হবে না।
  4. শুধু ফাঁকটুকু। c42-এ seq > 1207 আর c77-এ seq > 88 — দুটি range scan, প্রতিটি সর্বোচ্চ ৫০টির পাতায় (keyset). ফোন পেল ৮ + ২ = ১০টি বার্তা। পুরো ইতিহাস নয়, তাই ডেটা আর সময় দুটোই বাঁচে।
  5. Cursor এগোল। ফোন বার্তা জমা করে জানাল delivered_upto: c42-এ 1215, c77-এ 90. Server cursor হালনাগাদ করল, প্রেরকেরা পেলেন দুই টিক। মাঝপথে সংযোগ আবার ভাঙলে? ফোন পুরোনো cursor থেকে আবার চাইবে; duplicate এলে seq দেখে ফেলে দেবে।

একজনের ৫০০টি কথোপকথন থাকলে ৫০০টি “শেষ seq” পড়া ভারী। তাই প্রতিটি ব্যবহারকারীর একটি ছোট “বদলানো কথোপকথন” তালিকা রাখা হয়: নতুন বার্তা এলে প্রতিটি সদস্যের তালিকায় শুধু conv_id যোগ হয়। এটি ছোট মাপের fan-out on write — বার্তার কপি নয়, এক লাইনের ইঙ্গিত। Group-এ এই fan-out কেমন দেখায়, দেখুন:

  1. Chat servicegroup ‘পরিবার’
  2. Registryএকটি batch lookup
  3. Gateways১৪টি
  4. Pushপর্ব ৫৭
একটি group বার্তার খরচ
store-এ লেখা
gateway-তে বার্তা
socket-এ লেখা
push
রিনা দেখছেন
  1. একবার লেখা। ৪০ সদস্যের group “পরিবার”-এ রিনার বার্তা seq 3301 হিসেবে conversation log-এ একবারই জমা হলো। ৩৯ জন প্রাপকের জন্য ৩৯টি কপি নয় — storage-এ একটি সারি, পড়ার সময় সবাই একই log পড়েন।
  2. কে কোথায়? সদস্য তালিকা (cache থেকে) বলছে ৩৯ জন অন্য সদস্যের ৫২টি ডিভাইস। Registry-তে একটি batch lookup: ৩৭টি অনলাইন, ১৪টি gateway-তে ছড়ানো; ১৫টি অফলাইন।
  3. Gateway ধরে batch. ৩৭টি আলাদা অনুরোধ নয়, প্রতিটি gateway-তে একটি — মোট ১৪টি। প্রতিটি gateway নিজের socket-গুলোতে লিখল: ৩৭টি লেখা। এখানেই fan-out-এর খরচ, যা সদস্যসংখ্যার সাথে সমানুপাতে বাড়ে।
  4. অফলাইনদের push. ১৫টি অফলাইন ডিভাইসের জন্য push অনুরোধ। একই group থেকে পরপর বার্তা এলে push service সেগুলো জমিয়ে একটি notification দেয় — “পরিবার: ৩টি নতুন বার্তা” (পর্ব ৫৭)।
  5. Receipt জমিয়ে দেখানো। ৩৭টি delivered ack এলো; অন্তত একটি ডিভাইস অনলাইন এমন ২৮ জন সদস্যে বার্তা পৌঁছেছে। রিনা প্রতিটি ack আলাদা দেখেন না, দেখেন “৩৯-এর মধ্যে ২৮ জন”. Read-ও একইভাবে প্রতি সদস্যের একটি read_upto — ৫০০ সদস্যের সীমা না থাকলে এই হিসাবও অসম্ভব হতো।

কেন শুধু একটি conversation log, প্রতিটি সদস্যের আলাদা inbox কপি নয়? ৫০০ সদস্যের group-এ একটি বার্তা ৫০০ গুণ লেখা হতো, আর E2EE-তে প্রতিটি কপি আলাদা encrypt করার খরচও যোগ হতো। Log একবার লিখে ডিভাইসে পৌঁছানোকে fan-out করাই এখানে সস্তা পথ। News feed-এ হিসাবটি উল্টে যেতে পারে, কারণ সেখানে প্রতিটি পাঠকের feed আলাদা ক্রমে সাজাতে হয় — পর্ব ৫৬-এ।

৮ · Deep dive ৩ — Presence: heartbeat, TTL আর fan-out-এর দাম

Presence নির্ণয় সহজ: heartbeat এলে presence key-এর মেয়াদ নবায়ন, না এলে TTL-এ মুছে যাওয়া; শেষ heartbeat-এর সময়ই last seen (পর্ব ১০, তত্ত্ব পর্ব ৪৪)। কঠিন অংশ অন্যটি — খবরটি কাকে জানাবেন। সবচেয়ে সরল উত্তর: প্রতিটি অনলাইন contact-কে। ট্রেনের দুর্বল নেটওয়ার্কে তানভীর ঘণ্টায় কয়েকবার অনলাইন-অফলাইন হলে প্রতিবার তাঁর শত শত contact-এর কাছে খবর যায়। নিচে ২ লাখ ব্যবহারকারীর একটি নমুনায় তিনটি নীতি তুলনা করা হয়েছে:

// Presence fan-out for a sample of 200,000 users (ধরা যাক). Who must hear "X went online/offline"?
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rnd = mulberry32(57), U = 200000, ONLINE = 0.3;
const CHANGES_PER_H = 6;   // online/offline flips per online user per hour on a flaky mobile link
const FLAP = 0.5;          // share of flips undone within 10 s (a short network drop)
const VIEW = 8;            // presence dots on one open screen (chat list rows or an open chat)
const contacts = [], online = [];
for (let u = 0; u < U; u++) {   // heavy tail: most have 50-300 contacts, a few have thousands
  contacts.push(Math.min(5000, Math.floor(40 / Math.pow(1 - rnd(), 0.8))));
  online.push(rnd() < ONLINE);
}
const pool = [];                // a user appears in others' lists in proportion to contacts
contacts.forEach((c, u) => { for (let k = 0; k < c; k += 40) pool.push(u); });
const subs = new Array(U).fill(0);
for (let u = 0; u < U; u++) if (online[u])            // each online viewer watches VIEW users
  for (let k = 0; k < VIEW; k++) subs[pool[Math.floor(rnd() * pool.length)]]++;
let naive = 0, onView = 0, busiest = 0, n = 0;
for (let u = 0; u < U; u++) if (online[u]) { n++;
  naive += CHANGES_PER_H * contacts[u] * ONLINE;     // tell every online contact
  onView += CHANGES_PER_H * subs[u];                 // tell only those looking right now
  busiest = Math.max(busiest, subs[u]); }
const avgC = contacts.reduce((a, b) => a + b, 0) / U, f = (x) => Math.round(x / 3600).toLocaleString("en-US");
console.log("online users " + n.toLocaleString("en-US") + ", avg contacts " + avgC.toFixed(0) + ", max " + contacts.reduce((a, b) => Math.max(a, b)));
console.log("naive broadcast to online contacts  " + f(naive).padStart(9) + " events/s");
console.log("subscribe-on-view                   " + f(onView).padStart(9) + " events/s");
console.log("subscribe-on-view + 10 s debounce   " + f(onView * (1 - FLAP)).padStart(9) + " events/s");
console.log("most-watched user has " + busiest + " live viewers");

আউটপুট:

online users 59,930, avg contacts 152, max 5000
naive broadcast to online contacts      4,594 events/s
subscribe-on-view                         240 events/s
subscribe-on-view + 10 s debounce         120 events/s
most-watched user has 92 live viewers

নমুনায় 59,930 জন অনলাইন, গড়ে 152 জন contact. সব অনলাইন contact-কে জানালে সেকেন্ডে 4,594টি event; শুধু যাঁদের পর্দায় এই মুহূর্তে সেই মানুষটির বিন্দু দেখা যাচ্ছে তাঁদের জানালে (subscribe-on-view) 240, প্রায় ১৯ গুণ কম। দশ সেকেন্ডের মধ্যে উল্টে যাওয়া বদল চেপে রাখলে (debounce) 120 — মোট প্রায় ৩৮ গুণ কম। সবচেয়ে বেশি দেখা মানুষটিরও লাইভ দর্শক মাত্র 92 জন, যদিও কারও contact 5000. ৫ কোটি DAU-তে নমুনাটি ২৫০ গুণ: সরল নীতিতে সেকেন্ডে প্রায় ১১.৫ লাখ presence event — অংশ ২-এর ১৩ লাখ বার্তা-কপির কাছাকাছি! দ্বিতীয় নীতিতে প্রায় ৬০ হাজার, debounce-সহ ৩০ হাজার।

তিনটি কৌশল এখান থেকেই আসে:

  • Lazy presence: chat তালিকা বা একটি chat খুললে ফোন শুধু দৃশ্যমান মানুষদের presence-এ subscribe করে, পর্দা বদলালে ছেড়ে দেয়। খোলার মুহূর্তে একবার পড়ে নেওয়া (pull), তারপর বদল এলে push.
  • Debounce আর coalescing: অনলাইন থেকে অফলাইন জানানোর আগে কয়েক সেকেন্ড অপেক্ষা; এর মধ্যে ফিরে এলে কিছুই পাঠানো হয় না। অনলাইন-অফলাইনের এই দোল (flapping-এর মতো) এভাবেই থামে। একই সময়ে কয়েকটি বদল একটি batch-এ।
  • Group-এ মোটা দাগে: ৫০০ সদস্যের group-এ প্রত্যেকের সবুজ বিন্দু নয়, বড়জোর “১২ জন অনলাইন” — বা কিছুই নয়। Typing indicator আরও ক্ষণস্থায়ী: জমা হয় না, হারালেও ক্ষতি নেই, তাই at-most-once যথেষ্ট (পর্ব ৩৪)। বার্তা, receipt আর presence — তিন রকম খবরের তিন রকম নিশ্চয়তা।

গোপনীয়তার সেটিংও এখানে ঢোকে: কেউ last seen লুকালে তাঁর খবর কাউকে পাঠানো হয় না, যা খরচও কমায়। Presence store বসে গেলে সবাই কিছুক্ষণ “অফলাইন” দেখাবে, কিন্তু বার্তা চলবে — এটিই graceful degradation.

৯ · ভাঙন, trade-off আর বিস্তার — encryption, region, spam আর wrap-up

কী মরলে কী হয়

যা ব্যর্থব্যবহারকারী যা দেখেনপ্রতিরক্ষা
একটি gatewayকয়েক সেকেন্ড “সংযোগ হচ্ছে…”প্রথম চেষ্টাও ছড়ানো, admission limit, cursor থেকে sync
Chat service-এর একটি instanceকিছু বার্তায় ঘড়ি ⏱ একটু বেশিক্ষণমালিকানা অন্য instance-এ (lease, fencing); ফোন একই cid-এ retry
Message store-এর partition leaderসেই কথোপকথনে সংক্ষিপ্ত বিরতিReplica থেকে নতুন leader (পর্ব ৪২); ack শুধু টেকসই লেখার পরে, তাই ack-পাওয়া বার্তা হারায় না
Registryপ্রায় কিছুই নাGateway-রা আবার লেখে; মাঝে ভুল হলে push আর sync ঢেকে দেয়
Presence storeসবাই “অফলাইন”Graceful degradation; বার্তা চলে
Push providerঅফলাইন ফোনে দেরিতে খবরবার্তা log-এ নিরাপদ; অ্যাপ খুললেই sync
পুরো regionবিভ্রাট, বা দ্বিতীয় region-এ সরে যাওয়াReplication আর failover (পর্ব ৫১)

End-to-end encryption — ধারণার স্তরে

End-to-end encryption (E2EE)-এ বার্তা প্রেরকের ডিভাইসে encrypt হয়, খোলা যায় শুধু প্রাপকের ডিভাইসে; server বহন করে অস্বচ্ছ bytes. বহুল ব্যবহৃত নকশা Signal protocol, যার specification Signal-এর পক্ষ থেকে প্রকাশিত (মূল অংশগুলো ২০১৬ সালের দিকে): চাবি বিনিময়ের একটি handshake, তারপর প্রতিটি বার্তায় চাবি বদলে চলা “double ratchet”, যা forward secrecy দেয়। খুঁটিনাটি আর সংস্করণ সময়ের সাথে বদলেছে; নিরাপত্তার বিচার বিশেষজ্ঞ পর্যালোচনার বিষয়, এখানে আমরা দেখি নকশায় প্রভাব (পর্ব ৫২, Computer Networks · cryptography).

  • Server-এ search নেই: বিষয়বস্তু server পড়তে পারে না, তাই পর্ব ২৭-এর inverted index ফোনেই বানাতে হয়।
  • Moderation বদলায়: বিষয়বস্তু স্ক্যান অসম্ভব; ভরসা ব্যবহারকারীর report (তখন ডিভাইস নিজেই বার্তাগুলো পাঠায়), আর metadata-র সংকেত — হার, নতুন অ্যাকাউন্ট, অচেনা মানুষকে গণবার্তা।
  • প্রতিটি ডিভাইসের নিজের চাবি: নতুন ডিভাইসে পুরোনো ইতিহাস আনতে আলাদা encrypted backup বা ডিভাইস-থেকে-ডিভাইস স্থানান্তর লাগে। Group-এ প্রতিটি প্রেরকের একটি sender key সব সদস্যকে আগে দেওয়া থাকলে বার্তা একবার encrypt করলেই চলে।
  • Media: ফোন ছবিটি encrypt করে upload করে; চাবিটি যায় encrypted বার্তার ভেতরে। Object storage রাখে শুধু ciphertext.
  • Metadata থেকেই যায়: কে কাকে, কখন, কত বড় বার্তা — server দেখে। এটিকে কমানো আলাদা গবেষণার বিষয়।

Multi-region, ১০ গুণ বৃদ্ধি, spam আর observability

Multi-region: প্রতিটি কথোপকথনের একটি home region, যেখানে তার একক লেখক থাকে। দূরের প্রবাসী সদস্য কাছের gateway-তে যুক্ত হন, বার্তা home region-এ গিয়ে seq পায় — বাড়তি latency মোটামুটি একটি দূরপাল্লার round trip. দুই region-এ একসাথে লেখক রাখলে conflict-এর জটিলতা আসে (পর্ব ২২, পর্ব ৪৬); chat-এ একক লেখকই সাধারণত সরল আর নিরাপদ।

১০ গুণে কী ভাঙবে: ডিভাইস-কপি সেকেন্ডে কোটির ঘরে, receipt তার চেয়েও বেশি, registry lookup, আর storage. প্রতিকার: gateway ধরে batch, receipt-কে watermark বানানো, registry-কে shard করা, পুরোনো bucket সস্তা storage-এ নামানো।

Spam আর অপব্যবহার (প্রতিরক্ষামূলক): নম্বর যাচাইয়ে OTP — SMS-এর নির্ভরযোগ্যতা পর্ব ৫৭-এ; অ্যাকাউন্ট আর ডিভাইস ধরে পাঠানো, নতুন অচেনা কথোপকথন আর group-এ যোগ করার rate limit (পর্ব ৪৯); forward-এর সংখ্যা সীমিত করা আর “অনেকবার forward করা” চিহ্ন; block আর report. E2EE-তে এই সংকেতগুলোই প্রধান ভরসা।

কী মাপবেন আর কিসে alert: send থেকে ack-এর p99, অনলাইন-থেকে-অনলাইন delivery-র p99, ack-ব্যর্থতার হার, reconnect-এর হার, প্রতি gateway-তে সংযোগ, sync-এর পিছিয়ে থাকা, push-ব্যর্থতা (পর্ব ৫০)। হঠাৎ reconnect বেড়ে যাওয়া আর delivery latency-র SLO burn — এই দুটি alert সবার আগে। খরচ: সবচেয়ে বড় খাত media storage আর CDN-এর egress, তারপর gateway; text তুলনায় সস্তা।

ইন্টারভিউর ৬০ সেকেন্ডের সারাংশ

“৫ কোটি DAU-তে peak-এ সেকেন্ডে প্রায় ১.২ লাখ বার্তা আর ১.৪ কোটি খোলা সংযোগ। ফোন WebSocket-এ পাতলা gateway-তে যুক্ত, registry TTL-সহ জানে কে কোথায়। প্রতিটি বার্তায় client id, যাতে retry নিরাপদ; প্রতিটি কথোপকথনের একক লেখক replica-সহ টেকসই লেখার পর seq দিয়ে ack দেয়। বার্তা conversation_id-তে partition করা LSM-ভিত্তিক store-এ একবার লেখা হয়; অনলাইন ডিভাইসে gateway ধরে batch-এ পাঠানো, অফলাইনদের push, আর প্রতিটি ডিভাইস নিজের cursor থেকে sync করে। Delivery at-least-once, গ্রাহক seq দেখে dedup আর ক্রম মেলায়; receipt হলো ‘কোন seq পর্যন্ত’. Presence lazy আর debounce করা। Media presigned URL-এ object storage আর CDN-এ। E2EE-র কারণে search ফোনে, moderation report আর metadata-য়। ঝুঁকি: reconnect storm, বড় group-এর fan-out আর home region-এর বিভ্রাট — প্রতিটির প্রতিরক্ষা আছে।”

আরও গভীরে: XMPP একটি খোলা chat protocol, যার core আর instant messaging ও presence-এর অংশ ২০১১ সালে IETF-এর RFC 6120 আর 6121 হিসেবে প্রকাশিত; presence-এর subscription মডেল সেখানে সরাসরি আছে। বড় group-এ E2EE-র জন্য IETF-এর Messaging Layer Security (MLS) প্রকাশিত হয় ২০২৩ সালে (RFC 9420), যা হাজারো সদস্যেও চাবি বদলানো সাশ্রয়ী করার লক্ষ্যে নকশা করা। E2EE-তে abuse report যাচাইয়ের একটি কৌশল “message franking” নিয়ে প্রকাশিত গবেষণা আছে (যেমন CRYPTO ২০১৭-এর একটি paper).

১০ · AI-এর যুগে — AI-এর দেওয়া chat নকশা যাচাই

“একটি জনপ্রিয় messaging অ্যাপের মতো chat system design করো” — AI কয়েক সেকেন্ডে সুন্দর ছবি দেয়, প্রায়ই Kafka, Redis আর microservices দিয়ে ভরা। কিন্তু chat-এর আসল কঠিন অংশগুলো — ক্রম, duplicate, অফলাইন ডিভাইস আর reconnect — সেখানে প্রায়ই এক লাইনে সারা হয়। চেনা ভুলগুলো:

AI-এর ভুলকেমন দেখায়কীভাবে যাচাই করবেন
সাধারণ, ভারী উত্তরপ্রতিটি ব্যবহারকারীর জন্য একটি Kafka topic, সবকিছুতে microserviceকোটি topic কি সম্ভব? এক লেখক আর একটি log-এ কি চলে?
সময় দিয়ে ক্রম“client timestamp দিয়ে sort”, বা একটি global auto-incrementঘড়ি ভুল ফোনে কী হবে? Global counter কি bottleneck?
Exactly-once-এর দাবি“Kafka exactly-once, তাই duplicate হবে না”ফোন থেকে server, server থেকে ফোন — প্রতিটি কড়ায় id আর dedup আছে কি?
Ack-এর ভুল জায়গালেখার আগেই “sent”Ack আর টেকসই লেখার মাঝে crash ভাবুন
অফলাইন আর multi-device উপেক্ষাশুধু “B অনলাইন থাকলে পাঠানো”Push, per-device cursor আর sync-এর পথ খুঁজুন
হিসাবের ভুলদিনে-সেকেন্ডে গুলিয়ে ফেলা, ডিভাইস-কপি আর receipt বাদঅংশ ২-এর estimator নিজে চালান
পণ্যের সীমা বানানো“একটি server এক কোটি WebSocket ধরে”, কোনো push সেবার সীমাঅফিশিয়াল ডকুমেন্টেশন আর নিজের load test
Presence সবাইকেপ্রতিটি বদল সব contact-এ broadcastঅংশ ৮-এর মতো event/s হিসাব করুন
E2EE আর server-side search একসাথে“E2EE চালু, আর server-এ full-text search”Server কি plaintext দেখে? দেখলে সেটি E2EE নয়

AI-কে নকশাকার নয়, কড়া reviewer বানান। আগে নিজে requirement আর সংখ্যা লিখুন, তারপর এভাবে প্রশ্ন করুন:

তুমি একজন অভিজ্ঞ distributed systems reviewer. নিচে একটি chat অ্যাপের নকশা: 1:1 আর ৫০০ সদস্যের group, multi-device, offline delivery, E2EE, দুর্বল মোবাইল নেটওয়ার্ক। [এখানে আপনার requirement, হিসাব, ছবির বর্ণনা আর data model পেস্ট করুন] ১. Peak-এ বার্তা/সেকেন্ড, ডিভাইস-কপি/সেকেন্ড, receipt/সেকেন্ড আর খোলা সংযোগ ধাপে ধাপে আবার হিসাব করো; আমার কোনো ভুল থাকলে দেখাও। ২. একটি বার্তা ফোন থেকে অন্য ফোনের দুই ডিভাইসে যাওয়ার প্রতিটি ধাপে কোথায় crash বা ack হারালে বার্তা হারায় বা দুবার আসে, বলো। ৩. ক্রম কে ঠিক করে, কোন scope-এ, আর মালিক বদলের সময় দুই লেখক ঠেকানো হয় কীভাবে? ৪. একটি gateway মরলে reconnect-এর peak কত, আর client ও server-এ কী প্রতিরক্ষা আছে? ৫. Presence-এর fan-out-এর খরচ হিসাব করো আর সস্তা বিকল্প দাও। ৬. পণ্যের কোনো সীমা বললে উৎস ও সংস্করণ দাও, নিশ্চিত না হলে স্পষ্ট লেখো; নতুন প্রযুক্তি যোগের আগে সরলতর বিকল্পটি বলো।

AI-এর নকশা মানার আগে — চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে: এই case-এ দুর্বল উত্তর শুরু হয় “WebSocket, Kafka আর Cassandra” দিয়ে, সংখ্যা ছাড়া; ক্রম আর duplicate-এর প্রশ্নে চুপ, অফলাইন ডিভাইস ভুলে যায়। শক্ত উত্তর আগে পরিধি আর হিসাব ঠিক করে, তারপর ack কখন, seq কে দেয়, অফলাইন ডিভাইস কীভাবে ধরে — এই তিনটি স্পষ্ট বলে, আর নিজেই ব্যর্থতা তোলে। সাধারণ প্রশ্ন আর এক লাইনের উত্তর:

  • “Delivered আর read কীভাবে?” — প্রতি ডিভাইসের delivered_upto আর প্রতি ব্যবহারকারীর read_upto; একটি seq অনেক বার্তা ঢাকে।
  • “Exactly-once দেবেন?” — Delivery at-least-once, প্রভাব একবার: client id-তে dedup আর seq-এ duplicate ফেলা।
  • “এক gateway মরলে?” — Registry TTL-এ মোছে, client ছড়ানো jitter-এ ফেরে, cursor থেকে sync.
  • “Group ১ লাখ হলে?” — সেটি broadcast channel: fan-out on read, receipt আর presence বাদ।
  • “E2EE-তে search?” — ফোনে index; server দেখে শুধু metadata.

ভাবনার প্রশ্ন

ইন্টারভিউয়ার মূল নকশার পর যে প্রশ্নগুলো দিয়ে চাপ দেন — নিজে ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১ক্রমের জন্য ফোনের timestamp কেন নয়, আর পুরো সিস্টেমের একটি global sequence-ই বা কেন নয়?

Timestamp-এর সমস্যা: ফোনের ঘড়ি ব্যবহারকারী নিজেই বদলাতে পারেন, NTP ছাড়া চলতে পারে, আর দুটি ফোনের ঘড়ির পার্থক্য কয়েক সেকেন্ড থেকে কয়েক মিনিট হওয়া অস্বাভাবিক নয়। রিনার ঘড়ি দুই মিনিট পিছিয়ে থাকলে তাঁর উত্তর প্রশ্নের আগে বসবে। Server-এর ঘড়ি দিয়েও পুরো সমাধান হয় না: আলাদা server-এর ঘড়িতেও skew আছে, আর একই millisecond-এ দুটি বার্তা এলে কোনটি আগে, ঘড়ি তা বলে না (পর্ব ৩৯)।

Global sequence-এর সমস্যা: সেকেন্ডে লাখের বেশি বার্তায় একটি counter হয় একক bottleneck, নয়তো consensus-এর খরচে ধীর। অথচ আমাদের requirement-এ দুটি আলাদা কথোপকথনের মধ্যে ক্রমের দাবিই নেই — রিনার অফিসের group আগে না পরিবারের group, কেউ জিজ্ঞেস করে না। তাই ক্রমের scope ছোট করা হয়েছে: প্রতিটি কথোপকথনে একটি counter. কোটি কথোপকথন মানে কোটি স্বাধীন counter, প্রতিটি হালকা।

কার্যকারণের (causality) কী হলো? একক লেখক থাকলে একটি সুন্দর ফল পাওয়া যায়: B একটি বার্তা দেখে তবেই উত্তর লেখেন, আর B সেটি দেখতে পান কেবল server seq দেওয়ার পরে। তাই উত্তরটি server-এ পরে পৌঁছায় এবং বড় seq পায়। অর্থাৎ এক কথোপকথনের ভেতরে “দেখে উত্তর দেওয়া”-র কার্যকারণ ক্রম নিজে থেকেই রক্ষা হয় — vector clock লাগে না। একই মুহূর্তে দুজন লিখলে দুটি বার্তা আসলে concurrent; server যেকোনো একটি ক্রম বেছে নেয়, আর সবাই সেই একই ক্রম দেখে — এটুকুই দরকার।

পর্দায় সময় কোথা থেকে? বার্তার পাশে লেখা সময় server-এর ack-এর সময়, শুধু দেখানোর জন্য; সাজানো হয় seq দিয়ে।

মূল উপলব্ধি: ক্রমের নিশ্চয়তা যত বড় scope-এ, তত দামি। Requirement যতটুকু চায়, ঠিক ততটুকু scope বেছে নেওয়াই ভালো নকশা।

প্র ০২Server কি বার্তা চিরকাল রাখবে, নাকি পৌঁছে দিয়েই মুছে ফেলবে? এই এক সিদ্ধান্ত নকশার কী কী বদলায়?

দুটি মডেল আছে। Store-and-forward-এ server ডাকঘরের মতো: সব প্রাপক-ডিভাইস ack না দেওয়া পর্যন্ত বার্তা রাখে, তারপর মুছে ফেলে। ইতিহাস থাকে শুধু ডিভাইসে। অন্যটি server-side history: conversation log স্থায়ী, যেকোনো নতুন ডিভাইস ইতিহাস টেনে আনতে পারে।

Store-and-forward-এর সুবিধা: server-এ জমা অনেক কম — আমাদের বছরে ৪৩৮ TB-র বেশিরভাগই লাগত না। গোপনীয়তাও বেশি: যা জমা নেই, তা ফাঁস বা জব্দ হতে পারে না। Signal-এর প্রকাশিত নথি অনুযায়ী তাদের server বার্তা মূলত পৌঁছানো পর্যন্তই রাখে — এই মডেলের পরিচিত উদাহরণ।

অসুবিধা: ফোন হারালে, আলাদা backup না থাকলে, ইতিহাসও হারায়। নতুন ল্যাপটপে পুরোনো বার্তা দেখতে হলে পুরোনো ডিভাইস থেকে স্থানান্তর লাগে। Multi-device কঠিন হয়, কারণ প্রতিটি ডিভাইস অনলাইন না হওয়া পর্যন্ত server-কে রাখতে হয়; একটি ডিভাইস মাসের পর মাস বন্ধ থাকলে কী করবেন, সেই নিয়মও লাগে।

Server-side history-র সুবিধা ও দাম: যেকোনো ডিভাইসে পুরো ইতিহাস, ক্লাউডের মতো অভিজ্ঞতা। দাম: storage, retention নীতি, আইনি অনুরোধের ঝুঁকি, আর E2EE থাকলে ইতিহাসের চাবি নতুন ডিভাইসে কীভাবে যাবে, সেই কঠিন প্রশ্ন।

মাঝামাঝি পথও আছে: নির্দিষ্ট দিনের পর বার্তা নিজে থেকে মুছে যাওয়া (disappearing messages), বা শুধু ইতিহাসের encrypted backup রাখা, যার চাবি ব্যবহারকারীর হাতে।

ইন্টারভিউতে এটিই প্রথম দিকের প্রশ্ন হওয়া উচিত, কারণ এর উত্তর storage-এর হিসাব, data model আর E2EE-র নকশা — তিনটিই বদলে দেয়।

মূল উপলব্ধি: “কতদিন রাখব” প্রশ্নটি storage-এর নয়, পণ্য আর গোপনীয়তার। আগে সেটি ঠিক করুন, তারপর terabyte গুনুন।

প্র ০৩একটি সার্ভার যদি ২০ লাখ সংযোগ ধরতে পারে, তাহলে gateway-তে ১ লাখের সীমা কেন? এটি কি অপচয় নয়?

এক যন্ত্রে বিপুল সংযোগ সম্ভব — এটি পুরোনো প্রমাণিত কথা। ১৯৯৯ সালে Dan Kegel “C10K” প্রশ্ন তোলেন: এক সার্ভারে দশ হাজার সংযোগ। Event-driven I/O-র কল্যাণে সেই সীমা বহু আগেই পেরোনো গেছে (Computer Networks · event-driven server). WhatsApp-এর ২০১২ সালের একটি প্রকাশিত blog post অনুযায়ী তাঁরা এক FreeBSD সার্ভারে ২০ লাখের বেশি TCP সংযোগ ধরেছিলেন। প্রশ্নটি তাই “পারে কি না” নয়, “উচিত কি না”.

Blast radius: ২০ লাখ সংযোগের একটি gateway মরলে ২০ লাখ client একসাথে ফিরবে। অংশ ৬-এর হিসাবে ন্যূনতম ছড়ানোর জানালা = client ÷ (বেঁচে থাকা gateway × প্রতি সেকেন্ডের handshake). ৫০ হাজারের জায়গায় ২০ লাখ হলে জানালা ৪০ গুণ লম্বা — ৯টি gateway, সেকেন্ডে ২,০০০ ধরলে প্রায় ১১১ সেকেন্ড, প্রায় দুই মিনিট ধরে লাখো মানুষের “সংযোগ হচ্ছে…”.

Deploy: নতুন সংস্করণ চালাতে প্রতিটি gateway drain করতে হয়। বড় gateway-তে drain ধীর, আর প্রতিটি deploy নিজেই একটি ছোট reconnect storm.

ভারসাম্য আর অন্য সীমা: দীর্ঘজীবী সংযোগ সহজে সরে না, তাই নতুন gateway যোগ করলে ভার ধীরে আসে। RAM ছাড়াও TLS-এর CPU, kernel-এর buffer আর fan-out-এর ভার আছে — বড় gateway-তে একটি জিনিস গরম হলে সবাই ভোগে।

খরচের হিসাব: ছোট gateway মানে বেশি যন্ত্র, কিন্তু প্রতিটি সস্তা। ১৪০টি বনাম ৭টি বড় যন্ত্রের খরচের পার্থক্য সাধারণত reconnect storm-এর বিভ্রাটের দামের তুলনায় ছোট। সঠিক সংখ্যা ঠিক হয় load test আর failure-এর মহড়ায়, অনুমানে নয়।

মূল উপলব্ধি: ক্ষমতার সীমা নির্ধারণ করে শুধু যন্ত্র নয়, ব্যর্থতার মাপও। “একটি গেলে কতজন ভোগেন” — এই প্রশ্নের উত্তর দিয়েই সীমা টানুন।

প্র ০৪End-to-end encryption থাকলে server বার্তা পড়তে পারে না। তাহলে spam, প্রতারণা আর ক্ষতিকর বিষয়বস্তু ঠেকানো হবে কীভাবে?

এটি প্রযুক্তি আর নীতির একটি সত্যিকারের টানাপোড়েন; সহজ উত্তর নেই। প্রতিরক্ষার দিক থেকে কয়েকটি স্তর আছে।

১. Metadata আর আচরণের সংকেত: server জানে কে কাকে, কত দ্রুত, কতজন অচেনা মানুষকে বার্তা পাঠাচ্ছে। নতুন অ্যাকাউন্ট থেকে এক ঘণ্টায় শত শত অচেনা নম্বরে বার্তা — বিষয়বস্তু না পড়েও এটি সন্দেহজনক। Rate limit, অ্যাকাউন্টের বয়স আর block-এর হার মিলিয়ে ঝুঁকি মাপা যায়।

২. ব্যবহারকারীর report: প্রাপক report করলে তাঁর ডিভাইস নিজেই সাম্প্রতিক বার্তাগুলো পাঠায় — প্রাপকের কাছে plaintext আছেই। প্রশ্ন ওঠে: কেউ বানানো বার্তা দিয়ে মিথ্যা report করলে? এর জন্য “message franking” নামের কৌশল প্রকাশিত হয়েছে — Facebook Messenger-এর secret conversation-এর প্রকাশিত whitepaper (২০১৬) আর পরে একাডেমিক গবেষণা (CRYPTO ২০১৭) — যাতে server যাচাই করতে পারে যে report করা বার্তা সত্যিই সেই প্রেরক পাঠিয়েছিলেন, অথচ আগে পড়তে পারেনি।

৩. ছড়ানোর গতি কমানো: গুজব আর প্রতারণা ছড়ায় forward-এর শিকলে। একটি বড় messaging অ্যাপ ২০১৮ সাল থেকে প্রকাশ্যে forward-এর সংখ্যা সীমিত করেছে আর “অনেকবার forward করা” চিহ্ন দেখায় — বিষয়বস্তু না পড়েই গতি কমানো।

৪. ডিভাইসে যাচাই: ফোনে বিষয়বস্তু স্ক্যান করার প্রস্তাবও এসেছে, কিন্তু এটি বিতর্কিত — Apple ২০২১ সালে এমন একটি পরিকল্পনা ঘোষণা করে, সমালোচনার পর ২০২২ সালে বাদ দেয়। গোপনীয়তার গবেষকদের যুক্তি: ফোনের ভেতরে স্ক্যানার বসানো মানে encryption-এর প্রতিশ্রুতি দুর্বল করা।

“আলাপ”-এর নকশায় তাই: কড়া rate limit, অচেনা প্রেরকের বার্তা আলাদা “অনুরোধ” তালিকায়, সহজ report আর block, forward-সীমা, আর OTP দিয়ে নম্বর যাচাই।

মূল উপলব্ধি: E2EE moderation-কে অসম্ভব করে না, তার ভিত্তি বদলায় — বিষয়বস্তু থেকে আচরণ, report আর ছড়ানোর গতিতে।

প্র ০৫একটি বড় chat সেবা message store বদলেছে — প্রকাশিত অভিজ্ঞতা থেকে আমাদের নকশা কী শিখতে পারে?

Discord-এর প্রকাশিত engineering blog-এ দুটি লেখা প্রায়ই উদ্ধৃত হয়। ২০১৭ সালের লেখা অনুযায়ী তাঁরা বার্তা রাখতে Cassandra বেছে নেন, আর partition key বানান channel আর সময়ের bucket মিলিয়ে — ঠিক আমাদের (conv_id, bucket)-এর মতো (পর্ব ২৩)। ২০২৩ সালের লেখা অনুযায়ী বার্তা বহুগুণ বাড়ার পর তাঁরা ScyllaDB-তে সরে যান। নিচের শিক্ষাগুলো তাঁদের প্রকাশিত বর্ণনা থেকে নেওয়া সাধারণ শিক্ষা, ভেতরের সম্পূর্ণ নকশা নয়।

১. Hot partition আসল শত্রু: খুব ব্যস্ত একটি channel-এর partition-এ একসাথে বিপুল পড়া এলে সেই node ধীর হয়, আর তার সাথে যুক্ত সবার latency বাড়ে। আমাদের ৫০০ সদস্যের সীমা এই ঝুঁকি অনেকটা কমায়, কিন্তু একটি ভাইরাল group-ও গরম হতে পারে (পর্ব ২৩-এর hot spot).

২. Store-এর সামনে একটি পাতলা data service: প্রকাশিত বর্ণনা অনুযায়ী তাঁরা database-এর সামনে একটি স্তর বসান, যা একই partition-এর একসাথে আসা অনেক পড়া মিলিয়ে একটিতে পাঠায় — request coalescing (পর্ব ২৯)। একটি জনপ্রিয় বার্তায় হাজারজন একসাথে চাপ দিলে database এক পড়াই দেখে।

৩. Storage engine-এর রক্ষণাবেক্ষণ: LSM-ভিত্তিক store-এ compaction আর মুছে ফেলা ডেটার চিহ্ন (tombstone) ধীরে ধীরে latency বাড়ায় (পর্ব ১৯)। Retention নীতি আর bucket-এর আকার এর ওপর প্রভাব ফেলে।

৪. বদল সম্ভব, কারণ সীমানা পরিষ্কার: সব service একটি data service-এর মাধ্যমে store-এ যায়, তাই store বদলানো একটি নিয়ন্ত্রিত migration — dual write আর backfill (পর্ব ২৩)।

আমাদের নকশায় তাই: message store-এর সামনে একটি সরু interface, ব্যস্ত partition-এর metric আর alert, আর bucket-এর আকার বাস্তব ডেটায় মেপে ঠিক করা।

মূল উপলব্ধি: প্রথম দিনের database পছন্দ চিরকালের নয়। যে নকশা store-কে একটি পরিষ্কার সীমানার পেছনে রাখে, সে বৃদ্ধির সাথে নিরাপদে বদলাতে পারে।

অনুশীলন

খাতা-কলম নিয়ে বসুন — হিসাব নিজে করুন, তারপর উত্তর মেলান।

  1. হিসাব — অন্য একটি বাজার: ধরা যাক DAU ২ কোটি, প্রত্যেকে দিনে ৬০টি বার্তা, peak factor ৫; ৪০% বার্তা group-এ, সেখানে গড়ে ৩০ জন অন্য সদস্য; প্রাপকপিছু ১.৩টি ডিভাইস; peak-এ ২৫% DAU সংযুক্ত; প্রতি gateway-তে ৫০ হাজার সংযোগের সীমা, তিন zone. বার্তা ২০০ byte, তিন কপি; ৫% বার্তায় ২০০ KB media. বের করুন: peak লেখা/সেকেন্ড, peak ডিভাইস-কপি/সেকেন্ড, সংযোগ, zone-নিরাপদ gateway, আর বছরের text ও media storage.

    দিনে ২ কোটি × ৬০ = ১২০ কোটি বার্তা; গড়ে ১২০ কোটি ÷ ৮৬,৪০০ ≈ ১৩,৮৮৯/s; peak ×৫ = ৬৯,৪৪৪/s.

    প্রাপক = ০.৬ × ১ + ০.৪ × ৩০ = ১২.৬; ডিভাইস-কপি = ৬৯,৪৪৪ × ১২.৬ × ১.৩ ≈ ১১,৩৭,৫০০/s — DAU কম হলেও বড় group-এর কারণে অংশ ২-এর ১৩.৩ লাখের কাছাকাছি। (Receipt-এর ঊর্ধ্বসীমা ৬৯,৪৪৪ × ১২.৬ × ২ = ১৭,৫০,০০০/s.)

    সংযোগ = ২ কোটি × ০.২৫ × ১.৩ = ৬৫ লাখ; gateway = ⌈৬৫ লাখ ÷ ৫০ হাজার⌉ = ১৩০, zone-নিরাপদ ⌈১৩০ ÷ ২⌉ × ৩ = ১৯৫.

    Text = ১২০ কোটি × ২০০ B × ৩৬৫ × ৩ ≈ ২৬২.৮ TB; media = ১২০ কোটি × ০.০৫ × ২০০ KB × ৩৬৫ ≈ ৪.৩৮ PB. অংশ ২-এর estimator-এ এই মানগুলো বসিয়ে চালালে হুবহু এই সংখ্যা আসে।

  2. Requirement বদলান — ১ লাখ সদস্যের channel: পণ্য দল চায় group-এর সীমা ৫০০ থেকে বাড়িয়ে ১ লাখ, যেখানে শুধু কয়েকজন admin লেখেন। নকশার কোন অংশ বদলাবেন?
    • Fan-out: প্রতিটি পোস্টে ১ লাখ socket-লেখা আর push — একটি পোস্টই একটি ছোট storm. সমাধান: অনলাইন দর্শকদের কাছে ধীরে ধীরে ছড়ানো, অফলাইনদের push না দিয়ে অ্যাপ খুললে টেনে আনা (fan-out on read, পর্ব ৫৬), আর পোস্ট CDN-যোগ্য করা।
    • Receipt আর presence: প্রতি সদস্যের read_upto রাখা অর্থহীন আর দামি; শুধু মোট দর্শকসংখ্যা, আনুমানিক হলেও চলে (HyperLogLog, পর্ব ৪৫)। Presence বাদ।
    • লেখার অধিকার: শুধু admin লেখেন, তাই আলাদা permission আর admin-এর rate limit.
    • E2EE: লাখো সদস্যে চাবি বদলানো দামি; অনেক নকশায় broadcast channel E2EE নয় — পণ্য দলকে এই trade-off স্পষ্ট জানাতে হবে।
    • Partition: পড়া অনেক বেশি, তাই হালনাগাদ পোস্ট cache-এ; hot partition-এর জন্য request coalescing.

    সারকথা: এটি আর chat নয়, আচরণে news feed — তাই আলাদা পণ্য-ধরন হিসেবে নকশা করাই ঠিক।

  3. Critique — এই নকশায় কী ভুল? একজন সহকর্মী লিখেছেন: “প্রতিটি gateway একটি Redis Pub/Sub channel-এ subscribe করে। বার্তা এলে আগে প্রাপকের gateway-তে publish করি, তারপর ডেটাবেসে লিখি, তারপর প্রেরককে ack দিই। ফোন বার্তা সাজায় নিজের ঘড়ির timestamp দিয়ে। প্রাপক অফলাইন হলে বার্তা বাদ — অ্যাপ খুললে আবার পুরো ইতিহাস টানে। Presence-এর প্রতিটি বদল সব contact-কে যায়।”
    • Pub/sub-কে সত্যের উৎস ধরা: Redis Pub/Sub ডকুমেন্টেশন অনুযায়ী at-most-once; subscriber সেই মুহূর্তে না থাকলে বার্তা হারায়। আগে টেকসই লেখা, pub/sub শুধু দ্রুত পথ।
    • লেখার আগে পৌঁছানো: ডেটাবেসে লেখা ব্যর্থ হলে প্রাপক এমন বার্তা দেখবেন, যা ইতিহাসে নেই; প্রেরক retry করলে duplicate. ক্রম হোক: dedup → seq → টেকসই লেখা → ack → fan-out.
    • Client id নেই: ack হারালে retry-তে duplicate.
    • ঘড়ি দিয়ে ক্রম: ভুল ঘড়িতে উত্তর প্রশ্নের আগে বসে; ভিন্ন ডিভাইসে ভিন্ন ক্রম। Server-এর per-conversation seq লাগবে।
    • অফলাইনে বাদ, তারপর পুরো ইতিহাস: বার্তা জমা না থাকায় ভরসা শুধু DB; প্রতি reconnect-এ পুরো ইতিহাস টানলে reconnect storm সরাসরি database-এ পৌঁছায়। চাই push আর per-device cursor থেকে sync.
    • Presence broadcast: অংশ ৮-এর হিসাবে বার্তার সমান খরচ; lazy presence আর debounce.
  4. আউটপুট অনুমান করুন: অংশ ৬-এর reconnect storm simulator-এ শেষ সারির প্রথম অপেক্ষা r() * 5 বদলে r() * 2 করা হলো (০–২ সেকেন্ডে ছড়ানো; সারির নামও বদলে "spread 0-2 s + full jitter" লেখা হলো), বাকি সব একই। (ক) শেষ সারিতে refused কি এখনো 0 থাকবে? কারণ বলুন। (খ) আগের কোডে ফিরে, এবার শুধু GW = 9 বদলে GW = 4 করলে (মাত্র চারটি বেঁচে থাকা gateway) কোন কৌশলে ৯৯% ফিরতে সবচেয়ে বেশি সময় লাগবে বলে মনে করেন? চালিয়ে মিলিয়ে দেখুন।

    (ক) না। ন্যূনতম জানালা ৫০,০০০ ÷ (৯ × ২,০০০) ≈ ২.৮ সেকেন্ড; ২ সেকেন্ডে প্রতি gateway-তে গড়েই সেকেন্ডে প্রায় ২,৭৭৮টি চেষ্টা আসে, সামর্থ্যের বেশি। চালালে শেষ সারি:

    spread 0-2 s + full jitter    3706    24220      4 s

    Peak 3706 — প্রথম সেকেন্ডের নতুন চেষ্টার ওপর আগের সেকেন্ডের প্রত্যাখ্যাতদের retry জমেছে। প্রত্যাখ্যান 24220, তবু প্রথম তিন সারির প্রায় অর্ধেক।

    (খ) jitter-হীন exponential backoff. চারটি gateway-তে প্রথম ঢেউ প্রতি gateway-তে ১২ হাজারের বেশি, সামর্থ্যের ছয় গুণ; প্রত্যাখ্যানের খরচ সামর্থ্য আরও কমায়। Jitter না থাকায় প্রত্যাখ্যাতরা একসাথে ১, ৩, ৭, ১৫ … সেকেন্ড পরে ফেরে, প্রতিবার আবার একই ভিড়:

    strategy (cap 2000/s/gw)    peak  refused  99% back
    retry every 1 s              12576   245799     10 s
    exp backoff, no jitter       12576   245799    160 s
    full jitter                  12576   123906     12 s
    spread 0-5 s + full jitter    4103    30490     10 s

    শিক্ষা: jitter ছাড়া backoff ভিড় কমায় না, শুধু ভিড়ের মাঝের ফাঁক বাড়ায়। আর ছোট fleet-এ ০–৫ সেকেন্ডের ছড়ানোও যথেষ্ট নয় — জানালা হওয়া উচিত অন্তত ৫০,০০০ ÷ (৪ × ২,০০০) ≈ ৬.৩ সেকেন্ড।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি chat বার্তা পাঠানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📝 ফোন বার্তাটি client id-সহ local outbox-এ রাখে
  2. 🚪 WebSocket-এ gateway হয়ে কথোপকথনের মালিকের কাছে পৌঁছায়
  3. 🔢 Client id দেখে dedup, তারপর পরের seq দেওয়া
  4. 🗄️ Replica-সহ টেকসই লেখা, তারপর প্রেরককে ack
  5. 📇 Registry দেখে অনলাইন ডিভাইসে পাঠানো, অফলাইনদের push
  6. ✓✓ প্রাপক-ডিভাইসের delivered ack, cursor এগোয়

একটি chat অ্যাপে প্রতিটি বার্তায় ফোন নিজের বানানো একটি অনন্য id (client id) জুড়ে দেয়। এর প্রধান উদ্দেশ্য কী?

  • বার্তাগুলোর ক্রম ঠিক করা
  • Retry-কে idempotent করা, যাতে server-এ duplicate না হয়
  • বার্তা encrypt করার চাবি বানানো
  • প্রাপকের gateway খুঁজে বের করা

একটি group chat-এ সব সদস্য একই ক্রমে বার্তা দেখুক — এর জন্য সবচেয়ে উপযুক্ত পদ্ধতি কোনটি?

  • প্রেরকের ফোনের timestamp দিয়ে সাজানো
  • সব কথোপকথনের জন্য একটি global auto-increment counter
  • প্রতিটি কথোপকথনে একক লেখকের দেওয়া sequence number
  • যে ক্রমে বার্তা ডিভাইসে পৌঁছায়, সেই ক্রমে দেখানো

একটি gateway-তে ৫০,০০০ সংযোগ ছিল, সেটি মরে গেল। বাকি ৯টি gateway-র প্রতিটি সেকেন্ডে ২,০০০টি নতুন handshake সামলাতে পারে। Client-রা প্রথম চেষ্টার আগে এলোমেলো অপেক্ষা করলে প্রত্যাখ্যান এড়াতে সেই অপেক্ষার জানালা অন্তত কত হওয়া উচিত?

  • প্রায় ২.৮ সেকেন্ড
  • প্রায় ০.৩ সেকেন্ড
  • প্রায় ২৫ সেকেন্ড
  • কোনো অপেক্ষা লাগবে না, retry-তে jitter থাকলেই যথেষ্ট

একজন ব্যবহারকারীর ফোন আর ল্যাপটপ — দুটি ডিভাইস। ফোন অফলাইন থাকার পর ফিরে এলে কোন তথ্যের ভিত্তিতে শুধু না-পাওয়া বার্তাগুলো আনা হয়?

  • ব্যবহারকারীর read receipt
  • শেষ বার্তার timestamp
  • প্রতিবার পুরো ইতিহাস এনে মিলিয়ে দেখা
  • ওই ডিভাইসের প্রতিটি কথোপকথনের cursor (শেষ পাওয়া seq)

একটি chat অ্যাপে presence-এর প্রতিটি বদল প্রত্যেক অনলাইন contact-কে পাঠানো হয়। খরচ কমানোর সবচেয়ে কার্যকর জোড়া কোনটি?

  • Heartbeat আরও ঘন করা
  • Subscribe-on-view (lazy presence) আর debounce
  • প্রতিটি presence event-কে exactly-once করা
  • Presence-এর বদল message store-এ লিখে রাখা

একটি chat অ্যাপে end-to-end encryption চালু। নিচের কোন দাবিটি এর সাথে খাপ খায় না?

  • Server দেখে কে কাকে কখন বার্তা পাঠাল
  • প্রাপক report করলে ডিভাইস বার্তাটি পাঠায়
  • Server-এ সব বার্তার full-text search index
  • Object storage-এ ছবি থাকে encrypted অবস্থায়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. সংখ্যা নকশা ঠিক করে: লেখার চেয়ে ডিভাইস-কপি ১০ গুণের বেশি, receipt আরও বেশি; gateway গোনা হয় সংযোগে; storage-এর বড় খাত media.
  2. Client id + per-conversation seq: retry নিরাপদ, ক্রম এক; একক লেখক টেকসই লেখার পরেই ack দেয়, মালিক বদলে fencing.
  3. একবার লেখা, cursor থেকে sync: conversation log-এ এক কপি; অনলাইনদের gateway ধরে batch, অফলাইনদের push (পর্ব ৫৭), প্রতিটি ডিভাইসের নিজের cursor; receipt হলো “কোন seq পর্যন্ত”.
  4. সংযোগ ভাঙবেই: registry TTL-সহ, heartbeat মাপা, gateway-র সীমা blast radius দেখে; প্রথম reconnect-ও ছড়ানো — ন্যূনতম জানালা client ÷ (gateway × handshake/s).
  5. Presence lazy, E2EE সৎ: শুধু দর্শককে আর debounce করে জানানো; E2EE-তে search ফোনে, moderation আচরণ আর report-এ।

পরের পর্বে: পর্ব ৫৬ — Case study: news feed. Chat-এ একটি log সবাই পড়েন; feed-এ প্রত্যেক পাঠকের আলাদা, সাজানো তালিকা। Fan-out on write না read, জনপ্রিয় লেখকের সমস্যা আর ranking — সেখানে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান