Part 01 · Backend Mindset

Stateless vs Stateful Thinking

StatelessStateful

দুই request এর মাঝে কিছু মনে না রাখলে server কেন সহজে scale হয়, আর তার দাম কী।

⏱ 1 ঘণ্টা · Station 5 / 153

যে cart বারবার ভুলে যায়

দোকান বড় হচ্ছে, তাই server এর দ্বিতীয় একটা copy চালু করলে, আর সামনে একটা load balancer বসালে যাতে traffic ভাগ হয়। Test এ সব ঠিকঠাক চলছে।

তারপর অভিযোগ আসা শুরু। "চা নিলাম, তারপর cake, cart এ শুধু cake দেখাচ্ছে।" "Refresh দিলাম, cart খালি।" "প্রতিবার reload দিলে একবার চা দেখায়, একবার cake, আবার চা।"

Code একটুও বদলায়নি। বদলেছে শুধু একটা জিনিস: এখন code এর দুটো copy চলছে। এই lesson এ দেখবো cart ভাঙার জন্য এটুকুই কেন যথেষ্ট, আর সেই একটা প্রশ্ন যেটা ঠিক করে দেয় একটা backend বড় হতে পারবে কিনা: state থাকে কোথায়?

সমস্যা: memory শুধু একটা process এর

আগের lesson এ value গুলোকে data, state, logic আর computation এ ভাগ করেছিলাম। State মানে "এই মুহূর্তে অবস্থা কী": একটা cart, একটা login, অর্ধেক পূরণ করা একটা form।

State রাখার সবচেয়ে সহজ জায়গা হলো server এর নিজের memory, চলতে থাকা program এর ভেতরে একটা map। দ্রুত, আর code এ এক লাইন। কিন্তু এই memory র তিনটা ব্যাপার মানুষ ভুলে যায়:

Process memory র ব্যাপারState এর জন্য এর মানে
এটা একটা process এরServer এর দ্বিতীয় copy এটা দেখতে পায় না
Process থামলে এটা মুছে যায়প্রতিটা deploy, crash বা restart এ সব শেষ
এটা থাকে একটা machine এওই machine নষ্ট হলে state ও সাথে যায়

যে server দুই request এর মাঝে নিজের memory তে state রাখে, তাকে বলে stateful। যে server দুই request এর মাঝে কিছুই রাখে না, যা লাগে তা request থেকে বা একটা shared store থেকে নেয়, তাকে বলে stateless।

একটা চেম্বারের কথা ভাবো

একটা চেম্বার ভাবো যেখানে একই shift এ দুজন ডাক্তার বসেন।

Stateful চেম্বারে প্রত্যেক ডাক্তার রোগীর কথা নিজের মাথায় রাখেন। সোমবার তুমি Dr. A কে দেখালে। বুধবার Dr. A ব্যস্ত, তাই Dr. B কে দেখালে, যিনি তোমার নামই শোনেননি। Dr. A ছুটিতে গেলে তোমার পুরো history ও তাঁর সাথে চলে যায়।

Stateless চেম্বারে ডাক্তাররা এক রোগী থেকে আরেক রোগীর মাঝে কিছুই মনে রাখেন না। তোমার file থাকে একটা shared record room এ, আর যে ডাক্তার ফাঁকা থাকেন তিনি file টা বের করেন, পড়েন, একটা note লেখেন, আবার রেখে দেন। যেকোনো ডাক্তার তোমাকে দেখতে পারেন। একজন চলে গেলে নতুন একজন আসতে পারেন, history একটুও হারায় না।

Stateless ডাক্তাররা কম দক্ষ না। শুধু কোনো কিছু তাঁদের নিজের কাছে থাকে না। এজন্যই একজনের জায়গায় আরেকজন বসলে কিছু যায় আসে না।

where does the cart live?browserload balancercopy Acopy Bshared storetea, caketeacakeown memory: the cart is splitteacakestateless copies: any one can answer
Load balancer পালা করে দুটো copy তে request পাঠায়। প্রত্যেক copy নিজের memory তে cart রাখলে চা যায় A তে আর cake যায় B তে, cart ভাগ হয়ে যায়। দুটো copy একটা shared store ব্যবহার করলে যেকোনোটা উত্তর দিতে পারে, cart পুরোটা থাকে।

Analogy টা কোথায় খাটে না। কাগজের file আনতে কয়েক মিনিট লাগে; shared store থেকে পড়তে লাগে মোটামুটি এক millisecond, কিন্তু ওই millisecond টা প্রতিটা request এ লাগে, আর shared store হয়ে যায় একটা জরুরি machine যার ওপর সবকিছু নির্ভর করে। Stateless server state কে গায়েব করে দেয় না। State কে এমন জায়গায় সরায় যেটা যত্ন করে বানাতে হয়। (Module 6।)

মূল জিনিসগুলো

  1. কাজের যেকোনো system এর state থাকে। Login, cart, game এর score, chat এর history। "Stateless" মানে কখনো "state নেই" না। মানে হলো server process দুই request এর মাঝে এটা রাখে না।
  2. State তিন জায়গায় থাকতে পারে:
    • Client এ। Cookie, local storage, token, বা form এর লুকানো একটা field। প্রতিটা request এর সাথে client এটা পাঠায়। সস্তা, কিন্তু client এটা পড়তে আর বদলাতে পারে (lesson 2), আর size এর সীমা আছে।
    • Server এর memory তে। দ্রুত আর সহজ। Restart এ হারায়, অন্য copy দেখতে পায় না।
    • Shared store এ। একটা database, বা Redis এর মতো দ্রুত key-value store। প্রতিটা copy একই জায়গায় পড়ে আর লেখে। Restart এর পরেও থাকে। প্রতিটা request এ একবার network এ যেতে হয়।
  3. Stateless server প্রতিটা request কে এমনভাবে দেখে যেন এটাই প্রথম। যা লাগে তা আসে request থেকে (একটা token, item এর id), নয়তো shared store থেকে খুঁজে আনে। Process এর ভেতরে আগের কিছু থেকে যায় না।
  4. বেশিরভাগ web server এ stateless কেন জেতে:
    • Copy বাড়িয়ে scale করা যায়। যেকোনো copy যেকোনো request সামলাতে পারে।
    • যখন খুশি restart করা যায়। Deploy, crash বা autoscaling এ কিছু হারায় না।
    • Load ভাগ করা সহজ। কে কোথায় গিয়েছিল load balancer কে মনে রাখতে হয় না।
  5. যেখানে stateful ই ঠিক: যা স্বভাবতই একটা চালু connection এর সাথে বাঁধা, বা এত ঘন ঘন লাগে যে প্রতিবার আনা চলে না: একটা WebSocket chat connection, multiplayer game এর room, database নিজে, একটা in-memory cache। এগুলো জেনেবুঝে stateful বানানো হয়, নষ্ট হলে কী হবে তার plan সহ।
আগে অনুমান করো

তোমার stateful server memory তে cart রাখে। দ্বিতীয় একটা copy চালু করলে, আর load balancer পালা করে request পাঠায়: A, B, A, B। একজন user চা নিলো (গেলো A তে), তারপর cake (গেলো B তে), তারপর cart খুললো (গেলো A তে)। সে কী দেখবে?

ভেতরে কী হয়: একটা cart, দুটো copy

একই user, একই চারটা click, state রাখা process এর memory তে।

ধাপে ধাপে দেখো

Load balancer এর পেছনে একটা stateful cart

BrowserLoad balancerCopy ACopy B

ধাপ 1/6 চা যোগ

Browser Alice এর session cookie সহ POST /cart {item: tea} পাঠায়। দুটো server আছে, সে জানেই না।

request
add tea

মাথায় রাখার ছবি

তোমার server গুলোকে একই রকম, খালি বাক্স হিসেবে আঁকো, আর state আঁকো তাদের বাইরে:

  • বাঁ দিকে client এর state (cookie, token),
  • মাঝে server এর copy গুলো, ভেতরে কিছু নেই,
  • ডান দিকে shared state (database, Redis)।

মাঝের যেকোনো বাক্স যখন খুশি মুছে দিলে যদি কোনো user টের না পায়, তোমার server গুলো stateless। কোনো বাক্স মুছলে যদি user এর দরকারি কিছু হারায়, ওই বাক্স stateful, আর তোমাকে জানতে হবে ঠিক কী কী ওর ভেতরে আছে, আর ওটা মরে গেলে কী হবে।

চেষ্টা করো: stateful নাকি stateless?

প্রতিটা design এ server process stateful কিনা, আর সেটা সমস্যা কিনা, ঠিক করো।

প্রতিটা case চালিয়ে দেখো

এই state থাকে কোথায়?

sessions = {abc123: alice}
process memory তে
ভাঙে: Stateful। প্রতিটা restart এ সবাই logout হয়ে যায়, আর দুটো copy থাকলে কোন copy তে গেলো তার ওপর user হঠাৎ হঠাৎ logout হয়। Session কে Redis বা database এ সরাও, নয়তো এমন signed token ব্যবহার করো যেটা client নিজে নিয়ে ঘোরে।

গোড়া থেকে code

একই server এর দুটো copy, A আর B, আর একটা load balancer যে প্রতিটা request এদের একটায় পাঠায়। একই ছয়টা ধাপ পাশাপাশি দুবার চলে:

  • stateful: প্রত্যেক copy cart রাখে নিজের memory তে।
  • stateless: দুটো copy কিছুই রাখে না, ব্যবহার করে একটা shared store (database এর জায়গায় বসানো)।

Copy restart করা মানে এখানে ওটার জায়গায় নতুন একটা বসানো, আসল process এর সাথেও ঠিক এটাই হয়।

package main

import (
	"fmt"
	"strings"
)

// A store maps a user to their cart. Each server copy can have its own, or share one.
type store map[string][]string

// server is one running copy of the backend. It only knows the store it was given.
type server struct{ carts store }

func (s *server) add(user, item string) { s.carts[user] = append(s.carts[user], item) }

func (s *server) view(user string) string {
	if len(s.carts[user]) == 0 {
		return "(empty)"
	}
	return strings.Join(s.carts[user], ", ")
}

func main() {
	// Stateful: each copy keeps carts in its own memory.
	stateful := []*server{{store{}}, {store{}}}
	// Stateless: the copies keep nothing; both use one shared store (think: a database).
	shared := store{}
	stateless := []*server{{shared}, {shared}}

	steps := []struct{ do, item, copy string }{
		{"add", "tea", "A"},
		{"add", "cake", "B"},
		{"view", "", "A"},
		{"view", "", "B"},
		{"restart", "", "A"},
		{"view", "", "A"},
	}
	fmt.Printf("%-10s %-4s %-26s %s\n", "request", "copy", "stateful (own memory)", "stateless (shared store)")
	for _, st := range steps {
		i := strings.Index("AB", st.copy) // the load balancer sent this request to copy A or B
		switch st.do {
		case "add":
			stateful[i].add("alice", st.item)
			stateless[i].add("alice", st.item)
		case "restart":
			stateful[i] = &server{store{}} // a fresh process starts with empty memory
			stateless[i] = &server{shared} // ...but it reconnects to the same shared store
			fmt.Printf("%-10s %-4s %-26s %s\n", "restart", st.copy, "memory wiped", "nothing lost")
			continue
		}
		label := strings.TrimSpace(st.do + " " + st.item)
		fmt.Printf("%-10s %-4s %-26s %s\n", label, st.copy, stateful[i].view("alice"), stateless[i].view("alice"))
	}
}

Output (পাঁচটা language এই একই):

request    copy stateful (own memory)      stateless (shared store)
add tea    A    tea                        tea
add cake   B    cake                       tea, cake
view       A    tea                        tea, cake
view       B    cake                       tea, cake
restart    A    memory wiped               nothing lost
view       A    (empty)                    tea, cake

Output টা পড়ি:

  • B তে add cake: stateful column এ শুধু cake, কারণ B একদম নতুন cart শুরু করেছে। Stateless column এ tea, cake: B সেই store এই লিখেছে যেখানে A লিখেছিল।
  • A তে view, B তে view: একই user, একই মুহূর্ত, অথচ stateful এর উত্তর মেলে না (tea বনাম cake)। "আমার cart বারবার বদলাচ্ছে" অভিযোগটা এটাই।
  • restart A: stateful copy যা রেখেছিল সব হারায়; stateless copy কিছুই হারায় না, কারণ সে কিছু রাখেইনি।
  • Restart এর পর A তে view: (empty) বনাম tea, cake। Copy stateless হলে যখন খুশি server restart করা, বদলানো বা বাড়ানো যায়।

খেয়াল করো, দুই column এই server এর code হুবহু এক। পার্থক্য শুধু তাকে কোন store দেওয়া হয়েছে। Stateless মানে আলাদা ধরনের code না, state কোথায় থাকবে সেই সিদ্ধান্ত।

আসল engineering

  • Web server কে default ভাবে stateless বানাও। এজন্যই platform গুলো নিজে নিজে scale করতে পারে, downtime ছাড়া deploy করতে পারে, আর নষ্ট machine নিজে নিজে বদলে দিতে পারে। Twelve-factor app এর ভাষায়: "process গুলো stateless আর কিছু share করে না"।
  • Sticky session জোড়াতালি, সমাধান না। Load balancer একজন user কে একটা copy র সাথে বেঁধে রাখতে পারে (cookie বা IP দেখে)। Cart আর ভাগ হয় না, কিন্তু load অসমান হয়ে যায়, আর ওই copy restart হলে user এর state তবুও হারায়।
  • State এর ঘর বাছো ব্যবহার দেখে। Session আর cart: Redis বা database। File: object storage। লম্বা সময়ের connection (WebSocket): stateful server, আবার connect করার plan সহ। অল্প সময়ের, হারালেও ক্ষতি নেই এমন copy: in-memory cache।
  • Client এর state check করতে হয়। যে token বা cookie server sign করেনি, তাকে বিশ্বাস করা যায় না (lesson 2)। Signed token দিলে খোঁজাখুঁজি লাগে না, কিন্তু logout আর token বাতিল করা কঠিন হয়।
  • Shared store টা জরুরি হয়ে যায়। Stateless server scale করা সহজ; পেছনের database না। এজন্যই Module 6 এ database নিয়ে এত কথা, আর Module 8 এ দেখবো একটা database ও যখন যথেষ্ট না তখন কী হয়।

যখন ভাঙে

প্রতিটা case চালিয়ে দেখো

একটা failure বাছো

2টা copy → 50% request বলে 'please log in'
ভাঙে: Session ছিল প্রতিটা process এর memory তে। ধরার উপায়: fail করা প্রতিটা request কোন copy সামলেছে দেখো (log এ instance id রাখো)। সমাধান: session কে shared store এ সরাও।

পড়ার আগে ভাবো

একটা team বলছে: "আমাদের API stateless। JWT token ব্যবহার করি, তাই session কোথাও রাখি না।" একজন user জানালো, password বদলানোর পরেও তার চুরি হওয়া phone এ এখনো login করা আছে। API কি আসলেই stateless, আর কী ভুল হলো?

উত্তর দেখো

Server গুলো stateless, কিন্তু system এর state তবুও আছে: চুরি হওয়া phone এর token এর ভেতরে, আর server রাজি হয়েছিল মেয়াদ শেষ না হওয়া পর্যন্ত ওটা বিশ্বাস করবে। Password বদলালেও token বাতিল হয়নি, কারণ server কোথাও check ই করে না। সাধারণ সমাধানগুলো একটু state ফেরত আনে: token এর মেয়াদ ছোট রাখা আর server এ রাখা refresh token, অথবা প্রতিটা request এ check হওয়া একটা বাতিল-list। Stateless একটা trade-off, বিনা পয়সার কিছু না।

নিজেকে check করো

1. "Stateless server" আসলে মানে কী?

উত্তর

Server process দুই request এর মাঝে কিছু রাখে না। প্রতিটা request নিজের যা লাগে নিয়ে আসে, নয়তো server একটা shared store থেকে খুঁজে নেয়, তাই যেকোনো copy যেকোনো request সামলাতে পারে।

2. State থাকতে পারে এমন তিনটা জায়গার নাম বলো, প্রতিটার একটা দুর্বলতা সহ।

উত্তর

Client (user পড়তে আর বদলাতে পারে, size এর সীমা), server এর memory (restart এ হারায়, অন্য copy দেখতে পায় না), shared store (প্রতিটা request এ বাড়তি একবার network এ যাওয়া, আর পুরো system এর জন্য জরুরি একটা নির্ভরতা)।

3. কোনো code না বদলালেও দ্বিতীয় copy যোগ করলে stateful server কেন ভেঙে যায়?

উত্তর

প্রত্যেক copy র নিজের memory। একজন user এর request বিভিন্ন copy তে পড়ে, তাই তার state ওদের মধ্যে ভাগ হয়ে যায়, আর প্রত্যেকে আলাদা উত্তর দেয়।

4. এমন একটা উদাহরণ দাও যেখানে stateful server ই ঠিক design।

উত্তর

Multiplayer game এর room, WebSocket chat server, database বা in-memory cache: যেখানে state এত ঘন ঘন লাগে বা একটা চালু connection এর সাথে এমনভাবে বাঁধা যে প্রতিটা request এ আনা চলে না।

তোমার challenge: restart সামলাও

Program এ একটা copy C যোগ করো যেখানেও load balancer request পাঠাতে পারে। তারপর ধাপগুলো এমনভাবে বদলাও যাতে মাঝখানে copy B restart হয়, আর শেষ view যায় C তে।

  • চালানোর আগে দুটো column এ কী আসবে আন্দাজ করো।
  • তারপর তৃতীয় একটা column যোগ করো, sticky: প্রত্যেক copy র নিজের memory, কিন্তু load balancer alice কে সবসময় সেই copy তেই পাঠায় যেটা সে প্রথম ব্যবহার করেছিল।

সফল হয়েছো যখন output এ stateless column সবসময় পুরো cart দেখায়, stateful column copy অনুযায়ী আলাদা cart দেখায়, আর sticky column ততক্ষণ ঠিক চলে যতক্ষণ না Alice যে copy র সাথে বাঁধা সেটা restart হয়, তারপর (empty) দেখায়।

Mental model

সব system এর state আছে; প্রশ্ন হলো সেটা থাকে কোথায়। Server এর memory তে রাখলে ওই server বিশেষ হয়ে যায়: কিছু না হারিয়ে ওটাকে copy, restart বা বদলানো যায় না। Server কে খালি রাখো আর state রাখো client এ বা shared store এ, তাহলে প্রতিটা copy একটার জায়গায় আরেকটা বসানো যায়। Web server default ভাবে stateless বানাও, আর যা stateful তা বানাও জেনেবুঝে, মরে গেলে কী হবে তার plan সহ।

Station stamp

Chapter শেষ করো

পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।

Pro নিয়ে XP পাও →