পাঠ ১২ · ৫১-এর মধ্যে · মডিউল ৩
Home / Courses / System Design / Stateless বনাম Stateful

Stateless বনাম Stateful সার্ভিস ডিজাইন

Stateless vs stateful service design
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • Stateless ও Stateful সার্ভিসের মধ্যে সঠিক পার্থক্য
  • কেন Sticky Session হরাইজন্টাল স্কেলিংকে জটিল করে তোলে
  • কীভাবে state-কে একটি শেয়ারড স্টোরে externalize করে সার্ভারকে স্টেটলেস রাখা যায়
  • Python দিয়ে প্রমাণ — একই সেশন লুকআপ ভুল সার্ভারে ফেইল করে (stateful) কিন্তু শেয়ারড স্টোরে যেকোনো সার্ভার থেকে সফল হয় (stateless)

১ · Stateless ও Stateful সার্ভিস আসলে কী

Stateless সার্ভিসStateless Serviceএমন একটি সার্ভার/সার্ভিস যা দুটি ভিন্ন রিকোয়েস্টের মাঝে কোনো ইউজার-স্পেসিফিক ডেটা নিজের মেমরিতে সংরক্ষণ করে না — প্রতিটি রিকোয়েস্ট নিজে থেকেই সম্পূর্ণ, বা প্রয়োজনীয় ডেটা একটি বাহ্যিক স্টোর থেকে টেনে আনে। মানে সার্ভারটি ইউজারের ব্যাপারে কিছুই "মনে রাখে না" — প্রতিটি রিকোয়েস্ট স্বয়ংসম্পূর্ণ। অন্যদিকে Stateful সার্ভিসStateful Serviceএমন একটি সার্ভার/সার্ভিস যা একটি নির্দিষ্ট ইউজারের ডেটা (যেমন লগইন সেশন, শপিং কার্ট) নিজের মেমরি/ডিস্কে সংরক্ষণ করে — সেই ইউজারের পরবর্তী রিকোয়েস্টও একই সার্ভারে পৌঁছাতে হবে। একটি নির্দিষ্ট ইউজারের ডেটা মনে রাখে — এবং সেই ইউজারকে পরের বার একই সার্ভারেই যেতে হয়।

Stateless উদাহরণ
একটি স্ট্যাটিক ইমেজ রিসাইজ API — ইনপুট ইমেজ পেলেই আউটপুট দিতে পারে, আগের কোনো রিকোয়েস্ট মনে রাখার দরকার নেই।
Stateful উদাহরণ
একটি সার্ভার যেখানে লগইন করার পর সেশন ডেটা সেই সার্ভারের মেমরিতে (in-memory dict) সংরক্ষিত থাকে।
Sticky Session
লোড ব্যালেন্সারকে বাধ্য করা হয় একই ইউজারকে বারবার একই সার্ভারে পাঠাতে (IP hash বা কুকি দিয়ে) — L11-এর IP Hash অ্যালগরিদম এভাবেই ব্যবহৃত হয়।

২ · কেন Stateful সার্ভিস স্কেল করা কঠিন

ধরুন একটি অ্যাপে লগইন করলে সেশন ডেটা সরাসরি যে সার্ভার রিকোয়েস্টটি হ্যান্ডল করেছে তার মেমরিতে সেভ হয়। এখন লোড ব্যালেন্সার পরের রিকোয়েস্টটি (round robin অনুযায়ী) একটি ভিন্ন সার্ভারে পাঠাল। সেই সার্ভারের মেমরিতে এই ইউজারের সেশন নেই — ফলাফল: ইউজার হঠাৎ "লগ-আউট" হয়ে যায়, যদিও সে ঠিকই লগইন করেছিল।

Stateful সার্ভিসের তিনটি সমস্যা

(১) লোড ব্যালেন্সিং জটিল হয়ে যায় — সাধারণ round robin/least connections (L11) আর কাজ করে না, sticky session বাধ্যতামূলক হয়ে যায়। (২) অসম লোড — কিছু সার্ভারে বেশি "ভারী" সেশন জমে গেলেও সেই ইউজারদের অন্য সার্ভারে সরানো যায় না। (৩) একক ব্যর্থতার বিন্দু (single point of failure) — যে সার্ভারে সেশন আছে সেটি ক্র্যাশ করলে সেই ইউজারের সেশন সম্পূর্ণ হারিয়ে যায়, যদিও বাকি সার্ভারগুলো সুস্থ।

৩ · সমাধান — State-কে একটি শেয়ারড স্টোরে সরানো

বাস্তব সিস্টেমে সমাধান হলো: অ্যাপ সার্ভার নিজে কোনো স্টেট রাখবে না, বরং প্রতিটি সেশন একটি কেন্দ্রীয় শেয়ারড স্টোরে (সাধারণত Redis বা একটি ডেটাবেস) রাখা হবে যা প্রতিটি অ্যাপ সার্ভার থেকে অ্যাক্সেসযোগ্য। এভাবে অ্যাপ সার্ভারগুলো সম্পূর্ণ স্টেটলেস থাকে — যেকোনো ইনস্ট্যান্স যেকোনো রিকোয়েস্ট হ্যান্ডল করতে পারে, কারণ "সত্যিকারের" স্টেট সার্ভারে নয়, শেয়ারড স্টোরে থাকে।

ক্লায়েন্ট Client লোড ব্যালেন্সার Load Balancer App Server 1 (stateless) App Server 2 (stateless) Session Store (Redis)
উভয় App Server-ই স্টেটলেস — সেশন ডেটা কোনো নির্দিষ্ট সার্ভারে নয়, শেয়ারড Session Store-এ থাকে, তাই যেকোনো সার্ভার যেকোনো ইউজারকে সার্ভ করতে পারে।
মূল নীতি

"সার্ভার কোনো স্টেট রাখে না, শেয়ারড স্টোর রাখে" — এই একটি বাক্যেই স্টেটলেস আর্কিটেকচারের সবচেয়ে গুরুত্বপূর্ণ নীতি লুকিয়ে আছে। এই প্যাটার্নটি পরবর্তী পাঠগুলোতেও ফিরে আসবে — L39-এ টোকেন-বেসড অথেন্টিকেশন (JWT) দেখব, যা সেশন স্টোরও লাগে না কারণ টোকেনটি নিজেই স্বয়ংসম্পূর্ণ।

৪ · কোড দিয়ে প্রমাণ — stateful ব্যর্থতা বনাম stateless সাফল্য

নিচে দুটি ফেইক সার্ভার সিমুলেট করা হলো। প্রথমে প্রতিটি সার্ভারের নিজস্ব লোকাল dict ব্যবহার করে stateful সেশন লুকআপ দেখানো হয়েছে — ভুল সার্ভারে গেলে সেশন পাওয়া যায় না। এরপর একটি শেয়ারড dict ব্যবহার করে দেখানো হয়েছে যে যেকোনো ফেইক সার্ভার থেকেই সেশন সঠিকভাবে পাওয়া যায়।

Python
# --- স্টেটফুল: প্রতিটি ফেইক সার্ভারের নিজস্ব লোকাল session dict ---
server_a_local_sessions = {}
server_b_local_sessions = {}

def stateful_login(server_local_store, session_id, username):
    server_local_store[session_id] = username

def stateful_lookup(server_local_store, session_id):
    return server_local_store.get(session_id, None)

# ইউজার লগইন করল, লোড ব্যালেন্সার রিকোয়েস্টটি Server A-তে পাঠাল
stateful_login(server_a_local_sessions, "sess-123", "rahim")

# পরের রিকোয়েস্ট (round robin-এর কারণে) Server B-তে চলে গেল
wrong_server_result = stateful_lookup(server_b_local_sessions, "sess-123")
right_server_result = stateful_lookup(server_a_local_sessions, "sess-123")

print("=== Stateful (লোকাল dict) ===")
print("Server B-তে লুকআপ (ভুল সার্ভার):", wrong_server_result)
print("Server A-তে লুকআপ (সঠিক সার্ভার):", right_server_result)

# --- স্টেটলেস: একটি শেয়ারড session store (Redis-এর মতো) ---
shared_session_store = {}

def stateless_login(shared_store, session_id, username):
    shared_store[session_id] = username

def stateless_lookup(shared_store, session_id, handling_server_name):
    return handling_server_name, shared_store.get(session_id, None)

stateless_login(shared_session_store, "sess-456", "karim")

result_from_a = stateless_lookup(shared_session_store, "sess-456", "Server-A")
result_from_b = stateless_lookup(shared_session_store, "sess-456", "Server-B")

print("\n=== Stateless (শেয়ারড dict) ===")
print("Server-A থেকে লুকআপ:", result_from_a)
print("Server-B থেকে লুকআপ:", result_from_b)

    
লক্ষ্য করুন — stateful কেসে Server B থেকে লুকআপ করলে None পাওয়া যায় (সেশন "হারিয়ে গেছে"), কিন্তু stateless কেসে Server-A এবং Server-B দুটো থেকেই একই সঠিক ইউজারনেম (karim) পাওয়া যায় — কারণ আসল ডেটা সার্ভারে নয়, শেয়ারড স্টোরে আছে।

৫ · এই ট্রেড-অফ কোথায় প্রযোজ্য

স্টেটলেস ডিজাইন সবসময় "বিনামূল্যে" আসে না — শেয়ারড স্টোরে প্রতিটি রিকোয়েস্টে একটি অতিরিক্ত নেটওয়ার্ক হপ যোগ হয়, এবং সেই শেয়ারড স্টোর নিজেই একটি নতুন নির্ভরতা (dependency) হয়ে ওঠে যাকে নিজেও রিলায়েবল ও স্কেলেবল রাখতে হয় (L16-এর রেপ্লিকেশন এখানে প্রাসঙ্গিক)। তবু বেশিরভাগ বড় সিস্টেমে এই ট্রেড-অফ মূল্যবান, কারণ এটি হরাইজন্টাল স্কেলিং (L10) ও লোড ব্যালেন্সিং (L11)-কে অনেক সহজ ও নির্ভরযোগ্য করে তোলে।

মূল কথা · Key takeaway

যেখানেই সম্ভব, সার্ভিসকে স্টেটলেস রাখুন এবং স্টেট externalize করুন। এটি এই কোর্সের সবচেয়ে ঘন ঘন ব্যবহৃত ডিজাইন প্যাটার্নগুলোর একটি — L39 (token-based auth), L44 (rate limiter), L45 (chat) সবগুলোতেই এই একই নীতি ফিরে আসবে।

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

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

প্র ০১ শেয়ারড সেশন স্টোরে সরিয়েও তো একটি "কেন্দ্রীয়" নির্ভরতা তৈরি হচ্ছে — তাহলে এটি কি আসলেই সমস্যার সমাধান, নাকি সমস্যাটা শুধু সরানো হলো?

সমস্যাটা সরানো হয়েছে ঠিকই, কিন্তু এটি একটি লাভজনক ট্রেড-অফ। আগে প্রতিটি অ্যাপ সার্ভারকেই আলাদাভাবে স্কেল ও রিলায়েবল করতে হতো (কঠিন, কারণ স্টেট ছড়িয়ে থাকে)। এখন শুধু একটি স্তরকে (Session Store) খুব ভালোভাবে স্কেল ও রেপ্লিকেট করলেই (L16) হয়, আর অ্যাপ সার্ভার লেয়ার সম্পূর্ণ স্টেটলেস ও তুচ্ছভাবে হরাইজন্টালি স্কেলেবল থাকে। জটিলতা কমেনি, কিন্তু এক জায়গায় কেন্দ্রীভূত ও সমাধানযোগ্য হয়ে গেছে।

প্র ০২ যদি Sticky Session ব্যবহার করেই সমস্যাটি "সমাধান" করা যায় (একই ইউজারকে সবসময় একই সার্ভারে পাঠিয়ে), তাহলে শেয়ারড স্টোরের ঝামেলা কেন নেওয়া হয়?

Sticky session লোড ব্যালেন্সিংকে অসম করে তোলে (কিছু সার্ভার বেশি "ভারী" ইউজার পেয়ে যেতে পারে, কিন্তু তাদের সরানো যায় না) এবং একক ব্যর্থতার ঝুঁকি রাখে — সেই নির্দিষ্ট সার্ভার ক্র্যাশ করলে ইউজারের সেশন সম্পূর্ণ হারিয়ে যায়। শেয়ারড স্টোরে এই দুটো সমস্যাই থাকে না — যেকোনো সার্ভার যেকোনো ইউজারকে সমানভাবে সার্ভ করতে পারে, এবং একটি অ্যাপ সার্ভার ক্র্যাশ করলেও সেশন ডেটা অক্ষত থাকে।

প্র ০৩ সব সার্ভিসকেই কি স্টেটলেস বানানো সম্ভব?

বেশিরভাগ HTTP API সার্ভিসকে স্টেটলেস বানানো সম্ভব ও কাম্য, কিন্তু কিছু ক্ষেত্রে নিজস্ব চ্যালেঞ্জ থাকে — যেমন দীর্ঘস্থায়ী WebSocket কানেকশন (L08) নিজেই একধরনের স্টেট (কোন সার্ভার কার কানেকশন ধরে আছে), যা L45-এর চ্যাট কেস স্টাডিতে একটি pub/sub ব্যাকএন্ড দিয়ে সমাধান করা হবে। মূল কথা হলো স্টেট থাকবেই — প্রশ্ন হলো সেটা অ্যাপ সার্ভারের লোকাল মেমরিতে থাকবে, নাকি একটি সঠিকভাবে ডিজাইন করা শেয়ারড লেয়ারে।

অনুশীলন

  1. চিন্তা করুন: একটি অনলাইন শপিং কার্ট ফিচার ডিজাইন করছেন — ইউজার লগইন না করেই পণ্য কার্টে যোগ করতে পারে। এই কার্ট ডেটা স্টেটলেসভাবে সংরক্ষণ করার দুটি উপায় প্রস্তাব করুন।

    উদাহরণ: (১) কার্ট ডেটা ক্লায়েন্ট-সাইড কুকিতে/localStorage-এ রাখা, প্রতিটি রিকোয়েস্টে ক্লায়েন্টই কার্টের বর্তমান অবস্থা পাঠায় — সার্ভার কিছুই মনে রাখে না। (২) একটি অ্যানোনিমাস "গেস্ট সেশন ID" জেনারেট করে শেয়ারড সেশন স্টোরে (Redis) কার্ট রাখা, ID-টি কুকিতে পাঠানো — উভয় ক্ষেত্রেই অ্যাপ সার্ভার নিজে স্টেটলেস থাকে।

  2. ট্রেস করুন: উপরের কোড সেলে stateful_login-কে server_b_local_sessions-এ কল করার পর stateful_lookup-কে server_a_local_sessions দিয়ে কল করলে ফলাফল কী হবে তা কোড চালানোর আগে অনুমান করুন, তারপর কোড পরিবর্তন করে যাচাই করুন।

    ফলাফল হবে None — কারণ সেশনটি server_b_local_sessions-এ লেখা হয়েছিল, server_a_local_sessions-এ নয়। এটি ঠিক সেই একই ব্যর্থতা যা মূল কোড সেলে দেখানো হয়েছে, শুধু বিপরীত দিক থেকে — যেকোনো "ভুল" লোকাল dict-এ লুকআপ করলেই স্টেটফুল ব্যর্থতা ঘটে।

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

আগের পাঠ
লোড ব্যালেন্সিং — অ্যালগরিদম ও কৌশল