এই পর্বে যা শিখবেন
- State কী, কোথায় লুকিয়ে থাকে, আর stateful সার্ভার কেন horizontal scaling ও failover আটকায়
- Sticky session (cookie আর IP affinity) কীভাবে চলে আর কোথায় ভাঙে — seeded simulation-সহ
- Shared session store-এর নকশা: cookie-র flag, TTL আর sliding expiry, memory-র হিসাব, HA আর store বিকল হলে কী হয়
- JWT-এর গঠন, HMAC বনাম asymmetric স্বাক্ষর, claim আর revocation সমস্যা — নিজে HMAC-SHA256 লিখে test vector-এ মিলিয়ে
- Encrypted cookie, চার পথের তুলনা, অন্য state, কখন stateful-ই সঠিক, আর deploy-এর সময় session
১ · State কী — সার্ভার যা মনে রাখে
একটি ব্যাংকের তিনটি কাউন্টারের কথা ভাবুন। প্রথম ব্যবস্থায় প্রতিটি ক্যাশিয়ার নিজের খাতায় অর্ধেক-শেষ কাজ লিখে রাখেন: “রহিম সাহেবের চেক যাচাই হয়েছে, টাকা দেওয়া বাকি।” রহিম সাহেবকে তখন ঠিক সেই কাউন্টারেই ফিরতে হবে। ক্যাশিয়ার দুপুরের খাবারে গেলে কাজ আটকে থাকে, অসুস্থ হলে খাতাসহ সব হারায়। দ্বিতীয় ব্যবস্থায় সব হিসাব থাকে ব্যাংকের কেন্দ্রীয় খাতায়, আর গ্রাহকের হাতে থাকে একটি টোকেন নম্বর। যেকোনো কাউন্টার টোকেন দেখে কেন্দ্রীয় খাতা থেকে বাকিটা জেনে নেন। নতুন কাউন্টার খোলা বা একটি বন্ধ করা তখন সহজ।
প্রথমটি stateful কাউন্টার, দ্বিতীয়টি stateless. State হলো সেই তথ্য, যা একটি অনুরোধ শেষ হওয়ার পরও টিকে থাকে আর পরের অনুরোধের উত্তর বদলে দেয়। প্রায় প্রতিটি অ্যাপেই state আছে; তাই আসল প্রশ্ন “state আছে কি না” নয়, বরং state কোথায় থাকে. অ্যাপ সার্ভারের নিজের memory বা disk-এ থাকলে সার্ভারটি stateful. Database, cache, object storage বা client-এর কাছে সরিয়ে দিলে অ্যাপ সার্ভার stateless.
| State | উদাহরণ | সার্ভারের memory-তে রাখলে বিপদ | কোথায় সরানো যায় |
|---|---|---|---|
| Login session | কে লগইন করেছেন, কোন ভূমিকা | সার্ভার পড়লে তার সবাই লগআউট | Shared session store, বা স্বাক্ষর করা token (অংশ ৩, ৫) |
| Shopping cart | চাল ৫ কেজি, ডাল ১ কেজি | কেনাকাটার মাঝপথে cart উধাও | Database-এ, user ID ধরে |
| Upload-এর অগ্রগতি | ২০০ MB ভিডিওর ১৪০ MB পৌঁছেছে | পরের অংশ অন্য সার্ভারে গেলে আবার শুরু থেকে | Object storage-এর multipart upload (পর্ব ২৬) |
| In-memory cache | পণ্যের দাম, config | কপিগুলো আলাদা মান দেখায় | Shared cache, ছোট TTL (পর্ব ৩০) |
| WebSocket সংযোগ | খোলা socket, subscription | সংযোগ অন্য সার্ভারে সরানো যায় না | আলাদা gateway স্তর আর registry (পর্ব ১০) |
| Scheduled job | রাত ২টার রিপোর্ট | ১০টি কপি থাকলে ১০ বার চলে | আলাদা scheduler, lock (পর্ব ৩৭) |
Stateful সার্ভার কেন বাধা হয়ে দাঁড়ায়
- Scale out আটকে যায়: নতুন সার্ভার যোগ করলে সে খালি। পুরোনো ব্যবহারকারীরা পুরোনো সার্ভারেই বাঁধা, তাই নতুন যন্ত্র সঙ্গে সঙ্গে ভার নেয় না। আর autoscaling-এ সার্ভার কমানো মানে তার ব্যবহারকারীদের state হারানো।
- Failover অর্থহীন হয়: সার্ভার পড়লে তার memory-ও যায়। পাশের replica সুস্থ থাকলেও তার কাছে সেই state নেই।
- Deploy মানে লগআউট: rolling deployment-এ প্রতিটি সার্ভার একবার restart হয়, আর প্রতিবার কিছু মানুষের কাজ হারায়।
পর্ব ১৩-এ stateless নীতি দেখেছি: যেকোনো কপি যেকোনো অনুরোধ সামলাবে, তবেই horizontal scaling সহজ। এই পর্বের প্রশ্ন তার পরের ধাপ: নীতিটি মানলে state যায় কোথায়?
HTTP নিজেই stateless — সার্ভার আগের অনুরোধ মনে রাখে না (REST-এর এই নিয়ম পর্ব ৮-এ)। তাই সার্ভার Set-Cookie header-এ একটি ছোট নাম-মান — cookie — পাঠায়, আর ব্রাউজার পরের প্রতিটি অনুরোধে সেটি ফেরত দেয় (ইতিহাস ভাবনার প্রশ্ন ১-এ, খুঁটিনাটি CN কোর্সের HTTP পর্বে)। এভাবে পরপর কয়েকটি অনুরোধকে “একই মানুষের একই বসা” হিসেবে জোড়াই session.
Stateless সার্ভিস মানে state নেই — তা নয়। মানে state সার্ভারের বাইরে: এমন জায়গায়, যা সব কপি দেখতে পায় (shared store), অথবা প্রতিটি অনুরোধ নিজেই বয়ে আনে (স্বাক্ষর করা token-এ)। সার্ভার তখন রেলস্টেশনের যেকোনো টিকিট কাউন্টারের মতো — যেটি খালি, সেখানেই দাঁড়ালে চলে।
২ · Sticky session — সহজ পথ, আর তার দাম
সবচেয়ে কম পরিশ্রমের সমাধান: session সার্ভারের memory-তেই থাক, শুধু load balancer নিশ্চিত করুক একই মানুষ সবসময় একই সার্ভারে যান। একে বলে sticky session বা session affinity. Load balancer-এর algorithm আর L4-L7-এর পার্থক্য পর্ব ১৪-এ; এখানে শুধু বাঁধনের দুটি উপায়:
- Cookie-ভিত্তিক: প্রথম উত্তরে load balancer নিজে একটি cookie বসায় (যেমন
SRV=s2), পরের অনুরোধে সেটি দেখে একই সার্ভারে পাঠায়। এর জন্য HTTP পড়তে হয়, অর্থাৎ L7 load balancer লাগে। - IP-ভিত্তিক (IP hash): client-এর ঠিকানার hash থেকে সার্ভার — cookie লাগে না, L4-এও চলে। কিন্তু CGNAT-এর পেছনে হাজারো মোবাইল ব্যবহারকারী একই public IP ভাগ করেন, সবাই পড়েন এক সার্ভারে; Nginx-এর
ip_hashতো IPv4-এর প্রথম তিন অংশ (octet) দেখে। আর Wi-Fi থেকে মোবাইল ডেটায় গেলে IP বদলায়, বাঁধন ছুটে যায়।
# HAProxy-ধাঁচ: load balancer নিজে SRV cookie বসায়
backend app
balance roundrobin
cookie SRV insert indirect nocache httponly secure
server s1 10.0.1.11:8080 check cookie s1
server s2 10.0.1.12:8080 check cookie s2
server s3 10.0.1.13:8080 check cookie s3
# Nginx-ধাঁচ: client IP-র hash (IPv4-এর প্রথম তিন octet)
upstream app {
ip_hash;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
server 10.0.1.13:8080;
}দাম চার রকম
- অসম ভার: সমান সংখ্যক session মানেই সমান ভার নয় — কেউ মিনিটে ২টি অনুরোধ করেন, কেউ (ধরুন একটি দোকানের POS যন্ত্র) কয়েকশো, আর sticky-তে তাঁর পুরো ভার একটি সার্ভারেই।
- সার্ভার পড়লে session হারায়: memory-র সব session, cart আর অর্ধেক-ভরা ফর্ম উধাও।
- Deploy-এর যন্ত্রণা: restart-এর আগে হয় সবাইকে লগআউট, নয়তো session শেষ হওয়া পর্যন্ত অপেক্ষা — ৩০ মিনিটের idle timeout হলে প্রতি সার্ভারে প্রায় ৩০ মিনিট; ফেরার পর সার্ভারটি খালি।
- Scale in কঠিন: ভিড় কমলে কোন সার্ভার বন্ধ করবেন? প্রতিটিতেই কারও না কারও session আছে।
নিচের ডায়াগ্রামে তিনটি সার্ভারে ২০০ করে session; রিনা S2-তে বাঁধা, তাঁর cart-এ বাজার। “পরে” চেপে দেখুন S2 পড়লে কী ঘটে:
| Cookie | |
| Cart |
- প্রথম login. রিনার কাছে কোনো
SRVcookie নেই, তাই load balancer round robin-এ S2 বাছল। S2 নিজের memory-তে session বানাল, আর উত্তরে গেল দুটি cookie: load balancer-এরSRV=s2আর অ্যাপেরsid. তিন সার্ভারে ২০০টি করে session আছে। - বাঁধন কাজ করছে। রিনা cart-এ চাল আর ডাল যোগ করলেন;
SRV=s2দেখে load balancer প্রতিবার S2-তেই পাঠায়। S1 আর S3-এর memory-তে রিনার কিছুই নেই। - S2 পড়ে গেল। Memory ফুরিয়ে প্রসেসটি মারা গেল। Health check পরপর তিনবার ব্যর্থ না হওয়া পর্যন্ত (প্রায় ১০–১৫ সেকেন্ড) load balancer তখনো রিনাকে S2-তে পাঠায় — উত্তর আসে
502. - অন্য সার্ভার, কিন্তু খালি হাত। S2 এখন “down”; load balancer রিনাকে S1-এ পাঠিয়ে cookie বদলাল। S1-এর memory-তে
k3J…নেই — আবার login, cart খালি। S2-এর ২০০ জনেরই একই দশা (simulation-এ ৬৭ জনের cart-এ জিনিস ছিল); আবার login করে তাঁরা S1 আর S3-তে ভাগ হলেন, ৩০০ আর ৩০০। - S2 ফিরল, কিন্তু ভার ফিরল না। S2-এর memory খালি; পুরোনোরা S1/S3-তে বাঁধা, শুধু নতুন ১৫০ জন যান S2-তে। Session ৩০০ / ১৫০ / ৩০০, মিনিটে অনুরোধ 1688 / 696 / 2123 — মেয়াদ না ফুরানো পর্যন্ত এই অসমতা চলবে (সংখ্যাগুলো অংশ ৩-এর simulation থেকে)।
২০১১ সালের দিকে Heroku-র Adam Wiggins-এর লেখা “The Twelve-Factor App” নির্দেশিকা সরাসরি বলে: sticky session এই নীতির লঙ্ঘন, এর ওপর কখনো ভরসা করবেন না; session-এর তথ্য রাখুন মেয়াদ-সহ এমন store-এ, যেমন Memcached বা Redis. কারণটি বাস্তব: PHP-র মতো ভাষার ডিফল্ট session ব্যবস্থা সার্ভারের নিজের disk-এ ফাইল হিসেবে session রাখে। একটি সার্ভারে নিখুঁত চলে; দ্বিতীয় সার্ভার যোগ করার দিনই ব্যবহারকারীরা এলোমেলোভাবে লগআউট হতে থাকেন।
Sticky কখন চলে: যখন stickiness শুধু গতির জন্য, সঠিকতার জন্য নয় — যেমন একই সার্ভারে গেলে সেখানকার local cache গরম থাকে, অন্য সার্ভারে গেলে শুধু একটু ধীর। Long polling fallback-এর জন্যও অনেক library sticky চায় (পর্ব ১০)। নিয়ম: সার্ভার বদলালেও সব ঠিকঠাক চলা চাই।
৩ · Shared session store — cookie-তে শুধু চাবি, তথ্য store-এ
অংশ ১-এর দ্বিতীয় ব্যাংকটিই এখানে নকশা। ব্রাউজারের হাতে শুধু একটি টোকেন — session ID; তাতে কোনো তথ্য লেখা নেই, শুধু একটি দীর্ঘ এলোমেলো নম্বর। আসল তথ্য (কে, কোন ভূমিকা, CSRF token) থাকে সব সার্ভারের ভাগ করা session store-এ, ID ধরে। এই store হতে পারে Redis বা Memcached-ধাঁচের in-memory key-value store, কিংবা ছোট অ্যাপে একটি database টেবিল (Redis-এর গভীরে DBMS কোর্সের Redis পর্ব)।
Session ID কেমন হবে
- অনুমান-অযোগ্য: নিরাপদ random generator থেকে। OWASP-এর session management নির্দেশিকা অন্তত ৬৪ বিট entropy চায়; অনেক দল ১২৮ বিট নেয় — Base64URL-এ ঠিক ২২ অক্ষর। ID-তে user ID, সময় বা ক্রমিক নম্বর নয়।
- Login-এর মুহূর্তে নতুন ID: login-এর আগের ID পরে চলতে দিলে session fixation-এর ঝুঁকি — আগে থেকে ID জানা কেউ লগইন-করা session পেয়ে যান। একই কারণে ভূমিকা বদলালেও (যেমন admin হলে) নতুন ID দেওয়া হয়।
- Session-এ শুধু “আপনি কে”: cart, অর্ডার বা ব্যালান্স ব্যবসার ডেটা — database-এ, user ID ধরে। Session ছোট হলে store ছোট, দ্রুত আর হারালেও ক্ষতি কম।
Cookie-র পাঁচটি flag
| Attribute | কী করে | কেন দরকার |
|---|---|---|
HttpOnly | পাতার JavaScript cookie পড়তে পারে না | পাতায় ঢুকে পড়া script (XSS) session ID চুরি করতে পারে না |
Secure | শুধু HTTPS-এ পাঠানো হয় | খোলা Wi-Fi-তে এনক্রিপশনহীন পথে ফাঁস হয় না |
SameSite=Lax বা Strict | অন্য সাইট থেকে শুরু হওয়া অনুরোধে cookie পাঠানো সীমিত | CSRF-এর ঝুঁকি কমায়; Lax-এ অন্য সাইটের সাধারণ link-এ যায়, তাদের form POST-এ নয় |
Path=/, Domain না দেওয়া | cookie শুধু এই host-এর | subdomain-এ ছড়ায় না; নামের আগে __Host- দিলে ব্রাউজার এটি বাধ্য করে |
Max-Age বা Expires না দেওয়া | “session cookie” — ব্রাউজার বন্ধে সাধারণত মোছে | আসল মেয়াদ ঠিক করে সার্ভারের TTL, cookie নয় |
Chrome, Edge-এর মতো Chromium-ভিত্তিক ব্রাউজার SameSite না লিখলে Lax ধরে, কিন্তু Firefox আর Safari-র ডিফল্ট আলাদা (সংস্করণভেদে বদলায়) — তাই নিজে লিখুন; CSRF-এর পুরো আলোচনা CN কোর্সের HTTP পর্বের অংশ ৮-এ।
POST /login HTTP/1.1
Host: shop.example
Content-Type: application/x-www-form-urlencoded
user=rina&password=••••••••
HTTP/1.1 303 See Other
Location: /orders
Set-Cookie: __Host-sid=q7Lx2-Vd9KfT0mWbA8sR4u; Path=/; Secure; HttpOnly; SameSite=Lax
GET /orders HTTP/1.1
Host: shop.example
Cookie: __Host-sid=q7Lx2-Vd9KfT0mWbA8sR4uSET sess:q7Lx2-Vd9KfT0mWbA8sR4u '{"uid":1042,"role":"customer","csrf":"Pq9…"}' EX 1800 NX
GET sess:q7Lx2-Vd9KfT0mWbA8sR4u # প্রতিটি অনুরোধে, যেকোনো সার্ভার থেকে
TTL sess:q7Lx2-Vd9KfT0mWbA8sR4u # → 640 (বাকি সেকেন্ড)
EXPIRE sess:q7Lx2-Vd9KfT0mWbA8sR4u 1800 # sliding: বাকি মেয়াদ অর্ধেকের কম হলে নবায়ন
DEL sess:q7Lx2-Vd9KfT0mWbA8sR4u # logout: সঙ্গে সঙ্গে বাতিলমেয়াদ — TTL, sliding আর absolute
- TTL: store নিজেই মেয়াদ শেষে key মুছে দেয়; পুরোনো session পরিষ্কারের আলাদা কাজ লাগে না।
- Sliding expiration: সক্রিয় থাকলে মেয়াদ নবায়ন, ৩০ মিনিট নিষ্ক্রিয় থাকলে শেষ। দাম: প্রতিটি পড়া একটি লেখাও হয়ে যায় — তাই বাকি মেয়াদ অর্ধেকের নিচে নামলে তবেই নবায়ন।
- Absolute timeout: নবায়ন যতই চলুক, login-এর ধরা যাক ১২ ঘণ্টা পর আবার login — চুরি যাওয়া session অনন্তকাল চলে না। ব্যাংকিং অ্যাপে idle timeout আরও ছোট, কয়েক মিনিট।
জীবিত session কতগুলো, তা পর্ব ৪-এর Little-এর সূত্র থেকেই আসে: জীবিত session = নতুন login-এর হার × একটি session-এর গড় আয়ু. সেকেন্ডে ৫০০টি login, আর গড়ে ৭০ মিনিট ব্যবহার + ৩০ মিনিট idle timeout = ১০০ মিনিট (৬,০০০ সেকেন্ড) আয়ু হলে ৫০০ × ৬,০০০ = ৩০ লাখ session — অংশ ৪-এর হিসাব এই সংখ্যা দিয়েই।
Stateless অ্যাপ সার্ভার — দুই পথে state
LB → stateless app servers → shared session store (path ক) · signed JWT verified locally (path খ)
এবার পথ ক ধাপে ধাপে। খেয়াল করুন, login হলো App 1-এ, অথচ পরের অনুরোধ গেল App 3-এ — তবু কিছু হারাল না:
- ব্রাউজার
রিনা - Load balancer
round robinsticky নয় statelessযেকোনোটি- Session store
sess:q7Lx2…সবার ভাগ করা
| ব্রাউজারের cookie | |
| Store-এ sess:q7Lx2… | |
| অনুরোধ সামলাল |
- Login-এর অনুরোধ। রিনা HTTPS-এ নাম আর password পাঠালেন; round robin-এ App 1 অনুরোধটি পেয়ে password-এর hash মিলিয়ে দেখল — ঠিক আছে।
- নতুন session, store-এ। App 1 নতুন ১২৮-বিট এলোমেলো ID বানাল (আগের কোনো ID থাকলে বাতিল) আর store-এ লিখল
sess:q7Lx2…→ uid 1042, customer, TTL ১,৮০০ সেকেন্ড; নিজের memory-তে কিছুই রাখল না। - Cookie-তে শুধু চাবি। উত্তরে
Set-Cookie: __Host-sid=q7Lx2…; Path=/; Secure; HttpOnly; SameSite=Lax. পাতার কোনো script এটি পড়তে পারবে না, আর এতে নাম, ভূমিকা বা cart কিছুই নেই। - পরের অনুরোধ অন্য সার্ভারে। রিনা অর্ডারের তালিকা খুললেন; round robin-এ এবার App 3. App 3 আগে কখনো রিনাকে দেখেনি, তবু store থেকে
GET sess:q7Lx2…→ uid 1042 পেয়ে গেল (একই data center-এ সাধারণত এক মিলিসেকেন্ডের কম)। বাকি মেয়াদ ১,৭৯৭ সেকেন্ড — অর্ধেকের অনেক ওপরে, তাই নবায়নের লেখা লাগল না; অর্ধেকের নিচে নামলে তবেইEXPIRE. - Logout — সঙ্গে সঙ্গে কার্যকর। এবার App 2 অনুরোধটি পেল; সে store থেকে key মুছে দিল আর cookie মেয়াদোত্তীর্ণ করল। কেউ পুরোনো cookie-টি নকল করে রাখলেও লাভ নেই — store-এ আর কিছু নেই, পরের অনুরোধেই
401. এই সহজ revocation-ই shared store-এর সবচেয়ে বড় গুণ।
দুই পথ পাশাপাশি — একটি seeded simulation
এবার মাপি। ৬০০ জন ব্যবহারকারী, তিনটি সার্ভার; মিনিটে অনুরোধের সংখ্যা ভারী-লেজের (Pareto) বণ্টন থেকে — বেশিরভাগ হালকা, অল্প কয়েকজন খুব ব্যস্ত; ৩০%-এর cart-এ জিনিস — সব ধরা যাক। Sticky-তে login-এর সময় প্রত্যেকে সবচেয়ে কম session-এর সার্ভারে বাঁধা হন; shared store-এ load balancer প্রতিটি অনুরোধ আলাদাভাবে round robin-এ পাঠায়। Seed নির্দিষ্ট, তাই প্রতিবার একই ফল:
// Sticky sessions vs a shared session store: S2 crashes, then restarts empty (seeded)
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 SERVERS = 3, USERS = 600, NEW = 150, rand = mulberry32(15);
// ধরা যাক: requests/min per user are heavy-tailed (Pareto, alpha 1.5, min 2); 30% have a cart
const rate = Array.from({ length: USERS + NEW }, () => Math.round(2 / (1 - rand()) ** (1 / 1.5)));
const cart = rate.map(() => rand() < 0.3);
function show(label, load, note = "") {
const live = load.filter(x => x !== null), avg = live.reduce((s, x) => s + x, 0) / live.length;
console.log(label.padEnd(16) + load.map((x, i) => ("S" + (i + 1) + "=" + (x === null ? "down" : x)).padStart(9)).join("") +
" max/avg " + (Math.max(...live) / avg).toFixed(2) + note);
}
// ---- (a) sticky: at login a user is pinned to the live server holding the fewest sessions
const up = Array(SERVERS).fill(true), pin = [];
const count = s => pin.filter(p => p === s).length;
const login = u => { let best = -1;
for (let s = 0; s < SERVERS; s++) if (up[s] && (best < 0 || count(s) < count(best))) best = s; pin[u] = best; };
const sticky = () => up.map((ok, s) => ok ? rate.reduce((sum, x, u) => sum + (pin[u] === s ? x : 0), 0) : null);
console.log("req/min per server: " + USERS + " users log in; S2 dies; S2 returns, " + NEW + " new users log in");
for (let u = 0; u < USERS; u++) login(u);
show("sticky before", sticky());
up[1] = false; const lost = pin.map((p, u) => p === 1 ? u : -1).filter(u => u >= 0);
lost.forEach(login); // they must log in again elsewhere
show("sticky S2 dead", sticky(), " lost " + lost.length + " sessions, " + lost.filter(u => cart[u]).length + " carts");
up[1] = true; for (let u = USERS; u < USERS + NEW; u++) login(u);
show("sticky S2 back", sticky(), " sessions " + up.map((_, s) => count(s)).join("/"));
// ---- (b) shared store: any server reads any session, so the LB spreads every request
const shared = (users, alive) => { const total = rate.slice(0, users).reduce((s, x) => s + x, 0);
const k = alive.filter(Boolean).length; let i = 0;
return alive.map(ok => ok ? Math.floor(total / k) + (i++ < total % k ? 1 : 0) : null); };
const all = Array(SERVERS).fill(true), s2down = all.map((_, s) => s !== 1);
show("shared before", shared(USERS, all));
show("shared S2 dead", shared(USERS, s2down), " lost 0 sessions, 0 carts");
show("shared S2 back", shared(USERS + NEW, all));আউটপুট:
req/min per server: 600 users log in; S2 dies; S2 returns, 150 new users log in
sticky before S1=1121 S2=1032 S3=1658 max/avg 1.31
sticky S2 dead S1=1688 S2=down S3=2123 max/avg 1.11 lost 200 sessions, 67 carts
sticky S2 back S1=1688 S2=696 S3=2123 max/avg 1.41 sessions 300/150/300
shared before S1=1271 S2=1270 S3=1270 max/avg 1.00
shared S2 dead S1=1906 S2=down S3=1905 max/avg 1.00 lost 0 sessions, 0 carts
shared S2 back S1=1503 S2=1502 S3=1502 max/avg 1.00
প্রথম লাইনেই চমক: তিন সার্ভারে session ঠিক ২০০ করে, তবু অনুরোধ 1121, 1032, 1658 — সবচেয়ে ভারী সার্ভার গড়ের ১.৩১ গুণ। কারণ একজন খুব ব্যস্ত ব্যবহারকারী (মিনিটে ৭১৬টি অনুরোধ) S3-তে বাঁধা; তাঁর পুরো ভার একটি সার্ভারেই। S2 মরলে lost 200 sessions, 67 carts — ২০০ জনের আবার login, ৬৭টি cart উধাও। S2 ফেরার পর session 300/150/300, অনুরোধ 696 বনাম 2123: নতুন সার্ভারটি প্রায় অলস, আর ভারী সার্ভারটি তিন গুণ ব্যস্ত; max/avg 1.41. Shared store-এ তিনটি লাইনেই max/avg 1.00 — S2 মরলে বাকি দুটি সমান ভাগে নেয়, ফিরলে সঙ্গে সঙ্গে আবার তিন ভাগ, কেউ লগআউট হননি। মোট ভার দুই পথেই এক (৩,৮১১, পরে ৪,৫০৭); পার্থক্য শুধু state কোথায়। SERVERS বদলে নিজে চালিয়ে দেখুন (অনুশীলন ৪)।
৪ · Session store-এর মাপ, HA আর store বিকল হলে
Store-এ কত memory লাগবে, তা পর্ব ৩-এর ধাঁচে হিসাব করা যায়। প্রতি session-এ খরচ = key + value + store-এর নিজের হিসাব-খাতা (প্রতি key-তে কয়েক দশ বাইট, সংস্করণভেদে আলাদা), তার ওপর memory allocator-এর ভাঙা জায়গা, অর্থাৎ fragmentation; তারপর replica-র জন্য গুণ। নিচে ৩০ লাখ জীবিত session ধরে চারটি value-আকারে হিসাব; সব সংখ্যা ধরা যাক:
// Session store sizing — ধরা যাক: every input is an assumption; measure your own store
const active = 3000000; // sessions alive (inside their TTL) at peak
const keyBytes = 48; // "sess:" + a 22-character random id, plus a prefix
const perKeyOverhead = 80; // the store's own bookkeeping per key (version-dependent)
const frag = 1.25; // memory allocator fragmentation factor
const copies = 2; // primary + 1 replica (survive one node)
const nodeGiB = 32, maxUse = 0.6; // per node; keep 40% headroom (snapshot, spikes, failover)
const qps = 40000; // requests/s that read the session
const GiB = 1024 ** 3, usable = nodeGiB * maxUse;
console.log("usable per node: " + usable.toFixed(1) + " GiB");
console.log("value B bytes/session one copy all copies shards nodes read MB/s");
for (const value of [512, 2048, 8192, 32768]) {
const per = (keyBytes + value + perKeyOverhead) * frag;
const one = active * per / GiB, shards = Math.ceil(one / usable);
console.log(String(value).padStart(7) + String(per).padStart(15) + (one.toFixed(1) + " GiB").padStart(10) +
((one * copies).toFixed(1) + " GiB").padStart(12) + String(shards).padStart(8) +
String(shards * copies).padStart(7) + (qps * (keyBytes + value) / 1e6).toFixed(1).padStart(11));
}আউটপুট:
usable per node: 19.2 GiB
value B bytes/session one copy all copies shards nodes read MB/s
512 800 2.2 GiB 4.5 GiB 1 2 22.4
2048 2720 7.6 GiB 15.2 GiB 1 2 83.8
8192 10400 29.1 GiB 58.1 GiB 2 4 329.6
32768 41120 114.9 GiB 229.8 GiB 6 12 1312.6
প্রতি node-এ ৩২ GiB-এর ৬০% = 19.2 GiB ব্যবহারযোগ্য; বাকি ৪০% snapshot, হঠাৎ ভিড় আর failover-এর headroom. ২ KiB value-তে প্রতি session (৪৮ + ২,০৪৮ + ৮০) × ১.২৫ = 2720 বাইট, ৩০ লাখে এক কপি 7.6 GiB — একটি node-ই যথেষ্ট; তবু node 2টি, কারণ দ্বিতীয়টি memory-র জন্য নয়, availability-র জন্য। সবচেয়ে বড় শিক্ষা শেষ সারিতে: পুরো cart আর সাম্প্রতিক অর্ডার session-এ ঢোকালে value ৩২ KiB, এক কপি 114.9 GiB, 6টি shard আর 12টি node. শুধু memory নয় — প্রতি সেকেন্ডে ৪০,০০০ অনুরোধে store থেকে পড়া হয় 1312.6 MB, প্রায় ১০.৫ Gbit/s. এখানে GiB মানে 10243 বাইট, MB মানে 106.
Store এখন নতুন single point of failure
সব অ্যাপ সার্ভার stateless হলো, কিন্তু state গিয়ে জমল এক জায়গায়। Store পড়লে একসাথে সবাই লগআউট বা সব অনুরোধ ব্যর্থ — নতুন single point of failure. তাই:
- Replica আর স্বয়ংক্রিয় failover: Redis-এ Sentinel বা Cluster primary-র মৃত্যু টের পেয়ে replica-কে primary বানায়; Memcached-এর মূল প্রকল্পে replication নেই, তাই node হারালে তার session-ও হারায়।
- Asynchronous replication-এর ফাঁক: Redis-এর replication ডিফল্টে asynchronous — primary পড়ার মুহূর্তের শেষ লেখাগুলো replica-তে না-ও পৌঁছাতে পারে, ফলে সদ্য login করা কয়েকজনকে আবার login করতে হয়। Session-এ এটি সাধারণত চলে, টাকার হিসাবে নয় (তত্ত্ব পর্ব ২২-এ)।
- Shard: এক node-এ না ধরলে session ID-র hash দিয়ে কয়েকটি shard-এ ভাগ — কোন key কোন node-এ, আর node যোগ করলে কত key সরে, তা পরের পর্ব ১৬-এ। Session ID এলোমেলো বলে hot key প্রায় হয় না (বাকিটা পর্ব ৩০-এ)।
| পরিস্থিতি | খারাপ প্রতিক্রিয়া | ভালো প্রতিক্রিয়া |
|---|---|---|
| Store পুরো বন্ধ | প্রতিটি অনুরোধ লম্বা timeout পর্যন্ত ঝুলে থাকে, thread ফুরায়, পুরো সাইট বসে যায় | ছোট timeout (ধরা যাক ৫০ ms); login লাগে না এমন পাতা (পণ্যের তালিকা) চালু রাখা — graceful degradation |
| Store ধীর | প্রতিটি অনুরোধে সেই দেরি যোগ হয়, সারি লম্বা হয় | timeout, retry-র সীমা আর load shedding (পর্ব ১৮) |
| Memory ভরে গেল | Eviction নিয়মে সক্রিয় session মুছে যায় — এলোমেলো মানুষ হঠাৎ লগআউট | Session store-এ eviction বন্ধ (Redis-এ noeviction), memory-র সতর্কবার্তা, আকার আগেই হিসাব |
কখনো “fail open” নয়: store না পেলে “ধরে নিই লগইন আছেন” — নিরাপত্তার সিদ্ধান্তে এমন ছাড় বিপজ্জনক। পরিচয় যাচাই করা না গেলে অনুরোধ ফিরিয়ে দেওয়াই নিয়ম (fail closed); সহনশীলতা আসবে login ছাড়া চলা পাতাগুলো চালু রেখে। অ্যাপে session ১–২ সেকেন্ড local cache করলে store-এর ভার কমে, কিন্তু logout ততটা দেরিতে পৌঁছায়।
৫ · JWT — স্বাক্ষর করা token, state থাকে client-এর হাতে
তৃতীয় পথ: state client-কেই বইতে দিন, কিন্তু এমনভাবে যাতে সে বদলাতে না পারে। ঈদের আগে বাসের টিকিটের কথা ভাবুন। কাউন্টার থেকে কেনা টিকিটে সিল আর সই আছে; বাসের সুপারভাইজার সই চিনলেই বিশ্বাস করেন, কাউন্টারে ফোন করেন না। কিন্তু টিকিট একবার হাতে দিলে বাতিল করা কঠিন: সুপারভাইজার জানবেন কীভাবে যে টিকিটটি ফেরত দেওয়া হয়েছে?
JWT (JSON Web Token, RFC 7519, ২০১৫) ঠিক এই টিকিট। তিনটি অংশ, ডট দিয়ে জোড়া — header.payload.signature — প্রতিটি Base64URL-এ লেখা (সাধারণ Base64-এর URL-নিরাপদ রূপ)। Payload-এর প্রতিটি নাম-মান একটি claim:
header {"alg":"HS256","typ":"JWT"}
payload {"sub":"u-1042","role":"customer","iat":1767225600,"exp":1767226500}
signature HMAC-SHA256(key, base64url(header) + "." + base64url(payload)) → 32 byte| Claim | অর্থ | যাচাইয়ের সময় |
|---|---|---|
sub | কার token (subject) — ব্যবহারকারীর ID | পরিচয় হিসেবে ব্যবহার |
iss | কে ইস্যু করেছে (issuer) | প্রত্যাশিত issuer না হলে বাতিল |
aud | কার জন্য (audience) — কোন সার্ভিস | নিজের নাম না থাকলে বাতিল |
exp | মেয়াদ শেষ, Unix সেকেন্ডে | এখনকার সময় ≥ exp হলে বাতিল, সামান্য clock skew-এর ছাড়সহ |
iat, nbf | কখন ইস্যু, কখনের আগে অচল | nbf-এর আগে এলে বাতিল; iat খুব পুরোনো বা ভবিষ্যতের হলে সন্দেহজনক |
jti | token-এর নিজস্ব ID | deny list-এ বাতিল চিহ্নিত করতে (অংশ ৬) |
স্বাক্ষরের দুই ধরন — HMAC বনাম public key
🔑 HMAC (HS256)
একই গোপন চাবিতে সই আর যাচাই। দ্রুত, স্বাক্ষর ছোট (৩২ byte → ৪৩ অক্ষর)। কিন্তু যে যাচাই করতে পারে, সে সইও করতে পারে: ৩০টি সার্ভিসে চাবি দিলে ৩০টি জায়গা থেকে ফাঁসের ঝুঁকি, আর যেকোনোটি token বানাতে পারবে।
✍️ Public key (RS256, ES256, EdDSA)
Auth service private key দিয়ে digital signature বানায়; বাকিরা public key দিয়ে শুধু যাচাই করে, token বানাতে পারে না। Public key-গুলো প্রকাশ হয় একটি JWKS ঠিকানায়, header-এর kid বলে কোন চাবি; তাতে key rotation সহজ। দাম: ২০৪৮-বিট চাবিতে RS256-এর স্বাক্ষর ২৫৬ byte → ৩৪২ অক্ষর, ES256-এর ৬৪ byte → ৮৬; যাচাই HMAC-এর চেয়ে ধীর, তবে সাধারণত এক মিলিসেকেন্ডের অনেক কম।
HMAC কেন এমন জোড়া-hash
HMAC-এর সূত্র (Bellare, Canetti ও Krawczyk, ১৯৯৬; RFC 2104):
K′ = চাবি (৬৪ byte-এর বড় হলে আগে hash), শূন্য দিয়ে ৬৪ byte-এ ভরা · ipad = 0x36…, opad = 0x5c…
সরলভাবে H(K ‖ m) নিলে চলত না কেন? SHA-256-এর মতো Merkle–Damgård ধাঁচের hash-এর ফল আসলে শেষ অবস্থার ছবি — সেখান থেকে hash-টি “আরও কিছু যোগ করে” চালিয়ে যাওয়া যায়, চাবি না জেনেও — একে বলে length extension. ফলে H(K ‖ m) থেকে লম্বা একটি বার্তার বৈধ মান বানানো সম্ভব। HMAC-এ ভেতরের hash-এর ফলকে আরেকটি চাবি-মেশানো hash-এর ভেতরে মুড়ে দেওয়া হয়; বাইরের স্তর না ভেঙে ভেতরের ফল বাড়ানো যায় না। প্রমাণের ধারণা: compression function যদি একটি pseudorandom function-এর মতো আচরণ করে, তবে HMAC-ও করে — এটি Bellare-এর ২০০৬ সালের প্রমাণ; ১৯৯৬ সালের মূল প্রমাণে অনুমান কিছুটা ভিন্ন ছিল। Cryptography-র ভিত্তি CN কোর্সের MAC আর HMAC অংশে।
নিচের কোড কোনো library ছাড়াই SHA-256, তার ওপর HMAC, তার ওপর JWT-এর সই ও যাচাই লেখে, আর আগে test vector দিয়ে নিজেকে মিলিয়ে নেয়: RFC 4231 (২০০৫)-এর test case 2-এ চাবি "Jefe" আর বার্তা "what do ya want for nothing?"-এর HMAC-SHA256 প্রকাশিত মান 5bdcc146…ec3843. ঘড়ি স্থির — ১ জানুয়ারি ২০২৬, রাত ১২টা (UTC):
// HS256 JWT from scratch: SHA-256 (FIPS 180-4) -> HMAC (RFC 2104) -> sign / verify (RFC 7519)
const K = [], H0 = [], r = (x, n) => x >>> n | x << (32 - n);
for (let n = 2; K.length < 64; n++) { if ([...Array(n).keys()].slice(2).some(d => n % d === 0)) continue;
if (H0.length < 8) H0.push(n ** (1 / 2) * 2 ** 32 | 0); K.push(n ** (1 / 3) * 2 ** 32 | 0); }
function sha256(bytes) { // array of bytes -> array of 32 bytes
const m = [...bytes, 0x80], w = [], L = bytes.length * 8; let h = H0.slice();
while (m.length % 64 !== 56) m.push(0); for (let i = 7; i >= 0; i--) m.push(Math.floor(L / 2 ** (8 * i)) & 255);
for (let j = 0; j < m.length; j += 64) {
for (let i = 0; i < 64; i++) w[i] = i < 16 ? m[j + 4 * i] << 24 | m[j + 4 * i + 1] << 16 | m[j + 4 * i + 2] << 8 | m[j + 4 * i + 3]
: (r(w[i - 2], 17) ^ r(w[i - 2], 19) ^ w[i - 2] >>> 10) + w[i - 7] + (r(w[i - 15], 7) ^ r(w[i - 15], 18) ^ w[i - 15] >>> 3) + w[i - 16] | 0;
let [a, b, c, d, e, f, g, k] = h; for (let i = 0; i < 64; i++) {
const t1 = k + (r(e, 6) ^ r(e, 11) ^ r(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] | 0;
const t2 = (r(a, 2) ^ r(a, 13) ^ r(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
k = g; g = f; f = e; e = d + t1 | 0; d = c; c = b; b = a; a = t1 + t2 | 0; }
h = h.map((x, i) => x + [a, b, c, d, e, f, g, k][i] | 0); }
return h.flatMap(x => [x >>> 24, x >>> 16 & 255, x >>> 8 & 255, x & 255]); }
function hmac(key, msg) { if (key.length > 64) key = sha256(key); const k = [...key, ...Array(64 - key.length).fill(0)];
return sha256([...k.map(x => x ^ 0x5c), ...sha256([...k.map(x => x ^ 0x36), ...msg])]); }
const enc = s => [...new TextEncoder().encode(s)], hex = b => b.map(x => x.toString(16).padStart(2, "0")).join("");
const b64u = b => btoa(String.fromCharCode(...b)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const json = s => JSON.parse(atob(s.replace(/-/g, "+").replace(/_/g, "/"))); // ASCII-only demo
const SECRET = enc("demo-only-key: real keys are 32+ random bytes from a vault"), HEAD = b64u(enc('{"alg":"HS256","typ":"JWT"}'));
const sign = p => { const hp = HEAD + "." + b64u(enc(JSON.stringify(p))); return hp + "." + b64u(hmac(SECRET, enc(hp))); };
function verify(token, now) { const [h, p, s = ""] = token.split(".");
if (json(h).alg !== "HS256") return "REJECT: alg not allowed"; // allow-list, never trust the header
const good = b64u(hmac(SECRET, enc(h + "." + p))); let diff = good.length ^ s.length;
for (let i = 0; i < good.length; i++) diff |= good.charCodeAt(i) ^ (s.charCodeAt(i) | 0); // constant-time compare
if (diff) return "REJECT: bad signature";
const c = json(p); return now >= c.exp ? "REJECT: expired" : "OK sub=" + c.sub + " role=" + c.role; }
console.log("RFC 4231 test case 2:\n" + hex(hmac(enc("Jefe"), enc("what do ya want for nothing?"))));
const now = 1767225600, t = sign({ sub: "u-1042", role: "customer", iat: now, exp: now + 900 }); // 2026-01-01 00:00 UTC
const [h, , s] = t.split("."), forged = h + "." + b64u(enc(JSON.stringify({ sub: "u-1042", role: "admin", iat: now, exp: now + 900 }))) + "." + s;
console.log("token (" + t.length + " chars):\n" + t.match(/.{1,44}/g).join("\n"));
for (const [label, tok, at] of [["1 valid, 60 s later ", t, now + 60], ["2 payload edited ", forged, now + 60], ["3 valid, 901 s later", t, now + 901]])
console.log(label + " : " + verify(tok, at));আউটপুট:
RFC 4231 test case 2:
5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
token (172 chars):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWI
iOiJ1LTEwNDIiLCJyb2xlIjoiY3VzdG9tZXIiLCJpYXQ
iOjE3NjcyMjU2MDAsImV4cCI6MTc2NzIyNjUwMH0.2ji
Lp1cKl-pW6qlFLS1e6DNLjLpFzJYs3Run3ZIDcn8
1 valid, 60 s later : OK sub=u-1042 role=customer
2 payload edited : REJECT: bad signature
3 valid, 901 s later : REJECT: expired
Test case 2-এর মান RFC 4231-এর প্রকাশিত মানের সাথে অক্ষরে অক্ষরে মেলে — তাই নিচের সবকিছুর ভিত্তি ঠিক। Token 172 অক্ষর (৪৪ অক্ষর করে ভেঙে ছাপা): header ৩৬, payload ৯১, স্বাক্ষর ৪৩। (১) ৬০ সেকেন্ড পরে যাচাই: স্বাক্ষর মেলে, exp পেরোয়নি — OK; কোনো store-এ যেতে হলো না। (২) কেউ মাঝের অংশে role বদলে admin লিখল, স্বাক্ষর আগেরটিই রইল: যাচাইকারী নতুন header.payload-এর HMAC নিজে হিসাব করে দেখে মেলে না — bad signature. চাবি ছাড়া সঠিক স্বাক্ষর বানানো যায় না, এটিই অখণ্ডতা। (৩) ৯০১ সেকেন্ড পরে স্বাক্ষর ঠিক থাকলেও exp (iat + ৯০০) পেরিয়েছে — expired. দুটি অভ্যাসও খেয়াল করুন: header-এর alg-কে বিশ্বাস না করে নিজের allow-list মেলানো, আর স্বাক্ষর তুলনায় constant-time comparison. বাস্তবে নিজে লিখবেন না — পরীক্ষিত library ব্যবহার করুন; এই কোড শুধু ভেতরটা দেখানোর জন্য।
Payload গোপন নয়। উপরের token-এর মাঝের অংশ যে কেউ Base64URL থেকে উল্টে পড়তে পারেন — {"sub":"u-1042","role":"customer",…}. স্বাক্ষর শুধু বলে “কেউ বদলায়নি”, বলে না “কেউ দেখেনি”. তাই জাতীয় পরিচয়পত্র নম্বর, ফোন নম্বর, password hash বা ভেতরের কোনো গোপন তথ্য JWT-এ কখনো নয় — এটিই data minimisation-এর নীতি। গোপন রাখা দরকার হলে এনক্রিপ্ট করা token (JWE) বা opaque token.
আকারের দাম
উপরের ছোট token-ও প্রতি অনুরোধে Authorization: Bearer -সহ ১৯৪ বাইট; একটি session cookie (Cookie: __Host-sid= + ২২ অক্ষর) মাত্র ৪১। বাস্তবের JWT-এ RS256 স্বাক্ষর আর দশ-পনেরোটি claim থাকলে প্রায়ই কয়েকশো থেকে হাজার বাইট ছাড়ায়। ১ KB-এর token × সেকেন্ডে ৫০,০০০ অনুরোধ = সেকেন্ডে ৫০ MB, প্রায় ৪০০ Mbit/s শুধু token বইতে।
৬ · Revocation সমস্যা আর JWT-এর নিরাপদ ব্যবহার
JWT-এর শক্তিই তার দুর্বলতা। যাচাইয়ের জন্য সার্ভার কিছু মনে রাখে না — তাই কোনো token “ভুলে যেতে” বলার জায়গাও নেই। Logout, password বদল, ভূমিকা কমানো বা ফোন চুরি — সব ক্ষেত্রে আগে দেওয়া token exp পর্যন্ত বৈধই থাকে। একেই বলে token revocation-এর সমস্যা। নিচে access token-এর মেয়াদ ১৫ মিনিট, সাথে একটি refresh token:
- Auth service
private key Bearer JWTaccess + refresh- Orders service
public key cacheনিজেই যাচাই
| সময় | |
| Access JWT (exp 10:15) | |
| Refresh token | |
| Store-এ যেতে হলো? |
- Login, দুটি token. ১০:০০-এ রিনা ফোনে login করলেন। Auth service দিল ১৫ মিনিটের access JWT (
exp১০:১৫, private key-এ সই,kid-সহ) আর একটি দীর্ঘমেয়াদি refresh token — যা একটি opaque এলোমেলো স্ট্রিং, store-এ লেখা থাকে। - যাচাই store ছাড়াই। ১০:০১-এ অর্ডার দেখতে Orders service-এ অনুরোধ। সার্ভিসটি cache-এ রাখা public key দিয়ে স্বাক্ষর মেলাল, তারপর algorithm,
iss,aud,exp— সব ঠিক। কোনো network hop নেই — এটিই JWT-এর আকর্ষণ। - ফোন চুরি, “সব ডিভাইস থেকে logout”. ১০:০৩-এ রিনা ল্যাপটপ থেকে সব ডিভাইস থেকে বেরোলেন; auth service store-এ refresh token বাতিল করল। কিন্তু ১০:০০-এর access JWT ফোনেই আছে, আর Orders service কাউকে জিজ্ঞেস করে না — খবরই জানে না।
- Revocation সমস্যা। ১০:০৫-এ চুরি যাওয়া ফোন থেকে পুরোনো access JWT দিয়ে অনুরোধ এলো। স্বাক্ষর ঠিক,
expপেরোয়নি — গৃহীত! প্রতিকার দুটি:jti-টি একটি shared deny list-এ ১০:১৫ পর্যন্ত, অথবা রিনার token version ৭ থেকে ৮ করা — তবে দুটিতেই সার্ভিসকে আবার কিছু একটা দেখতে হয়। - জানালা বন্ধ। ১০:১৫-এ access JWT-এর মেয়াদ শেষ। ১০:১৬-এ ফোনটি refresh token দিয়ে নতুন access চাইল — store-এ সেটি বাতিল, তাই প্রত্যাখ্যান। ঝুঁকির জানালা ছিল সর্বোচ্চ access token-এর আয়ু, ১৫ মিনিট।
প্রতিকার তিনটি — আর তাদের দাম
⏱️ ছোট access + refresh token
Access ৫–১৫ মিনিট; refresh token দিনভিত্তিক, opaque, store-এ। প্রতি refresh-এ নতুন refresh token, পুরোনোটি বাতিল (rotation); বাতিলটি আবার এলে বোঝা যায় চুরি হয়ে থাকতে পারে — তখন পুরো শিকলটি বাতিল। ঝুঁকির জানালা = access-এর আয়ু।
store-এ যাওয়া: শুধু refresh-এ🚫 Deny list
বাতিল token-এর jti shared cache-এ, TTL = বাকি মেয়াদ — তাই তালিকা ছোট থাকে। সেকেন্ডে ২০টি revoke × গড়ে ৭.৫ মিনিট (৪৫০ সেকেন্ড) বাকি = মাত্র ৯,০০০ entry (আবার Little-এর সূত্র)।
🔢 Token versioning
ব্যবহারকারীর রেকর্ডে tv=7, token-এও tv:7. সব ডিভাইস থেকে logout বা password বদলে tv=8 — আগের সব token একসাথে অচল। প্রতি ব্যবহারকারীর একটি ছোট সংখ্যা cache-এ।
খেয়াল করুন, deny list আর token version দুটোতেই একটি shared lookup ফিরে এসেছে — এটিই সৎ trade-off: যত দ্রুত revocation চাই, তত বেশি state লাগে। OAuth 2.0, ID token আর access token-এর পার্থক্য পর্ব ৫২ (নকশায় নিরাপত্তা)-এ; এখানে প্রশ্ন শুধু state কোথায় থাকে।
JWT-এর নিরাপদ ব্যবহার — প্রতিরক্ষার নিয়ম
- শুধু decode নয়, verify: অনেক library-তে
decodeস্বাক্ষর যাচাই করে না। প্রতিবার স্বাক্ষর,exp,iss,aud— সব। - Algorithm নিজে ঠিক করুন: সার্ভার আগে থেকেই জানবে কোন algorithm চলবে; token-এর header-এর
alg-কে বিশ্বাস করবেন না,noneকখনো নয়, আর প্রতিটি চাবি একটি নির্দিষ্ট algorithm-এ বাঁধা। এই নিয়মগুলো এখন RFC 8725 (২০২০)-এ লেখা; পেছনের ইতিহাস ভাবনার প্রশ্ন ২-এ। - শক্ত, গোপন চাবি: HS256-এ অন্তত ২৫৬ বিট এলোমেলো চাবি, কোডে বা repository-তে নয় — secret manager-এ। মনে রাখার মতো password চাবি হিসেবে চলবে না: একটি token হাতে পেলেই দুর্বল চাবি offline-এ অনুমানের চেষ্টা করা যায়।
- Token কোথায় রাখবেন: ব্রাউজারের localStorage-এ রাখলে পাতার যেকোনো script পড়তে পারে, তাই একটি XSS-ই যথেষ্ট। HttpOnly, Secure, SameSite cookie-তে রাখলে script পড়তে পারে না, তবে তখন CSRF-এর প্রতিরক্ষা লাগে। মোবাইল অ্যাপে OS-এর নিরাপদ ভান্ডার, যেমন Android Keystore বা iOS Keychain.
- Role পুরোনো হয়ে যায়: token-এ লেখা ভূমিকা ইস্যুর মুহূর্তের; admin থেকে সরালেও token-এ admin থেকে যায় মেয়াদ পর্যন্ত। তাই ছোট মেয়াদ বা token version, আর “এই অর্ডারটি কি এঁর?” ধরনের প্রশ্ন প্রতিবার সার্ভিসে যাচাই — একে বলে object-level authorization.
৭ · Encrypted cookie আর চার পথের তুলনা
চতুর্থ পথ JWT-এর আত্মীয়: encrypted cookie. সার্ভার পুরো session-এর তথ্য নিজের চাবিতে authenticated encryption (যেমন AES-GCM) দিয়ে বন্ধ করে cookie-তেই রেখে দেয়। Client পড়তে বা বদলাতে পারে না, যেকোনো সার্ভার একই চাবিতে খোলে — store লাগে না। ডকুমেন্টেশন অনুযায়ী Ruby on Rails-এর ডিফল্ট session ব্যবস্থা এভাবেই চলে, আর Django-তে signed cookie-র একটি ব্যবস্থা আছে যা শুধু সই করে, এনক্রিপ্ট করে না (সংস্করণভেদে আলাদা)।
সীমা তিনটি। (ক) আকার: RFC 6265 ব্রাউজারকে প্রতি cookie-তে অন্তত ৪,০৯৬ বাইট ধরতে বলে, তার বেশি নির্ভরযোগ্য নয়; আর প্রতিটি অনুরোধে — ছবি আর CSS-এরও — পুরোটা যায়। (খ) Revocation: JWT-এর মতোই কঠিন। (গ) পুরোনো cookie আবার পাঠানো: ব্যবহারকারী নিজেই আগের একটি বৈধ cookie রেখে দিয়ে পরে পাঠাতে পারেন; তাই “বাকি ফ্রি ডাউনলোড ৩” ধরনের বদলাতে থাকা অধিকার কখনো client-এর cookie-তে নয় — সেটি থাকবে সার্ভারে।
| মাপকাঠি | Sticky session | Shared session store | JWT (স্বাক্ষর করা token) | Encrypted cookie |
|---|---|---|---|---|
| Horizontal scaling | দুর্বল — নতুন সার্ভার খালি, ভার অসম | ভালো — store-কেও scale করতে হয় | খুব ভালো — যেকোনো সার্ভারে যাচাই | খুব ভালো |
| সার্ভার পড়লে | তার সব session হারায় | কিছু হারায় না | কিছু হারায় না | কিছু হারায় না |
| Revocation, logout | সহজ (memory থেকে মোছা) | সহজ, সঙ্গে সঙ্গে (key মোছা) | কঠিন — exp পর্যন্ত বৈধ; deny list বা version লাগে | কঠিন, সাথে পুরোনো cookie-র replay |
| প্রতি অনুরোধে আকার | ছোট cookie (~৪০ byte) | ছোট cookie (~৪০ byte) | বড় — কয়েকশো থেকে হাজার byte | বড়, সর্বোচ্চ ~৪ KB |
| Latency | সবচেয়ে কম — নিজের memory | +১টি store hop (একই data center-এ সাধারণত ১ ms-এর কম) | স্থানীয় যাচাই, মাইক্রোসেকেন্ড | স্থানীয় decrypt |
| ব্যর্থতার ধরন | সার্ভার crash = লগআউট; deploy-যন্ত্রণা | store = SPOF; বিকল হলে login-নির্ভর সব কাজ বন্ধ | চাবি ফাঁস = যে কারও token জাল সম্ভব; ঘড়ির গরমিল | চাবি ফাঁস; আকারের সীমা |
ব্রাউজার-নির্ভর একটি অ্যাপ? Server-side session, HttpOnly cookie-তে — সবচেয়ে সরল আর সঙ্গে সঙ্গে বাতিলযোগ্য। বহু সার্ভিস, মোবাইল অ্যাপ বা partner API? ছোট-মেয়াদি JWT access token + store-এ রাখা refresh token. অনেক দল দুটি মেশায়: ব্রাউজার পায় শুধু opaque session cookie, আর সামনের API gateway (পর্ব ১১) store দেখে ভেতরের সার্ভিসগুলোর জন্য কয়েক মিনিটের একটি JWT বানিয়ে দেয়। “Stateless JWT দিয়ে session” বেছে নেওয়ার আগে একটি প্রশ্ন: logout সঙ্গে সঙ্গে কাজ করবে তো?
৮ · Session ছাড়াও state — আর যখন stateful-ই সঠিক
আরও যে state বাইরে সরাতে হয়
- File upload: অ্যাপ সার্ভারের local disk-এ জমালে পরের খণ্ড অন্য সার্ভারে গিয়ে হারায়। ভালো নকশায় client সরাসরি object storage-এ খণ্ড খণ্ড করে পাঠায় (multipart বা resumable upload), আর কোন খণ্ড পৌঁছেছে তা storage জানে (পর্ব ২৬)।
- Local cache: প্রতিটি কপির নিজের cache আলাদা সময়ে হালনাগাদ হয় — একই মানুষ পরপর দুটি অনুরোধে দুটি দাম দেখতে পারেন। প্রতিকার: ছোট TTL, বদলের খবর pub/sub-এ, বা shared cache; stock বা ব্যালান্স local cache-এ নয় (পর্ব ২৯, ৩০)।
- Scheduled job: প্রতিটি কপিতে cron চালালে ১০টি কপিতে ১০ বার — একই বিল দশবার ইমেইলে! প্রতিকার: আলাদা scheduler, বা lease দিয়ে একজনকেই অধিকার, আর কাজটি idempotent (পর্ব ৩৭, ৪৩)।
- Rate limit-এর counter: প্রতিটি সার্ভার নিজে গুনলে ১০টি সার্ভারে সীমা কার্যত দশ গুণ — counter থাকবে shared store-এ (পর্ব ৪৯)।
- WebSocket সংযোগ: সংযোগ নিজেই state, সরানো যায় না; তাই পাতলা gateway স্তর আর connection registry (পর্ব ১০)।
যখন stateful-ই সঠিক
কিছু কাজে state সরানোই ভুল — প্রতি পদক্ষেপে বাইরের store-এ যাওয়া খুব ধীর বা দামি:
- Database: state রাখাই তার কাজ।
- Game server: একটি ম্যাচের অবস্থান সেকেন্ডে কয়েক ডজন বার বদলায়; state থাকে একটি সার্ভারের memory-তে, আর matchmaking ম্যাচের সবাইকে সেখানেই পাঠায় (ভাবনার প্রশ্ন ৪)।
- Stream processor: চলমান যোগফল বা সময়ের window প্রতিটি event-এ বাইরে গেলে ধীর; state থাকে স্থানীয়ভাবে, আর নিয়মিত checkpoint যায় টেকসই storage-এ (পর্ব ৩৬)।
এদের সামলানোর কৌশল তিনটি। Key ধরে ভাগ: প্রতিটি key-এর (ম্যাচ, ব্যবহারকারী, partition) একজন নির্দিষ্ট মালিক; কোন key কার, তা ঠিক করে consistent hashing বা একটি partition map (পর্ব ১৬, পর্ব ২৩)। Replication: মালিকের কপি আরেক node-এ, মালিক পড়লে কপি দায়িত্ব নেয় (পর্ব ২২)। Checkpoint আর replay: নিয়মিত snapshot, তার পরের ঘটনাগুলো log থেকে আবার চালিয়ে state ফেরানো। খেয়াল করুন, এখানে “একই key সবসময় একই node-এ” — দেখতে sticky session-এর মতোই, কিন্তু পার্থক্য মৌলিক: মালিকানা জানা, কপি আছে, failover পরিকল্পিত।
Stateful-কে লুকাবেন না, ঘোষণা করুন — লিখে রাখুন: কোন state, মালিক কে, কপি কোথায়, আর মালিক পড়লে কে দায়িত্ব নেবে।
Deploy-এর সময় session
- Shared store বা JWT হলে: rolling deploy সহজ। প্রতিটি সার্ভারে connection draining — load balancer আগে নতুন অনুরোধ পাঠানো বন্ধ করে, চলমান অনুরোধ শেষ হতে দেয় (ধরা যাক ৩০ সেকেন্ড), তারপর বন্ধ। Session store-এ থাকায় কেউ লগআউট হন না।
- Sticky আর memory-র session হলে: session শেষ হওয়া পর্যন্ত draining (ধীর); session migration — বন্ধের আগে session-গুলো অন্য node বা store-এ লিখে দেওয়া (Tomcat-এর cluster-এর মতো কিছু ব্যবস্থা session অন্য node-এ কপি রাখে); অথবা কিছু মানুষের লগআউট মেনে নেওয়া।
- Session-এর format বদলালে: rolling deploy-এ পুরোনো আর নতুন কোড একসাথে চলে, আর একই session দুই সংস্করণই পড়ে। নতুন field ঐচ্ছিক রাখুন, অজানা field উপেক্ষা করুন — পর্ব ১২-এর schema evolution-এর নিয়ম এখানেও।
- JWT-এর চাবি বদলালে: নতুন public key আগে JWKS-এ প্রকাশ, সব সার্ভিসের cache-এ পৌঁছানোর পর তা দিয়ে সই শুরু, আর পুরোনো চাবি সরানো যায় তার দেওয়া শেষ token-এর মেয়াদ ফুরোলে। বাকি release-কৌশল পর্ব ৫৩-এ।
আরও গভীরে: Chandy ও Lamport-এর distributed snapshot (১৯৮৫)-এর ধারণা থেকে Carbone ও সহকর্মীরা ২০১৫ সালে “asynchronous barrier snapshotting” প্রস্তাব করেন, যা Apache Flink-এর checkpoint-এর ভিত্তি — stateful stream processor চলতে চলতেই সামঞ্জস্যপূর্ণ snapshot নেয়। Token-এর দিকে, RFC 8725 (২০২০) JWT-এর সাধারণ ভুলের নিয়মমাফিক প্রতিকার লিখেছে, আর ২০১৮ সালের দিকে আসা PASETO-র মতো বিকল্প format সংস্করণভিত্তিক নির্দিষ্ট algorithm বেঁধে দিয়ে বাছাইয়ের ভুলের সুযোগই সরিয়ে দেয়।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Stateless সার্ভিস মানে কী? — state নেই তা নয়; state সার্ভারের বাইরে (store বা token-এ), তাই যেকোনো কপি যেকোনো অনুরোধ সামলায়।
- Sticky session-এর সমস্যা? — অসম ভার, সার্ভার পড়লে session হারানো, deploy আর scale in কঠিন, IP affinity-তে CGNAT আর IP বদলের ঝামেলা।
- JWT-এ logout কীভাবে? — ছোট মেয়াদের access token, store-এ রাখা refresh token বাতিল, জরুরি হলে jti-এর deny list বা token version.
- JWT-এর payload কি গোপন? — না, শুধু Base64URL; স্বাক্ষর অখণ্ডতা দেয়, গোপনীয়তা নয়।
৯ · AI-এর যুগে session আর token নকশা যাচাই
Login-এর নকশা চাইলে AI প্রায় নিশ্চিতভাবে লিখবে “JWT নিন, stateless হবে, সাথে Redis আর Kafka”. শব্দগুলো ভুল নয়, কিন্তু ছোট অ্যাপে প্রায়ই over-engineering — একটি database টেবিলেই session চলে — আর আসল প্রশ্ন বাদ পড়ে:
| AI-এর ভুল | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
| “Scale করতে JWT, session store লাগবেই না” | Logout, password বদল, চুরি যাওয়া ফোন — বাতিল করা যায় না; শেষে deny list-এর store লাগেই | জিজ্ঞেস করুন: logout-এর এক মিনিট পর পুরোনো token দিয়ে কী হয়? |
| JWT-এ ইমেইল, ফোন নম্বর, এমনকি গোপন তথ্য | Payload শুধু Base64URL | উদাহরণ token-এর মাঝের অংশ নিজে decode করে দেখুন |
| Memory-র হিসাবে overhead, replica বা headroom বাদ; KB-GB-তে হাজার গুণ ভুল | Store ছোট পড়ে, eviction-এ এলোমেলো লগআউট | অংশ ৪-এর মতো নিজে আবার হিসাব, Little-এর সূত্রে session-সংখ্যা মিলিয়ে |
| “Redis replication-এ কোনো লেখা হারায় না” | ডিফল্টে asynchronous; ACID-এর durability আর replica-র consistency (CAP অর্থে, পর্ব ৪০) গুলিয়ে ফেলা | অফিসিয়াল ডকুমেন্টেশন; staging-এ failover ঘটিয়ে দেখা |
| বানানো সীমা — “cookie ৮ KB পর্যন্ত চলে”, “JWT সবসময় ২৫৬ byte” | ব্রাউজার, proxy আর সংস্করণভেদে আলাদা | RFC 6265 আর নিজের proxy-র ডকুমেন্টেশন |
| Store বা auth service বিকল হলে কী হবে, তা নেই | নতুন SPOF-এর failure mode উপেক্ষা | Failure-mode review; staging-এ store বন্ধ করে পরীক্ষা |
AI-কে কাজে লাগান বিকল্প তুলনা, হিসাব যাচাই আর failure mode-এর তালিকা বানাতে — অনুমান নিজে দিন, আর উত্তরে অনুমানগুলো আলাদা করে চান:
AI-এর দেওয়া session নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১HTTP-কে stateless বানানো হলো কেন, আর তার ওপর বসানো cookie আজও বিতর্কিত কেন?
১৯৮৯–৯১ সালে CERN-এ Tim Berners-Lee যখন HTTP বানান, লক্ষ্য ছিল নথি আনা: একটি অনুরোধ, একটি উত্তর, সংযোগ শেষ। সার্ভারকে কাউকে মনে রাখতে হয় না বলে সার্ভার সরল, ক্র্যাশ থেকে দ্রুত ফেরে, আর মাঝের proxy ও cache নির্ভয়ে উত্তর জমিয়ে রাখতে পারে। ২০০০ সালে Roy Fielding তাঁর PhD গবেষণাপত্রে REST-এর নিয়মগুলো লেখেন, আর সেখানে stateless একটি মূল শর্ত: প্রতিটি অনুরোধে বোঝার মতো সব তথ্য থাকবে। কারণ হিসেবে তিনি দেখান visibility, reliability আর scalability — ঠিক এই পর্বের যুক্তি: সার্ভার কাউকে মনে না রাখলে যেকোনো সার্ভার যেকোনো অনুরোধের উত্তর দিতে পারে, আর একটি সার্ভার পড়লে কারও অর্ধেক কাজ হারায় না।
কিন্তু কেনাকাটার cart বা login-এর জন্য মনে রাখা লাগে। ১৯৯৪ সালে Netscape-এ Lou Montulli একটি অনলাইন দোকানের cart-এর সমস্যা মেটাতে cookie আনেন — state থাকবে client-এ, সার্ভারের দেওয়া ছোট টুকরো হিসেবে। IETF পরে নিয়ম লেখে: RFC 2109 (১৯৯৭), RFC 2965 (২০০০), আর আজকের RFC 6265 (২০১১)।
বিতর্কের উৎস cookie-র নিজের সরলতা। ব্রাউজার প্রতিটি অনুরোধে নিজে থেকেই cookie জুড়ে দেয় — অন্য সাইট থেকে শুরু হওয়া অনুরোধেও। এ থেকে দুটি বড় সমস্যা: CSRF, আর বিজ্ঞাপন-নেটওয়ার্কের third-party cookie দিয়ে সাইট থেকে সাইটে মানুষকে অনুসরণ। প্রতিকার এসেছে ধাপে ধাপে: ২০০২ সালের দিকে Microsoft-এর Internet Explorer-এ HttpOnly, ২০১৬ সালের দিকে Chrome-এ SameSite, আর ২০২০ সালের দিকে Chrome-এ SameSite না লিখলে Lax ধরা। Safari আর Firefox ডিফল্টে third-party cookie সীমিত করে; Chrome-এর পরিকল্পনা কয়েকবার বদলেছে। আইনেও এর ছাপ আছে — ইউরোপের ePrivacy নিয়মের কারণে cookie-সম্মতির সেই পরিচিত ব্যানার।
মূল উপলব্ধি: stateless প্রোটোকল state মুছে দেয় না, শুধু ঠেলে দেয় কিনারায় — client-এ বা store-এ। কোথায় ঠেলবেন, সেই সিদ্ধান্তেই নিরাপত্তা, গোপনীয়তা আর scale-এর দাম লুকিয়ে থাকে।
প্র ০২“JWT দিয়ে session” — অভিজ্ঞ প্রকৌশলীদের অনেকে এর বিরোধিতা করেন কেন, আর কখন JWT-ই সঠিক?
JWT (RFC 7519) এসেছিল ২০১৫ সালে, OAuth আর OpenID Connect-এর মতো পরিচয়-ব্যবস্থার জন্য — এক পক্ষ আরেক পক্ষকে স্বাক্ষর দিয়ে বলবে “এই মানুষটি আমার কাছে লগইন করেছেন”. সেই কাজে এটি চমৎকার: যাচাইকারী ইস্যুকারীকে ডাকে না। কিন্তু জনপ্রিয়তার সাথে এটি অনেক অ্যাপে browser session-এর বদলি হয়ে দাঁড়ায়, আর ২০১৬ সালের দিকে “Stop using JWT for sessions” শিরোনামের একটি বহুল আলোচিত লেখাসহ অনেকে আপত্তি তোলেন।
আপত্তিগুলো এই পর্বেই দেখেছি। এক, revocation: logout সঙ্গে সঙ্গে কার্যকর করতে হলে deny list বা token version লাগে, অর্থাৎ আবার server-side state — তখন একটি সাধারণ session ID-ই সরল হতো। দুই, আকার: প্রতি অনুরোধে কয়েকশো থেকে হাজার বাইট। তিন, পুরোনো তথ্য: token-এ লেখা ভূমিকা মেয়াদ পর্যন্ত বদলায় না। চার, ভুলের সুযোগ: ২০১৫ সালে Tim McLean দেখান কয়েকটি library header-এর alg মেনে নিত; দুর্বল HS256 চাবি, payload-এ ব্যক্তিগত তথ্য, localStorage-এ রাখা — বাস্তবে বারবার দেখা যায়। এই অভিজ্ঞতা থেকেই RFC 8725 (২০২০)।
তাহলে JWT কখন সঠিক? (ক) অল্প মেয়াদের access token, যা বহু সার্ভিস বা partner নিজেরাই যাচাই করবে — gateway-তে একবার আর সার্ভিসে আবার, database না ছুঁয়ে। (খ) OpenID Connect-এর ID token, যা login-এর ফল জানায় (পর্ব ৫২)। (গ) সার্ভিস থেকে সার্ভিসে কয়েক মিনিটের পরিচয়পত্র। (ঘ) এককালীন কাজের link — ইমেইল যাচাই বা password reset — ছোট মেয়াদে আর একবার ব্যবহারের নিয়মে। অনেক দল তাই মিশ্র নকশা নেয়: ব্রাউজারের কাছে opaque session cookie, gateway ভেতরের জন্য স্বল্পমেয়াদি JWT বানায়।
মূল উপলব্ধি: একটি JWT আসলে একটি অনুমতি-সিদ্ধান্তের cache, আর তার exp হলো সেই cache-এর TTL — কতক্ষণ পুরোনো সিদ্ধান্ত মেনে নিতে রাজি। এই TTL-কে consistency-র সিদ্ধান্ত হিসেবে দেখলেই বোঝা যায় কোথায় JWT মানায়, কোথায় নয়।
প্র ০৩Session store-ও তো একটি সার্ভার। তাহলে আমরা single point of failure শুধু এক জায়গা থেকে আরেক জায়গায় সরালাম না?
আংশিক হ্যাঁ — state কোথাও না কোথাও থাকবেই, আর যেখানে থাকবে সেখানেই ঝুঁকি। কিন্তু সরানোটা অর্থহীন নয়, তিনটি কারণে।
এক, জড়ো করা state-এর যত্ন সহজ। আগে ৫০টি অ্যাপ সার্ভারের প্রতিটিতে কিছু state ছিল, প্রতিটিই একটি ছোট SPOF; deploy, crash, autoscaling — সবখানে ঝুঁকি। এখন state একটি বিশেষ-উদ্দেশ্যের ব্যবস্থায়, যার কাজ সরল: key ধরে পড়া-লেখা আর TTL. এমন ব্যবস্থাকে replica, স্বয়ংক্রিয় failover, monitoring আর নিয়মিত পরীক্ষা দিয়ে নির্ভরযোগ্য করা অনেক সহজ; অ্যাপ সার্ভার তখন নির্ভয়ে বদলানো যায়।
দুই, availability-র হিসাব বদলায়। পর্ব ৫-এর ভাষায় পুরো পথ এখন serial: অ্যাপ স্তর × store স্তর। অ্যাপ স্তরে N-টি stateless কপি থাকায় তার availability প্রায় ১-এর কাছে; store-এ primary + replica আর failover-এ কয়েক সেকেন্ডের বিভ্রাট। আগের নকশায় প্রতিটি অ্যাপ সার্ভারের পতনেই তার ভাগের মানুষ লগআউট হতেন — আংশিক বিভ্রাট, কিন্তু অনেক বেশি ঘনঘন।
তিন, ঝুঁকি কমানোর আরও উপায় খোলে। (ক) store বিকল হলে login-ছাড়া পাতা চালু রাখা; (খ) JWT-নির্ভর নকশায় auth service বা store লাগে শুধু login আর refresh-এ, তাই তাদের কয়েক মিনিটের বিভ্রাট চলমান ব্যবহারকারীরা টেরই পান না; (গ) একাধিক region-এ প্রতিটি region-এর নিজের store, ব্যবহারকারী তাঁর “বাড়ির” region-এ, region বদলালে আবার login — একটি সচেতন trade-off. তবু সাবধান: Redis-এর replication asynchronous, আর নেটওয়ার্ক ভাগ হলে দুই দিকেই primary দাবি করার (split-brain) ঝুঁকি থাকে — তত্ত্ব পর্ব ২২ আর ৪০-এ।
মূল উপলব্ধি: state সরানো মানে state-কে এমন জায়গায় জড়ো করা, যেখানে তার যত্ন নেওয়া সহজ। SPOF পুরো মুছে ফেলা যায় না; লক্ষ্য হলো সংখ্যা কমানো, আর যেটি থাকে তাকে জেনেশুনে শক্ত করা।
প্র ০৪Game server, stream processor আর database stateful — তাহলে এরা scale আর failover করে কীভাবে?
সূত্র একটাই: key ধরে ভাগ করা, প্রতিটি ভাগের একজন জানা মালিক, আর মালিকের কপি রাখা। প্রয়োগ ভিন্ন।
Database: Amazon-এর Dynamo গবেষণাপত্র (২০০৭) consistent hashing-এ key ভাগ করে প্রতিটি key-এর কয়েকটি কপি রাখার নকশা দেখায়; Cassandra-র মতো ব্যবস্থা সেই ধারা থেকে এসেছে। Leader-follower database-এ প্রতিটি shard-এর একজন leader লেখা নেয়, follower-রা কপি রাখে, leader পড়লে একজন follower দায়িত্ব নেয় (পর্ব ২২, ২৩)। নতুন node যোগ করলে কিছু shard তার কাছে সরে — scale out মানে এখানে ডেটা সরানো, শুধু কপি বাড়ানো নয়।
Stream processor: Apache Flink প্রতিটি key-এর state স্থানীয়ভাবে রাখে, আর Carbone ও সহকর্মীদের ২০১৫ সালের পদ্ধতিতে চলতে চলতেই সামঞ্জস্যপূর্ণ checkpoint নেয়। একটি node পড়লে শেষ checkpoint থেকে state ফিরিয়ে তারপরের event-গুলো আবার চালায়। Kafka Streams state-এর প্রতিটি বদল changelog topic-এ লেখে; নতুন node সেটি পড়ে state গড়ে নেয়। Scale out মানে key-গুলো নতুন node-এ ভাগ করে দেওয়া, সাথে তাদের state-ও।
Game server: এখানে কৌশল উল্টো — state-কে স্বল্পায়ু রাখা। একটি ম্যাচ কয়েক মিনিটের; matchmaking ম্যাচগুলোকে সার্ভারে ছড়িয়ে দেয়, নতুন সার্ভারে শুধু নতুন ম্যাচ যায়। সার্ভার পড়লে একটি ম্যাচ হারায়, আর খেলোয়াড়ের স্থায়ী তথ্য (র্যাঙ্ক, কেনা জিনিস) থাকে আলাদা database-এ। সার্ভার কমানোর আগে নতুন ম্যাচ বন্ধ, চলমানগুলো শেষ হলে বন্ধ — draining-এর খেলার সংস্করণ। State-এর আয়ু ছোট রাখাই এখানে failover-এর কৌশল।
তিন ক্ষেত্রেই একটি সূক্ষ্ম সমস্যা: “আমিই মালিক” কে ঠিক করে? দুজন একসাথে নিজেকে মালিক ভাবলে state দুই ভাগ হয়। এজন্য lease আর fencing token (পর্ব ৪৩) বা consensus (পর্ব ৪২) লাগে।
মূল উপলব্ধি: stateful scale-এর তিন খুঁটি — ভাগ করা, কপি রাখা আর মালিকানা স্পষ্ট রাখা। Sticky session এর মধ্যে শুধু প্রথমটির ভাঙা রূপ, বাকি দুটি ছাড়া।
প্র ০৫“Logout” বোতাম চাপার পর আসলে কী কী ঘটা উচিত — আর কেন এটি distributed system-এর একটি ছোট সমস্যা?
এক ডিভাইসের সাধারণ logout সহজ: store থেকে session key মোছা আর cookie মেয়াদোত্তীর্ণ করা। কিন্তু “সব ডিভাইস থেকে বেরোন” — যা একটি mobile banking সেবায় (কাল্পনিক) চুরি যাওয়া ফোনের পর সবচেয়ে জরুরি — অনেক জায়গায় ছড়িয়ে থাকা state-কে একসাথে অচল করার প্রশ্ন:
- Server-side session: ব্যবহারকারীর সব session খুঁজে মুছতে হয় — তাই user ID থেকে তাঁর session-গুলোর একটি সূচি রাখা লাগে, নইলে ৩০ লাখের মধ্যে খোঁজা অসম্ভব।
- অ্যাপের local session cache: ২ সেকেন্ড cache থাকলে logout ২ সেকেন্ড দেরিতে পৌঁছায়; pub/sub-এ “এই session বাতিল” ছড়িয়ে তা কমানো যায়।
- JWT access token: মেয়াদ পর্যন্ত বৈধ; token version বাড়ানো বা deny list দরকার।
- Refresh token: store-এ বাতিল, যাতে নতুন access না মেলে।
- খোলা WebSocket: সংযোগ খোলার সময় একবার পরিচয় যাচাই হয়েছিল; logout-এর পরও সংযোগ খোলা থাকলে বার্তা আসতেই থাকে। তাই gateway-কে খবর দিয়ে সেই ব্যবহারকারীর সংযোগ বন্ধ করাতে হয় (পর্ব ১০)।
- Cache করা ব্যক্তিগত পাতা আর push token: CDN-এ ভুল করে ব্যক্তিগত উত্তর cache হলে তা থেকে যায় (পর্ব ১৭); push বিজ্ঞপ্তির token-ও মুছতে হয়।
এটি distributed system-এর একটি ক্লাসিক প্রশ্ন: একটি পরিবর্তন সব কপিতে কত দ্রুত পৌঁছাতে হবে? সবখানে সঙ্গে সঙ্গে চাইলে প্রতিটি অনুরোধে একটি কেন্দ্রীয় উৎস দেখতে হয় — ধীর আর সেই উৎস বিকল হলে সব বন্ধ। কয়েক মিনিটের দেরি মানলে কপি আর cache চলে — দ্রুত, কিন্তু ঝুঁকির একটি জানালা থাকে। ব্যাংকিংয়ে সেই জানালা কয়েক সেকেন্ড, সংবাদ-সাইটে কয়েক ঘণ্টাও চলে। Consistency-র এই বর্ণালির তত্ত্ব পর্ব ৪০-এ।
মূল উপলব্ধি: অনুমতির প্রতিটি কপি — cookie, token, cache, খোলা সংযোগ — একটি পুরোনো হতে পারা তথ্য। নকশার কাজ হলো প্রতিটির জন্য লিখে রাখা, সর্বোচ্চ কত পুরোনো চলবে, আর সেই সীমা কোন যন্ত্র দিয়ে রক্ষা হবে।
অনুশীলন
খাতা-কলম আর উপরের কোডগুলো পাশে রাখুন। আগে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।
-
কোন session কৌশল? তিনটি কাল্পনিক অ্যাপের জন্য session কৌশল বেছে নিন, যুক্তিসহ: (ক) একটি mobile banking সেবা — চুরি যাওয়া ফোনে সঙ্গে সঙ্গে সব বন্ধ করা জরুরি, idle timeout ছোট; (খ) একটি সংবাদ-সাইট — পিকে লাখো পাঠক, পাতা CDN-এ cache হয়, লগইন করেন শুধু সম্পাদকেরা; (গ) একটি অনলাইন বাজার — ওয়েব, মোবাইল অ্যাপ আর partner দোকানের API, পেছনে ৩০টি সার্ভিস।
(ক) Server-side session (বা opaque token), shared store-এ। “সব ডিভাইস থেকে বেরোন” মানে store থেকে ব্যবহারকারীর সব session মোছা — পরের অনুরোধেই কার্যকর। Idle timeout কয়েক মিনিট (sliding), absolute timeout কয়েক ঘণ্টা; ভেতরের সার্ভিসের জন্য JWT লাগলে খুব ছোট মেয়াদ (দুই-পাঁচ মিনিট) আর token version. Store-এ replica আর failover; store বিকল হলে fail closed.
(খ) পাঠকের জন্য কোনো session নয়। ফন্ট বা ভাষার পছন্দ একটি সাধারণ cookie বা localStorage-এ। সতর্কতা: প্রতিটি পাতায়
Set-Cookieপাঠালে বা cookie দেখে উত্তর বদলালে অনেক CDN পাতাটি cache করে না, বা করলেও hit ratio ধসে পড়ে (পর্ব ১৭) — পাঠকের পাতা হোক cookie-মুক্ত। সম্পাদকদের জন্য আলাদা admin subdomain-এ ছোট server-side session, একটি database টেবিলই যথেষ্ট।(গ) ছোট-মেয়াদি JWT access token, সাথে store-এ রাখা refresh token. ৩০টি সার্ভিস নিজেরাই যাচাই করবে, তাই asymmetric স্বাক্ষর (ES256 বা RS256), JWKS আর
kid; প্রতিটি সার্ভিস নিজেরaudমেলাবে। Access ১০–১৫ মিনিট, refresh rotation-সহ, জরুরি revoke-এর জন্য jti-এর deny list থাকবে। Cart session-এ নয়, cart service-এর database-এ user ID ধরে — যাতে ফোন আর ল্যাপটপে একই cart. -
Store-এর memory হিসাব করুন: একটি কাল্পনিক ভর্তি-আবেদন পোর্টালে ফল প্রকাশের দিন পিকে সেকেন্ডে ৪০০টি নতুন login; একজন গড়ে ১২ মিনিট সক্রিয় থাকেন, sliding idle timeout ২০ মিনিট। Session-এর value গড়ে ৪ KB (৪,০৯৬ বাইট), key ৪৮ বাইট, প্রতি key-তে store-এর নিজস্ব খরচ ৮০ বাইট, fragmentation-এর গুণক ১.২৫, replication factor ৩ (primary + ২টি replica); প্রতিটি node-এ ১৬ GiB RAM, ৬০%-এর বেশি ব্যবহার নয়। (ক) পিকে জীবিত session কত? (খ) এক কপিতে আর সব কপিতে কত memory, কয়টি shard আর node? (গ) প্রতি সেকেন্ডে ১৫,০০০ অনুরোধ session পড়লে store থেকে কত MB/s? (ঘ) দল পুরো ফলাফল-পাতা session-এ রাখায় value ৪০ KB (৪০,৯৬০ বাইট) হলে কী বদলায়?
(ক) শেষ কাজের পর আরও ২০ মিনিট session বাঁচে, তাই গড় আয়ু W = ১২ + ২০ = ৩২ মিনিট = ১,৯২০ সেকেন্ড। Little-এর সূত্রে L = λ × W = ৪০০ × ১,৯২০ = ৭,৬৮,০০০ session.
(খ) প্রতি session (৪৮ + ৪,০৯৬ + ৮০) × ১.২৫ = ৪,২২৪ × ১.২৫ = ৫,২৮০ বাইট। এক কপি ৭,৬৮,০০০ × ৫,২৮০ = ৪,০৫,৫০,৪০,০০০ বাইট ÷ 10243 ≈ ৩.৭৮ GiB; তিন কপি ≈ ১১.৩৩ GiB. প্রতি node-এ ব্যবহারযোগ্য ১৬ × ০.৬ = ৯.৬ GiB, তাই ⌈৩.৭৮ ÷ ৯.৬⌉ = ১টি shard, × ৩ কপি = ৩টি node — memory-র জন্য একটিই যথেষ্ট ছিল, বাকি দুটি availability-র জন্য।
(গ) ১৫,০০০ × (৪৮ + ৪,০৯৬) = ৬,২১,৬০,০০০ বাইট/সেকেন্ড ≈ ৬২.২ MB/s, প্রায় ৪৯৭ Mbit/s.
(ঘ) প্রতি session (৪৮ + ৪০,৯৬০ + ৮০) × ১.২৫ = ৫১,৩৬০ বাইট; এক কপি ৭,৬৮,০০০ × ৫১,৩৬০ ≈ ৩৯.৪৪ × ১০৯ বাইট ≈ ৩৬.৭৪ GiB, তিন কপি ≈ ১১০.২ GiB. Shard ⌈৩৬.৭৪ ÷ ৯.৬⌉ = ⌈৩.৮৩⌉ = ৪টি, node ১২টি — node চার গুণ, memory প্রায় দশ গুণ। পড়ার চাপ ১৫,০০০ × ৪১,০০৮ ≈ ৬১৫ MB/s, প্রায় ৪.৯ Gbit/s. সমাধান হিসাব বাড়ানো নয়, নকশা বদলানো: ফলাফল database বা cache-এ (যেমন রোল নম্বর ধরে), session-এ শুধু পরিচয়।
-
নিরাপত্তার ভুল খুঁজুন: একটি কাল্পনিক অ্যাপের login কোডের সারসংক্ষেপ নিচে। প্রতিটি ভুল চিহ্নিত করুন আর প্রতিরক্ষামূলক সংশোধন লিখুন।
// login: token তৈরি token = jwt.sign({ sub: user.id, email: user.email, role: user.role, nid: user.nid_number, pw: user.password_hash }, "secret123", { expiresIn: "30d" }) browser: localStorage.setItem("token", token) // প্রতিটি API অনুরোধে claims = jwt.decode(bearer) // শুধু খুলে পড়া if (claims.role == "admin") allowAdmin() // logout: ব্রাউজার থেকে localStorage-এর token মুছে দেওয়া, ব্যস- Payload-এ ব্যক্তিগত ও গোপন তথ্য (NID নম্বর, password hash, ইমেইল): Base64URL যে কেউ পড়তে পারেন। সংশোধন: শুধু
sub, দরকারি ভূমিকা,iss,aud,exp,jti; বাকি তথ্য সার্ভার database থেকে নেবে। - দুর্বল, কোডে লেখা চাবি
"secret123": একটি token হাতে পেলেই offline-এ অনুমানযোগ্য, আর repository দেখা যে কেউ জানেন। সংশোধন: ২৫৬ বিট এলোমেলো চাবি secret manager-এ, নিয়মিত rotation; বহু সার্ভিস যাচাই করলে asymmetric স্বাক্ষর। - যাচাই নেই — শুধু decode: যে কেউ নিজের token বানিয়ে
role: "admin"লিখতে পারেন। সংশোধন: স্বাক্ষর যাচাই, algorithm-এর allow-list (noneনিষিদ্ধ), সাথেexp,iss,aud. - ৩০ দিনের মেয়াদ, আর logout শুধু ব্রাউজারে: token-এর কোনো কপি চুরি হলে এক মাস খোলা দরজা। সংশোধন: access ১০–১৫ মিনিট, store-এ rotation-সহ refresh token, logout-এ তা বাতিল, জরুরি হলে jti deny list বা token version.
- localStorage-এ token: পাতার যেকোনো XSS পড়তে পারে। সংশোধন: HttpOnly, Secure, SameSite cookie (সাথে CSRF প্রতিরক্ষা), অথবা access token শুধু memory-তে আর refresh HttpOnly cookie-তে।
- Token-এর role-এ অন্ধ বিশ্বাস: admin থেকে সরানো মানুষ ৩০ দিন admin থাকেন। সংশোধন: ছোট মেয়াদ, token version, আর admin কাজে সার্ভারে বর্তমান ভূমিকা আবার যাচাই; প্রতিটি অনুরোধে object-level authorization.
- Payload-এ ব্যক্তিগত ও গোপন তথ্য (NID নম্বর, password hash, ইমেইল): Base64URL যে কেউ পড়তে পারেন। সংশোধন: শুধু
-
আউটপুট অনুমান করুন: অংশ ৩-এর simulation-এ
SERVERS = 3বদলেSERVERS = 4করুন (বাকি সব একই; S2-ই মরে)। চালানোর আগে লিখুন: (ক) sticky-তে প্রতিটি সার্ভারে শুরুতে কয়টি session, S2 মরলে কয়টি হারাবে, আবার login-এর পর আর S2 ফেরার পর session কত করে? (খ) Shared store-এর তিনটি লাইনে প্রতি সার্ভারে অনুরোধ কত? (গ) কোন সংখ্যাগুলো চালানো ছাড়া বলা যায় না, আর কেন? তারপর চালিয়ে মিলিয়ে নিন।(ক) ৬০০ জন “সবচেয়ে কম session” নিয়মে পালাক্রমে বসেন, তাই প্রতিটিতে ১৫০। S2 মরলে ১৫০টি session হারায়; আবার login করে তাঁরা S1, S3, S4-এ ৫০ করে যান — ২০০ / ✗ / ২০০ / ২০০। S2 শূন্য নিয়ে ফেরে, তাই নতুন ১৫০ জনের সবাই S2-তে (১৫০ তখনো ২০০-এর কম): ২০০/১৫০/২০০/২০০।
(খ) মোট ভার ৩ সার্ভারের সময়ের মতোই, ৩,৮১১: ৪ ভাগে ৯৫২.৭৫ → 953, 953, 953, 952; S2 মরলে ৩ ভাগে ১,২৭০.৩৩ → 1271, 1270, 1270; নতুনদের ৬৯৬ যোগে ৪,৫০৭, ৪ ভাগে → 1127, 1127, 1127, 1126. তিনটিতেই max/avg 1.00, lost 0.
(গ) Sticky-র অনুরোধ-সংখ্যা বলা যায় না — কোন ভারী মানুষ কোন সার্ভারে পড়েন, তা চালালেই জানা যায়। আসল আউটপুট:
req/min per server: 600 users log in; S2 dies; S2 returns, 150 new users log in sticky before S1=870 S2=1464 S3=740 S4=737 max/avg 1.54 sticky S2 dead S1=1172 S2=down S3=1683 S4=956 max/avg 1.32 lost 150 sessions, 34 carts sticky S2 back S1=1172 S2=696 S3=1683 S4=956 max/avg 1.49 sessions 200/150/200/200 shared before S1=953 S2=953 S3=953 S4=952 max/avg 1.00 shared S2 dead S1=1271 S2=down S3=1270 S4=1270 max/avg 1.00 lost 0 sessions, 0 carts shared S2 back S1=1127 S2=1127 S3=1127 S4=1126 max/avg 1.00এবার মিনিটে ৭১৬টি অনুরোধের সেই ব্যস্ত মানুষটি S2-তে পড়েছেন, তাই শুরুতেই S2 সবচেয়ে ভারী (
1464, max/avg1.54— তিন সার্ভারের ১.৩১-এর চেয়েও খারাপ: সার্ভার বাড়লে একজন ভারী মানুষের অংশ গড়ের তুলনায় বড় হয়)। S2 মরলে তিনি আবার login করে S3-তে যান — S31683. হারানো cart34টি। S2 ফেরার পর696, তিন সার্ভারের সময়ের মতোই, কারণ নতুন ১৫০ জন একই মানুষ।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমShared session store ব্যবহার করা একটি ওয়েব অ্যাপে login থেকে logout পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🔑 ব্যবহারকারী HTTPS-এ নাম আর password পাঠান
- 🎲 সার্ভার password মিলিয়ে নতুন এলোমেলো session ID বানায়
- 🗄️ Store-এ session ID ধরে তথ্য লেখা হয়, TTL-সহ
- 🍪 উত্তরে HttpOnly, Secure, SameSite cookie-তে শুধু session ID যায়
- 🖥️ পরের অনুরোধ অন্য সার্ভারে গেলেও সে store থেকে session পড়ে
- 🚪 Logout-এ store থেকে key মোছা হয়, cookie মেয়াদোত্তীর্ণ হয়
একটি ওয়েব অ্যাপের ৩টি সার্ভার; প্রতিটি সার্ভার session রাখে নিজের memory-তে, আর load balancer প্রতিটি অনুরোধ round robin-এ পাঠায় (sticky নয়)। একজন ব্যবহারকারী login করার পর কী দেখবেন?
- সব ঠিকঠাক চলবে, কারণ round robin ভার সমান ভাগ করে
- প্রায় দুই-তৃতীয়াংশ অনুরোধে আবার login করতে বলা হবে
- Load balancer নিজেই session অন্য সার্ভারে কপি করে দেবে
- শুধু প্রথম অনুরোধটি ব্যর্থ হবে, তারপর সব ঠিক
Session cookie-র কোন attribute পাতার JavaScript-কে cookie পড়তে বাধা দেয়, যাতে পাতায় ঢুকে পড়া কোনো script session ID চুরি করতে না পারে?
SecureSameSite=LaxHttpOnlyMax-Age=0
একটি HS256 JWT-এর মাঝের অংশ (payload) সম্পর্কে কোনটি সত্য?
- এটি AES দিয়ে এনক্রিপ্ট করা, চাবি ছাড়া পড়া যায় না
- এটি hash করা, তাই উল্টে মূল তথ্য পাওয়া যায় না
- আসল তথ্য সার্ভারের database-এ থাকে, token-এ শুধু একটি ID
- যে কেউ পড়তে পারেন, কিন্তু চাবি ছাড়া বদলালে যাচাইয়ে ধরা পড়ে
৩০টি সার্ভিস একটি auth service-এর দেওয়া JWT যাচাই করবে। HS256-এর বদলে RS256 বা ES256 বেছে নেওয়ার মূল কারণ কোনটি?
- যাচাইকারীরা শুধু public key পায়, তাই তাদের কেউ token জাল করতে পারে না
- RS256 payload এনক্রিপ্ট করে, তাই গোপন তথ্য রাখা যায়
- RS256-এর token আকারে ছোট
- RS256-এ প্রতিটি যাচাই auth service-কে জিজ্ঞেস করে হয়, তাই revoke সহজ
একটি API-তে access JWT-এর মেয়াদ ১৫ মিনিট; কোনো deny list বা token version নেই। ১০:০০-এ ইস্যু হওয়া token-এর মালিক ১০:০৩-এ “সব ডিভাইস থেকে logout” চাপলেন, আর তাঁর refresh token সঙ্গে সঙ্গে বাতিল হলো। চুরি যাওয়া ফোনের সেই access token দিয়ে সর্বোচ্চ কখন পর্যন্ত API ব্যবহার করা যাবে?
- ১০:০৩ পর্যন্ত
- ১০:১৫ পর্যন্ত
- ১০:১৮ পর্যন্ত
- Refresh token-এর মূল মেয়াদ শেষ হওয়া পর্যন্ত
একটি অ্যাপে সেকেন্ডে ৫০০টি নতুন login হয়, আর একটি session গড়ে ১০০ মিনিট বাঁচে। প্রতিটি session store-এ (overhead-সহ) ২,৭২০ বাইট নেয়। এক কপিতে মোটামুটি কত memory লাগবে?
- প্রায় ৭৮০ MiB
- প্রায় ১৩০ MiB
- প্রায় ৭.৬ GiB
- প্রায় ৭৬ GiB
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Stateless মানে state নেই নয় — state সার্ভারের বাইরে; প্রশ্ন সবসময় “state কোথায় থাকে”.
- Sticky session: সহজ, কিন্তু অসম ভার, সার্ভার পড়লে session হারানো আর deploy-যন্ত্রণা; থাকলে শুধু গতির জন্য, সঠিকতার জন্য নয়।
- Shared session store: cookie-তে এলোমেলো ID (HttpOnly, Secure, SameSite), store-এ তথ্য TTL-সহ; জীবিত session = login-এর হার × আয়ু; store-এর replica, headroom আর fail closed.
- JWT = header.payload.signature; payload গোপন নয়; যেকোনো সার্ভারে যাচাই, কিন্তু revoke কঠিন — ছোট access + refresh, deny list বা token version; algorithm নিজে ঠিক করা, চাবি গোপন।
- Upload, cache, job, counter, সংযোগ — সব state-এর ঠিকানা চাই; সত্যিকারের stateful কাজে ভাগ, কপি আর স্পষ্ট মালিকানা, deploy-এ draining.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৬ · Consistent hashing — ring, virtual node আর rendezvous hashingপরবর্তী পর্বSession store আর cache-কে node-এ ভাগ করা, কম key সরিয়ে।
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- HTTP — request, response, header, cookie আর RESTগভীরে যানCookie, session, CSRF আর CORS বাইট ধরে।
- Cryptography-র ভিত্তি — symmetric, public key, hash আর signatureপাশের ট্র্যাকHMAC, digital signature আর চাবি রাখার নিয়ম।
- Redis — in-memory key-value আর cacheপাশের ট্র্যাকTTL, replication, Sentinel আর Cluster হাতে-কলমে।