Part 01 · Backend Mindset

When Software Serves Many Users

ScaleUsers

একটা program যখন একজনের বদলে হাজার মানুষকে একসাথে serve করে, তখন কী কী বদলে যায়।

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

তোমার app চলছে। তারপর হাজার মানুষ একসাথে খুললো।

ধরো, তুমি একটা ছোট notes app বানালে। তোমার laptop এ চলে, একটা file এ save করে, একদম ঠিকঠাক কাজ করে। তারপর এক বন্ধু link টা share করলো, আর সেই সন্ধ্যায় এক হাজার মানুষ app টা খুললো।

তুমি code এর একটা লাইনও বদলাওনি। তাহলে আর কী ভুল হতে পারে?

দেখা যায়, প্রায় সবকিছুই। দুজন একই note edit করছে। পাঁচশো মানুষ একই সেকেন্ডে "save" চাপছে। কেউ একটা 2 GB file paste করে দিলো। কেউ অন্য একজনের নাম লিখে তার notes পড়ে ফেললো। Program একবার crash করলো, আর এখন সেটা সবার জন্য বন্ধ।

এই lesson ওই মুহূর্তটা নিয়ে: software যখন একজনের বদলে অনেক মানুষকে serve করা শুরু করে, তখন কী কী বদলায়। এই course এর প্রায় প্রতিটা topic এই কারণেই আছে।

সমস্যা: একজনের জন্য বানানো program

তোমার নিজের laptop এর program চুপচাপ অনেক কিছু ধরে নেয়, যেগুলো অন্য মানুষ ব্যবহার শুরু করা মাত্র আর সত্য থাকে না।

একজন userঅনেক user
কে চাইছে?সবসময় তুমিঅচেনা মানুষ, আর তাদের আলাদা করে চিনতে হবে
Data কোথায় থাকে?Program এর পাশে একটা file এএক জায়গায়, যেটা সবাই পড়ে আর লেখে
কাজ কখন হয়?একবারে একটা, তুমি যখন click করোহাজারটা একসাথে, মানুষ যখন খুশি click করে
কাজ কতটা?একজন মানুষ যতটা করেসবার যোগফল, হঠাৎ হঠাৎ spike সহ
Crash করলে?তুমি আবার চালু করোফিরে না আসা পর্যন্ত সবাই আটকে থাকে
User কে বিশ্বাস করা যায়?ওটা তো তুমিইকেউ কেউ অসাবধান, আর অল্প কয়েকজন খারাপ উদ্দেশ্যের

এগুলোর কোনোটাই তোমার code এর bug না। এগুলো নতুন পরিস্থিতি, যার মধ্যে এখন তোমার code চলছে। Backend মোটামুটি ডান দিকের column এর উত্তরগুলোর সমষ্টি।

একটা চায়ের দোকানের কথা ভাবো

বাসায় নিজের জন্য চা বানানো সহজ। একটা কেটলি, একটা কাপ, আর কেউ অপেক্ষায় নেই।

এবার ভাবো, সকাল ৮টায় একটা ব্যস্ত বাসস্ট্যান্ডের চায়ের দোকান। প্রতি কাপ বানাতে দোকানদারের লাগে এক মিনিট। Customer রা মিনিটে একজন করে সুন্দরভাবে আসে না। আসে দল বেঁধে: একটা bus থামলে একসাথে তিনজন, তারপর কিছুক্ষণ কেউ না।

  • ঘণ্টায় ৩০ জন হলে দোকানদার অর্ধেক সময় ব্যস্ত, আর বেশিরভাগ মানুষ প্রায় সাথে সাথেই চা পায়।
  • ঘণ্টায় ৫৪ জন হলে দোকানদার ৯০% সময় ব্যস্ত। প্রায় সবসময় ছোট একটা ভিড় থাকে, আর কেউ কেউ দশ মিনিট দাঁড়িয়ে থাকে।
  • ঘণ্টায় ৭০ জন হলে একজন মানুষ যত জনকে দিতে পারে তার চেয়ে বেশি আসছে। ভিড় কখনো কমে না, শুধু বাড়ে।

দোকানদারকে আরও মনে রাখতে হয় কোন কাপ কার, একটা চিনির কৌটা সবার মধ্যে ভাগ করতে হয়, আর খেয়াল রাখতে হয় যেন একজন customer আধা ঘণ্টা counter আটকে দাঁড়িয়ে বাকিদের থামিয়ে না রাখে।

one usermany usersyour appon your laptopnotes.txtonly yourssavenobody else touches ituser Auser Buser Cserverone, for everyonedatabaseone shared copyreqreqreqwritethree at the same moment
With one user, the app and its file sit together and nobody else touches them. With many users, everyone shares one server and one copy of the data, and sometimes they all arrive in the same moment.

Analogy টা কোথায় খাটে না। Customer লাইন দেখতে পায়, চাইলে চলে যেতে পারে। Request পারে না: সে শুধু অপেক্ষা করে, যতক্ষণ না কেউ হাল ছেড়ে দেয়, সাধারণত একটা timeout। আর computer কয়েক সেকেন্ডে দ্বিতীয় একটা "কেটলি" যোগ করতে পারে, কিন্তু শুধু তখনই যখন কাজটা আসলে ভাগ করা যায়। কিছু কাজ ভাগ করা যায় না, আর সেটা নিয়ে আলাদা একটা পুরো module আছে।

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

Software যখন একজন user থেকে অনেক user এ যায়, তখন পাঁচটা জিনিস বদলায়। প্রতিটাই এই course এর একটা অংশ।

  1. Shared data। সবাইকে একই সত্য দেখতে হবে, তাই data একটা private file থেকে বেরিয়ে এক shared জায়গায় যায়, সাধারণত একটা database এ। (Lesson 7, আর Module 6।)
  2. একসাথে আসা request। একই মুহূর্তে অনেক request আসে, তাই program কে একসাথে একাধিক request এর কাজ করতে হয়। এটাই concurrency। (Module 4।)
  3. সীমিত capacity। প্রতিটা request কিছুটা সময় খায়। যথেষ্ট request এলে server এর সময় ফুরিয়ে যায়, আর request গুলো লাইনে দাঁড়াতে শুরু করে। (এই lesson।)
  4. Identity আর trust। Program কে জানতে হবে কে চাইছে, আর সে কী করার অনুমতি পায়। (Module 9।)
  5. Blast radius। একটা crash, বা একটা ধীর অংশ, এখন একসাথে সবাইকে ভোগায়। (Module 7।)

এর মধ্যে capacity এমন জিনিস যেটা আজই সংখ্যায় মাপা যায়, মাত্র দুটো সংখ্যা দিয়ে:

  • Arrival rate: প্রতি সেকেন্ডে কয়টা request আসে।
  • Service time: একটা request একজন worker কে কতক্ষণ ব্যস্ত রাখে।

দুটো গুণ করলে পাও server কতটা busy:

busy = প্রতি সেকেন্ডে আসা request × service time ÷ worker এর সংখ্যা

প্রতি সেকেন্ডে 10টা request × প্রতিটা 0.1 s ÷ 1 জন worker = 1.0, মানে 100% busy, একটুও ফাঁকা মুহূর্ত নেই।

আরেকটা নিয়ম, যার নাম Little's law, বলে দেয় যেকোনো মুহূর্তে তোমার system এর ভেতরে কয়টা request আছে:

চলমান request = প্রতি সেকেন্ডে আসা request × প্রতিটা ভেতরে গড়ে কতক্ষণ থাকে

প্রতি সেকেন্ডে 200টা request, প্রতিটা 0.25 s নিলে, প্রতিটা মুহূর্তে প্রায় 50টা request চলমান থাকে। তোমার server এর একসাথে 50টার জায়গা লাগবে: 50টা connection, 50টা database call, 50 ভাগ memory। এই সংখ্যাটা ধরে আগে থেকে plan করা যায়।

আগে অনুমান করো

বিশজন user একই মুহূর্তে click করলো। Server একবারে একটা করে request চালায়, আর প্রতিটায় লাগে 100 ms। শেষ user কতক্ষণ অপেক্ষা করবে?

ভেতরে কী হয়: তিনজন user, একটা server

Predict প্রশ্নের সেই মুহূর্তটাই, তিনজন user এ নামিয়ে ধীরে ধীরে দেখা।

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

তিনটা request একসাথে আসে

User AUser BUser CServerDatabase

ধাপ 1/6 তিনজন একসাথে "save" চাপলো

তিনটা phone থেকে তিনটা request বেরিয়ে একই millisecond এর মধ্যে server এ পৌঁছায়। কে আগে যাবে, তাদের গায়ে এমন কিছু লেখা নেই।

arrived
A, B, C
running
none

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

প্রতিটা server, যত বড়ই হোক, একই আকারের: request আসে → লাইনে দাঁড়ায় → একজন worker কাজ করে → চলে যায়।

লাইন কত লম্বা হবে, দুটো জিনিস ঠিক করে: worker রা কতটা busy, আর মানুষ কতটা এলোমেলোভাবে আসে। যেহেতু request দল বেঁধে আসে, লাইন সমানভাবে বাড়ে না। অনেকক্ষণ ছোট থাকে, তারপর হঠাৎ বিস্ফোরণ।

latency vs how busy the server is50%70%80%90%95%going from 50% to 80% busy: wait is 2.5 times longergoing from 80% to 95%: 4 times longer again
The wait doesn't grow in a straight line as the server gets busier. Going from 50% to 80% busy makes the wait a few times longer; going from 80% to 95% makes it several times longer again.

এই curve এর কারণেই engineer রা কখনো server কে 100% এ চালানোর plan করে না। Capacity এর শেষ কয়েক % সবচেয়ে দামি।

নিজে চেষ্টা করো: লাইন ছোট রাখো

Slider গুলো নাড়াও আর দেখো অপেক্ষার সময়ের কী হয়। এই ক্রমে চেষ্টা করো:

  1. প্রতি সেকেন্ডে 8 জন, 100 ms, 1 জন worker। Server 80% busy, আর গড় অপেক্ষা (400 ms) already কাজের সময়ের চার গুণ।
  2. এবার 9 জন করো। প্রতি সেকেন্ডে মাত্র একজন বেশি, আর অপেক্ষা লাফিয়ে 900 ms।
  3. এবার দ্বিতীয় একজন worker যোগ করো। অপেক্ষা নেমে আসে কয়েক দশ millisecond এ। Capacity দ্বিগুণ করায় অপেক্ষা অর্ধেক হলো না, প্রায় মুছেই গেলো।
  4. আবার 1 জন worker এ ফিরে 12 জন করো। Server যত শেষ করতে পারে তার চেয়ে বেশি আসছে, আর লাইন চিরকাল বাড়তেই থাকে।
Live lab

তোমার server কতটা busy?

busy
80%
গড় wait
400 ms
গড় response
500 ms
এখনো চলছে, কিন্তু wait কাজের সময়কে ছাড়িয়ে যাচ্ছে। আর অল্প কয়েকজন user এলেই অবস্থা দ্রুত খারাপ হবে।

সংখ্যাগুলো আসে standard queueing formula থেকে (M/M/c, Erlang C দিয়ে), যেটা ধরে নেয় মানুষ এলোমেলোভাবে আসে। Real traffic প্রায়ই আরও বেশি দল বেঁধে আসে, তাই real অপেক্ষা সাধারণত একটু বেশি হয়।

একদম গোড়া থেকে code

Step 1: একই server, একটা একটা করে আর পাশাপাশি। শুধু standard library দিয়ে বানানো একটা আসল HTTP server, যেখানে প্রতিটা request এ 100 ms লাগে। তারপর বিশজন user একই মুহূর্তে সেটায় request পাঠায়।

package main

import (
	"fmt"
	"net"
	"net/http"
	"slices"
	"sync"
	"time"
)

// Each request needs 100 ms, like waiting for a database to answer.
func work() { time.Sleep(100 * time.Millisecond) }

// serve starts a real HTTP server on a free port and returns its address.
func serve(oneAtATime bool) string {
	var mu sync.Mutex
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if oneAtATime {
			mu.Lock() // only one request may be inside at a time
			defer mu.Unlock()
		}
		work()
		fmt.Fprint(w, "ok")
	})
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	go http.Serve(ln, handler)
	return "http://" + ln.Addr().String()
}

// load sends `users` requests at the same moment and returns when each one finished.
func load(url string, users int) []time.Duration {
	start := time.Now()
	done := make([]time.Duration, users)
	var wg sync.WaitGroup
	for i := range users {
		wg.Add(1)
		go func() {
			defer wg.Done()
			resp, err := http.Get(url)
			if err != nil {
				panic(err)
			}
			resp.Body.Close()
			done[i] = time.Since(start)
		}()
	}
	wg.Wait()
	slices.Sort(done)
	return done
}

func secs(d time.Duration) string {
	return fmt.Sprintf("%.1f s", d.Truncate(100*time.Millisecond).Seconds()) // rounded down to 0.1 s
}

func main() {
	const users = 20
	fmt.Printf("%d users click at the same moment; each request needs 100 ms\n", users)
	for _, mode := range []struct {
		name string
		one  bool
	}{{"one at a time", true}, {"side by side ", false}} {
		t := load(serve(mode.one), users)
		fmt.Printf("%s: first done after %s, last after %s\n", mode.name, secs(t[0]), secs(t[users-1]))
	}
}

Output (পাঁচটা language এই একই; সময় 0.1 s এ নিচের দিকে round করা):

20 users click at the same moment; each request needs 100 ms
one at a time: first done after 0.1 s, last after 2.0 s
side by side : first done after 0.1 s, last after 0.1 s

Output টা পড়া:

  • one at a time: প্রথম user 0.1 s এ শেষ, কিন্তু শেষজন অপেক্ষা করে 2 সেকেন্ড, কারণ প্রতিটা request কে তার সামনের সবার জন্য অপেক্ষা করতে হয়।
  • side by side: সবাই 0.1 s এ শেষ। প্রতিটা request তার 100 ms অপেক্ষায় কাটায়, আর অপেক্ষা একসাথে চলতে পারে।
  • কাজের কিছুই বদলায়নি। বদলেছে শুধু server একসাথে কয়টা request ঢুকতে দেয়। এই একটা সিদ্ধান্তই 0.1 s এর app আর 2 s এর app এর মধ্যে পার্থক্য।

(Java তে program টা আগে একটা warm-up request পাঠায়। একদম প্রথম call এ JVM class load করে আর প্রথম connection খোলে, আর সেই start-up এর খরচ না হলে প্রথম user এর ঘাড়ে পড়তো।)

Step 2: traffic বাড়লে কী হয়। Step 1 ছিল একটা burst। Real user রা এলোমেলোভাবে আসে, প্রতি সেকেন্ডে কয়েকজন করে। এই program একজন worker এর queueing formula, wait = busy ÷ (1 − busy) × service time, দিয়ে বিভিন্ন traffic এ গড় অপেক্ষা দেখায়।

package main

import (
	"fmt"
	"math"
)

const serviceMs = 100.0 // one request keeps the server busy for 100 ms

func main() {
	fmt.Println("one server, one request at a time, 100 ms each (users arrive at random)")
	fmt.Println("users/s   busy   avg wait   avg total")
	for _, rate := range []float64{2, 5, 8, 9, 9.5, 10, 12} {
		busy := rate * serviceMs / 1000 // share of each second the server is working
		switch {
		case busy < 1:
			wait := busy / (1 - busy) * serviceMs // average time spent in line
			fmt.Printf("%7.1f  %4.0f%%  %6.0f ms  %7.0f ms\n", rate, busy*100, math.Round(wait), math.Round(wait+serviceMs))
		case busy == 1:
			fmt.Printf("%7.1f  %4.0f%%  the line never settles down\n", rate, busy*100)
		default:
			extra := rate - 1000/serviceMs // arrivals the server can't keep up with
			fmt.Printf("%7.1f  %4.0f%%  the line grows by %.0f people every second\n", rate, busy*100, extra)
		}
	}
}

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

one server, one request at a time, 100 ms each (users arrive at random)
users/s   busy   avg wait   avg total
    2.0    20%      25 ms      125 ms
    5.0    50%     100 ms      200 ms
    8.0    80%     400 ms      500 ms
    9.0    90%     900 ms     1000 ms
    9.5    95%    1900 ms     2000 ms
   10.0   100%  the line never settles down
   12.0   120%  the line grows by 2 people every second

Output টা পড়া:

TrafficBusy100 ms কাজের তুলনায় অপেক্ষা
5 user/s50%সমান (100 ms)
9 user/s90%9 গুণ
9.5 user/s95%19 গুণ
12 user/s120%কখনো শেষ হয় না

Step 1 এর সাথে তুলনা: step 1 দেখালো একটা burst কী করে। Step 2 দেখায়, server প্রায় ভরে গেলে সমান হারে আসা traffic ও লম্বা অপেক্ষা তৈরি করে। 9 থেকে 9.5 user/s মানে মাত্র 5% বেশি traffic, অথচ অপেক্ষা দ্বিগুণের বেশি।

আসল engineering এ

  • Headroom রাখো। Team গুলো সাধারণত peak এ server কে মোটামুটি 60–70% busy ধরে plan করে, 95% না। ওপরের curve টাই কারণ: শেষ 30% এই অপেক্ষার বিস্ফোরণ ঘটে।
  • Average না, ধীর user দের মাপো। 200 ms এর একটা average লুকিয়ে রাখতে পারে যে 1% user 5 সেকেন্ড অপেক্ষা করছে। Engineer রা 95th আর 99th percentile (p95, p99) response time দেখে।
  • অপেক্ষা আর হিসাব আলাদা সমস্যা। Request পাশাপাশি চালানো (step 1) তখন কাজে দেয় যখন সময় যায় database বা network এর অপেক্ষায়। সময় যদি CPU তেই যায়, যেমন image resize করা, তাহলে বেশি core বা বেশি machine লাগে। Module 4 এ এটা বিস্তারিত।
  • প্রতিটা shared জিনিসের নিজের লাইন আছে। Database connection, file handle, অন্য service এ call, সবই সীমিত। Little's law বলে দেয় কোনটা কয়টা লাগবে।
  • User দের আগে knee টা খুঁজে বের করো। k6, wrk বা hey এর মতো load-testing tool server এ বাস্তবের মতো traffic পাঠায়, যাতে তুমি দেখতে পাও কোথা থেকে অপেক্ষা বাড়তে শুরু করে।

যখন ভাঙে

একই পাঁচটা পরিবর্তন, এবার failure হিসেবে। প্রতিটাই production এর পরিচিত incident।

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

একটা failure বেছে নাও

স্বাভাবিক: 50 request/s
রাত 9:00: দশ মিনিট ধরে 2,000 request/s
ভাঙে: Capacity এর অনেক ওপরে একটা burst। কয়েক সেকেন্ডে লাইন ভরে যায় আর request timeout হতে শুরু করে। সমাধান: জানা event এর আগে থেকে capacity plan করা, result page cache করা, আর সামনের দরজায় একটা queue বা rate limit।

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

তোমার server 100 ms per request এ 50% busy, আর response গড়ে 200 ms। আগামী মাসে traffic দ্বিগুণ হবে। গড় response time কি দ্বিগুণ হয়ে 400 ms হবে?

উত্তর দেখো

না, এর চেয়ে অনেক খারাপ হবে। Traffic দ্বিগুণ হলে server 50% থেকে 100% busy হয়ে যায়, আর 100% এ লাইন কখনো থিতু হয় না: response দ্বিগুণ হয় না, সীমাহীনভাবে বাড়ে। অপেক্ষার সময় traffic এর সাথে সরলরেখায় বাড়ে না। দ্বিগুণ traffic সামলাতে মোটামুটি দ্বিগুণ capacity লাগবে, আর সেটা traffic আসার আগেই।

নিজেকে check করো

1. একটা server প্রতি সেকেন্ডে 40টা request সামলায়, আর প্রতিটা ভেতরে 0.5 s থাকে। যেকোনো মুহূর্তে মোটামুটি কয়টা request চলমান?

উত্তর

প্রায় 20টা, Little's law অনুযায়ী: প্রতি সেকেন্ডে 40 × 0.5 s = 20। অন্তত এতগুলো একসাথে চলা request এর জায়গা রাখা উচিত, database connection সহ।

2. Request গুলো তাদের 100 ms এর বেশিরভাগ database এর অপেক্ষায় কাটায়। একটা মাত্র CPU core এ ও কি এগুলো পাশাপাশি চালালে লাভ হবে?

উত্তর

হ্যাঁ। একটা request যখন database এর জন্য অপেক্ষা করছে, CPU তখন অন্যগুলো শুরু করতে পারে। সময় যখনই অপেক্ষায় যায়, পাশাপাশি চালানো কাজে দেয়, আর বেশিরভাগ web API তে ঠিক এটাই হয়।

3. একটা server 90% busy। Traffic মাত্র 5% বাড়লো। এতে user দের অপেক্ষা দ্বিগুণের বেশি হতে পারে কেন?

উত্তর

কারণ অপেক্ষা বাড়ে busy ÷ (1 − busy) এর মতো করে। 90% এ সেটা service time এর 9 গুণ, আর 95% এ 19 গুণ। প্রায় ভরা অবস্থায় traffic এর ছোট বৃদ্ধি অপেক্ষায় বিশাল বৃদ্ধি ঘটায়।

4. Software একজন থেকে অনেক user এ গেলে যে পাঁচটা জিনিস বদলায়, তার যেকোনো দুটোর নাম বলো।

উত্তর

এর যেকোনো দুটো: data shared হয়ে যায়, request একসাথে আসে, capacity ফুরিয়ে যায়, কে চাইছে আর সে কী করতে পারে জানতে হয়, আর একটা failure এখন সবাইকে ভোগায়।

তোমার challenge: চারজন worker এর server

Step 1 বদলাও যাতে server একসাথে সর্বোচ্চ 4টা request সামলায়: একটা না, আবার সীমাহীনও না। Go তে buffered channel কে semaphore হিসেবে ব্যবহার করতে পারো, Java তে 4 জনের fixed thread pool, Python এ ThreadPoolExecutor(4), আর Node.js বা Rust এ ফাঁকা slot গোনার একটা counter।

চালানোর আগে অনুমান করো: বিশজন user একই মুহূর্তে click করলে শেষজন কখন শেষ হবে?

তুমি সফল যখন তোমার program print করবে যে শেষ user 0.5 s এ শেষ হয়েছে (20টা request ÷ একসাথে 4টা × 100 ms), আর তুমি ব্যাখ্যা করতে পারবে কেন এটা একটা একটা করে চালানোর চেয়ে ভালো, কিন্তু সীমাহীনের চেয়ে ধীর।

Bonus: একটা real team তবু কেন "সীমাহীন" এর বদলে 4 এর একটা limit বেছে নিতে পারে? (Hint: database এর কী হয়?)

মনে রাখার model

অনেক user একটা program কে একটা shared service বানিয়ে দেয়। Data share করতে হয়, request একটার ওপর আরেকটা পড়ে, identity জরুরি হয়, আর একটা failure সবার কাছে পৌঁছায়। প্রতিটা server হলো কয়েকজন worker এর সামনের একটা লাইন: busy = আসা request × service time ÷ worker, আর busy 100% এর দিকে গেলে অপেক্ষা বিস্ফোরিত হয়। তাই headroom রাখো, আর request যখন অপেক্ষা করছে তখন তাদের পাশাপাশি চলতে দাও।

Station stamp

Chapter শেষ করো

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

Pro নিয়ে XP পাও →