সরাসরি পর্বে যান
পর্ব ১০ · ৬৬-এর মধ্যে · মডিউল ২

Real-time যোগাযোগ — polling, long polling, SSE আর WebSocket

Real-time Communication — Short & Long Polling, Server-Sent Events, WebSocket & Scaling Persistent Connections

Taught in Bangla, this lesson fills HTTP's push gap with short polling, long polling, Server-Sent Events and WebSocket, plus WebRTC data channels and platform push. It derives polling cost and delay, resumes SSE streams with Last-Event-ID, sizes gateways for hundreds of thousands of persistent connections, routes messages across gateways with pub/sub and presence, and handles ordering, delivery guarantees, reconnect storms and mobile realities, using runnable simulators.

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

  • কোন অ্যাপে “push” লাগে, আর short polling, long polling, SSE, WebSocket, WebRTC data channel ও platform push-এর মধ্যে কোনটি কখন
  • Polling-এর অনুরোধ, খালি উত্তর আর দেরির হিসাব — Poisson অনুমানে সূত্র আর প্রমাণসহ
  • SSE-র ঘটনা-স্রোত, Last-Event-ID দিয়ে resume, আর WebSocket-এর upgrade, frame, ping/pong ও subprotocol-এর ধারণা
  • লাখো স্থায়ী সংযোগের capacity হিসাব, load balancer, sticky routing, gateway স্তর, pub/sub, presence আর ক্রম ও delivery-র নিশ্চয়তা
  • Reconnect storm, fallback, মোবাইলের ব্যাটারি ও NAT-এর বাস্তবতা, আর AI-এর দেওয়া real-time নকশা যাচাইয়ের উপায়

১ · সমস্যা — সার্ভার নিজে থেকে কথা বলতে পারে না

রাত ৮টা, একটি কাল্পনিক ক্রিকেট-স্কোর অ্যাপে শেষ ওভার — কয়েক লাখ মানুষ প্রতিটি বলের ফল দেখতে চান প্রায় সাথে সাথে। একই সময়ে চ্যাটে কেউ লিখলেন “রওনা দিয়েছি”, একটি ride-sharing অ্যাপে (কাল্পনিক) যাত্রী দেখছেন চালকের গাড়ি এগোচ্ছে, কারও ফোনে এলো “পার্সেল পৌঁছে গেছে”, আর শেয়ারবাজারে দাম সেকেন্ডে কয়েকবার বদলাচ্ছে। মিল একটাই: নতুন খবর তৈরি হয় সার্ভারে, আর সেটি ব্যবহারকারীর কাছে যেতে হবে তিনি না চাইতেই।

সমস্যা হলো, ওয়েবের মূল protocol HTTP (পর্ব ৭) একটি অনুরোধ-উত্তরের চুক্তি: client প্রশ্ন করে, সার্ভার উত্তর দেয় — তারপর চুপ। সার্ভারের হাতে নতুন খবর এলেও সে নিজে থেকে client-কে ডাকতে পারে না, কারণ client তখন কোনো প্রশ্নই করছে না। একে বলে pull মডেল — client টেনে আনে। Real-time অ্যাপ চায় push — সার্ভার খবর ঠেলে দেবে। এই পর্বের পুরো আলোচনা এই একটি ফাঁক পূরণের নানা কৌশল আর তাদের দাম নিয়ে।

ফেরিঘাটে একজন ট্রাকচালক সিরিয়ালের অপেক্ষায়। (১) প্রতি পাঁচ মিনিটে টিকিট ঘরে গিয়ে জিজ্ঞেস করা, “আমার সিরিয়াল এলো?” — বেশিরভাগ উত্তর “না”; এটি short polling. (২) কাউন্টারের সামনে দাঁড়িয়ে থাকা, ডাক এলেই কর্মী বলবেন, আধা ঘণ্টায় কিছু না হলে নতুন করে দাঁড়ানো — long polling. (৩) ঘাটের মাইকে ঘোষণা শোনা, খবর আসে শুধু এক দিকে — Server-Sent Events. (৪) কর্মীর সাথে ওয়াকিটকি, দুজনেই যখন খুশি বলতে পারেন — WebSocket. আর চালক দূরের চায়ের দোকানে থাকলে ফোনে SMS — platform push.

“Real-time” কতটা real?

এখানে real-time মানে গাড়ির ব্রেক কন্ট্রোলারের মতো কঠোর সময়সীমা নয়, মানুষের চোখে “সঙ্গে সঙ্গে”. Jakob Nielsen-এর বহুল-উদ্ধৃত হিসাবে (১৯৯৩) প্রায় ০.১ সেকেন্ডের সাড়া তাৎক্ষণিক মনে হয়, আর ১ সেকেন্ড পর্যন্ত চিন্তার ধারা ভাঙে না। তবে প্রতিটি কাজের প্রয়োজন আলাদা:

কাজখবরের দিককতটা টাটকা লাগবেবিশেষ চাহিদা
চ্যাট বার্তাদুই দিক১ সেকেন্ডের নিচেক্রম, একটিও না হারানো
লাইভ স্কোরসার্ভার → বহু পাঠককয়েক সেকেন্ড চলেসবাই একই ডেটা পান
চালকের অবস্থানচালক → সার্ভার → যাত্রীপ্রতি কয়েক সেকেন্ডেশুধু সর্বশেষটি জরুরি
বিজ্ঞপ্তিসার্ভার → একজনসেকেন্ড থেকে মিনিটঅ্যাপ বন্ধ থাকলেও পৌঁছাতে হয়
দামের টিকসার্ভার → বহুসেকেন্ডের ভগ্নাংশমাঝের মান বাদ দেওয়া চলে
দুটি মাপে প্রতিটি কৌশলের বিচার

টাটকা ভাব — খবর তৈরি থেকে পর্দায় পৌঁছানোর দেরি, অর্থাৎ latency; আর খরচ — অনুরোধ/সেকেন্ড, খোলা সংযোগ, memory, ফোনের ব্যাটারি আর ডেটা। একটি কমালে অন্যটি প্রায়ই বাড়ে — এটিই এই পর্বের মূল trade-off.

২ · Short polling — নির্দিষ্ট বিরতিতে “নতুন কিছু আছে?”

সবচেয়ে সরল সমাধান: client প্রতি I সেকেন্ডে একটি সাধারণ HTTP অনুরোধ পাঠায় — যেমন GET /v1/updates?since=41 — আর সার্ভার সঙ্গে সঙ্গে উত্তর দেয়: নতুন কিছু থাকলে সেগুলো, না থাকলে খালি তালিকা। since=41 একটি cursor — “৪১ নম্বরের পরের খবরগুলো দিন”; এতে কোনো খবর বাদ পড়ে না, দুবারও আসে না (cursor-এর ধারণা পর্ব ৮-এর pagination-এ)। এটিই short polling. প্রতিটি অনুরোধ স্বাধীন, তাই stateless সার্ভার, load balancer আর CDN-এর cache — সবই কাজে লাগে।

দেরি আর খরচের হিসাব

ধরা যাক N জন client অনলাইন, প্রত্যেকে প্রতি I সেকেন্ডে poll করে, আর প্রত্যেকের জন্য গড়ে প্রতি সেকেন্ডে λ টি নতুন খবর আসে — এলোমেলো সময়ে, একটি আরেকটির ওপর নির্ভর না করে (Poisson প্রক্রিয়া)। তিনটি সূত্র:

অনুরোধ/সেকেন্ড = N ÷ I
গড় দেরি ≈ I ÷ 2 (+ নেটওয়ার্কের সময়)
একটি poll খালি ফেরার সম্ভাবনা = e−λI
সূত্রের যুক্তি

খবর দুটি poll-এর মাঝের যেকোনো মুহূর্তে সমান সম্ভাবনায় আসে, তাই পরের poll পর্যন্ত অপেক্ষা ০ থেকে I-এর মধ্যে সমভাবে ছড়ানো — গড় I/২, সর্বোচ্চ I. খালি উত্তর মানে এক বিরতিতে একটি খবরও না আসা; Poisson প্রক্রিয়ায় I সময়ে ঠিক k টি ঘটনার সম্ভাবনা (λI)ke−λI/k!, তাই k = ০ বসালে e−λI. এবার প্রথম দুটি সূত্র গুণ করুন: গড় দেরি × অনুরোধ/সেকেন্ড = (I/২)(N/I) = N/২ — I যা-ই হোক, স্থির। দেরি অর্ধেক করতে অনুরোধ দ্বিগুণ করতেই হবে; short polling-এ এই দেয়াল ভাঙা যায় না।

নিচের ক্যালকুলেটরে ১ লাখ client, প্রত্যেকের জন্য গড়ে মিনিটে একটি খবর (λ = ১/৬০) — সব ধরা যাক; নিজের মান বসিয়ে চালান। Long polling-এর সূত্র আসছে অংশ ৩-এ।

// Short polling vs long polling vs SSE/WebSocket for N online clients.
// ধরা যাক (assumed): updates reach each client at random (Poisson),
// on average 1 per 60 s; network one-way delay 0.1 s; long-poll timeout 30 s.
const N = 100000;            // online clients
const rate = 1 / 60;         // updates per second, per client
const oneWay = 0.1;          // seconds
const n = (x) => Math.round(x).toLocaleString("en-US");
const row = (name, req, empty, delay) => console.log(name.padEnd(17) +
  n(req).padStart(9) + n(empty).padStart(9) +
  ((req ? 100 * empty / req : 0).toFixed(1) + "%").padStart(8) +
  (delay.toFixed(1) + " s").padStart(11));
console.log("method               req/s  empty/s  empty%  avg delay");
for (const I of [1, 5, 30]) {                    // poll every I seconds
  const req = N / I;
  const pEmpty = Math.exp(-rate * I);            // no update in one interval
  row("short poll " + I + " s", req, req * pEmpty, I / 2 + oneWay);
}
const T = 30;                                    // long-poll timeout
const pTimeout = Math.exp(-rate * T);            // held request ends empty
const lpReq = N * rate / (1 - pTimeout);         // one request per cycle
row("long poll " + T + " s", lpReq, lpReq * pTimeout, oneWay);
row("SSE / WebSocket", 0, 0, oneWay);
console.log("push: " + n(N * rate) + " messages/s; keep-alive ping every 30 s = " +
  n(N / 30) + " tiny frames/s; open connections = " + n(N));

আউটপুট:

method               req/s  empty/s  empty%  avg delay
short poll 1 s     100,000   98,347   98.3%      0.6 s
short poll 5 s      20,000   18,401   92.0%      2.6 s
short poll 30 s      3,333    2,022   60.7%     15.1 s
long poll 30 s       4,236    2,569   60.7%      0.1 s
SSE / WebSocket          0        0    0.0%      0.1 s
push: 1,667 messages/s; keep-alive ping every 30 s = 3,333 tiny frames/s; open connections = 100,000

১ সেকেন্ডের poll-এ সার্ভার সেকেন্ডে 100,000 অনুরোধ সামলায়, যার 98,347টি (৯৮.৩%) শুধু জানায় “নতুন কিছু নেই” — তবু গড় দেরি ০.৬ সেকেন্ড। ৩০ সেকেন্ডে অনুরোধ নামে 3,333-এ, কিন্তু দেরি ১৫ সেকেন্ড — চ্যাটের জন্য অচল। প্রতিটি সারিতে দেরি × অনুরোধ ≈ ৫০,০০০ = N/২ (নেটওয়ার্কের ০.১ বাদে)। Long polling (৩০ সেকেন্ড timeout) সেকেন্ডে 4,236 অনুরোধেই দেরি নামায় 0.1 s-এ — শুধু নেটওয়ার্কের সময়। খেয়াল করুন, ৩০ সেকেন্ডের short poll আর long poll-এর খালি অংশ একই (60.7%), কারণ দুটিতেই একই ৩০ সেকেন্ডের জানালা — e−০.৫; পার্থক্য দেরিতে। SSE বা WebSocket-এ সংযোগ খোলার পর অনুরোধই নেই: সেকেন্ডে 1,667টি আসল বার্তা আর ছোট keep-alive ping — দাম সবসময় খোলা 100,000 সংযোগ (অংশ ৬)। আর প্রতিটি poll-এর সাথে যায় পুরো HTTP header — cookie আর token মিলিয়ে প্রায়ই কয়েকশো বাইট থেকে কয়েক KB.

Short polling সব জায়গায় খারাপ নয়। (ক) খবর কম, দেরি সহ্য করা যায়। (খ) সবার জন্য একই ডেটা — যেমন লাইভ স্কোর: স্কোরের JSON CDN-এ কয়েক সেকেন্ডের TTL-এ cache করলে লাখো poll edge-এই উত্তর পায়, origin-এ যায় প্রতি edge থেকে প্রতি TTL-এ মোটামুটি একটি (পর্ব ১৭)। (গ) খুব কড়া নিয়ন্ত্রিত নেটওয়ার্ক।

৩ · Long polling — উত্তর আটকে রেখে অপেক্ষা

খালি উত্তর বাঁচানোর কৌশল: খবর না থাকলে সার্ভার অনুরোধটি ধরে রাখে, নতুন খবর এলেই সেই ঝুলন্ত অনুরোধের উত্তরে পাঠায়; client উত্তর পাওয়ামাত্র নতুন cursor দিয়ে আবার অনুরোধ পাঠায়। নির্দিষ্ট সময়ে (ধরা যাক ২৫–৩০ সেকেন্ড) কিছু না এলে সার্ভার খালি উত্তর দেয়, আর client আবার শুরু করে। এটিই long polling — দেখতে সাধারণ HTTP, আচরণে প্রায় push.

যা সামলাতে হয়

  • Timeout: পথের proxy বা load balancer নীরব অনুরোধ কেটে দেয় — অনেক সফটওয়্যারের ডিফল্ট idle timeout প্রায় ৬০ সেকেন্ড (সংস্করণভেদে আলাদা); long poll-এর timeout তার চেয়ে কম রাখা হয়।
  • মাঝের ফাঁক: উত্তর আর নতুন অনুরোধের মাঝে আসা খবর সার্ভার ছোট একটি সারিতে জমিয়ে রাখে; পরের অনুরোধের since দেখে সঙ্গে সঙ্গে দেয়।
  • খবরের ঝাঁক: এক সেকেন্ডে ২০টি বার্তা এলে ২০টি উত্তর-অনুরোধের চক্র; প্রতিকার batching — কয়েকশো millisecond জমিয়ে একসাথে পাঠানো।
  • ঝুলন্ত অনুরোধের ভার: Little-এর সূত্রে (পর্ব ৪) concurrency L = λW; প্রতিটি অনুরোধ অনেক সেকেন্ড বাঁচে বলে L ≈ N. প্রতি অনুরোধে একটি thread রাখা সার্ভার এতে ডুবে যায়; লাগে event-driven সার্ভার (CN কোর্সের event-driven সার্ভার পর্ব)।
  • অনুরোধের হার: একটি অনুরোধ শেষ হয় খবর এলে বা T সেকেন্ডে, যেটি আগে ঘটে। গড় আয়ু (১ − e−λT)/λ, তাই প্রতি client-এর অনুরোধ/সেকেন্ড = λ ÷ (১ − e−λT). খবর বিরল হলে এটি প্রায় ১/T — অনুরোধ T সেকেন্ডের short polling-এর সমান, কিন্তু দেরি প্রায় শূন্য।

এবার একটি বার্তা ধরে দুই পদ্ধতি পাশাপাশি দেখি। ব্যবহারকারী B-এর জন্য বার্তা #42 সার্ভারে আসে t = ৭.০ সেকেন্ডে; short polling প্রতি ৫ সেকেন্ডে, long polling-এর timeout ৩০ সেকেন্ড, আর নেটওয়ার্কের এক দিকের সময় ০.১ সেকেন্ড — সব ধরা যাক. “পরে” চাপুন।

ঘড়ি আর দুই client-এর খাতা
ঘড়ি
Short polling
Long polling
পাঠানো অনুরোধ (short │ long)
#42-এর দেরি
  1. t = ০: দুজনেই প্রথম অনুরোধ পাঠালেন। Short polling-এর অনুরোধ ০.১ সেকেন্ডে পৌঁছায়; নতুন কিছু নেই, তাই ০.২ সেকেন্ডে খালি উত্তর ফেরে। Long polling-এর অনুরোধের উত্তর সার্ভার দেয় না — ধরে রাখে।
  2. t = ৫ s: short poll ২ — আবার খালি। আরেকটি পূর্ণ HTTP অনুরোধ-উত্তর, ফল শূন্য। Long polling-এর একমাত্র অনুরোধটি তখনো ঝুলে আছে।
  3. t = ৭.০ s: #42 এলো। ঝুলন্ত অনুরোধ ছিল বলে সার্ভার সঙ্গে সঙ্গে উত্তর দিল; long polling-এর B পেলেন ৭.১ সেকেন্ডে — দেরি ০.১ সেকেন্ড — আর সঙ্গে সঙ্গে পাঠালেন অনুরোধ ২, since=42. Short polling-এর দিকে বার্তা সার্ভারে বসে আছে।
  4. t = ১০ s: short poll ৩ বার্তা পেল। অনুরোধ ১০.১ সেকেন্ডে পৌঁছে #42 নিয়ে ১০.২-এ ফিরল — দেরি ৩.২ সেকেন্ড: poll পৌঁছানো পর্যন্ত ৩.১ আর ফেরার ০.১. গড়ে এই অপেক্ষা I/২ = ২.৫ সেকেন্ড।
  5. t = ১৫ থেকে ৩৫ s: চুপচাপ সময়। Short polling আরও পাঁচবার (১৫, ২০, ২৫, ৩০, ৩৫ সেকেন্ডে) জিজ্ঞেস করে খালি হাতে ফিরল। Long polling-এর খরচ শুধু একটি খোলা সংযোগ।
  6. t = ৩৭.৩ s: long poll-এর timeout. অনুরোধ ২ পৌঁছেছিল ৭.২ সেকেন্ডে; ৩০ সেকেন্ড পূর্ণ হলো ৩৭.২-এ, খালি উত্তর (204) এলো ৩৭.৩-এ, আর client পাঠাল অনুরোধ ৩। খাতা: short polling ৮টি অনুরোধ, ৭টি খালি, দেরি ৩.২ সেকেন্ড; long polling ৩টি অনুরোধ, ১টি খালি, দেরি ০.১ সেকেন্ড।

২০০৬ সালে Alex Russell এই ধরনের push-কৌশলের নাম দেন Comet; SSE আর WebSocket এসেছে সেই অভিজ্ঞতা থেকে — ইতিহাস ভাবনার প্রশ্ন ১-এ।

৪ · Server-Sent Events — এক দিকের স্থায়ী স্রোত

যদি একটি উত্তর কখনো শেষই না হয়? Server-Sent Events (SSE)-এ client একটি সাধারণ GET পাঠায়, সার্ভার উত্তর দেয় Content-Type: text/event-stream দিয়ে, তারপর সংযোগ খোলা রেখে একের পর এক ঘটনা লিখতে থাকে। প্রতিটি ঘটনা কয়েকটি লাইনে, শেষে একটি ফাঁকা লাইন। এর নিয়ম এখন WHATWG-এর HTML Living Standard-এর অংশ।

GET /v1/match/4217/stream HTTP/1.1
Accept: text/event-stream

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache

retry: 3000

id: 101
event: score
data: {"ov":"12.3","s":"86/2"}

: ping

id: 102
event: score
data: {"ov":"12.4","s":"87/2"}

চারটি field: data: আসল লেখা, event: ঘটনার নাম (না দিলে message), id: পরিচয়, আর retry: — ভাঙলে কত millisecond পরে আবার চেষ্টা। : দিয়ে শুরু লাইন মন্তব্য — ব্রাউজার উপেক্ষা করে, কিন্তু proxy দেখে সংযোগ নীরব নয়; সস্তা heartbeat.

ব্রাউজারে EventSource নামের একটি built-in API সব সামলায়: new EventSource("/v1/match/4217/stream") লিখলে সংযোগ খোলে, প্রতিটি ঘটনায় একটি JavaScript event আসে, আর সংযোগ ভাঙলে কোনো কোড না লিখেই নিজে থেকে আবার জোড়ে। জোড়ার সময় সে শেষ পাওয়া id পাঠায় Last-Event-ID header-এ, আর সার্ভার সেখান থেকে বাকি ঘটনাগুলো আবার পাঠাতে পারে। এই স্বয়ংক্রিয় reconnect আর resume SSE-র সবচেয়ে বড় উপহার; WebSocket-এ এটি পুরোটাই নিজেকে বানাতে হয়। ধাপে ধাপে দেখুন:

ব্রাউজার আর সার্ভারের অবস্থা
সংযোগ
ব্রাউজারের lastEventId
সার্ভারের নতুন ঘটনা
পর্দার স্কোর
  1. সংযোগ খোলা। পাতা খুলতেই EventSource GET পাঠাল; সার্ভার text/event-stream দিয়ে উত্তর শুরু করল, কিন্তু শেষ করল না। প্রথমে retry: 3000, তারপর বর্তমান অবস্থা — ঘটনা ১০১: ১২.৩ ওভারে ৮৬/২।
  2. ঘটনা ১০২ এলো। একই খোলা উত্তরে নতুন কয়েকটি লাইন — নতুন অনুরোধ নেই। ব্রাউজার মনে রাখল শেষ id ১০২; : ping মন্তব্য proxy-কে বোঝায় সংযোগ জীবিত।
  3. সংযোগ ভাঙল। দর্শক বাসা থেকে বেরোলেন, ফোন Wi-Fi থেকে মোবাইল ডেটায় বদলাল — IP ঠিকানা বদল মানে পুরোনো TCP সংযোগ শেষ। এর মধ্যে মাঠে চার হলো (১০৩) আর উইকেট পড়ল (১০৪); সার্ভার পাঠাতে না পেরে সেগুলো সাম্প্রতিক ঘটনার buffer-এ রাখল। পর্দায় এখনো পুরোনো স্কোর।
  4. নিজে থেকেই ফেরা। অ্যাপের কোনো কোড ছাড়াই EventSource ৩ সেকেন্ড পর নতুন GET পাঠাল, সাথে header: Last-Event-ID: 102 — অর্থাৎ “আমি ১০২ পর্যন্ত পেয়েছি”.
  5. ফাঁক পূরণ। সার্ভার buffer থেকে ১০৩ আর ১০৪ পাঠাল — কোনোটি বাদ পড়েনি, দুবারও আসেনি; পর্দায় ৯১/৩, উইকেটের খবরসহ। Buffer-এ ১০৩ না থাকলে সার্ভার পুরো বর্তমান অবস্থা (snapshot) পাঠিয়ে নতুন করে শুরু করে।
  6. আবার live. ঘটনা ১০৫ সরাসরি এলো, lastEventId এখন ১০৫। অ্যাপ-লেখকের কাজ ছিল শুধু দুটি: প্রতিটি ঘটনায় id দেওয়া আর সার্ভারে সাম্প্রতিক ঘটনার buffer রাখা।

SSE-র সীমা। (১) একমুখী — client থেকে কিছু পাঠাতে আলাদা POST লাগে। (২) শুধু UTF-8 লেখা; binary পাঠাতে base64, যা প্রায় ৩৩% বড়। (৩) EventSource নিজের header (যেমন Authorization) বসাতে দেয় না — cookie বা স্বল্পমেয়াদি token লাগে; token URL-এ রাখলে log-এ জমার ঝুঁকি। (৪) HTTP/1.1-এ বেশিরভাগ ব্রাউজার একটি সাইটে মোটামুটি ৬টির বেশি সংযোগ একসাথে খোলে না, তাই কয়েকটি ট্যাবে SSE খুললেই অন্য অনুরোধ আটকায়; HTTP/2-র multiplexing-এ সমস্যাটি প্রায় মেটে (CN কোর্সের HTTP/2 পর্ব)। (৫) পথের proxy উত্তর জমিয়ে রাখলে স্রোত থেমে থেমে আসে, তাই অংশ ৬-এর config-এ buffering বন্ধ। (৬) সার্ভার 204 No Content দিলে EventSource আর reconnect করে না — ইচ্ছা করে থামানোর উপায় এটিই।

SSE মানায় যেখানে খবর মূলত এক দিকে যায়: লাইভ স্কোর, বিজ্ঞপ্তি, ড্যাশবোর্ড — আর AI chat-এর উত্তর শব্দে শব্দে ভেসে ওঠা; অনেক AI API-র অফিসিয়াল ডকুমেন্টেশনে streaming উত্তর SSE দিয়েই আসে।

৫ · WebSocket আর অন্য পথ — WebRTC, platform push আর তুলনা

চ্যাট, কুইজ বা একসাথে নথি সম্পাদনায় দুই দিকেই ঘনঘন ছোট বার্তা যায়। WebSocket (RFC 6455, ২০১১) একটি TCP সংযোগকে দুই দিকের স্থায়ী নালিতে বদলে দেয়। শুরু হয় সাধারণ HTTP/1.1 অনুরোধ হিসেবে, যাতে Upgrade: websocket header থাকে (HTTP Upgrade); সার্ভার রাজি হলে 101 Switching Protocols, আর তখন থেকে একই সংযোগে দুই পক্ষ যখন খুশি ছোট frame পাঠায় — full duplex. Frame-এ text বা binary, প্রতিটির header মাত্র ২–১৪ বাইট; ঠিকানা TLS-সহ wss://. Handshake-এর বাইট, frame আর masking আছে CN কোর্সের real-time ও streaming পর্বে; এখানে নকশার দিক।

WebSocket যা দেয় না

WebSocket শুধু নালি; ওপরের অ্যাপ-protocol আপনার:

  • Reconnect আর resume: সংযোগ ভাঙলে ব্রাউজার নিজে জোড়ে না; কোন বার্তা পর্যন্ত পাওয়া গেছে, কেউ মনে রাখে না।
  • বার্তার id আর ack: TCP বাইট পৌঁছায় অপর প্রান্তের kernel পর্যন্ত; অ্যাপ বার্তাটি সংরক্ষণ করল কি না, জানায় না। কোন উত্তর কোন প্রশ্নের, তাও id দিয়ে মেলাতে হয়।
  • Heartbeat: protocol-এ ping/pong control frame আছে, আর ব্রাউজার সার্ভারের ping-এ নিজেই pong দেয়; কিন্তু ব্রাউজারের JavaScript থেকে ping পাঠানো যায় না, তাই client-এর দিকে অ্যাপ-স্তরের heartbeat লাগে।
  • ধীর client: একটি client পড়তে না পারলে সার্ভারে তার জন্য জমা বাড়তেই থাকে; সীমা লাগে — উৎসকে ধীর করা (backpressure), নয়তো পুরোনো আপডেট বাদ দেওয়া বা সংযোগ কেটে দেওয়া।

Handshake-এর Sec-WebSocket-Protocol header-এ দুই পক্ষ একটি subprotocol-এ রাজি হতে পারে — যেমন mqtt, বা GraphQL subscription-এর graphql-transport-ws (পর্ব ৯) — তখন বার্তার নিয়ম আগেই ঠিক করা থাকে।

নিজের protocol-এ প্রতিটি বার্তা একটি ছোট “খাম”-এ যায়: cid (client-এর দেওয়া id) দিয়ে সার্ভার retry-র দ্বিতীয় কপি চেনে (idempotency), seq সার্ভারের দেওয়া ক্রম। JSON না Protobuf — পর্ব ১২-এ।

→ {"type":"send",      "conv":"c9", "cid":"a-77", "text":"আসছি"}
← {"type":"ack",       "cid":"a-77", "seq":58}
← {"type":"msg",       "conv":"c9", "seq":59, "from":"B", "text":"ঠিক আছে"}
→ {"type":"delivered", "conv":"c9", "seq":59}
→ {"type":"resume",    "conv":"c9", "after":57}
→ {"type":"ping"}                    ← {"type":"pong"}

আরও দুটি পথ — WebRTC data channel আর platform push

WebRTC-এর data channel দুই ব্রাউজার বা অ্যাপের মধ্যে সরাসরি (peer-to-peer) ডেটা পাঠায় — UDP-র ওপর, চাইলে ক্রমহীন বা হারানো-সহনীয় মোডে; দ্রুত খেলা বা সরাসরি ফাইল আদানপ্রদানে কাজের। কিন্তু পরিচয় করাতে আলাদা signaling সার্ভার (প্রায়ই WebSocket দিয়েই) আর NAT পার হতে STUN বা TURN লাগে (CN কোর্সের WebRTC অংশ); লাখো মানুষের কাছে ছড়ানোর জন্য এটি তৈরি নয়।

অ্যাপ বন্ধ বা background-এ থাকলে ফোনের OS অ্যাপের নিজের সংযোগ প্রায়ই থামিয়ে দেয়। তখন ভরসা platform push service — অপারেটিং সিস্টেমের নিজস্ব push সেবা (যেমন Apple-এর APNs বা Google-এর FCM; নিয়ম প্রদানকারীভেদে আলাদা); ব্রাউজারে একই কাজ করে Web Push (RFC 8030, ২০১৬)। OS সব অ্যাপের হয়ে একটি সংযোগ রাখে, তাই ব্যাটারি বাঁচে। শর্ত: নিশ্চয়তা নেই, দেরি হতে পারে, বার্তা মিশে যেতে পারে, আর payload ছোট — লেখার সময়ে মোটামুটি কয়েক KB. তাই এটি “ঘুম ভাঙানোর সংকেত”; আসল ডেটা অ্যাপ নিজের সার্ভার থেকে আনে — পুরো নকশা পর্ব ৫৭-এ।

পদ্ধতিদিকদেরিপ্রতি বার্তার overheadProxy ও firewallBrowser supportScaling-এর কষ্ট
Short pollingclient জিজ্ঞেস করে (pull)গড়ে I/২পুরো HTTP অনুরোধ, বেশিরভাগ খালিসবচেয়ে সহজসর্বত্রসহজ (stateless), কিন্তু অনুরোধ অনেক
Long pollingসার্ভার → client (ঘুরিয়ে)প্রায় নেটওয়ার্কের সময়; ঝাঁকে বেশিপ্রতি উত্তরে নতুন HTTP অনুরোধচলে; idle timeout সামলাতে হয়সর্বত্রমাঝারি: ঝুলন্ত অনুরোধ ≈ N
SSEশুধু সার্ভার → clientকমঘটনাপ্রতি কয়েক বাইটের fieldসাধারণ HTTP; buffering বন্ধ রাখতে হয়আধুনিক ব্রাউজারমাঝারি: স্থায়ী সংযোগ, HTTP-র যন্ত্র কাজে লাগে
WebSocketদুই দিক, যেকোনো সময়সবচেয়ে কম (TCP-র ওপর)frame header ২–১৪ বাইটwss:// সাধারণত চলে; কিছু পুরোনো proxy বাধা দেয়আধুনিক ব্রাউজারকঠিন: stateful, নিজস্ব protocol
WebRTC data channelpeer থেকে peer, দুই দিকখুব কমUDP; signaling আলাদাSTUN/TURN লাগেআধুনিক ব্রাউজারবহু-জনে ছড়ানোর জন্য নয়
Platform pushসার্ভার → ফোন (OS-এর মাধ্যমে)সেকেন্ড থেকে মিনিট; নিশ্চয়তা নেইছোট payloadOS নিজে সামলায়মোবাইল OS; ব্রাউজারে Web Pushসহজ; নিয়ন্ত্রণ প্রদানকারীর

📡 SSE

খবর শুধু সার্ভার থেকে, বহু পাঠক; সবার একই ডেটা হলে cache করা polling-ও।

স্কোর, বিজ্ঞপ্তি, AI-এর উত্তর

🔁 WebSocket

দুই দিকে ঘনঘন ছোট বার্তা, কম দেরি, প্রয়োজনে binary.

চ্যাট, সম্পাদনা, কুইজ

🎮 WebRTC

Peer-to-peer; ক্রমের চেয়ে গতি জরুরি।

ভয়েস-ভিডিও, দ্রুত খেলা

🔔 Platform push

অ্যাপ বন্ধ থাকলেও জানাতে হবে।

“নতুন বার্তা”

WebSocket আটকে গেলে — fallback

কিছু প্রতিষ্ঠানের proxy, firewall বা antivirus Upgrade header বোঝে না বা কেটে দেয়, বিশেষত এনক্রিপ্ট না করা ws://-এ। তাই বাস্তবে প্রায় সবসময় wss:// (port 443): মাঝপথের যন্ত্র ভেতরটা দেখতে না পেয়ে সাধারণ HTTPS ভেবে ছেড়ে দেয়, যদিও TLS খুলে পরীক্ষা করা proxy-তে তবু আটকাতে পারে। Handshake ব্যর্থ হলে বা খোলার পর বার্তা না এলে পরের পথে নামা — WebSocket → SSE (পাঠাতে POST) → long polling. লেখার সময়ের ডকুমেন্টেশন অনুযায়ী Microsoft-এর SignalR পরপর এই তিনটি চেষ্টা করে, আর Socket.IO শুরুতে long polling দিয়ে সংযোগ পাকা করে পরে WebSocket-এ ওঠে। দাম — সব পথে একই অ্যাপ-protocol আর resume-যুক্তি রাখতে হয়।

৬ · লাখো স্থায়ী সংযোগ — capacity, load balancer আর deploy

Polling-এ সার্ভারের ভার মাপা হয় অনুরোধ/সেকেন্ডে; persistent connection-এ নতুন একটি মাপ যোগ হয় — একসাথে কতগুলো সংযোগ খোলা. নীরব সংযোগেরও খরচ আছে: একটি file descriptor, kernel-এর buffer, TLS-এর অবস্থা আর অ্যাপের তথ্য (কে, কোন channel, কোন বার্তা অপেক্ষায়)। এক সংযোগে কত memory, তা runtime, TLS library আর buffer-ভেদে কয়েক KB থেকে কয়েকশো KB — নিজের load test-এ মাপা ছাড়া উপায় নেই।

পর্ব ৩-এর ধাঁচে একটি হিসাব করি। পিকে ২০ লাখ ব্যবহারকারী একসাথে অনলাইন, গড়ে ১.৩টি সংযোগ (ফোন আর কিছু ব্রাউজার ট্যাব), প্রতিটি সার্ভারে ১৬ GB RAM-এর অর্ধেক সংযোগের জন্য বরাদ্দ। তিনটি আলাদা “প্রতি সংযোগে memory” ধরে দেখি — সবই ধরা যাক.

// How many gateway servers hold the persistent connections? (ধরা যাক — assumed inputs)
const peakUsers = 2000000;        // concurrent online users at peak (assumed)
const connsPerUser = 1.3;         // phone + some web tabs (assumed)
const ramGB = 16, usable = 0.5;   // per server; share of RAM allowed for connections
const capPerServer = 250000;      // our own limit: CPU, reconnect blast radius
const zones = 3;                  // spread evenly; must survive losing one zone
const conns = peakUsers * connsPerUser;
console.log("connections at peak: " + conns.toLocaleString("en-US"));
console.log("KB/conn  total RAM  RAM limit/server  used/server  servers  " + zones + "-zone safe");
for (const kb of [20, 64, 200]) {                // memory per idle connection: MEASURE yours
  const totalGB = conns * kb / 1024 / 1024;
  const byRam = Math.floor(ramGB * usable * 1024 * 1024 / kb);
  const perServer = Math.min(byRam, capPerServer);
  const servers = Math.ceil(conns / perServer);
  const perZone = Math.ceil(servers / (zones - 1));   // the other zones carry everyone
  console.log(String(kb).padStart(7) + (totalGB.toFixed(1) + " GB").padStart(11) +
    byRam.toLocaleString("en-US").padStart(18) + perServer.toLocaleString("en-US").padStart(13) +
    String(servers).padStart(9) + String(perZone * zones).padStart(13));
}

আউটপুট:

connections at peak: 2,600,000
KB/conn  total RAM  RAM limit/server  used/server  servers  3-zone safe
     20    49.6 GB           419,430      250,000       11           18
     64   158.7 GB           131,072      131,072       20           30
    200   495.9 GB            41,943       41,943       62           93

মোট সংযোগ 2,600,000. ২০ KB-তে RAM একটি সার্ভারে ৪ লাখের বেশি ধরে, কিন্তু আমরা নিজেরাই সীমা রেখেছি আড়াই লাখ — এক সার্ভার পড়লে যত client একসাথে reconnect করবে (অংশ ৮), সেটিই তার blast radius; তাই 11টি। ৬৪ KB-তে RAM-ই সীমা, 20টি; ২০০ KB-তে 62টি — একই ব্যবহারকারী, প্রায় ছয় গুণ সার্ভার! শেষ কলাম: তিনটি availability zone-এর একটি হারালে বাকি দুটিকে সবাইকে ধরতে হবে — প্রতি zone-এ ⌈সার্ভার/২⌉, মোট 18, 30 বা 93 (পর্ব ৫-এর N+1 চিন্তা)। RAM-ই একমাত্র সীমা নয় — TLS-এর CPU আর fan-out-ও মাপতে হয়। এখানে GB মানে 1024 × 1024 KB.

OS-এর ডিফল্ট সীমা (সংস্করণভেদে আলাদা): প্রতি প্রসেসে খোলা ফাইলের সীমা — অনেক Linux-এ মাত্র ১,০২৪ — বাড়াতে হয়; proxy backend-এর দিকে সংযোগ খুললে প্রতিটিতে একটি ephemeral port লাগে, আর একটি উৎস IP থেকে একটি গন্তব্যে Linux-এর ডিফল্ট পরিসর মোটামুটি ২৮ হাজার — তাই বেশি backend ঠিকানা বা উৎস IP লাগে।

Load balancer আর দীর্ঘজীবী সংযোগ

সাধারণ HTTP-তে load balancer প্রতি অনুরোধে সার্ভার বাছে (L4 বনাম L7 — পর্ব ১৪); স্থায়ী সংযোগে বাছাই একবারই, সংযোগ খোলার মুহূর্তে, তারপর ঘণ্টার পর ঘণ্টা বাঁধা। ফল:

  • Round robin চলমান সংযোগ গোনে না, তাই least connections ভালো।
  • Load balancer আর proxy-র idle timeout heartbeat-এর বিরতির চেয়ে বড় হতে হবে, নইলে নীরব সংযোগ কাটা পড়ে।
  • Sticky session (একই client-কে বারবার একই সার্ভারে পাঠানো, পর্ব ১৫): WebSocket সংযোগ এমনিতেই এক সার্ভারে বাঁধা; কিন্তু long polling বা SSE fallback-এর পরপর অনুরোধ সার্ভারের memory-র অবস্থার ওপর নির্ভর করলে একই সার্ভারে যেতে হয় — যেমন Socket.IO-র ডকুমেন্টেশন একাধিক সার্ভারে long polling-এর জন্য sticky session চায়। ভালো নকশায় অবস্থা থাকে সার্ভারের বাইরে (registry, database), যাতে reconnect যেকোনো সার্ভারে পড়লেও চলে।

সামনের reverse proxy-তেও স্থায়ী সংযোগের জন্য আলাদা নিয়ম লাগে। Proxy আর gateway-র পূর্ণ আলোচনা পরের পর্ব ১১-এ; এখানে শুধু real-time-এর অংশটুকু:

upstream gateways { least_conn; server g1:8080; server g2:8080; }

location /ws/ {                          # WebSocket
    proxy_pass http://gateways;
    proxy_http_version 1.1;              # Upgrade চলে HTTP/1.1-এ
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 120s;             # ডিফল্ট 60s; ping-বিরতির চেয়ে বড়
}
location /stream/ {                      # SSE
    proxy_pass http://gateways;
    proxy_buffering off;                 # জমিয়ে না রেখে সঙ্গে সঙ্গে
    proxy_read_timeout 120s;
}

Deploy-এর পর অসম ভার

ধরুন ১০টি gateway-তে ২ লাখ করে সংযোগ, আর rolling deploy-এ একে একে restart হচ্ছে। G1-এর ২ লাখ client বাকি ৯টিতে ছড়ায় — প্রতিটিতে প্রায় ২.২২ লাখ; G1 ফেরে শূন্য সংযোগ নিয়ে, আর least connections-এ শুধু নতুন সংযোগ সেখানে যায় — পুরোনোরা নড়ে না। দশটি restart শেষে আগেরগুলো ভরা, শেষেরটি প্রায় খালি — ঘণ্টার পর ঘণ্টা, আর সবচেয়ে ভরা সার্ভারই পরের বিপদে সবচেয়ে বড় ঢেউ তোলে। প্রতিকার:

  • Connection draining: restart-এর আগে নতুন সংযোগ বন্ধ, তারপর পুরোনোদের প্রতি সেকেন্ডে অল্প অল্প করে, এলোমেলো দেরিসহ, “অন্য সার্ভারে যান” বলে বন্ধ করা।
  • সংযোগের সর্বোচ্চ বয়স: কয়েক ঘণ্টা পরপর এলোমেলো সময়ে নতুন করে জুড়তে বলা — ভার নিজেই সমান হয়।
  • প্রতি সার্ভারে সংযোগের সীমা, যাতে ভরা সার্ভার নতুন সংযোগ দ্রুত ফিরিয়ে দেয়; আর পিকের সময় deploy নয় (পর্ব ৫৩)।

৭ · Gateway স্তর, pub/sub আর presence — যেকোনো সার্ভার থেকে যেকোনো ব্যবহারকারী

বহু সার্ভারে নতুন সমস্যা: A-র সংযোগ G1-এ, B-র G7-এ — G1-এর হাতে B-র socket নেই। তাই দুটি স্তর আলাদা রাখা হয়:

  • Connection gateway: পাতলা সার্ভার — সংযোগ, TLS, পরিচয় যাচাই, heartbeat; ব্যবসার যুক্তি নেই বলে কম deploy, কম reconnect storm.
  • Backend সার্ভিস (chat, score, notification): stateless, কপি বাড়িয়ে scale হয় (পর্ব ১৩)।

দুই স্তরকে জোড়ে একটি pub/sub backplane: প্রতিটি gateway নিজের channel-এ (যেমন gw.G7) subscribe করে রাখে; কোনো সার্ভিস connection registry দেখে জানে B এখন G7-এ, তারপর gw.G7-এ publish করে। পাইকারি আড়ত থেকে মাল যায় খুচরা দোকানে, দোকান পৌঁছায় বাড়ি বাড়ি — আড়তদারকে শুধু জানতে হয় কোন বাড়ি কোন দোকানের এলাকায়।

Real-time সিস্টেমের কাঠামো — client, gateway, pub/sub আর সার্ভিস

Clients ↔ WebSocket gateways ↔ pub/sub backplane ↔ stateless services

Clients, load balancer, WebSocket gateways, pub/sub backplane, chat-presence-notification সার্ভিস, message DB আর platform push ফোন A WebSocket ব্রাউজার SSE fallback ফোন B WebSocket L4 load balancer · least connections সংযোগ খোলার মুহূর্তে একবারই বাছাই Gateway G1 A-র socket Gateway G2 আরও লাখো Gateway G7 B-র socket Pub/sub backplane gw.G1 … gw.G7 · match.4217 Chat service stateless seq দেয় Presence user → gateway TTL-সহ registry Notification অফলাইন হলে push পাঠায় Message DB conv c9 · seq 58 Platform push সেবা OS-এর নিজস্ব ① ⑤ ④ gw.G7 ③ PUBLISH gw.G7 লেখা push ② send (RPC) ① A → G1 · ② সংরক্ষণ, seq 58 · ③ publish ④ G7 পায় · ⑤ B-র socket-এ B অফলাইনে: push, ফিরে resume
ছবি ১০.১ — Gateway শুধু সংযোগ ধরে, যুক্তি থাকে stateless সার্ভিসে; registry জানে কে কোন gateway-তে, pub/sub সেখানে পৌঁছায়।

Routing-এর দুই ধরন: (ক) প্রতি ব্যবহারকারীর channel (user.B) — registry লাগে না, কিন্তু broker-এ লাখো channel আর subscribe-এর ঝড়; (খ) প্রতি gateway-র channel আর registry — channel কম, কিন্তু মৃত gateway-র entry যেন TTL-এ নিজেই মুছে যায়। একই খবর বহু জনের জন্য হলে topic channel match.4217: শুধু যেসব gateway-তে অন্তত একজন দর্শক আছেন, তারাই subscribe করে — একটি publish → ৪০টি gateway → ৫ লাখ socket; দুই ধাপের fan-out, broker-এর কাজ মাত্র ৪০টি কপি।

  1. AG1-এ যুক্তপাঠালেন
  2. Gateway G1WebSocketশুধু দরজা
  3. Chat + DBseq 58আগে সংরক্ষণ
  4. Pub/subgw.G7registry দেখে
  5. Gateway G7subscribedB-র socket খোঁজে
  6. Bseq 58পেলেন
দুই পর্দার অবস্থা
এই ধাপে
A-র পর্দায়
B-র পর্দায়
  1. A বার্তা পাঠালেন। A-র WebSocket সংযোগ G1-এ। Frame-এ কথোপকথন c9, লেখা, আর client-এর দেওয়া id a-77 — নেটওয়ার্ক ভেঙে অ্যাপ আবার পাঠালেও সার্ভার এই id দেখে বুঝবে এটি একই বার্তা। G1 শুধু দরজা, সিদ্ধান্ত নেয় না।
  2. আগে সংরক্ষণ, তারপর ack. G1 বার্তাটি RPC-তে chat service-কে দিল; সে database-এ লিখে পরের ক্রমিক নম্বর দিল — seq ৫৮ — আর তারপরেই A পেলেন ack, একটি টিক। আগে ack দিয়ে পরে লেখা ব্যর্থ হলে বার্তা হারাত।
  3. B কোথায়? Registry বলছে B এখন G7-এ (entry TTL-সহ)। Chat service G7-এর মেশিন-ঠিকানা খোঁজে না — শুধু pub/sub-এ gw.G7 channel-এ publish করে।
  4. Pub/sub থেকে G7. G7 চালু হওয়ার সময়েই নিজের channel-এ subscribe করেছিল; বার্তা পেয়ে নিজের তালিকায় খুঁজে নিল B-র socket — আড়ত থেকে মাল এলো দোকানে।
  5. B পেলেন, A জানলেন। G7 বার্তাটি B-র socket-এ লিখল। B-র অ্যাপ seq ৫৮ মনে রেখে delivered ack দিল, যা G7 → pub/sub → G1 পথে A-র কাছে গেল — দুটি টিক। Retry-তে seq ৫৮ দুবার এলে অ্যাপ দ্বিতীয়টি ফেলে দেয়।
  6. B অফলাইন থাকলে? Registry-তে B নেই — TTL পেরিয়েছে। বার্তা আগেই database-এ নিরাপদ; platform push B-র ফোনে “নতুন বার্তা” পাঠাল। অ্যাপ খুলে B চাইলেন ৫৭-এর পরের সব — seq ৫৮ এলো। এই resume-ই শেষ নিরাপত্তা-জাল।

Pub/sub নিজে স্মৃতিহীন হতে পারে। Redis Pub/Sub ডকুমেন্টেশন অনুযায়ী at-most-once: subscriber সেই মুহূর্তে সংযুক্ত না থাকলে বার্তা হারায়। তাই আসল ঠিকানা টেকসই database, pub/sub শুধু দ্রুত পথ। Kafka-র মতো log-ভিত্তিক broker বার্তা জমিয়ে রাখে — পর্ব ৩২ (pub/sub ও event-driven) আর ৩৩-এ।

Presence — কে অনলাইন, কে কখন শেষ ছিলেন

Presence — সবুজ বিন্দু আর “শেষ দেখা ১০ মিনিট আগে”. নেটওয়ার্ক নিঃশব্দে মরলে সার্ভার জানবে কীভাবে? Heartbeat আর TTL দিয়ে: client প্রতি H সেকেন্ডে “আছি” পাঠায়, gateway presence:B key-এর মেয়াদ নবায়ন করে; TTL ধরা যাক H-এর আড়াই গুণ। Heartbeat থামলে TTL শেষে key নিজেই মুছে যায় — B অফলাইন, শেষ heartbeat-ই “last seen”. ইচ্ছা করে বেরোলে key সঙ্গে সঙ্গে মোছা হয়; TTL শুধু নিঃশব্দ মৃত্যুর জন্য।

presence store-এ লেখা/সেকেন্ড = N ÷ H
নিঃশব্দ মৃত্যু ধরা পড়ে (TTL − H) থেকে TTL সেকেন্ডে

২০ লাখ অনলাইন আর H = ৩০ সেকেন্ডে সেকেন্ডে প্রায় ৬৬,৬৬৭টি নবায়ন; H = ৬০-এ অর্ধেক, কিন্তু ধরতে দ্বিগুণ সময়। TTL = ২.৫H হলে একটি heartbeat হারালেও key টেকে, পরপর দুটি হারালে অফলাইন; H = ৩০, TTL = ৭৫-এ নিঃশব্দ মৃত্যু ধরা পড়ে ৪৫–৭৫ সেকেন্ডে। অনলাইন-অফলাইন দোল খেলে (flapping) প্রচারের আগে একটু অপেক্ষা, আর অবস্থা-বদল শুধু যাঁদের পর্দায় তালিকা খোলা তাঁদের কাছে — নইলে fan-out বার্তার চেয়েও বেশি খরচ করে (প্রশ্ন ৪, অনুশীলন ৩)। Failure detection-এর তত্ত্ব পর্ব ৪৪-এ, পুরো chat নকশা পর্ব ৫৫-এর case study-তে।

ক্রম আর delivery-র নিশ্চয়তা — শেষ মাইলে

TCP একটি সংযোগের ভেতরে বাইটের ক্রম রাখে; কিন্তু সংযোগ ভাঙলে, অন্য gateway-তে গেলে, বা ফোন আর ল্যাপটপ দুটিতেই বার্তা গেলে সেই নিশ্চয়তা থাকে না। তাই চারটি নিয়ম:

  1. ক্রম দেয় সার্ভার: প্রতিটি কথোপকথনে একজন একক লেখক (সেই partition-এর মালিক) পরপর sequence number বসায় (partition পর্ব ২৩-এ, সময় আর ক্রম পর্ব ৩৯-এ); client-এর ঘড়ি দেখে সাজালে ভুল হয়।
  2. At-least-once আর dedupe: ack না পেলে সার্ভার আবার পাঠায় (at-least-once), আর client seq বা id দেখে দ্বিতীয় কপি ফেলে দেয় (পর্ব ৩৪)।
  3. Resume: প্রতিটি ডিভাইস প্রতিটি কথোপকথনে শেষ পাওয়া seq মনে রাখে; reconnect-এর পর জানায় “৫৭ পর্যন্ত আছে”, সার্ভার টেকসই store থেকে বাকিটা পাঠায় — Last-Event-ID-এর মতোই।
  4. ফাঁক ধরা: ৫৮-এর পর ৬০ এলে client বোঝে ৫৯ নেই, আর সেটি আলাদা করে চেয়ে নেয়।

৮ · Reconnect storm আর মোবাইলের বাস্তবতা

রাত ৯টায় একটি gateway crash করল; তার ৫০ হাজার client একই সেকেন্ডে টের পেয়ে একসাথে ফিরতে চাইল — ঈদের আগে বাস কাউন্টার খোলার মুহূর্তের মতো, সবাই ঠেলাঠেলি করলে কেউই ঠিকমতো টিকিট পান না। প্রতিটি নতুন সংযোগে TLS handshake আর পরিচয় যাচাই CPU-ভারী; কয়েক গুণ চেষ্টা এলে সার্ভার প্রত্যাখ্যানেই শক্তি খরচ করে, সফল হয় আরও কম, ব্যর্থরা আবার আসে। একে বলে reconnect storm — thundering herd-এর একটি রূপ, খারাপ ক্ষেত্রে একটি retry storm.

নিচের simulation দুই রকম client-এর তুলনা করে। “No backoff” ব্যর্থ হলে পরের সেকেন্ডেই আবার চেষ্টা করে। “Full jitter” অপেক্ষা করে ০ থেকে min(সর্বোচ্চ, ১ × ২k) সেকেন্ডের মধ্যে এলোমেলো একটি সময়, যেখানে k আগের ব্যর্থতার সংখ্যা — এটিই exponential backoff, সাথে jitter যোগ করা। Gateway সেকেন্ডে সর্বোচ্চ ৫,০০০ handshake শেষ করতে পারে, আর প্রত্যাখ্যাত প্রতিটি চেষ্টাও একটি handshake-এর ৮% খরচ করে — সব ধরা যাক; seed নির্দিষ্ট, তাই প্রতিবার একই ফল।

// Reconnect storm: a gateway restarts at t = 0 and all 50,000 of its clients reconnect.
// ধরা যাক: it completes at most 5,000 handshakes/s; a rejected attempt still costs 8%
// of a handshake; a client learns of its rejection at the end of that second.
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, CAP = 5000, REJECT_COST = 0.08, BASE = 1, MAX_BACKOFF = 32;
function storm(jitter) {
  const rand = mulberry32(42), log = [];
  let wait = Array.from({ length: C }, () => [0, 0]);         // [next try at, failures]
  for (let t = 0; wait.length && t < 60; t++) {
    const now = wait.filter(w => w[0] < t + 1).sort((x, y) => x[0] - y[0]);
    wait = wait.filter(w => w[0] >= t + 1);
    const A = now.length;                                     // attempts in second t
    const ok = Math.max(0, A <= CAP ? A : Math.floor((CAP - REJECT_COST * A) / (1 - REJECT_COST)));
    for (const [, k] of now.slice(ok)) {                      // rejected: wait, retry
      const delay = jitter ? rand() * Math.min(MAX_BACKOFF, BASE * 2 ** k) : 0;
      wait.push([t + 1 + delay, k + 1]);
    }
    log.push([A, ok]);
  }
  return log;
}
function report(name, log) {
  let back = 0, half = "never", p95 = "never";
  log.forEach(([, ok], t) => { back += ok;
    if (half === "never" && back >= C / 2) half = t + 1 + " s";
    if (p95 === "never" && back >= 0.95 * C) p95 = t + 1 + " s"; });
  const all = back === C ? log.length + " s" : "never", tries = log.reduce((s, r) => s + r[0], 0);
  console.log(`${name}: half back ${half}, 95% back ${p95}, all back ${all}, tries ${tries}`);
}
const a = storm(false), b = storm(true), cell = (r) => r ? `${r[0]} (${r[1]})` : "0 (0)";
console.log(" t  no backoff: tries (ok)   full jitter: tries (ok)");
for (let t = 0; t <= 20; t++) console.log(String(t).padStart(2) + cell(a[t]).padStart(24) + cell(b[t]).padStart(26));
report("no backoff ", a); report("full jitter", b);

আউটপুট:

 t  no backoff: tries (ok)   full jitter: tries (ok)
 0            50000 (1086)              50000 (1086)
 1            48914 (1181)              48914 (1181)
 2            47733 (1284)              23819 (3363)
 3            46449 (1395)              28978 (2914)
 4            45054 (1517)              11112 (4468)
 5            43537 (1648)              11638 (4422)
 6            41889 (1792)              13014 (4303)
 7            40097 (1948)               8490 (4696)
 8            38149 (2117)               3651 (3651)
 9            36032 (2301)               3569 (3569)
10            33731 (2501)               3630 (3630)
11            31230 (2719)               3637 (3637)
12            28511 (2955)               3124 (3124)
13            25556 (3212)               2215 (2215)
14            22344 (3491)               1412 (1412)
15            18853 (3795)                 536 (536)
16            15058 (4125)                 236 (236)
17            10933 (4484)                 266 (266)
18             6449 (4874)                 252 (252)
19             1575 (1575)                 235 (235)
20                   0 (0)                 236 (236)
no backoff : half back 14 s, 95% back 19 s, all back 20 s, tries 632094
full jitter: half back 8 s, 95% back 15 s, all back 40 s, tries 219532

সেকেন্ড ০-তে দুই দলই সমান: ৫০,০০০ চেষ্টা, সফল মাত্র 1086 — ৫০,০০০ × ০.০৮ = ৪,০০০ ইউনিট গেল প্রত্যাখ্যানে, বাকি সামর্থ্যে (৫,০০০ − ৪,০০০) ÷ ০.৯২ ≈ ১,০৮৬টি সংযোগ। No backoff-এ সেকেন্ড ১০-এও 33731 চেষ্টা, সামর্থ্যের বড় অংশ যায় “না” বলতে — অর্ধেক মানুষ ফেরেন ১৪ সেকেন্ডে, সবাই ২০ সেকেন্ডে, মোট 632094 চেষ্টা। Full jitter-এ অপেক্ষা ছড়িয়ে পড়ে: সেকেন্ড ৪-এই চেষ্টা নেমে 11112, সেকেন্ড ৮ থেকে সামর্থ্যের নিচে। অর্ধেক ফেরেন ৮ সেকেন্ডে, ৯৫% ১৫ সেকেন্ডে, আর মোট চেষ্টা প্রায় এক-তৃতীয়াংশ, মাত্র 219532. দাম একটি লম্বা লেজ — শেষজন ফেরেন ৪০ সেকেন্ডে। এবার REJECT_COST ০.১ করে চালান: no backoff-এ প্রত্যাখ্যান-খরচই ৫০,০০০ × ০.১ = ৫,০০০, পুরো সামর্থ্য; প্রতিটি সারি 50000 (0), শেষ লাইনে never. সার্ভার সুস্থ, তবু সিস্টেম আটকে — metastable failure; full jitter তখনো সবাইকে ফেরায়।

Storm ঠেকানোর আরও উপায়

  • সার্ভারের ইঙ্গিত: বন্ধ করার আগে “এলোমেলো সময়ে, এত সেকেন্ডের মধ্যে ফিরুন” জানানো; SSE-তে একই কাজ করে retry: field.
  • Admission control: সেকেন্ডে নতুন handshake-এর সীমা, আর বাড়তি চেষ্টা TLS শুরুর আগেই সস্তায় ফেরানো — REJECT_COST যত কম, তত নিরাপদ (load shedding, পর্ব ১৮)।
  • ধীরে draining (অংশ ৬), আর নিয়মটি client library-তে বাঁধা — প্রতিটি দল নিজে retry লিখলে কেউ না কেউ ভুল করবেই (retry ও backoff পর্ব ৪৮-এ)।

মোবাইলের বাস্তবতা — ব্যাটারি, NAT আর বদলাতে থাকা নেটওয়ার্ক

ব্যাটারি: ফোনের cellular radio প্রতিটি ছোট আদানপ্রদানের পর কয়েক সেকেন্ড বেশি-শক্তিতে জেগে থাকে — tail energy. তাই ৩০ সেকেন্ড পরপর ১০ বাইটের ping-ও radio-কে বারবার জাগিয়ে ব্যাটারি খায়। এজন্য Android আর iOS background-এ অ্যাপের নিজের সংযোগ সীমিত করে, আর অ্যাপ বন্ধ থাকলে platform push-ই ভরসা।

NAT-এর ভুলে যাওয়া: মোবাইল নেটওয়ার্কে সাধারণত CGNAT আর firewall থাকে (CN কোর্সের NAT পর্ব)। IETF-এর সুপারিশ (RFC 5382, ২০০৮) প্রতিষ্ঠিত TCP সংযোগের নীরব মেয়াদ অন্তত ২ ঘণ্টা ৪ মিনিট রাখতে বলে, কিন্তু বাস্তবের অনেক NAT ও firewall কয়েক মিনিট বা তারও আগে ভুলে যায় (অপারেটরভেদে আলাদা)। তখন বার্তা হারায়, অথচ client ভাবে সব ঠিক (half-open সংযোগ)। Linux-এ TCP-র নিজের keepalive ডিফল্টে ২ ঘণ্টা নীরবতার পর শুরু হয় — অনেক দেরি — তাই অ্যাপ-স্তরের ping লাগে। ঘন ping-এ ব্যাটারি আর ডেটা যায়, বিরল ping-এ নিঃশব্দ মৃত্যু; অনেক অ্যাপ বিরতি রাখে কয়েক দশ সেকেন্ড থেকে কয়েক মিনিট।

বাংলাদেশের প্রেক্ষাপট: ধরে নেওয়া উচিত — অনেক ব্যবহারকারী মূলত মোবাইল ডেটায়; লিফট, বেজমেন্ট বা যাত্রাপথে সংযোগ বারবার ভাঙে; Wi-Fi আর মোবাইল ডেটার মধ্যে বদলালে IP বদলায় আর TCP সংযোগ মরে (QUIC-এর connection migration এখানে সাহায্য করতে পারে — CN কোর্সের QUIC পর্ব); ডেটা প্যাকের প্রতিটি MB-এর দাম আছে। তাই ছোট বার্তা-format (পর্ব ১২), শুধু ফাঁকটুকুর resume, background-এ platform push, আর পুনঃসংযোগে jitter — একটি মোবাইল টাওয়ার আবার চালু হলে হাজারো ফোন একসাথে ফেরে।

আরও গভীরে: Marc Brooker-এর AWS Architecture Blog-এর লেখা “Exponential Backoff And Jitter” (২০১৫) simulation দিয়ে দেখায়, jitter ছাড়া client-রা দল বেঁধে ফেরে আর full jitter মোট কাজ সবচেয়ে কমায়। Bronson ও সহকর্মীদের “Metastable Failures in Distributed Systems” (HotOS ২০২১) এমন আটকে থাকা অবস্থাকে আলাদা শ্রেণি হিসেবে সংজ্ঞায়িত করে, যেখানে retry-ই তাকে টিকিয়ে রাখে। Huang ও সহকর্মীদের LTE-র শক্তি-পরিমাপ (MobiSys ২০১২) দেখায়, ছোট ঘনঘন আদানপ্রদানে radio-র “লেজের” শক্তিই বড় খরচ।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • WebSocket আর SSE-র পার্থক্য? — WebSocket দুই দিকের, text ও binary; SSE শুধু সার্ভার থেকে client-এ, শুধু লেখা, সাধারণ HTTP-র ওপর, আর নিজেই reconnect করে Last-Event-ID দিয়ে resume করে।
  • A আর B আলাদা সার্ভারে থাকলে? — registry দেখে B-র gateway, তার channel-এ publish, সে B-র socket-এ লেখে; সাথে টেকসই store আর resume.
  • Gateway restart-এর পর হাজারো client একসাথে ফিরলে? — backoff আর full jitter, ধীরে draining, handshake-এর সীমা।
  • Presence কীভাবে? — প্রতি H সেকেন্ডে heartbeat, আর TTL তার দুই-তিন গুণ।

৯ · AI-এর যুগে real-time নকশা যাচাই

চ্যাটের real-time নকশা চাইলে AI প্রায় নিশ্চিতভাবে লিখবে “WebSocket, Redis আর Kafka”. নাম ভুল নয়, কিন্তু আসল প্রশ্ন বাদ পড়ে:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
সবখানে “WebSocket + Redis + Kafka”এক দিকের ডেটায় SSE বা cache করা polling সস্তা; প্রতিটি অংশ নতুন ব্যর্থতার জায়গাপ্রতিটি অংশ কোন requirement মেটায় জিজ্ঞেস করুন
বানানো সংখ্যা ও অঙ্কের ভুল“একটি সার্ভারে ১০ লাখ সংযোগ”, N/I উল্টে ফেলা, KB-GB-তে হাজার গুণ ভুল, zone হারানোর headroom বাদনিজে আবার হিসাব; নিজের load test
“WebSocket মানে বার্তা নিশ্চিত পৌঁছায়”TCP বাইট পৌঁছায় অপর kernel পর্যন্ত; অ্যাপ-স্তরের ack, seq আর resume নয়ack, dedupe আর resume-এর নকশা চান
Reconnect storm, ধীর client, deploy-এর অসম ভার বাদশুধু স্থির অবস্থার ছবি আঁকেfailure-mode review: gateway মরলে কী হবে?
“Presence সবসময় strongly consistent রাখতে হবে”ACID-এর consistency আর replica-র consistency (CAP অর্থে, পর্ব ৪০) গুলিয়ে অকারণ transactionকতটা পুরোনো চলবে, সংখ্যায় লিখুন
বানানো সীমা ও নিয়মpush payload, ব্রাউজারের সংযোগ-সীমা, proxy timeout আত্মবিশ্বাসের সাথে লেখেঅফিসিয়াল ডকুমেন্টেশন

AI-কে কাজে লাগান বিকল্প খোঁজা, হিসাব মেলানো আর failure mode-এর তালিকা বানাতে — অনুমান নিজে দিন, আর উত্তরে অনুমানগুলো আলাদা করে চান:

আমি একটি কাল্পনিক চ্যাট অ্যাপের real-time অংশ নকশা করছি। অনুমান: পিকে ২০ লাখ ব্যবহারকারী একসাথে অনলাইন, গড়ে ১.৩টি সংযোগ, প্রত্যেকে মিনিটে গড়ে ১টি বার্তা পান; ব্যবহারকারীদের বড় অংশ মোবাইল ডেটায়। ১. WebSocket, SSE, long polling আর platform push — কোনটি কোন কাজে, যুক্তিসহ বলো। ২. প্রতি সংযোগে ২০, ৬৪ আর ২০০ KB ধরে কতটি gateway লাগবে, সূত্রসহ হিসাব করো; একটি zone হারানোর headroom আলাদা দেখাও। ৩. আলাদা gateway-র দুজনের মধ্যে বার্তা, ack, sequence number আর resume কীভাবে কাজ করবে। ৪. Gateway restart, NAT timeout আর ধীর client-এ কী ঘটবে, প্রতিকার কী। ৫. কোনো পণ্যের সীমা বা benchmark-এর সংখ্যা নিজে থেকে লিখবে না; দরকারে লিখবে "অফিসিয়াল ডকুমেন্টেশনে যাচাই করতে হবে"। অনুমানগুলো আলাদা তালিকায় দাও।

AI-এর দেওয়া real-time নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Comet থেকে WebSocket — long polling-এর যুগে ওয়েব চ্যাট চলত কীভাবে, আর WebSocket কেন HTTP দিয়েই শুরু হয়?

২০০০-এর দশকের মাঝামাঝি ব্রাউজার শুধু অনুরোধ-উত্তর জানত, অথচ মানুষ ওয়েবেই চ্যাট করতে চাইছিল। প্রকৌশলীরা HTTP-কে “ঠকানোর” কৌশল বের করেন: long polling, লুকানো iframe-এ কখনো শেষ না হওয়া উত্তর (“forever frame”), ধীরে বাড়তে থাকা XHR উত্তর। ২০০৬ সালে Alex Russell এগুলোর একটি সাধারণ নাম দেন — Comet. ২০০৮ সালে Facebook তাদের ওয়েব চ্যাট নিয়ে একটি engineering note প্রকাশ করে; লেখাটির বর্ণনা অনুযায়ী ব্রাউজারের দিকে long polling ব্যবহার হতো, আর লাখো ঝুলন্ত সংযোগ ধরে রাখতে আলাদা “channel” সার্ভার লেগেছিল — এই পর্বের gateway স্তরের পূর্বসূরি।

সমস্যাও ছিল বিস্তর। HTTP/1.1-এর মূল নির্দেশিকা (RFC 2616) একজন client-কে একটি সার্ভারে দুটির বেশি স্থায়ী সংযোগ না রাখতে বলত; একটি long poll সেই দুটির একটি আটকে রাখলে পাতার বাকি কাজ ধীর হয়ে যেত। প্রতিটি বার্তায় header-এর বোঝা, proxy-র timeout, আর ব্রাউজারভেদে আলাদা কৌশল — সব মিলিয়ে একটি স্থানীয় সমাধানের দরকার স্পষ্ট হলো।

WebSocket-এর কাজ শুরু হয় HTML5-এর খসড়ায়, মোটামুটি ২০০৮ সালের দিকে, আর IETF তাকে RFC 6455 হিসেবে মান দেয় ২০১১ সালের ডিসেম্বরে। নকশার সবচেয়ে বড় সিদ্ধান্ত: নতুন port বা নতুন protocol দিয়ে শুরু না করে সাধারণ HTTP অনুরোধ দিয়ে শুরু, তারপর Upgrade. কারণ পৃথিবীর firewall, proxy আর load balancer তখন শুধু port 80 আর 443-এর HTTP চিনত; নতুন কিছু চালু করতে হলে পুরোনো দরজা দিয়েই ঢুকতে হবে। একই সার্ভার একই port-এ পাতা আর WebSocket দুটিই দিতে পারে, cookie আর origin-যাচাইও কাজে লাগে। তবু পুরোনো proxy-র সাথে মিলিয়ে চলতে গিয়ে client-এর frame mask করার মতো নিয়ম যোগ হয় (CN কোর্সের real-time পর্বে বিস্তারিত)।

মূল উপলব্ধি: নতুন protocol টিকে থাকে পুরোনো পথ দিয়ে ঢুকতে পারলে — QUIC যেমন UDP-র ওপর, WebSocket তেমনি HTTP-র ভেতর দিয়ে; আর সব কৌশলের মূল সমস্যা একটাই — সার্ভারকে আগে কথা বলার সুযোগ দেওয়া।

প্র ০২WebSocket সবচেয়ে “সক্ষম” — তবু অনেক দল SSE, এমনকি polling বেছে নেয় কেন?

সক্ষমতা আর চালানোর খরচ এক জিনিস নয়। SSE চলে সাধারণ HTTP-র ওপর, তাই আগে থেকে থাকা সব যন্ত্র কাজে লাগে: load balancer, পরিচয়-যাচাইয়ের middleware, log আর status code, HTTP/2-এর multiplexing, আর curl দিয়ে debug. Reconnect আর Last-Event-ID দিয়ে resume ব্রাউজারেই তৈরি। খবর মূলত এক দিকে গেলে — স্কোর, বিজ্ঞপ্তি, AI-এর লেখা উত্তর — উল্টো দিকের সামান্য বার্তা সাধারণ POST-এ পাঠানো যায়, সাথে একটি idempotency key.

WebSocket-এ নালির ওপর নিজের protocol বানাতে হয়: বার্তার id, ack, resume, অনুরোধ-উত্তর মেলানো। ঘণ্টার পর ঘণ্টা খোলা সংযোগে auth token-এর মেয়াদ ফুরালে কী হবে, সেটিও নকশা করতে হয়। প্রতিটি বার্তার আলাদা status code নেই বলে observability কঠিন; কিছু পুরোনো proxy Upgrade আটকায়; আর stateful সংযোগ deploy আর load balancing জটিল করে (অংশ ৬)। অনেক AI API-র অফিসিয়াল ডকুমেন্টেশনে streaming উত্তর SSE দিয়ে আসা এই হিসাবেরই ফল — উত্তর এক দিকে যায়, আর HTTP-র সব যন্ত্র তৈরি।

আর polling? সবার জন্য একই ডেটা হলে — লাইভ স্কোরের মতো — CDN-এ কয়েক সেকেন্ডের জন্য cache করা একটি JSON, প্রতি কয়েক সেকেন্ডে poll. Origin-এর ভার তখন পাঠক-সংখ্যার ওপর প্রায় নির্ভর করে না; ৫ লাখ স্থায়ী সংযোগের gateway-বহর, registry, pub/sub — কিছুই লাগে না। দেরি কয়েক সেকেন্ড, যা স্কোরের জন্য যথেষ্ট।

WebSocket জেতে যেখানে দুই দিকে ঘনঘন ছোট বার্তা আর কম দেরি দুটোই জরুরি: সহযোগী সম্পাদনা, অনলাইন খেলা, চ্যাট, ট্রেডিং — বিশেষত binary ডেটায়। GraphQL-এর subscription-ও (পর্ব ৯) WebSocket আর SSE দুই পথেই চালানো যায় — প্রমাণ যে প্রশ্নটি প্রযুক্তির নয়, প্রয়োজনের।

মূল উপলব্ধি: “সবচেয়ে সক্ষম” নয়, requirement মেটানো সবচেয়ে সরল পথটি বেছে নিন; চালানোর খরচ, debug আর deploy-এর কষ্টও নকশার অংশ।

প্র ০৩একটি সার্ভারে দশ লাখ সংযোগ — C10K থেকে আজ পর্যন্ত কী বদলেছে, আর তবু সব সংযোগ একটি সার্ভারে রাখা উচিত নয় কেন?

১৯৯৯ সালে Dan Kegel “The C10K problem” নামে একটি লেখায় প্রশ্ন তোলেন: একটি সার্ভার একসাথে দশ হাজার client সামলাবে কীভাবে? তখনকার প্রচলিত নকশা — প্রতি সংযোগে একটি thread বা process, আর select()-এর মতো call যা প্রতিবার সব সংযোগ ঘুরে দেখে — দশ হাজারেই ভেঙে পড়ত। উত্তর এলো OS-এর নতুন যন্ত্রে: Linux-এর epoll, BSD-র kqueue, যা শুধু তৈরি হওয়া সংযোগগুলো জানায়; আর তার ওপর event loop ও async runtime (CN কোর্সের event-driven সার্ভার পর্ব)।

এরপর সংখ্যা দ্রুত বেড়েছে। WhatsApp-এর প্রকাশিত engineering blog (২০১২) অনুযায়ী তারা Erlang-এ লেখা সার্ভারে একটি মেশিনে ২০ লাখের বেশি TCP সংযোগ ধরে রাখার কথা জানিয়েছিল; ২০১৫ সালে Phoenix framework-এর একটি প্রকাশিত benchmark-এ একটি বড় মেশিনে প্রায় ২০ লাখ WebSocket সংযোগ দেখানো হয়। অর্থাৎ “কতটা ধরে” প্রশ্নের উত্তর আজ লাখের ঘরে।

কিন্তু benchmark-এর সংযোগগুলো প্রায় নীরব। বাস্তবে আছে TLS handshake, বার্তা ছড়ানো, garbage collection, বাইরের সার্ভিসে অপেক্ষা। আর সবচেয়ে বড় কারণ blast radius: অংশ ৮-এর simulation-এ ৫০ হাজার client ফিরতে সময় লেগেছে ১৫–২০ সেকেন্ড। ২০ লাখ সংযোগের একটি সার্ভার পড়লে ২০ লাখ client একসাথে বাকি বহরে আছড়ে পড়বে; বাকি দশটি সার্ভার মিলে সেকেন্ডে ৫০ হাজার নতুন handshake নিতে পারলেও সবার ফিরতে অন্তত ৪০ সেকেন্ড — আর সেই চাপে সুস্থ সার্ভারগুলোও টলতে পারে। Deploy-এ draining-ও তত দীর্ঘ।

তাই দলগুলো সার্ভারপ্রতি সংযোগের একটি সীমা রাখে সর্বোচ্চ সামর্থ্যের অনেক নিচে, আর সেটি ঠিক করে পুনরুদ্ধারের সময় দিয়ে: একটি সার্ভারের সব client বাকিরা কত সেকেন্ডে নিরাপদে নিতে পারবে?

মূল উপলব্ধি: “কতটা ধরে” আর “কতটা ধরা উচিত” আলাদা প্রশ্ন; দ্বিতীয়টির উত্তর আসে ব্যর্থতার পরের হিসাব থেকে।

প্র ০৪“অনলাইন” সবুজ বিন্দুটি দেখাতে এত খরচ কেন? Presence-এর fan-out-এর অঙ্ক কী বলে?

Facebook-এর ২০০৮ সালের চ্যাট নিয়ে প্রকাশিত engineering note-এ একটি পর্যবেক্ষণ ছিল, যা আজও প্রাসঙ্গিক: চ্যাট সিস্টেমে সবচেয়ে বেশি সম্পদ খরচ হয় বার্তা পাঠাতে নয়, বরং প্রত্যেক অনলাইন ব্যবহারকারীকে তাঁর বন্ধুদের অনলাইন-অফলাইন অবস্থা জানিয়ে রাখতে।

অঙ্কটি দেখুন। N জন অনলাইন, প্রত্যেকের অবস্থা ঘণ্টায় s বার বদলায় (অ্যাপ খোলা-বন্ধ, background, নেটওয়ার্ক বদল), আর প্রতিটি বদল f জন অনলাইন দর্শকের কাছে যায়: সেকেন্ডে N·s·f ÷ ৩,৬০০টি বার্তা। ২০ লাখ ব্যবহারকারী, s = ৪, f = ৫০ ধরলে ২০,০০,০০০ × ৪ × ৫০ ÷ ৩,৬০০ ≈ ১,১১,১১১টি presence-বার্তা প্রতি সেকেন্ডে। তুলনায় প্রত্যেকে মিনিটে একটি চ্যাট বার্তা পেলে আসল বার্তা ২০,০০,০০০ ÷ ৬০ ≈ ৩৩,৩৩৩টি — presence তিন গুণের বেশি! তার ওপর heartbeat-এর লেখা (H = ৩০ সেকেন্ডে আরও ৬৬,৬৬৭টি)।

কৌশলগুলো তাই “কম পাঠানো”-র: (১) অলস টানা — চ্যাট বা তালিকা খুললে তবেই অবস্থা আনা; (২) শুধু পর্দায় দৃশ্যমান মানুষদের অবস্থায় subscribe; (৩) debounce — অবস্থা ১০–৩০ সেকেন্ড স্থির থাকলে তবেই প্রচার, যাতে দুর্বল নেটওয়ার্কের দোলাচল প্রতিবার না ছড়ায়; (৪) বড় গ্রুপে সব সদস্যের presence না দেখানো; (৫) “last seen”-কে মোটা দাগে দেখানো — “সম্প্রতি”, “এই সপ্তাহে”; গোপনীয়তার নিয়মও এতে সাহায্য করে।

আর consistency-র প্রশ্ন: presence স্বভাবতই একটু পুরোনো — কয়েক সেকেন্ড দেরি কেউ টের পান না। এখানে strong consistency বা transaction খোঁজা অপচয়; eventual consistency-ই সঠিক উত্তর (পর্ব ৪০)।

মূল উপলব্ধি: real-time সিস্টেমে ছোট একটি UI চিহ্নও fan-out-এর গুণে বিশাল খরচ হয়ে দাঁড়ায়; কতটা টাটকা দরকার, তা সংখ্যায় ঠিক করে তবেই নকশা।

প্র ০৫ফোনের প্রতিটি অ্যাপ নিজের সংযোগ খোলা রাখে না কেন? Platform push কীভাবে এলো, আর তার দাম কী?

ধরুন একটি ফোনে ত্রিশটি অ্যাপ, প্রত্যেকে নিজের সংযোগে কয়েক মিনিট পরপর ping পাঠায়। প্রতিটি ping-এ radio জাগে আর tail energy-র কারণে কয়েক সেকেন্ড বেশি-শক্তিতে থাকে — ত্রিশটির এলোমেলো ping মিলে radio প্রায় ঘুমায়ই না। তাই মোবাইল প্ল্যাটফর্মগুলো কাজটি কেন্দ্রীভূত করেছে: OS নিজে একটিমাত্র দীর্ঘস্থায়ী সংযোগ রাখে, আর সব অ্যাপের বিজ্ঞপ্তি সেই পথে আসে। Apple এই ধরনের push সেবা চালু করে মোটামুটি ২০০৯ সালে, Google-এর Android-ও পরের বছরগুলোতে নিজের সেবা আনে, যা নাম বদলে আজকের FCM-এ পৌঁছেছে। পাশাপাশি OS background-এর নিয়ম কড়া করেছে — যেমন Android 6.0 (২০১৫)-এর Doze মোড অলস ফোনে নেটওয়ার্ক ব্যবহার সীমিত করে। ব্রাউজারের জন্য একই ধারণা এসেছে Web Push (RFC 8030, ২০১৬) আর service worker দিয়ে।

দাম চারটি। (১) নিশ্চয়তা নেই: OS-এর শক্তি-নীতি বার্তা দেরি করাতে পারে, একাধিক বার্তা একটিতে মেশাতে পারে; কিছু নির্মাতার ব্যাটারি-সাশ্রয় নীতি আরও কড়া। (২) ছোট payload আর হারের সীমা — প্রদানকারী ঠিক করে, সময়ের সাথে বদলায়। (৩) নির্ভরতা: প্রদানকারীর availability আর নীতি আপনার পথে series-এ বসে (পর্ব ৫)। (৪) গোপনীয়তা: বার্তা প্রদানকারীর মধ্য দিয়ে যায়, তাই বিষয়বস্তু ন্যূনতম রাখা বা এনক্রিপ্ট করা ভালো — Web Push-এ payload এনক্রিপশন মানেরই অংশ।

তাই নকশার নিয়ম: push হলো দরজার কলিং বেল — “আপনার একটি নতুন বার্তা”; অ্যাপ খুললে নিজের সংযোগ নিয়ে resume করে আসল ডেটা আনে। আসল রেকর্ড থাকে আপনার সার্ভারে আর অ্যাপের ভেতরের তালিকায়, push-এ নয়। জরুরি কিছু (যেমন এককালীন কোড) হলে বিকল্প পথও লাগে — পর্ব ৫৭-এর notification case study-তে।

মূল উপলব্ধি: মোবাইলে real-time মানে অ্যাপ খোলা থাকলে নিজের সংযোগ, বন্ধ থাকলে OS-এর দরজায় কড়া নাড়া — আর দুটির মাঝের সেতু হলো resume.

অনুশীলন

খাতা-কলম আর উপরের ক্যালকুলেটরগুলো পাশে রাখুন। আগে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।

  1. সঠিক পথ বেছে নিন: চারটি কাজে কোন real-time পথ, আর কেন? (ক) একটি সংবাদ-সাইটের লাইভ ক্রিকেট পাতা: পিকে ৫ লাখ পাঠক, স্কোর বদলায় প্রতি ১৫–২০ সেকেন্ডে, পাঠক কিছু পাঠান না। (খ) একটি অনলাইন কুইজ: ২০০ জন খেলোয়াড়, প্রশ্ন আসে সার্ভার থেকে, উত্তর যায় সার্ভারে, লিডারবোর্ড বদলায় প্রতি সেকেন্ডে। (গ) একটি ride-sharing অ্যাপে (কাল্পনিক) যাত্রার সময় চালকের অবস্থান প্রতি ৪ সেকেন্ডে যাত্রীর পর্দায়। (ঘ) অ্যাপ বন্ধ থাকা অবস্থায় “আপনার অর্ডার রওনা হয়েছে” জানানো।

    (ক) SSE, অথবা আরও সহজে CDN-এ cache করা short polling. সবাই একই ডেটা পান; স্কোরের JSON কয়েক সেকেন্ডের TTL-এ CDN-এ রেখে প্রতি ১০–১৫ সেকেন্ডে poll করলে origin-এর ভার পাঠক-সংখ্যার ওপর প্রায় নির্ভর করে না, ৫ লাখ স্থায়ী সংযোগের gateway-বহরও লাগে না। দেরি কয়েক সেকেন্ড — স্কোরের জন্য যথেষ্ট। WebSocket অপ্রয়োজনীয়।

    (খ) WebSocket. দুই দিকে ঘনঘন ছোট বার্তা, কম দেরি জরুরি। ২০০ জনে একটি সার্ভারই যথেষ্ট, তবু reconnect-এ resume লাগে, আর প্রতিটি উত্তরে একটি id, যাতে retry-তে একই উত্তর দুবার গোনা না হয়।

    (গ) যাত্রীর দিকে WebSocket বা SSE; চালকের অবস্থান ওঠে WebSocket-এ বা POST-এ। শুধু সর্বশেষ অবস্থান জরুরি, জমে থাকা পুরোনোগুলো ফেলে দেওয়া চলে (পর্ব ৫৮)।

    (ঘ) Platform push service. অ্যাপ বন্ধ থাকলে নিজের সংযোগ নেই; push ঘুম ভাঙায়, বিস্তারিত আসে নিজের সার্ভার থেকে, আর অ্যাপের ভেতরের বিজ্ঞপ্তি-তালিকাই আসল রেকর্ড (পর্ব ৫৭)।

  2. Polling-এর অপচয় হিসাব করুন: একটি কাল্পনিক বিজ্ঞপ্তি-সেবায় ৩ লাখ ব্যবহারকারী একসাথে অনলাইন; প্রত্যেকে গড়ে প্রতি ১০ মিনিটে একটি বিজ্ঞপ্তি পান, এলোমেলো সময়ে। অ্যাপ প্রতি ১৫ সেকেন্ডে poll করে, আর প্রতিটি খালি অনুরোধ-উত্তরে মোটামুটি ১,০০০ বাইট যায়। (ক) অনুরোধ/সেকেন্ড আর গড় দেরি কত? (খ) সেকেন্ডে কতগুলো খালি, আর তাদের bandwidth কত? (গ) একজন এক ঘণ্টা অ্যাপ খোলা রাখলে শুধু poll-এ কত ডেটা যায়? (ঘ) ৩০ সেকেন্ড timeout-এর long polling-এ অনুরোধ/সেকেন্ড কত?

    (ক) ৩,০০,০০০ ÷ ১৫ = ২০,০০০ অনুরোধ/সেকেন্ড; গড় দেরি ১৫ ÷ ২ = ৭.৫ সেকেন্ড (+ নেটওয়ার্ক), সর্বোচ্চ ১৫।

    (খ) λ = ১/৬০০ প্রতি সেকেন্ডে। খালি হওয়ার সম্ভাবনা e−১৫/৬০০ = e−০.০২৫ ≈ ০.৯৭৫৩, তাই প্রায় ১৯,৫০৬টি খালি/সেকেন্ড (৯৭.৫%), কাজের মাত্র ~৪৯৪টি, যদিও বিজ্ঞপ্তি আসে ৫০০টি (কিছু poll দুটি নেয়)। খালিগুলোর bandwidth ≈ ১৯,৫০৬ × ১,০০০ বাইট ≈ ১৯.৫ MB/s, প্রায় ১৫৬ Mbit/s — শুধু খালি উত্তরে।

    (গ) ঘণ্টায় ৩,৬০০ ÷ ১৫ = ২৪০টি poll × ১ KB ≈ ২৪০ KB, প্রায় সবই খালি — ডেটা প্যাকে অদৃশ্য খরচ, সাথে ব্যাটারি।

    (ঘ) প্রতি client (১/৬০০) ÷ (১ − e−০.০৫) ≈ ০.০০১৬৬৭ ÷ ০.০৪৮৭৭ ≈ ০.০৩৪২; × ৩ লাখ ≈ ১০,২৫২ অনুরোধ/সেকেন্ড, যার ~৯,৭৫২টি timeout-এ খালি। বিজ্ঞপ্তি বিরল বলে long polling প্রায় “৩০ সেকেন্ডের poll” — অনুরোধ মাত্র অর্ধেক, কিন্তু দেরি ৭.৫ সেকেন্ড থেকে প্রায় শূন্যে। SSE বা WebSocket-এ অনুরোধ শূন্য, বার্তা ৫০০/সেকেন্ড, ৬০ সেকেন্ডের ping-এ সেকেন্ডে ৫,০০০টি ছোট frame — বিনিময়ে সবসময় ৩ লাখ খোলা সংযোগ।

  3. Presence নকশা করুন: একটি কাল্পনিক চ্যাট অ্যাপে পিকে ২০ লাখ ব্যবহারকারী অনলাইন। Heartbeat-এর বিরতি আর TTL কত, presence store-এ লেখার চাপ কত, নিঃশব্দ মৃত্যু কত দ্রুত ধরা পড়বে, আর flapping ও fan-out কীভাবে সামলাবেন?
    • বিরতি আর TTL: H = ৩০ সেকেন্ড, TTL = ৭৫ (২.৫H) — একটি হারানো heartbeat সহ্য হয়, পরপর দুটি হারালে অফলাইন। লেখা ২০,০০,০০০ ÷ ৩০ ≈ ৬৬,৬৬৭/সেকেন্ড; নিঃশব্দ মৃত্যু ধরা পড়ে ৪৫–৭৫ সেকেন্ডে। H = ৬০, TTL = ১৫০-এ লেখা ৩৩,৩৩৩, কিন্তু ধরা পড়তে ৯০–১৫০ সেকেন্ড। TTL = H হলে একটি দেরিতে আসা heartbeat-এই মিছিমিছি অফলাইন; TTL = ১ ঘণ্টা হলে বন্ধ ফোনও ঘণ্টাভর “অনলাইন”.
    • লেখা কমানো: heartbeat gateway-তেই থামে, আর gateway প্রতি H-এ তার সব ব্যবহারকারীর key ব্যাচে নবায়ন করে। আরও ভালো: প্রতি gateway-র একটি TTL-সহ lease key, আর “user → gateway” entry লেখা শুধু সংযোগ খোলা-বন্ধে; gateway মরলে lease শেষ, তার সব ব্যবহারকারী একসাথে অফলাইন।
    • Last seen আর flapping: শেষ heartbeat বা ইচ্ছাকৃত বেরোনোর সময়, মিনিটে গোল করা; “অফলাইন” প্রচারের আগে ১০–৩০ সেকেন্ড অপেক্ষা, এর মধ্যে ফিরলে বাতিল।
    • Fan-out: অবস্থা-বদল যায় শুধু যাঁদের পর্দায় মানুষটি দৃশ্যমান।
    • Consistency আর একাধিক ডিভাইস: কয়েক সেকেন্ড পুরোনো presence চলে — eventual, transaction লাগে না; অন্তত একটি ডিভাইস অনলাইন মানেই অনলাইন।
  4. আউটপুট অনুমান করুন: অংশ ৮-এর reconnect-storm কোডে MAX_BACKOFF = 32 বদলে MAX_BACKOFF = 4 করলে full jitter-এর সারিতে কী বদলাবে? মোট চেষ্টা বাড়বে না কমবে, অর্ধেক আর ৯৫% কখন ফিরবেন, শেষ client কখন? No backoff-এর সারি কি বদলাবে? আগে লিখে রাখুন, তারপর চালিয়ে মিলিয়ে নিন।

    No backoff-এর সারি হুবহু একই থাকে — সে MAX_BACKOFF ব্যবহারই করে না: no backoff : half back 14 s, 95% back 19 s, all back 20 s, tries 632094. Full jitter-এর শেষ লাইন হয়:

    full jitter: half back 8 s, 95% back 13 s, all back 16 s, tries 242102

    সর্বোচ্চ অপেক্ষা ৪ সেকেন্ডে আটকে যাওয়ায় লম্বা লেজ উধাও — শেষ client ৪০-এর বদলে ১৬ সেকেন্ডে, ৯৫% দুই সেকেন্ড আগে। কিন্তু ব্যর্থরা ঘন ঘন ফেরে বলে মোট চেষ্টা ২,১৯,৫৩২ থেকে বেড়ে ২,৪২,১০২: সেকেন্ড ৪ থেকে ১১ পর্যন্ত চেষ্টা সামর্থ্যের ওপরে (যেমন সেকেন্ড ৬-এ 16065 (4037)), অর্থাৎ প্রত্যাখ্যানে অপচয় বেশি। সেকেন্ড ০–৩-এর সারি হুবহু আগের মতো, কারণ তখনো কারও অপেক্ষার সীমা ২k চার পেরোয়নি। শিক্ষা: ছোট সীমা দ্রুত পুনরুদ্ধার দেয় যদি সার্ভার দ্রুত ফেরে; কিন্তু সার্ভার ধরুন ২ মিনিট বন্ধ থাকলে প্রতিটি client প্রতি কয়েক সেকেন্ডে ঠোকা দেবে — তখন বড় সীমাই নিরাপদ। বাস্তবে এই সীমা প্রায়ই কয়েক দশ সেকেন্ড থেকে কয়েক মিনিট।

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

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

ক্রমআলাদা gateway-তে থাকা দুজনের মধ্যে একটি চ্যাট বার্তার পথ সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📱 A-র ফোন G1-এ WebSocket frame পাঠায়, সাথে বার্তা-id
  2. 💾 বার্তা database-এ লেখা হয়, seq মেলে, A পান ack
  3. 🔎 Registry দেখে জানা যায় B এখন G7-এ
  4. 📢 G7-এর channel-এ বার্তাটি publish হয়
  5. 🚪 G7 বার্তাটি B-র socket-এ লেখে
  6. ✅ B-র delivered ack ফিরে এসে A-র পর্দায় দুটি টিক

একটি অ্যাপ প্রতি ১০ সেকেন্ডে short polling করে, আর খবর আসে এলোমেলো সময়ে। নেটওয়ার্কের সময় বাদ দিলে একটি খবর পর্দায় আসতে গড়ে কত দেরি হয়?

  • ১০ সেকেন্ড
  • ৫ সেকেন্ড
  • প্রায় শূন্য
  • ২০ সেকেন্ড

SSE-র সংযোগ ভেঙে আবার জোড়ার সময় ব্রাউজারের EventSource কোন তথ্য পাঠায়, যাতে মাঝের হারানো ঘটনাগুলো আবার পাওয়া যায়?

  • Sec-WebSocket-Key header
  • If-None-Match header
  • retry: field
  • Last-Event-ID header-এ শেষ পাওয়া ঘটনার id

নিচের কোন কাজে WebSocket-এর চেয়ে SSE বেশি মানানসই?

  • লাখো পাঠকের কাছে লাইভ স্কোর পাঠানো, যেখানে পাঠক কিছু পাঠান না
  • একসাথে একটি নথি সম্পাদনা, যেখানে সবাই ঘনঘন লেখেন
  • প্রতি ৫০ ms-এ দুই দিকে binary খেলার অবস্থান
  • অ্যাপ বন্ধ থাকা অবস্থায় বিজ্ঞপ্তি পাঠানো

ব্যবহারকারী A-র WebSocket সংযোগ gateway G1-এ, B-র G7-এ; gateway-গুলো stateless backend আর একটি pub/sub backplane দিয়ে জোড়া। A-র বার্তা B-র কাছে পৌঁছানোর সঠিক পথ কোনটি?

  • G1 সরাসরি B-র ফোনে নতুন একটি TCP সংযোগ খোলে
  • Load balancer B-র সংযোগ G7 থেকে G1-এ সরিয়ে আনে
  • বার্তা সংরক্ষণ করে registry থেকে B-র gateway জেনে সেই gateway-র channel-এ publish করা হয়; G7 B-র socket-এ লেখে
  • G1 প্রতিটি বার্তায় সব gateway-কে HTTP অনুরোধে জিজ্ঞেস করে B কোথায়

৫০,০০০ client-এর একটি gateway restart হলো; প্রতিটি client ব্যর্থ হলে ঠিক পরের সেকেন্ডেই আবার চেষ্টা করে। এই reconnect storm সামলানোর সবচেয়ে কার্যকর পদক্ষেপ কোনটি?

  • client-দের retry-র বিরতি ১ থেকে ০.১ সেকেন্ডে নামানো
  • সব client-কে ঠিক ৫ সেকেন্ড পর একসাথে ফিরতে বলা
  • Load balancer-এর idle timeout বাড়ানো
  • Exponential backoff আর full jitter, সাথে নতুন handshake-এর সীমা ও সস্তা প্রত্যাখ্যান

একটি presence ব্যবস্থায় client প্রতি ৩০ সেকেন্ডে heartbeat পাঠায়, আর প্রতিটি heartbeat presence key-র TTL নবায়ন করে। TTL কত রাখা সবচেয়ে যুক্তিসংগত?

  • ৭৫ সেকেন্ড
  • ৩০ সেকেন্ড
  • ১ ঘণ্টা
  • TTL নয়; শুধু close frame এলে অফলাইন

শব্দকোষ

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

মনে রাখুন

  1. HTTP-তে প্রশ্ন করে client; push পাওয়ার চার পথ — short polling, long polling, SSE, WebSocket — আর অ্যাপ বন্ধ থাকলে platform push.
  2. Short polling: অনুরোধ N/I, গড় দেরি I/২, খালি হওয়ার সম্ভাবনা e−λI — দেরি × অনুরোধ = N/২, স্থির।
  3. SSE এক দিকের HTTP স্রোত, নিজেই reconnect আর Last-Event-ID দিয়ে resume করে; WebSocket দুই দিকের নালি — ack, seq, resume আর heartbeat নিজেকে বানাতে হয়।
  4. স্থায়ী সংযোগে capacity মাপে memory আর সংযোগ-সংখ্যা; gateway + registry + pub/sub যেকোনো সার্ভার থেকে যেকোনো ব্যবহারকারীর পথ; presence = heartbeat + TTL.
  5. Reconnect storm ঠেকায় exponential backoff + full jitter, admission control আর ধীর draining; মোবাইলে ব্যাটারি, NAT timeout আর নেটওয়ার্ক বদল মাথায় রেখে keep-alive আর resume.

পরের পর্বে: সার্ভারের সামনে কে দাঁড়ায় — reverse proxy, API gateway আর BFF: TLS, routing, rate limiting, আর এই পর্বের WebSocket-SSE সংযোগ সামলানো “সামনের দরজা”.

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

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