এই পর্বে যা শিখবেন
- একটি অ্যাপ বড় হওয়ার পাঁচটি ধাপে কোন লক্ষণ কোন পরিবর্তন চায়, আর প্রতিটি পরিবর্তন কোর্সের কোন পর্বে শেখা
- দৈনিক ব্যবহারকারী থেকে peak req/s, database connection, দরকারি cache hit rate আর বছরে storage-এর হিসাব
- একটি আসল load test পড়া: index, cache আর দুটি একসাথে, আর কেন একটি run একটি run-ই
- Read replica-র read-after-write ভুল, cache stampede, ঈদের সেলের ঢেউ আর queue দিয়ে সামলানো
- যা ইচ্ছে করে করা হয়নি তার তালিকা: microservice, sharding, multi-region, আর কেন
১ · বাজারঘরের গল্প — আর একটিই নিয়ম: আগে মাপুন
এই কোর্সজুড়ে আপনি “বাজারঘর” বানিয়েছেন: পণ্যের তালিকা, কার্ট, অর্ডার, login, email, ছবি, chat, payment. এবার একটি কল্পনা করুন। দোকানটি চালু হলো, প্রথম মাসে ১০০ জন ক্রেতা। তারপর কয়েক বছরে ১০ লাখ। পথে প্রতিটি ধাপে কিছু না কিছু ভাঙে, আর দলকে বেছে নিতে হয় কী বদলাবে। এই পর্ব সেই পথের একটি সৎ গল্প।
সততার ঘোষণা। বাজারঘর একটি কাল্পনিক দোকান, আর এই গল্পও কাল্পনিক। ব্যবহারকারীর সংখ্যা, দিনে কতবার কে আসেন, ভিড় কখন, graph-এর মান, server-এর মাপ সবই ধরে নেওয়া, শেখানোর জন্য। আসল মাপা সংখ্যা এসেছে শুধু অংশ ৪-এর ল্যাব থেকে, আর সেগুলো এক কম্পিউটারের একটি local run, সাধারণ ফল নয়। কোনো দামের অঙ্ক এখানে নেই, কারণ hosting-এর দাম জায়গা আর সময়ভেদে বদলায়।
বড় কোম্পানির scaling-এর তত্ত্ব আর case study ABCL TECH-এর System Design কোর্স-এর বিষয়: vertical বনাম horizontal scaling, খামের পেছনের হিসাব, replication, partitioning, monolith বনাম microservices. এখানে সেগুলো আবার শেখানো হবে না। এখানে প্রশ্ন একটু আলাদা: একটি ছোট ফুল-স্ট্যাক দল, যাঁরা এই কোর্সের টুল দিয়ে অ্যাপ বানিয়েছেন, বাস্তবে কোন দিন কী বদলান, আর কেন।
পুরান ঢাকার একটি হোটেলের কথা ভাবুন। প্রথমে মালিক নিজেই রাঁধেন, নিজেই পরিবেশন করেন। ভিড় বাড়লে প্রথমে নতুন রাঁধুনি নয়, আগে দেখেন কোথায় দেরি হচ্ছে। হয়তো ডাল প্রতিবার নতুন করে রাঁধা হচ্ছে (cache নেই), হয়তো এক পাতিলে সব (index নেই), হয়তো বিল লেখার সময় সবাই দাঁড়িয়ে থাকে (background job নেই)। এসব সারানোর পরেও ভিড় না সামলালে তবে দ্বিতীয় চুলা, তারপর দ্বিতীয় রাঁধুনি, তারপর আলাদা মিষ্টির কাউন্টার। কেউ প্রথম দিনেই পাঁচটি শাখা খোলেন না।
প্রতিটি ধাপ আমরা একই ছকে দেখব:
লক্ষণ
কোন graph, log বা মাপ বলল কিছু ভুল? “মনে হচ্ছে ধীর” লক্ষণ নয়; “রাত ৯টায় checkout-এর p95 ১.৮ সেকেন্ড” লক্ষণ।
পরিবর্তন
সবচেয়ে সস্তা যে পরিবর্তন লক্ষণটি সারায়, আর কোন পর্বে শেখা।
দাম আর নতুন ঝুঁকি
প্রতিটি নতুন অংশ নতুনভাবে ভাঙতে পারে, আর কাউকে সেটি দেখতে হয়।
এখনো করিনি
যা দল জেনে-বুঝে পিছিয়ে রেখেছে, আর কোন লক্ষণ দেখলে করবে।
নিচের stepper পুরো পথের মানচিত্র। প্রতিটি ধাপে কোন অংশ নতুন এল, তা দেখুন; বাকিটা আগের ধাপ থেকে থেকে গেছে। “পরে” চাপুন।
- ১০০
1 VPSসব এক জায়গায় - ১,০০০
index, CDNমেপে সারানো - ১০,০০০
N processstateless - ১ লাখ
replica, queueঈদের ঢেউ - ১০ লাখ
modulesদল আর ডেটা
ক্রেতা ──HTTPS──▶ Caddy (TLS)
│
▼
Node (Next.js + API) ১টি process, pm2
│
▼
PostgreSQL একই VPS-এ
রোজ রাতে pg_dump → অন্য জায়গায় backup
CDN ◀── ছবি, JS, CSS (hash নাম, immutable) ক্রেতা ──▶ Caddy ──▶ Node (১টি) │ └──▶ Redis queue ──▶ worker (email) ▼ PostgreSQL + index N+1 সারানো, query log চালু
CDN ◀── object storage (পণ্যের ছবি) ক্রেতা ──▶ Caddy ──┬──▶ Node 1 ─┐ ├──▶ Node 2 ─┼──▶ Redis (session, queue) └──▶ Node 3 ─┘ │ pool: প্রতিটিতে ৫ ▼ PostgreSQL (নিজের VPS) monitoring: metric, error, SLO
CDN ──▶ load balancer ──▶ app server × ৩ (প্রতিটিতে ৪ process)
│
Redis ◀─────────────┼──▶ cache-aside + lock
(session, queue, │ Socket.IO adapter
rate limit) ▼
PostgreSQL primary ──WAL──▶ read replica
▲ লেখা, checkout (তালিকা, রিপোর্ট)
অর্ডারের queue → worker (ঈদের ঢেউ)
modular monolith: catalog │ orders │ accounts │ payments
│ (module-এর সীমা কোডে, এক deploy)
├──▶ search service (আলাদা index)
└──▶ notification service (email, SMS, push)
PostgreSQL: orders মাস ধরে partition
২ বছরের পুরোনো → archive
দ্বিতীয় region: শুধু backup আর দুর্যোগের প্রস্তুতি
- ১০০ জন: একটি VPS. একটি ছোট VPS-এ Caddy, একটি Node process আর PostgreSQL. এটিই পর্ব ৮২-এর সাধারণ VPS সাজ। নতুন কিছু নয়, শুধু একটি অভ্যাস জরুরি: রোজ রাতে backup অন্য জায়গায়, আর মাসে একবার সেটি ফিরিয়ে দেখা।
- ১,০০০ জন: আগে মাপা, তারপর সারানো। Server বাড়েনি। বদলেছে query (index, N+1), static ফাইল গেছে CDN-এ, আর email চলে গেছে request-এর বাইরে, Redis-এর queue আর একটি worker-এ।
- ১০,০০০ জন: কয়েকটি process, কোনো স্মৃতি নেই। একটি Node process একটি CPU core-এর বেশি ব্যবহার করে না, তাই কয়েকটি process, সামনে Caddy. তার শর্ত: process-এর ভেতরে কিছু রাখা চলবে না, তাই session Redis-এ, ছবি object storage-এ। PostgreSQL নিজের VPS-এ, আর প্রথমবার monitoring আর SLO.
- ১ লাখ জন: database-এর ভার ভাগ। তালিকা আর রিপোর্টের পড়া গেল read replica-তে, জনপ্রিয় পাতা cache-এ (stampede-এর পাহারাসহ), ঈদের সেলের অর্ডার queue-তে, আর chat ও অর্ডারের খবর কয়েকটি server জুড়ে Socket.IO-র Redis adapter দিয়ে।
- ১০ লাখ জন: দল আর ডেটার সমস্যা। কোড এখনো মূলত এক deploy, কিন্তু module-এর সীমা কড়া। আলাদা হয়েছে শুধু search আর notification. অর্ডারের বিশাল table মাস ধরে ভাগ, পুরোনো অংশ archive-এ। দ্বিতীয় region শুধু দুর্যোগের প্রস্তুতি, চলমান ভার নয়।
ধাপ ৪-এর বাজারঘর: কোন অনুরোধ কোথায় যায়
Request paths at the 1-lakh stage: CDN, load balancer, app processes, Redis, primary and replica
২ · ক্ষমতার হিসাব — দৈনিক ব্যবহারকারী থেকে req/s, connection আর GB
“১০ লাখ ব্যবহারকারী” শুনে ভয় লাগে, কিন্তু সংখ্যাটি নিজে কিছু বলে না। Server চেনে প্রতি সেকেন্ডের অনুরোধ, প্রতি সেকেন্ডের SQL, একসাথে খোলা connection আর disk-এর GB. তাই প্রথম কাজ capacity planning: ব্যবহারকারীর সংখ্যাকে এই ভাষায় অনুবাদ করা। পদ্ধতির পুরো তত্ত্ব System Design-এর estimation পর্বে আছে; এখানে বাজারঘরের সংখ্যায় চারটি সূত্র।
১. গড় req/s = DAU × প্রতি জনের দৈনিক অনুরোধ ÷ ৮৬,৪০০. ২. Peak req/s = গড় × peak factor, কারণ মানুষ সারাদিন সমানভাবে আসেন না; বাংলাদেশের দোকানে রাত ৮টা থেকে ১১টা, বেতনের সপ্তাহ আর ঈদের আগের রাত আলাদা। ৩. Connection = প্রতি সেকেন্ডের SQL × একটি SQL-এর সময় (Little-এর সূত্র)। ৪. Storage = দিনে নতুন সারি × সারির আকার × ৩৬৫.
ধরে নেওয়া সংখ্যাগুলো কোডের ওপরে লেখা আছে; প্রতিটি নিজের মতো বদলে আবার চালান। মনে রাখুন, এগুলো অনুমান। আসল দোকানে প্রথম দিন থেকেই log থেকে এই সংখ্যা মাপা যায় (পর্ব ৩১), আর তখন অনুমানের জায়গায় মাপ বসে।
// শিক্ষণ মডেল: দৈনিক ব্যবহারকারী থেকে peak req/s, SQL আর storage
// সব সংখ্যা ধরে নেওয়া; নিজের মাপা মান বসিয়ে আবার চালান
const REQ_PER_USER = 30; // একজনের দিনে HTML আর API অনুরোধ (ছবি, JS CDN থেকে)
const PEAK = 5; // রাত ৯টার ভিড়: দিনের গড়ের ৫ গুণ
const SQL_PER_REQ = 3; // প্রতি অনুরোধে গড়ে ৩টি SQL (cache ছাড়া)
const ORDERS_PER_USER = 0.04; // দিনে ১০০ জনে ৪টি অর্ডার
const KB_PER_ORDER = 4; // order, item, index আর audit মিলিয়ে
function capacity(dau) {
const avg = (dau * REQ_PER_USER) / 86400; // দিনে ৮৬,৪০০ সেকেন্ড
const peak = avg * PEAK;
const sql = peak * SQL_PER_REQ;
const ordersYear = dau * ORDERS_PER_USER * 365;
const gbYear = (ordersYear * KB_PER_ORDER) / (1024 * 1024);
return { avg, peak, sql, ordersYear, gbYear };
}
console.log("DAU avg r/s peak r/s peak SQL/s orders/yr GB/yr");
for (const dau of [100, 1000, 10000, 100000, 1000000]) {
const c = capacity(dau);
console.log(
String(dau).padEnd(9),
c.avg.toFixed(2).padStart(8),
c.peak.toFixed(1).padStart(9),
Math.round(c.sql).toString().padStart(11),
Math.round(c.ordersYear).toString().padStart(10),
c.gbYear.toFixed(2).padStart(7)
);
}
const eid = capacity(100000).avg * 20; // ঈদের সেলের প্রথম ঘণ্টা: গড়ের ২০ গুণ
console.log("Eid peak at 1 lakh DAU:", eid.toFixed(0), "r/s");আউটপুটের প্রথম সারি 100 0.03 0.2 1 1460 0.01: ১০০ জনে ভিড়ের সময়েও সেকেন্ডে একটি অনুরোধও নয়। 10000 3.47 17.4 52: ১০ হাজারে peak-এ ১৭.৪ req/s আর ৫২ SQL/s, যা একটি ঠিকঠাক index-ওয়ালা PostgreSQL-এর জন্য খুব কম। 1000000 347.22 1736.1 5208 14600000 55.69: ১০ লাখে peak-এ প্রায় ১,৭৩৬ req/s আর ৫,২০৮ SQL/s; বছরে প্রায় দেড় কোটি অর্ডার, আর শুধু অর্ডারের ডেটা প্রায় ৫৬ GB. শেষ লাইন Eid peak at 1 lakh DAU: 694 r/s: ১ লাখ ব্যবহারকারীর ঈদের রাত ১০ লাখের সাধারণ সন্ধ্যার প্রায় ৪০%. শিক্ষা দুটি। এক, “ব্যবহারকারী” শব্দটি ভয়ের, req/s সাধারণত অনেক ছোট। দুই, গড় নয়, peak-এর জন্য নকশা করতে হয়, আর বাংলাদেশের দোকানে সবচেয়ে বড় peak আসে উৎসবে।
Peak factor কোথা থেকে আসে? ঘণ্টা ধরে অনুরোধের সংখ্যার একটি graph থেকে: সবচেয়ে ব্যস্ত মিনিটের হার ÷ দিনের গড় হার। নতুন দোকানে এটি প্রায়ই বড়, কারণ অল্প ক্রেতা একই সময়ে আসেন (একটি পোস্টের পরে); বড় দোকানে ভিড় ছড়ায়, কিন্তু উৎসব নতুন চূড়া আনে। Latency আর throughput-এর পর্বে দেখেছেন গড় কেন লুকায়; এখানেও একই কথা।
Little-এর সূত্র দিয়ে connection pool
Connection pool (পর্ব ৩৮) কতটা বড় হবে, অনুমানে নয়, সূত্রে বের হয়। ব্যাংকের কাউন্টারে প্রতি মিনিটে ৬ জন আসেন, আর প্রতিজন ২ মিনিট থাকেন: গড়ে কাউন্টারে ৬ × ২ = ১২ জন। এটিই L = λ × W. Database-এর ক্ষেত্রে λ মানে প্রতি সেকেন্ডের SQL, W মানে একটি SQL কত সেকেন্ড connection ধরে রাখে, আর L মানে গড়ে কতটি connection ব্যস্ত।
// শিক্ষণ মডেল: Little-এর সূত্র L = λ × W দিয়ে connection pool-এর মাপ
// λ = প্রতি সেকেন্ডে কতটি SQL, W = একটি SQL কত সেকেন্ড connection ধরে রাখে
const busy = (sqlPerSec, msPerQuery) => sqlPerSec * (msPerQuery / 1000);
// ১. মাপা run যাচাই: autocannon-এর ৫০টি সংযোগ, গড়ে 291 req/s
console.log("lab: W = L / λ =", (50 / 291 * 1000).toFixed(0), "ms per request");
// ২. peak SQL/s (ক্যালকুলেটর থেকে) আর একটি SQL-এর সময়, দুটিই বদলে দেখুন
const cases = [
["10k DAU, 4 ms", 52, 4],
["1 lakh DAU, 4 ms", 521, 4],
["Eid peak, 4 ms", 2083, 4],
["Eid peak, 40 ms (index নেই)", 2083, 40],
];
for (const [name, sql, ms] of cases) {
const L = busy(sql, ms);
console.log(`${name.padEnd(28)} busy ${L.toFixed(1).padStart(5)} pool with 2x headroom ${Math.ceil(L * 2)}`);
}
// ৩. সব process মিলিয়ে কতটি connection চাওয়া হচ্ছে
const LIMIT = 100 - 3; // PostgreSQL ডিফল্ট max_connections 100, ৩টি superuser-এর জন্য
for (const [servers, procs, pool] of [[1, 4, 10], [3, 4, 10], [3, 4, 5]]) {
const want = servers * procs * pool;
console.log(`${servers} server × ${procs} process × pool ${pool} = ${want} -> ${want <= LIMIT ? "OK" : "too many"}`);
}প্রথম লাইন lab: W = L / λ = 172 ms per request অংশ ৪-এর আসল run যাচাই করে: autocannon ৫০টি সংযোগ খোলা রেখেছিল (L = ৫০), গড়ে ২৯১ req/s এসেছে, তাই প্রতিটি অনুরোধ গড়ে ১৭২ ms ব্যবস্থার ভেতরে ছিল, মাপা p50 ১৪২ ms-এর কাছেই। তারপর Eid peak, 4 ms … busy 8.3 pool with 2x headroom 17 আর Eid peak, 40 ms (index নেই) … busy 83.3 … 167: একই ভিড়ে query ১০ গুণ ধীর হলে connection-ও ১০ গুণ লাগে। ব্যবহারকারী নয়, query-র সময়ই pool-এর আকার ঠিক করে। শেষ তিন লাইন: 3 server × 4 process × pool 10 = 120 -> too many, কারণ PostgreSQL-এর ডিফল্ট max_connections ১০০, আর তার ৩টি superuser-এর জন্য রাখা (superuser_reserved_connections). প্রতিটি pool ৫-এ নামালে ৬০, ঠিক আছে। আরও process এলে একটি connection pooler (যেমন PgBouncer) লাগে।
৩ · ধাপ ১: ১০০ জন — একটি VPS, একটি process, একটি PostgreSQL
প্রথম মাসে বাজারঘরের দল দুজন। একটি ছোট VPS (ধরে নেওয়া: ২টি vCPU, ৪ GB RAM), তাতে পর্ব ৮২-এর মতো output: 'standalone'-এর Next.js build, pm2, সামনে Caddy, আর একই মেশিনে PostgreSQL. ক্যালকুলেটর বলছে peak-এ ০.২ req/s; এই মেশিন তার শত গুণ সামলাবে।
| লক্ষণ | কোনো লক্ষণ নেই। প্রশ্ন শুধু “যথেষ্ট কি?”, আর হিসাব বলছে হ্যাঁ। |
|---|---|
| পরিবর্তন | শুধু ভিত্তি: HTTPS, পর্ব ৭৮-এর মতো secret আলাদা, পর্ব ৮১-এর CI, রোজ রাতে pg_dump অন্য জায়গায়, আর একটি বাইরের uptime check. |
| দাম আর ঝুঁকি | সবকিছু একটি মেশিনে, তাই মেশিন গেলে দোকান বন্ধ (single point of failure). এই ধাপে এটি মেনে নেওয়া যুক্তিসঙ্গত, যদি backup ফিরিয়ে আনার পরীক্ষা হয় আর কতক্ষণ লাগে তা জানা থাকে। |
| এখনো করিনি | Kubernetes, microservice, Redis, CDN, দ্বিতীয় server. কোনোটির কোনো লক্ষণ নেই। |
এই ধাপে সবচেয়ে দামি সিদ্ধান্তগুলো কিন্তু scaling-এর নয়, ডেটার। টাকা পয়সায় (minor unit) রাখা (পর্ব ৩৯), অর্ডারে দামের snapshot, প্রতিটি টেবিলে primary key আর foreign key, payment-এ idempotency key (পর্ব ৬৫), আর migration দিয়ে schema বদল (পর্ব ৪০)। দশ লাখে গিয়ে server বদলানো সহজ, ভুল ডেটা মডেল বদলানো খুব কঠিন।
একটি process-কে vertical scaling দিয়ে বড় করা (বড় VPS) সবচেয়ে সস্তা প্রথম ধাপ: কোডের কোনো বদল লাগে না। সীমা: মেশিনের মাপের ছাদ, আর একটিই ব্যর্থতার জায়গা। Horizontal scaling (একই রকম কয়েকটি) দুটিই সারায়, কিন্তু কোডকে stateless হতে বাধ্য করে; বিস্তারিত System Design-এর scaling পর্বে।
৪ · ধাপ ২: ১,০০০ জন — index আর N+1, CDN, background job
ছয় মাস পরে প্রথম অভিযোগ: “আমার অর্ডার” পাতা খুলতে দেরি হয়। দল অনুমান করেনি; পর্ব ৩১-এর structured log থেকে route ধরে সময় বের করেছে। কল্পিত graph-এ GET /api/customers/:id/orders-এর p95 ধীরে ধীরে বাড়ছে, অর্ডারের সংখ্যার সাথে সাথে। একই সময়ে checkout-এর p50 অর্ধেক সেকেন্ডের বেশি, কারণ অর্ডার নিশ্চিতের email একই request-এ যায়।
| লক্ষণ | (ক) অর্ডারের ইতিহাসের p95 অর্ডার বাড়ার সাথে বাড়ছে, (খ) ঐ পাতায় প্রতি request-এ ৪১টি SQL, (গ) checkout-এর p50-এর বড় অংশ SMTP-র অপেক্ষা, (ঘ) ছবি আর JS প্রতিবার VPS থেকে। |
|---|---|
| পরিবর্তন | EXPLAIN ANALYZE দেখে composite index (পর্ব ৩৪); include আর select দিয়ে N+1 সারানো (পর্ব ৪২); hash নামের ফাইলে immutable আর সামনে CDN (পর্ব ৮৪); email গেল queue আর worker-এ (পর্ব ৬২)। |
| দাম আর ঝুঁকি | Index প্রতিটি লেখাকে একটু ধীর আর disk একটু বড় করে; বড় table-এ index বানানোর সময় lock এড়াতে CREATE INDEX CONCURRENTLY. CDN মানে deploy-এর পরে পুরোনো ফাইল, তাই hash নাম বাধ্যতামূলক। Queue মানে Redis আর একটি worker process, আর job আসতে পারে একাধিকবার, তাই idempotent হতে হবে। |
| এখনো করিনি | দ্বিতীয় app server, ডেটার cache, read replica. Query ঠিক হলে এগুলোর দরকারই পড়ে না। |
একটি আসল load test: index আর cache, একটি একটি করে
“Index দিলে কতটা লাভ” কথায় নয়, মেপে দেখা যাক। এই পর্বের ল্যাবে একটি ছোট Express app: একটিই endpoint, যা একজন ক্রেতার সর্বশেষ ২০টি অর্ডার আনে, PostgreSQL-এর orders table-এ ৫ লাখ সারি আর ৫০ হাজার ক্রেতা। Load দিয়েছে autocannon, একটি Node-ভিত্তিক load test টুল। লেখার সময়ের version: Node 24.15.0, Express 5.3.0, pg 8.23.1, autocannon 8.0.0, আর embedded-postgres 18.6.0-beta.19 (PostgreSQL 18.6; এই npm package-এর সব release-এর নামেই -beta থাকে)। সবকিছু চলেছে 127.0.0.1-এ, একটিই ল্যাপটপে।
const pool = new pg.Pool({ host: '127.0.0.1', port: 54887,
database: 'bazarghar_p87', user: 'postgres', password: 'example-secret', max: 10 });
const USE_CACHE = process.env.CACHE === '1';
const cache = new Map(); // ল্যাবে process-এর ভেতরের cache, TTL 30 s
app.get('/api/customers/:id/orders', async (req, res) => {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id < 1) return res.status(400).json({ error: 'bad id' });
const key = `orders:${id}`;
if (USE_CACHE) {
const c = cache.get(key);
if (c && Date.now() - c.at < 30_000) { stats.hits++; return res.json(c.value); }
}
stats.dbQueries++;
const { rows } = await pool.query(
'SELECT id, status, total, created_at FROM orders WHERE customer_id = $1 ORDER BY created_at DESC LIMIT 20',
[id]);
if (USE_CACHE) cache.set(key, { at: Date.now(), value: rows });
res.json(rows);
});Load-এর ধরন: ৫০টি একসাথে চলা সংযোগ, ১০ সেকেন্ড; প্রতিটি অনুরোধের ক্রেতা একটি seeded PRNG থেকে, ৮০% অনুরোধ ২,০০০ জন “নিয়মিত” ক্রেতার মধ্যে আর ২০% সব ৫০ হাজারের মধ্যে। চারটি অবস্থা: index ছাড়া আর সহ, cache ছাড়া আর সহ। প্রতিটির আগে একই query-র EXPLAIN ANALYZE. সত্যিকারের run, একবার, ছাঁটা:
server: { db: 'bazarghar_p87', port: '54887', v: '18.6' }
rows: { n: 500000, c: 50000 }
== A: no index, no cache
-> Parallel Seq Scan on orders (cost=0.00..6621.17 rows=4 width=28) (actual time=0.405..5.173 rows=3.33 loops=3)
Execution Time: 17.899 ms
req/s avg 291 latency p50 142 ms p99 457 ms non2xx 0 errors 0 total 2910
app stats: {"requests":2960,"dbQueries":2960,"hits":0} hit rate 0.0%
== B: index, no cache
-> Bitmap Heap Scan on orders (cost=4.50..43.13 rows=10 width=28) (actual time=0.083..0.093 rows=10.00 loops=1)
-> Bitmap Index Scan on orders_customer_created (cost=0.00..4.50 rows=10 width=0) (actual time=0.068..0.068 rows=10.00 loops=1)
Execution Time: 0.127 ms
req/s avg 10485.5 latency p50 3 ms p99 14 ms non2xx 0 errors 0 total 104847
app stats: {"requests":104897,"dbQueries":104897,"hits":0} hit rate 0.0%
== C: no index, cache
-> Parallel Seq Scan on orders (cost=0.00..6621.17 rows=4 width=28) (actual time=0.801..10.157 rows=3.33 loops=3)
Execution Time: 125.384 ms
req/s avg 468.3 latency p50 143 ms p99 415 ms non2xx 0 errors 0 total 4683
app stats: {"requests":4733,"dbQueries":2692,"hits":2041} hit rate 43.1%
== D: index + cache
… Execution Time: 0.127 ms
req/s avg 12364 latency p50 3 ms p99 13 ms non2xx 0 errors 0 total 123639
app stats: {"requests":123689,"dbQueries":20683,"hits":103006} hit rate 83.3%
চারটি শিক্ষা, প্রতিটি এই একটি run থেকে।
- Index-ই আসল ওষুধ। A থেকে B: query seq scan-এ পুরো table পড়ছিল, index-এ সরাসরি ১০টি সারি। req/s ২৯১ থেকে ১০,৪৮৬ (প্রায় ৩৬ গুণ), p50 ১৪২ থেকে ৩ ms. কোনো নতুন server লাগেনি।
- Index ছাড়া cache সমস্যা লুকায়, সারায় না। C-তে req/s ২৯১ থেকে ৪৬৮, কিন্তু p50 ১৪৩ ms, একটুও নড়েনি; কারণ প্রতিটি miss এখনো ধীর, আর ধীর miss-এর জন্য cache ১০ সেকেন্ডে গরমই হতে পারেনি (hit rate ৪৩.১%). নতুন ক্রেতা, মেয়াদ শেষ হওয়া key আর restart-এর পরের প্রতিটি অনুরোধ এই ধীর পথেই যায়।
- Index-এর পরে cache ডেটাবেসের ভার কমায়। B থেকে D-তে req/s মাত্র ১৮% বাড়ল, কিন্তু ডেটাবেসে query ১,০৪,৮৯৭ থেকে ২০,৬৮৩: ৮০% কম। Cache-এর আসল কাজ এখানে latency নয়, database-কে বাঁচানো।
- Hit rate-এর ছাদ ঠিক করে traffic-এর ধরন। D-তে ৮৩.৩%: ৮০% অনুরোধ ২,০০০ জনের মধ্যে, তাঁরা দ্রুত cache-এ ওঠেন; বাকি ২০% ছড়ানো ৫০ হাজারে, তাঁদের প্রায় সবাই miss. TTL বা মেমরি বাড়ালেও এই ছাদ সামান্যই সরবে।
একটি run একটি run-ই। লক্ষ করুন, একই index-ছাড়া query-র EXPLAIN ANALYZE A-তে ১৭.৯ ms, C-তে ১২৫.৪ ms: আগের load-এর পরে মেশিনের অবস্থা আলাদা ছিল। load টুল আর server একই ল্যাপটপে CPU ভাগ করেছে, network ছিল না, আর ল্যাবের cache একটি process-এর ভেতরের Map, Redis নয় (Redis হলে প্রতি hit-এ একটি network round trip যোগ হতো)। তাই সংখ্যাগুলোর অনুপাত শিক্ষণীয়, মান নয়। নিজের মেশিনে চালালে অন্য সংখ্যা পাবেন, দিক সম্ভবত একই।
N+1-এর গল্প একই রকম: পর্ব ৪২-এর একটি local run-এ ২০টি অর্ডারের পাতায় ৪১টি SQL নেমেছিল ৩টিতে, আর network-এ ৪০ ms RTT থাকলে সেটাই ১,৬৭৩ ms থেকে ৪২ ms. আর email: পর্ব ৬২-এর একটি run-এ checkout-এর p50 ৫১৫ ms থেকে ০.৭ ms, শুধু email-কে request-এর বাইরে পাঠিয়ে। এই ধাপের তিনটি পরিবর্তনই কোনো নতুন server ছাড়া।
৫ · ধাপ ৩: ১০,০০০ জন — কয়েকটি process, Redis-এ session, object storage আর SLO
দেড় বছর পরে তিনটি নতুন লক্ষণ, তিনটিই কল্পিত graph থেকে। রাত ৯টায় Node process-এর CPU ১০০%-এ আটকে থাকে, অথচ VPS-এর বাকি তিনটি core প্রায় ফাঁকা। প্রতিটি deploy-এ কয়েক সেকেন্ড 502. আর disk ৮০% ভরা, কারণ বিক্রেতাদের তোলা ছবি VPS-এর uploads/ folder-এ।
| লক্ষণ | (ক) একটি core ১০০%, বাকিগুলো ফাঁকা, (খ) deploy-এ 502, (গ) disk ভরছে ছবিতে, (ঘ) restart-এর পরে সবাই logout, কারণ session process-এর মেমরিতে। |
|---|---|
| পরিবর্তন | pm2 cluster-এ ৪টি process আর pm2 reload (পর্ব ৮২); session Redis-এ (পর্ব ৪৫, পর্ব ৪৪); প্রতিটি process-এর pool হিসাব করে (পর্ব ৩৮); ছবি object storage-এ, presigned upload (পর্ব ৬৪); PostgreSQL নিজের VPS-এ; error tracking, metric আর SLO (পর্ব ৮৩)। |
| দাম আর ঝুঁকি | Redis এখন একটি নতুন single point of failure: Redis থামলে login থামে। App আর database আলাদা মেশিনে মানে প্রতিটি query-তে network-এর একটু সময়। দুই মেশিন দেখাশোনা, দুই জায়গায় update. |
| এখনো করিনি | দ্বিতীয় app server, read replica, Kubernetes. একটি বড় VPS-এ ৪টি process এখনো যথেষ্ট। |
এই ধাপের মূল শব্দ stateless. কয়েকটি process-এর সামনে reverse proxy বা load balancer বসালে একজন ক্রেতার পরপর দুটি অনুরোধ দুটি ভিন্ন process-এ যেতে পারে। তাই process-এর মেমরিতে যা থাকে (session, upload-এর ফাইল, cron-এর দায়িত্ব) তা বাইরে নিতে হয়। Sticky session আপাতত বাঁচায়, কিন্তু process মরলে সেই ক্রেতারা তবুও logout হন। কেন stateless ভালো, তার তত্ত্ব System Design-এর stateless পর্বে; load balancing-এর algorithm load balancing পর্বে।
প্রথম SLO: কী মাপলে বোঝা যায় দোকান ভালো চলছে
এই ধাপে প্রথমবার দল একটি SLO লেখে। কল্পিত উদাহরণ: “২৮ দিনের মধ্যে checkout-এর ৯৯.৫% অনুরোধ সফল আর ৮০০ ms-এর ভেতরে।” তাহলে ভুলের সুযোগ (error budget) ০.৫%. মাসে ধরুন ৪ লাখ checkout-এর অনুরোধ হলে ২,০০০টি পর্যন্ত খারাপ হতে পারে। Budget দ্রুত খরচ হলে feature থামিয়ে নির্ভরযোগ্যতার কাজ, অক্ষত থাকলে feature. কখন scale করবেন, তা বলে SLO, অনুভূতি নয় (পর্ব ৮৩; গভীরে System Design-এর observability পর্ব)।
Process কয়টি? সাধারণত CPU core-এর সমান বা একটি কম। CPU-ভারী কাজ (ছবি, PDF) worker-এ পাঠানো ভালো, process বাড়িয়ে নয়। আর প্রতিটি নতুন process মানে নতুন একটি pool: অংশ ২-এর ক্যালকুলেটরে 3 server × 4 process × pool 10 = 120 ঠিক এই ফাঁদ।
৬ · ধাপ ৪ক: ১ লাখ জন — read replica আর stampede-এর পাহারাসহ cache
তিন বছরে বাজারঘর ১ লাখ দৈনিক ব্যবহারকারী। এখন app server তিনটি, একটি load balancer-এর পেছনে। নতুন লক্ষণ database-এ: কল্পিত graph-এ রাত ৯টায় PostgreSQL-এর CPU ৮৫%, সবচেয়ে বেশি সময় খায় ক্যাটাগরির তালিকা আর বিক্রেতাদের মাসিক রিপোর্ট। Slow query log বলছে রিপোর্টের একটি query একাই কয়েক সেকেন্ড ধরে চলে, আর তখন checkout-এর p95 লাফ দেয়।
| লক্ষণ | (ক) primary-র CPU peak-এ ৮৫%, (খ) রিপোর্টের ভারী query checkout-কে ধীর করে, (গ) জনপ্রিয় ক্যাটাগরির key-এর মেয়াদ শেষ হলে কয়েক সেকেন্ড database-এ একই query-র ঢেউ। |
|---|---|
| পরিবর্তন | একটি read replica; রিপোর্ট আর ক্যাটাগরির তালিকা সেখানে, checkout আর “আমার অর্ডার” primary-তে। জনপ্রিয় পাতায় cache-aside, লেখার পরে DEL, আর stampede-এর জন্য lock বা single-flight (পর্ব ৪৪)। |
| দাম আর ঝুঁকি | Replica একটু পিছিয়ে থাকে (replication lag), তাই read-after-write ভুল; কোন পড়া কোথায় যাবে, সেটি এখন কোডের একটি সিদ্ধান্ত। Cache মানে invalidation-এর দায়িত্ব। |
| এখনো করিনি | Sharding, দ্বিতীয় primary, multi-region. সব লেখা এখনো এক primary সামলাতে পারে। |
Read-after-write: নতুন ঠিকানা কোথায় গেল?
Replica primary-র লেখার log (WAL) পেয়ে নিজে প্রয়োগ করে, কিন্তু সামান্য দেরিতে; সাধারণত মিলিসেকেন্ডে, ভারী লেখার সময় সেকেন্ডেও। সমস্যা হয় যখন একজন ক্রেতা লিখে সাথে সাথে নিজের লেখা পড়তে চান। নিচের stepper-এ lag ৩০০ ms ধরা হয়েছে (শিক্ষণ মডেল)।
1POST /addresses → primary: INSERT2primary → replica: WAL পথে (lag 300 ms)3303 → GET /addresses → replica4replica: পুরোনো তালিকা ফেরত5সংশোধন: লেখার পরে ৫ s primary-তে পড়া6GET /addresses → primary: নতুন তালিকা
| সময় (ms) | |
| primary-তে ঠিকানা | |
| replica-তে ঠিকানা | |
| পড়া যাচ্ছে |
- লেখা primary-তে। ক্রেতার আগে ২টি ঠিকানা ছিল। নতুন ঠিকানা সংরক্ষণ হলো primary-তে, এখন সেখানে ৩টি। সময় ০ ms.
- WAL পথে। Primary পরিবর্তনটি replica-কে পাঠাচ্ছে। Replica-তে এখনো ২টি; ৩০০ ms পরে সেখানেও ৩টি হবে। ৫ ms পেরিয়েছে।
- Redirect, পড়া replica-তে। Form-এর পরে browser তালিকার পাতায় গেল (৪০ ms). কোড নিয়ম মেনেছে: “সব GET replica-তে।” Replica-তে এখনো ২টি।
- ভুল। ক্রেতা দেখলেন ২টি ঠিকানা; নতুনটি নেই। তিনি আবার যোগ করবেন, আর ৩০০ ms পরে একই ঠিকানা দুবার দেখবেন। কোনো error নেই, log-ও পরিষ্কার; এমন ভুল শুধু ক্রেতার অভিযোগে ধরা পড়ে।
- সংশোধন। আবার শুরু, একই লেখা (সময় আবার ০)। এবার লেখার পরে server এই ক্রেতার session-এ একটি চিহ্ন রাখে: “পরের ৫ সেকেন্ড পড়া primary-তে।” ৫ সেকেন্ড ধরা হয়েছে lag-এর স্বাভাবিক মানের অনেক ওপরে।
- পড়া primary-তে। তালিকা এল primary থেকে: ৩টি ঠিকানা, নতুনটিসহ। Replica এখনো ২টিতে, কিন্তু এই ক্রেতা তা টের পান না। অন্য সবার পড়া আগের মতোই replica-তে।
আরও পথ: নিজের ডেটা (প্রোফাইল, কার্ট) সবসময় primary থেকে, বা lag বেশি হলে primary-তে ফেরা। কোন নিশ্চয়তা কেন, তার পুরো আলোচনা System Design-এর replication পর্বে (read-your-writes, monotonic reads). নিচের কোডটি একটি উদাহরণ, চালানো হয়নি; ধারণাটি দেখানোর জন্য:
// উদাহরণ (চালানো হয়নি): কোন পড়া কোন database-এ
export function dbFor(req, kind) {
if (kind === 'write') return primary;
const recent = req.session.wroteAt && Date.now() - req.session.wroteAt < 5000;
if (recent || kind === 'own-data') return primary; // নিজের লেখা নিজে দেখবেন
return replica; // তালিকা, রিপোর্ট, খোঁজা
}
// লেখার পরে: req.session.wroteAt = Date.now();Cache hit rate কত হলে database বাঁচে
অংশ ৪-এর run-এ দেখেছেন cache ডেটাবেসে যাওয়া কমায়। কিন্তু কতটা hit rate লাগবে? উল্টো দিক থেকে হিসাব করুন: database আরামে কত SQL/s নিতে পারে (এটি নিজের load test আর graph থেকে আসে; এখানে ধরে নেওয়া), আর peak-এ কত আসবে।
// শিক্ষণ মডেল: cache hit rate ডেটাবেসের চাপ কতটা কমায়
// ধরে নেওয়া: ঈদের peak-এ 694 req/s × 3 SQL = 2,083 SQL/s, সবই cache করা যায় এমন পড়া
const PEAK_SQL = 2083;
const DB_BUDGET = 600; // primary-কে আরামে রাখতে যত SQL/s, ধরে নেওয়া
console.log("hit rate SQL/s to DB vs budget");
for (const h of [0, 0.5, 0.7, 0.8, 0.9, 0.95, 0.99]) {
const db = PEAK_SQL * (1 - h);
const mark = db <= DB_BUDGET ? "OK" : "over";
console.log(`${(h * 100).toFixed(0).padStart(4)}% ${db.toFixed(0).padStart(11)} ${mark}`);
}
// উল্টো প্রশ্ন: budget-এ থাকতে কত hit rate লাগবে?
const need = 1 - DB_BUDGET / PEAK_SQL;
console.log("needed hit rate:", (need * 100).toFixed(1) + "%");
// 90% থেকে 95% মানে miss অর্ধেক; 95% থেকে 99% মানে miss পাঁচ ভাগের এক ভাগ
const miss = (h) => 1 - h;
console.log("miss ratio 90->95:", (miss(0.95) / miss(0.9)).toFixed(2),
" 95->99:", (miss(0.99) / miss(0.95)).toFixed(2));
// বাজারঘরের মাপা run D: ১,২৩,৬৮৯ অনুরোধে ২০,৬৮৩টি গেল ডেটাবেসে
console.log("lab run D hit rate:", ((1 - 20683 / 123689) * 100).toFixed(1) + "%");টেবিলে 70% 625 over আর 80% 417 OK: ৭০%-এ এখনো budget ছাড়ায়, ৮০%-এ ভেতরে। needed hit rate: 71.2%, অর্থাৎ ১ − ৬০০ ÷ ২,০৮৩. পরের লাইন miss ratio 90->95: 0.50 95->99: 0.20 একটি গুরুত্বপূর্ণ সত্য: hit rate-এর শেষ কয়েক শতাংশ ডেটাবেসের ভারে বিশাল পার্থক্য আনে, কারণ ডেটাবেস দেখে শুধু miss. ৯৫% থেকে ৯৯% মানে ডেটাবেসের পড়া পাঁচ ভাগের এক ভাগ। শেষ লাইন lab run D hit rate: 83.3%: ল্যাবের traffic-এ ৮৩%, এই budget-এর জন্য যথেষ্ট। উল্টো দিকটিও মনে রাখুন: cache খালি হলে (Redis restart, deploy-এ key-এর নাম বদল) ডেটাবেস হঠাৎ পুরো ২,০৮৩ SQL/s দেখবে।
Stampede এই ঝুঁকিরই ছোট রূপ: একটি জনপ্রিয় key-এর মেয়াদ শেষ হলে একই মুহূর্তে শত অনুরোধ miss পায় আর সবাই database-এ যায়। পর্ব ৪৪-এর একটি run-এ ৮ সেকেন্ডে একটি key-এর জন্য ২৪০ বার database, Redis lock দিয়ে ৪ বার। সাথে TTL-এ একটু এলোমেলো যোগ (jitter), যাতে সব key একসাথে শেষ না হয়। কৌশলগুলোর তত্ত্ব System Design-এর eviction আর invalidation পর্বে।
৭ · ধাপ ৪খ: ঈদের সেল — queue, rate limit আর কয়েকটি server-এ WebSocket
গত বছরের ঈদের সেলের কল্পিত postmortem: রাত ৮টায় সেল শুরু, প্রথম ১০ মিনিটে checkout-এর অনুরোধ স্বাভাবিকের প্রায় ৮ গুণ, database-এর connection শেষ, ২০ মিনিট ধরে 502, আর কিছু ক্রেতার টাকা কাটা গেলেও অর্ডার তৈরি হয়নি। Payment-এর অংশ পর্ব ৬৫-এর idempotency আর reconciliation দিয়ে সারানো হয়েছে। এবার ভিড়ের অংশ।
| লক্ষণ | (ক) ঢেউয়ের সময় connection pool খালি, অনুরোধ timeout, (খ) bot আর বারবার “Refresh” চাপায় একই ক্রেতার শত অনুরোধ, (গ) অর্ডারের অবস্থার খবর এক server-এর ক্রেতা পান, অন্য server-এর ক্রেতা পান না। |
|---|---|
| পরিবর্তন | অর্ডার নেওয়া আর অর্ডার প্রক্রিয়া আলাদা: API অর্ডার যাচাই করে queue-তে রাখে, worker স্থির গতিতে stock কমায় আর অর্ডার বানায় (পর্ব ৬২, পর্ব ৪৩); প্রতি ক্রেতা আর প্রতি IP-তে rate limit (পর্ব ৪৯); queue ভরে গেলে ভদ্র “একটু পরে চেষ্টা করুন” (load shedding); WebSocket-এ Socket.IO-র Redis adapter (পর্ব ৬০)। |
| দাম আর ঝুঁকি | ক্রেতা আর তাৎক্ষণিক “অর্ডার হয়েছে” পান না, পান “অর্ডার গ্রহণ করা হয়েছে, নিশ্চিত হলে জানাব”; UI আর copy বদলাতে হয়। Queue-এর গভীরতা আর অপেক্ষার সময় এখন একটি নতুন metric. Redis-এর ওপর নির্ভরতা আরও বাড়ল। |
| এখনো করিনি | আলাদা “অর্ডার service”, Kafka-র মতো বড় event platform, অটো-scaling. Queue আর worker এক deploy-এর ভেতরেই। |
Queue কেন সাহায্য করে? কারণ database একটি নির্দিষ্ট গতিতেই নিরাপদে অর্ডার বানাতে পারে (stock-এর row lock, transaction). ঢেউ এলে সেই গতি বাড়ে না; queue ঢেউটিকে সময়ের ওপর ছড়িয়ে দেয়, যেমন মেট্রো স্টেশনের লাইন ভিড়কে প্ল্যাটফর্মে পৌঁছানোর আগেই সাজিয়ে দেয়। নিচের মডেলে worker সেকেন্ডে ২০০টি অর্ডার শেষ করে, queue-তে সর্বোচ্চ ৩,০০০টি জমতে পারে, আর সেল চলে t = ১০ থেকে ১৯ সেকেন্ড, সেকেন্ডে ৬০০টি করে (সব ধরে নেওয়া)।
// শিক্ষণ মডেল: ঈদের সেলে অর্ডারের ঢেউ, queue আর স্থির গতির worker
// কৃত্রিম ঘড়ি: প্রতিটি ধাপ ১ সেকেন্ড; কোনো এলোমেলো সংখ্যা নেই
const WORKER_RATE = 200; // worker প্রতি সেকেন্ডে কতটি অর্ডার শেষ করে
const MAX_DEPTH = 3000; // এর বেশি জমলে নতুন অর্ডার নয়: 429, একটু পরে চেষ্টা
function arrivals(t) { // রাত ৮টায় সেল শুরু: t=10 থেকে 19 পর্যন্ত ঢেউ
if (t >= 10 && t < 20) return 600;
return 80;
}
let depth = 0, done = 0, rejected = 0, maxDepth = 0;
for (let t = 0; t < 60; t++) {
let incoming = arrivals(t);
const room = MAX_DEPTH - depth;
const accepted = Math.min(incoming, room);
rejected += incoming - accepted;
depth += accepted;
const served = Math.min(depth, WORKER_RATE);
depth -= served; done += served;
maxDepth = Math.max(maxDepth, depth);
if (t % 5 === 4 || t === 19) {
const wait = (depth / WORKER_RATE).toFixed(1);
console.log(`t=${String(t).padStart(2)}s in=${incoming} queue=${String(depth).padStart(4)} wait≈${wait}s done=${done} rejected=${rejected}`);
}
}
console.log("max queue:", maxDepth, "total done:", done, "rejected:", rejected);t= 9s in=80 queue= 0: সেলের আগে worker আগমনের চেয়ে দ্রুত, queue খালি। t=14s in=600 queue=2000 wait≈10.0s: প্রতি সেকেন্ডে ৬০০ আসে, ২০০ শেষ হয়, তাই ৫ সেকেন্ডে ৪০০ × ৫ = ২,০০০ জমেছে; নতুন একজন ক্রেতাকে গড়ে ১০ সেকেন্ড অপেক্ষা করতে হবে। t=19s … queue=2800 wait≈14.0s … rejected=1200: t = ১৬ থেকে queue ৩,০০০-এর সীমায় পৌঁছেছে, তাই তিন সেকেন্ডে ৪০০ করে ১,২০০ জন ভদ্র “একটু পরে চেষ্টা করুন” পেয়েছেন। t=44s … queue= 0: ঢেউ শেষে প্রতি সেকেন্ডে ১২০ করে কমে, প্রায় ২৪ সেকেন্ডে খালি। শেষ লাইন total done: 8800 rejected: 1200: ১০,০০০ অনুরোধের কোনোটিই database-এর সীমা ছাড়ায়নি। Queue না থাকলে ঐ ১০ সেকেন্ড database সেকেন্ডে ৬০০টি অর্ডার বানাতে চেষ্টা করত, যা সে পারে না; ফল হতো সবার জন্য timeout.
একই ঢেউ, ধাপে ধাপে। মানগুলো উপরের মডেলের আউটপুট থেকে নেওয়া।
1t=0–9 s: স্বাভাবিক, 80/s2t=10–14 s: সেল শুরু, 600/s3t=15–19 s: queue সীমায়, 4294t=20–24 s: ঢেউ শেষ, 80/s5t=25–34 s: worker জমা কাজ শেষ করছে6t=35–44 s: queue খালি
| সময় | |
| আসছে প্রতি সেকেন্ডে | |
| queue-তে | |
| নতুন ক্রেতার অপেক্ষা | |
| মোট শেষ | |
| মোট ফেরত (429) |
- স্বাভাবিক সন্ধ্যা। সেকেন্ডে ৮০টি অর্ডার আসে, worker ২০০টি পর্যন্ত পারে, তাই queue খালি। ১০ সেকেন্ডে ৮০০টি অর্ডার শেষ।
- সেল শুরু। সেকেন্ডে ৬০০ আসে, ২০০ শেষ হয়; প্রতি সেকেন্ডে ৪০০ জমে। ৫ সেকেন্ডে queue ২,০০০. নতুন ক্রেতা পান “গ্রহণ করা হয়েছে”, আর ২,০০০ ÷ ২০০ = ১০ সেকেন্ডের মধ্যে নিশ্চিতকরণ।
- Queue সীমায়। t = ১৫-তে queue ২,৪০০, t = ১৬-তে সীমা ৩,০০০ ছুঁয়ে কাজের পরে ২,৮০০. এরপর প্রতি সেকেন্ডে শুধু ২০০ জনের জায়গা থাকে, বাকি ৪০০ পান 429 আর
Retry-After. তিন সেকেন্ডে মোট ১,২০০. খারাপ শোনায়, কিন্তু বিকল্প ছিল সবার জন্য timeout. - ঢেউ শেষ। আবার সেকেন্ডে ৮০. Worker এখনো পুরো গতিতে, তাই queue প্রতি সেকেন্ডে ১২০ করে কমে: ২,৮০০ থেকে ৫ সেকেন্ডে ২,২০০.
- জমা কাজ শেষ হচ্ছে। t = ৩৪-এ queue ১,০০০. যাঁদের অর্ডার নিশ্চিত হচ্ছে, তাঁরা WebSocket-এ খবর পাচ্ছেন। ক্রেতা কোন app server-এ যুক্ত, তা worker জানে না; Socket.IO-র Redis adapter খবরটি সব server-এ পৌঁছে দেয় (পর্ব ৬০-এর ল্যাবে adapter ছাড়া অন্য node-এর ক্রেতা খবর পাননি)।
- Queue খালি। t = ৪৪-এর আগেই queue শূন্য, মোট ৭,৬০০ শেষ। Database কোনো মুহূর্তে সেকেন্ডে ২০০-র বেশি অর্ডার দেখেনি। এই সীমাটিই worker-এর গতি, আর সেটি load test দিয়ে মেপে ঠিক করা হয়।
বাস্তবে আরও তিনটি জিনিস লাগে: queue-তে রাখার আগে stock-এর একটি দ্রুত আনুমানিক পরীক্ষা (আসল নিশ্চয়তা worker-এর transaction-এ); প্রতিটি অর্ডারে idempotency key, কারণ job একাধিকবার চলতে পারে; আর সেলের আগে load test, যাতে worker-এর আসল গতি মেপে সীমা ঠিক হয়। বড় মাপের waiting room আর টিকিট বিক্রির মতো চূড়ার নকশা System Design-এর টিকিট বুকিং case study-তে; queue-এর তত্ত্ব message queue পর্বে, rate limit-এর algorithm rate limiting পর্বে, আর ভিড় সামলানোর কৌশল overload পর্বে।
৮ · ধাপ ৫: ১০ লাখ জন — module, search, পুরোনো অর্ডার আর region
পাঁচ-ছয় বছরে বাজারঘর ১০ লাখ দৈনিক ব্যবহারকারী, আর দলে ২৫ জন প্রকৌশলী। মজার ব্যাপার, এই ধাপের বড় লক্ষণগুলোর অর্ধেক মেশিনের নয়, মানুষের। কল্পিত লক্ষণ: সপ্তাহে কয়েকবার deploy আটকে থাকে, কারণ এক দলের ভাঙা test আরেক দলের কাজ থামায়; খোঁজার পাতা ILIKE '%…%'-এ ধীর আর বাংলা বানানের ভুলে কিছুই পায় না; আর orders table-এ কয়েক কোটি সারি, যার বেশিরভাগ বছর পুরোনো, কিন্তু প্রতিটি index আর backup-এ সেগুলোর ভার।
| লক্ষণ | (ক) deploy-এর সারি আর এক দলের বদলে আরেক দলের ভাঙন, (খ) খোঁজা ধীর আর প্রাসঙ্গিকতা খারাপ, (গ) বিশাল অর্ডার table: backup, index বানানো আর vacuum-এর সময় বাড়ছে, (ঘ) notification-এর ভার (ঈদে কয়েক লাখ email আর SMS) অন্য সবকিছুকে ধীর করে। |
|---|---|
| পরিবর্তন | কোডে কড়া module-এর সীমা: catalog, orders, accounts, payments, প্রতিটির নিজের service আর repository, অন্যের table-এ সরাসরি হাত নয় (পর্ব ২৯); search আলাদা ব্যবস্থায়; notification আলাদা service; অর্ডার মাস ধরে partition, পুরোনো অংশ archive-এ; একটি দ্বিতীয় region-এ backup আর দুর্যোগের প্রস্তুতি। |
| দাম আর ঝুঁকি | দুটি নতুন service মানে network-এর ভুল, আলাদা deploy, আলাদা monitoring আর contract test. Search index ডেটাবেসের চেয়ে একটু পিছিয়ে থাকে। Partition-এ প্রতিটি query-তে partition key না থাকলে সব partition পড়া হয়। |
| এখনো করিনি | সব module-কে microservice বানানো, customer ধরে database sharding, দুই region-এ একসাথে লেখা। এগুলোর লক্ষণ এখনো আসেনি। |
Modular monolith আগে, service শুধু যেখানে লাভ
বাজারঘরের দল প্রথমে কোড ভাঙেনি, সীমা কড়া করেছে। একটি modular monolith-এ প্রতিটি module নিজের ডেটার মালিক; অন্য module শুধু তার প্রকাশ্য function ডাকে, তার table-এ সরাসরি query নয়। কোন module আলাদা service হবে, তার জন্য তিনটি প্রশ্ন: ভারের ধরন কি খুব আলাদা? আলাদা গতিতে deploy করতে চায় এমন আলাদা দল আছে কি? Network আর দুই জায়গার ডেটার দাম কি লাভের চেয়ে কম? Notification তিনটিতেই হ্যাঁ (ঈদে লাখো বার্তা, নিজস্ব retry, প্রায় স্বাধীন); checkout তিনটিতেই না। Stack আর architecture বাছাইয়ের বড় প্রশ্নগুলো পর্ব ৮৫-এ (Stack বাছাই), আর তত্ত্ব System Design-এর microservices পর্বে।
Search: আলাদা ব্যবস্থা, কিন্তু আগে PostgreSQL-এর সীমা দেখা
ILIKE '%ইলিশ%' সাধারণ B-tree index ব্যবহার করতে পারে না, তাই বড় table-এ পুরোটা পড়ে। প্রথম ধাপ PostgreSQL-এর নিজের full-text search বা pg_trgm extension-এর trigram index; অনেক দোকানের জন্য এটুকুই যথেষ্ট। বানান-ভুল সহ্য করা, প্রাসঙ্গিকতা ধরে সাজানো আর দ্রুত facet দরকার হলে দলগুলো প্রায়ই আলাদা search engine নেয়, যেমন OpenSearch, Meilisearch বা Typesense (উদাহরণ মাত্র; বাংলা সমর্থন নিজে পরীক্ষা করে দেখুন)। দাম: পণ্য বদলালে index-এ পাঠানোর পথ (queue বা outbox), আর index একটু পিছিয়ে থাকে। সত্যের উৎস PostgreSQL-ই থাকে। তত্ত্ব System Design-এর search পর্বে।
পুরোনো অর্ডার: partition আর archive
ক্যালকুলেটর বলছে ১০ লাখে বছরে প্রায় দেড় কোটি অর্ডার। PostgreSQL ১০ (২০১৭) থেকে declarative partitioning আছে: orders একটি “মা” table, আর প্রতিটি মাসের অর্ডার নিজের partition-এ। WHERE created_at >= … থাকলে PostgreSQL শুধু দরকারি partition পড়ে, আর পুরোনো মাস পুরো partition হিসেবে detach করে archive-এ পাঠানো যায়, এক এক সারি মোছার বদলে। দুই বছরের পুরোনো অর্ডার যায় সস্তা storage-এ; কত দিন রাখতে হবে, সেটি হিসাব আর আইনের প্রশ্ন। দাম: customer ধরে খোঁজায় partition key (তারিখ) না থাকলে সব partition পড়তে হয়। এটি এক database-এর ভেতরের ভাগ; কয়েকটি database-এ ভাগ (sharding) অনেক বেশি কঠিন, আর বাজারঘর তা করেনি। পার্থক্য System Design-এর partitioning পর্বে।
Region: একটি সতর্ক নোট
বাজারঘরের প্রায় সব ক্রেতা বাংলাদেশে। তাই multi-region মানে এখানে প্রথমে কাছের একটি region-এ মূল server আর CDN দিয়ে static অংশ কাছে আনা। দ্বিতীয় region-এ শুধু backup আর একটি replica, যাতে পুরো data center গেলে দোকান ফেরানো যায়; কত ঘণ্টায় আর কত মিনিটের ডেটা হারিয়ে, তা আগে লিখে মহড়া দেওয়া। দুই region-এ একসাথে লেখা conflict আর খরচে খুব কঠিন, আর এক দেশের দোকানে সাধারণত লাগে না। নিজের ক্রেতাদের latency মেপে নিশ্চিত হন; আর System Design-এর disaster recovery পর্ব দেখুন।
৯ · সাধারণ ভুল আর যা ইচ্ছে করে করা হয়নি
পাঁচ ধাপ পেরিয়ে চারটি ভুল বারবার দেখা যায়। প্রতিটির পাশে বাজারঘরের গল্পের প্রমাণ।
| ভুল | কেমন দেখায় | বদলে কী |
|---|---|---|
| মাপার আগে scale | “সাইট ধীর, আরেকটা server নিই।” কিন্তু ধীর query সব server-এ সমান ধীর, আর নতুন server আরও connection চায়। | আগে route ধরে p95, query log, EXPLAIN ANALYZE. ল্যাবে একটি index ৩৬ গুণ দিয়েছে, নতুন server দিত না। |
| খুব আগে microservice | ২,০০০ ব্যবহারকারী, দুজন প্রকৌশলী, ছয়টি service, প্রতিটির নিজের database. একটি checkout-এ পাঁচটি network call আর এক জায়গায় ভুল মানে পাঁচ জায়গায় খোঁজা। | Modular monolith; service শুধু যেখানে ভার, দল আর deploy-এর গতি সত্যিই আলাদা। |
| Invalidation ছাড়া cache | দাম বদলেছে, কিন্তু পাতায় পুরোনো দাম; checkout-এ নতুন দাম। ক্রেতার অভিযোগ, বিশ্বাস হারানো। | লেখার পরে DEL, ছোট TTL, version guard; দাম আর stock-এর মতো জিনিস checkout-এ সবসময় database থেকে। |
| Database উপেক্ষা | App server বাড়ছে, কিন্তু সবাই একই primary-তে; pool-এর যোগফল max_connections ছাড়ায়; একটি রিপোর্ট সব থামায়। | Index, N+1, pool-এর হিসাব, pooler, replica-তে রিপোর্ট; database-কে প্রথম দিন থেকে graph-এ রাখা। |
আর প্রতিটি ধাপে যা পিছিয়ে রাখা হয়েছিল, এক জায়গায়। লক্ষ করুন, প্রতিটির পাশে সেই লক্ষণ, যেটি এলে তবেই করা হবে।
| যা করা হয়নি | কোন লক্ষণ এলে করা হবে |
|---|---|
| Kubernetes, অটো-scaling | ভারের ওঠানামা এত বড় আর ঘন যে হাতে server যোগ করা দলের নিয়মিত কাজ হয়ে গেছে |
| Database sharding | সবচেয়ে বড় মেশিনেও primary-র লেখা আর আকার আর ধরছে না, আর partition ও archive যথেষ্ট নয় |
| সব module microservice | একটি নির্দিষ্ট module-এর দল তার deploy-এর জন্য বারবার অন্যদের অপেক্ষায়, আর সীমাটি কোডে আগে থেকেই পরিষ্কার |
| দুই region-এ লেখা | দেশের বাইরের ক্রেতা অনেক, latency মাপা সমস্যা, আর ব্যবসা conflict-এর নিয়ম মেনে নিতে রাজি |
| অন্য ভাষায় নতুন করে লেখা | প্রায় কখনো নয়; ধীর অংশ মেপে শুধু সেটিকে আলাদা করা সস্তা |
আরও গভীরে। Little-এর সূত্র L = λW-এর সাধারণ প্রমাণ John D. C. Little ১৯৬১ সালে Operations Research পত্রিকায় প্রকাশ করেন; এটি স্থিতিশীল যেকোনো ব্যবস্থায় খাটে, আগমনের ধরন যা-ই হোক। Queue-এর অপেক্ষা ব্যস্ততার সাথে সরলরেখায় নয়, প্রায় 1/(1−ρ) হারে বাড়ে, তাই ৮০% ব্যস্ত database-এ সামান্য ভিড় বিশাল অপেক্ষা আনে; এজন্যই budget-এ “আরামের” সীমা রাখা হয়। PostgreSQL-এর streaming replication ৯.০ (২০১০) থেকে, declarative partitioning ১০ (২০১৭) থেকে। Cache-এর ঢেউ আর পুরোনো মান বসার সমস্যায় Facebook-এর প্রকৌশলীদের “Scaling Memcache at Facebook” (NSDI ২০১৩) “lease” নামের একটি কৌশল বর্ণনা করে, যা single-flight-এর কাছাকাছি।
পরীক্ষা ও ইন্টারভিউতে। (১) ৫০ হাজার DAU, প্রতিজন দিনে ৪০টি অনুরোধ, peak factor ৬: peak req/s কত? ৫০,০০০ × ৪০ ÷ ৮৬,৪০০ × ৬ ≈ ১৩৯. (২) ৪০০ SQL/s, প্রতিটি ২৫ ms: কতটি connection ব্যস্ত? L = ৪০০ × ০.০২৫ = ১০. (৩) Read replica-র সবচেয়ে পরিচিত ভুল কী? লেখার পরপরই replica থেকে পড়লে নিজের লেখা না দেখা; সমাধান নিজের ডেটা বা লেখার পরের কিছুক্ষণ primary থেকে পড়া। (৪) কয়েকটি app server-এ login কেন হারায়? Session process-এর মেমরিতে; সমাধান Redis বা database-এ shared session store. (৫) Flash sale-এ queue কেন? Database-এর নিরাপদ গতিতে কাজ ছড়িয়ে দিতে; সীমা ছাড়ালে 429 আর Retry-After.
১০ · AI-এর যুগে scaling-এর সিদ্ধান্ত
শুধু “১০ লাখ ব্যবহারকারীর জন্য কীভাবে scale করব” জিজ্ঞেস করলে AI প্রায়ই একটি সুন্দর কিন্তু সাধারণ ছবি আঁকে: Kubernetes, microservice, Kafka, sharded database, multi-region. ছবিটি বড় কোম্পানির blog থেকে শেখা, আপনার graph থেকে নয়। AI আপনার query plan, আপনার peak factor বা আপনার দলের আকার জানে না। তাই তার কাছে মাপ নিয়ে যান, আর তার প্রতিটি পরামর্শকে একটি পরীক্ষাযোগ্য অনুমান হিসেবে নিন।
এমন কাঠামোয় জিজ্ঞেস করুন, যাতে AI-কে আপনার সংখ্যার সাথে কথা বলতে হয়:
AI-এর scaling পরামর্শ যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১“Premature optimization is the root of all evil” — তাহলে কি শুরুতে performance নিয়ে একটুও ভাবা উচিত নয়?
উক্তিটি Donald Knuth-এর, ১৯৭৪ সালের প্রবন্ধ “Structured Programming with go to Statements” থেকে। কিন্তু পুরো বাক্যটি সাধারণত অর্ধেক উদ্ধৃত হয়। Knuth বলেছিলেন, প্রায় ৯৭% সময় ছোট দক্ষতা নিয়ে চিন্তা না করাই ভালো, অকাল optimization সব মন্দের মূল; তবু বাকি গুরুত্বপূর্ণ ৩% সুযোগ হাতছাড়া করা উচিত নয়। আর সেই ৩% চেনার পথ তিনি বলেছিলেন মাপা: প্রোগ্রামাররা প্রায়ই ভুল অনুমান করেন কোথায় সময় যায়।
বাজারঘরের গল্পে এই দুই ভাগ স্পষ্ট। প্রথম দিনেই তিনটি server, Kafka আর sharding হলো সেই ৯৭%-এর অপচয়: লক্ষণ ছাড়া খরচ, জটিলতা আর ভাঙার নতুন পথ। কিন্তু কিছু সিদ্ধান্ত পরে বদলানো এত দামি যে সেগুলো “optimization” নয়, “ভিত্তি”: সঠিক primary key আর foreign key, টাকার জন্য পূর্ণসংখ্যা, idempotency key, migration, structured log আর request id, আর stateless হওয়ার জন্য session ও ফাইলের interface. এগুলো দশ লাখের জন্য নয়, যেকোনো সংখ্যার জন্য ঠিক।
মাঝখানে পড়ে পরিচিত খারাপ নকশা: N+1, pagination ছাড়া সব সারি আনা, লুপের ভেতরে network call. এগুলো এড়ানো optimization নয়, সাধারণ ভালো অভ্যাস।
তাই বাস্তব নিয়ম তিন স্তরের: ভিত্তি প্রথম দিন থেকে ঠিক রাখুন, পরিচিত খারাপ নকশা এড়িয়ে চলুন, আর বাকি সবকিছু মাপ দেখে, লক্ষণ এলে।
মূল উপলব্ধি: প্রশ্নটি “এখন ভাবব কি না” নয়, “এই সিদ্ধান্ত পরে বদলাতে কত দাম”। যা পরে বদলানো সস্তা, তা মাপ পর্যন্ত অপেক্ষা করুক; যা দামি, তা আজই ঠিক করুন।
প্র ০২কয়েকটি পরিচিত দল microservice থেকে monolith-এ ফিরে গেছে বলে লিখেছে। তাহলে কি microservice ভুল ধারণা?
না, তবে এর দাম প্রায়ই কম ধরা হয়। কয়েকটি প্রকাশ্য লেখা এই বিতর্ককে আকার দিয়েছে। Martin Fowler ২০১৫ সালে “MonolithFirst” লেখায় যুক্তি দেন যে সফল microservice-এর গল্পের বেশিরভাগ শুরু হয়েছে একটি বড় হয়ে যাওয়া monolith থেকে, আর শুরু থেকেই microservice দিয়ে বানানো অনেক প্রকল্প সমস্যায় পড়েছে; তিনি নিজেও বলেন এর উল্টো অভিজ্ঞতার লোকও আছেন। ২০১৮ সালে Segment-এর প্রকৌশল blog-এ “Goodbye Microservices” নামের লেখায় তারা শত শত ছোট service থেকে একটি service-এ ফেরার কারণ বর্ণনা করে: প্রতিটির আলাদা library-র version, আলাদা test আর ভার সামলানোর বোঝা। ২০২৩ সালে Amazon Prime Video-র একটি দলের blog জানায়, একটি নির্দিষ্ট monitoring ব্যবস্থাকে বিচ্ছিন্ন serverless অংশ থেকে একটি process-এ আনায় খরচ অনেক কমেছে; অনেকে মনে করিয়ে দেন, এটি একটি অংশের সিদ্ধান্ত, পুরো কোম্পানির নয়।
অন্যদিকে Shopify-এর প্রকৌশলীরা ২০১৯ সালের দিকে লিখেছেন কীভাবে তারা একটি বিশাল Rails monolith-কে না ভেঙে ভেতরে module-এর কড়া সীমা টেনেছেন: সেই modular monolith-এর পথ, যা বাজারঘরও নিয়েছে। আর বহু বড় প্রতিষ্ঠান সত্যিই অনেক service চালায়, কারণ তাদের শত শত দলকে স্বাধীনভাবে deploy করতে হয়।
মূল প্রশ্নটি তাই প্রযুক্তির নয়, সংগঠনের। Microservice আলাদা দল, আলাদা deploy-এর গতি আর আলাদা ভারের ধরনকে সম্ভব করে, বিনিময়ে প্রতিটি function call হয় network call (ব্যর্থ হতে পারে, ধীর হতে পারে), ডেটা দুই জায়গায় (transaction-এর বদলে saga বা outbox), আর monitoring, tracing ও contract test বাধ্যতামূলক। দুজনের দলে এই দাম বিশাল, লাভ প্রায় শূন্য।
মূল উপলব্ধি: Microservice একটি দল-কাঠামোর সমাধান, scaling-এর জাদু নয়। সীমা আগে কোডে টানুন; service তখনই, যখন ঐ সীমা ধরে আলাদা দল আর আলাদা ভার সত্যিই আছে।
প্র ০৩Cache তো সহজ, মান রেখে দিলেই হলো; তাহলে বলা হয় কেন যে cache invalidation কম্পিউটার বিজ্ঞানের সবচেয়ে কঠিন কাজগুলোর একটি?
একটি জনপ্রিয় রসিকতা Phil Karlton-এর নামে প্রচলিত: কম্পিউটার বিজ্ঞানে কঠিন জিনিস দুটি, cache invalidation আর জিনিসের নাম দেওয়া। উক্তির উৎস নিখুঁতভাবে নথিভুক্ত নয়, কিন্তু কথাটি টিকে আছে কারণ এটি সত্যি। রাখা সহজ; কঠিন হলো জানা কখন রাখা মানটি আর সত্য নয়।
বাজারঘরের অভিজ্ঞতায় তিনটি ফাঁদ। প্রথমত, race: একটি অনুরোধ পুরোনো দাম পড়ল, এর মধ্যে আরেকটি নতুন দাম লিখে cache মুছল, তারপর প্রথমটি পুরোনো দাম cache-এ বসাল। পর্ব ৪৪-এর একটি run-এ কোনো কৃত্রিম দেরি ছাড়াই ৪,০০০ বারে ১৩৯ বার এমন হয়েছিল, version guard দিয়ে ০ বার। দ্বিতীয়ত, নির্ভরতা: একটি পণ্যের দাম বদলালে শুধু পণ্যের key নয়, ক্যাটাগরির তালিকা, হোম পাতার “অফার” আর search-এর ফলও বদলায়; কোন key কোন ডেটার ওপর নির্ভর করে, তার হিসাব রাখা কঠিন। তৃতীয়ত, ঢেউ: একটি জনপ্রিয় key-এর মেয়াদ শেষ হলে একসাথে অনেকে database-এ যায়।
বড় মাপে এর গবেষণাও আছে। Facebook-এর প্রকৌশলীদের ২০১৩ সালের NSDI প্রবন্ধ “Scaling Memcache at Facebook” “lease” নামে একটি কৌশল বর্ণনা করে: miss হলে cache একজনকে একটি টোকেন দেয়, শুধু সে নতুন মান বসাতে পারে, আর বাকিরা একটু অপেক্ষা করে। এতে একই সাথে পুরোনো মান বসা আর ঢেউ দুটোই কমে। এটি single-flight আর lock-এর বড় ভাই।
ব্যবহারিক নিয়ম তাই: যেখানে ভুল মান বিপজ্জনক (দাম, stock, টাকা), সেখানে চূড়ান্ত সিদ্ধান্ত সবসময় database থেকে, cache শুধু দেখানোর জন্য; লেখার পরে SET নয়, DEL; ছোট TTL, যাতে ভুলের আয়ু সীমিত; আর cache-কে এমনভাবে নকশা করা যাতে সেটি খালি হলেও দোকান টিকে থাকে, শুধু ধীর হয়।
মূল উপলব্ধি: Cache-এর প্রতিটি key একটি প্রতিশ্রুতি: “এই মান কিছুক্ষণ সত্য থাকবে।” প্রতিশ্রুতি দেওয়ার আগে জানুন কে, কখন সেটি ভাঙবে।
প্র ০৪Connection pool বড় করলেই কি database দ্রুত সাড়া দেবে? না হলে কেন?
প্রায়ই উল্টোটা ঘটে। Little-এর সূত্র (L = λW, John Little-এর ১৯৬১ সালের প্রমাণ) বলে কতটি connection গড়ে ব্যস্ত থাকবে; সেটি আসে আগমনের হার আর query-র সময় থেকে, pool-এর আকার থেকে নয়। Pool শুধু ঠিক করে একসাথে কতটি query database-এ ঢুকতে পারবে। আর database-এর ভেতরে আসল কাজ করে সীমিত সংখ্যক CPU core আর disk.
ধরুন database-এর ৪টি core. একসাথে ৪টি query চললে প্রতিটি পুরো একটি core পায়। একসাথে ১০০টি চললে প্রতিটি পায় core-এর ২৫ ভাগের এক ভাগ, আর সাথে আসে context switch, lock-এর প্রতিযোগিতা, cache-এর অপচয়; মোট কাজ একই বা কম, কিন্তু প্রতিটি query-র সময় বাড়ে। W বাড়লে, Little-এর সূত্রে, আরও connection ব্যস্ত দেখায়, আর দল আরও বড় pool চায়: একটি খারাপ চক্র। PostgreSQL-এর community wiki আর কয়েকটি জনপ্রিয় pool library-র docs-এ (যেমন Java-র HikariCP) এই কারণেই একটি শুরুর সূত্র দেওয়া হয়, মোটামুটি core-এর সংখ্যার দ্বিগুণের কাছাকাছি, নিজের load test দিয়ে মেলানোর শর্তে।
অংশ ২-এর ক্যালকুলেটরে আরেকটি দিক দেখেছেন: একই ঈদের ভিড়ে query ৪ ms হলে ৮.৩টি connection ব্যস্ত, ৪০ ms হলে ৮৩.৩টি. অর্থাৎ ধীর query-ই connection খায়। pool বাড়ানো সেই ধীরতার সমাধান নয়; index বা query সংশোধন সমাধান। আর পুরো সংখ্যার দিকটিও আছে: প্রতিটি process-এর নিজের pool, তাই ৩টি server × ৪টি process × ১০ = ১২০, যা PostgreSQL-এর ডিফল্ট ১০০-র সীমা ছাড়ায়। এজন্য অনেক process থাকলে PgBouncer-এর মতো pooler সামনে বসে, যা অনেক client connection-কে কয়েকটি আসল connection-এ ভাগ করে চালায়।
যখন pool সত্যিই খুব ছোট, তখন লক্ষণ হলো app-এর ভেতরে অপেক্ষা (pool থেকে connection পেতে সময়) বাড়ছে অথচ database-এর CPU ফাঁকা। তখন সামান্য বাড়ানো ঠিক। তাই দুটি সংখ্যা পাশাপাশি দেখুন: pool-এর অপেক্ষা আর database-এর ব্যস্ততা।
মূল উপলব্ধি: Pool হলো দরজার মাপ, ঘরের মাপ নয়। দরজা বড় করলে ঘরে ভিড় বাড়ে, কাজ বাড়ে না।
প্র ০৫SLO আর error budget কীভাবে একটি ছোট দলকে বলে দেয় কখন scaling-এর কাজ করবে আর কখন feature-এর?
Google-এর Site Reliability Engineering বই (২০১৬) SLO আর error budget-এর ধারণাকে ব্যাপক পরিচিতি দেয়। মূল কথা: ১০০% নির্ভরযোগ্যতা ভুল লক্ষ্য। ক্রেতার ফোন, মোবাইল নেটওয়ার্ক, payment gateway নিজেরাই মাঝে মাঝে ব্যর্থ হয়, তাই আপনার দিকের ৯৯.৯৯% আর ১০০%-এর পার্থক্য ক্রেতা টেরই পান না, অথচ দাম আকাশছোঁয়া। তাই একটি লক্ষ্য বেছে নিন, যেমন বাজারঘরের কল্পিত “২৮ দিনে checkout-এর ৯৯.৫% সফল আর ৮০০ ms-এর ভেতরে”, আর বাকি ০.৫% হলো খরচ করার মতো budget.
এই budget দলের সিদ্ধান্তকে অনুভূতি থেকে সংখ্যায় আনে। মাসের মাঝে budget-এর অর্ধেক শেষ হলে (burn rate বেশি), নতুন feature থামে, আর দল লক্ষণ খোঁজে: কোন route, কোন সময়, কোন query. এটিই বাজারঘরের প্রতিটি ধাপের “লক্ষণ” সারির উৎস। Budget অক্ষত থাকলে, “হয়তো একদিন ভিড় হবে” ভেবে replica বা queue বসানোর বদলে feature বানানোই ঠিক। পর্ব ৮৩-এ দেখেছেন burn rate দিয়ে কীভাবে alert হয়।
ছোট দলের জন্য দুটি সতর্কতা। প্রথমত, SLO হবে ক্রেতার চোখে, server-এর নয়: “CPU ৭০%-এর নিচে” SLO নয়, কারণ ক্রেতা CPU দেখেন না; “checkout সফল আর দ্রুত” SLO. দ্বিতীয়ত, কম সংখ্যক গুরুত্বপূর্ণ পথ বেছে নিন (checkout, login, পণ্যের পাতা), সব route নয়; নইলে dashboard-এ সব লাল আর কেউ কিছু দেখে না।
মূল উপলব্ধি: Error budget হলো নির্ভরযোগ্যতা আর নতুন কাজের মাঝে একটি চুক্তি। Scaling তখন আর ভয় বা অহংকারের কাজ নয়, budget-এর হিসাব।
অনুশীলন
একটি হিসাব, একটি কমান্ডের কাজ, একটি নকশা পর্যালোচনা আর একটি ফল অনুমান।
-
হিসাব করুন: একটি দোকানের DAU ৫০,০০০, প্রতিজন দিনে ৪০টি অনুরোধ, peak factor ৬, প্রতি অনুরোধে ২টি SQL, প্রতিটি SQL ৫ ms. দিনে ৩% ব্যবহারকারী অর্ডার করেন, প্রতিটি অর্ডারে ৫ KB. (ক) গড় আর peak req/s? (খ) peak-এ SQL/s আর ব্যস্ত connection, দ্বিগুণ headroom-সহ pool? (গ) বছরে অর্ডারের storage?
(ক) গড় = ৫০,০০০ × ৪০ ÷ ৮৬,৪০০ ≈ ২৩.১ req/s; peak = ২৩.১৫ × ৬ ≈ ১৩৮.৯ req/s.
(খ) SQL/s = ১৩৮.৯ × ২ ≈ ২৭৭.৮; ব্যস্ত connection = ২৭৭.৮ × ০.০০৫ ≈ ১.৩৯; দ্বিগুণ headroom-এ ২.৭৮, অর্থাৎ ৩টি। সংখ্যাটি ছোট শুনে অবাক লাগে, কিন্তু query ৫০ ms হলে ১৩.৯, দ্বিগুণে ২৮.
(গ) দিনে ৫০,০০০ × ০.০৩ = ১,৫০০ অর্ডার; বছরে ৫,৪৭,৫০০; × ৫ KB = ২৭,৩৭,৫০০ KB ≈ ২.৬১ GB (১,০২৪ × ১,০২৪ দিয়ে ভাগ করে)। Index আর backup-এর কপি ধরলে আসল disk এর কয়েক গুণ।
-
কমান্ডের কাজ: নিজের যেকোনো Express বা Next.js API-র একটি ধীর route বেছে নিন। একই মেশিনে
npx autocannon -c 50 -d 10 http://127.0.0.1:<port>/<route>চালান, তারপর সেই route-এর মূল query-তেEXPLAIN ANALYZE. একটি index বা query সংশোধনের পরে দুটিই আবার চালান। কী কী লিখে রাখবেন, আর কোন ফাঁদ এড়াবেন?লিখে রাখুন: autocannon-এর গড় req/s, p50 আর p99 latency, non2xx আর error-এর সংখ্যা; EXPLAIN-এর scan-এর ধরন (Seq Scan থেকে Index Scan বা Bitmap Index Scan) আর Execution Time; আর version, connection-এর সংখ্যা, সময়কাল। প্রতিটি অবস্থা অন্তত তিনবার চালিয়ে মাঝের মান নিন। ফাঁদ: dev server মাপা (production build নয়); ডেটা খুব কম রেখে মাপা (১০০ সারিতে index ছাড়াও দ্রুত); load টুল আর server একই মেশিনে CPU ভাগ করছে ভুলে যাওয়া; প্রথম run-এ cache ঠান্ডা থাকা; আর একটি run থেকে সাধারণ সিদ্ধান্ত টানা। নিজের ডেটাবেসে কাজ করছেন কি না, port আর database-এর নাম দেখে নিশ্চিত হন।
-
নকশা পর্যালোচনা: ২,০০০ DAU-এর একটি দোকানের দুজনের দল প্রস্তাব দিয়েছে: Kubernetes cluster, ছয়টি microservice (প্রতিটির নিজের PostgreSQL), service-গুলোর মাঝে Kafka, আর প্রতিটি পাতায় ৫ মিনিটের cache. তাদের মূল অভিযোগ: “অর্ডারের ইতিহাস ধীর।” এই প্রস্তাবের সমালোচনা করুন আর বিকল্প দিন।
প্রথম প্রশ্ন: লক্ষণ মাপা হয়েছে কি? ২,০০০ DAU-এ peak কয়েক req/s মাত্র (ক্যালকুলেটরের ১,০০০-এর সারির দ্বিগুণের মতো), তাই সমস্যা প্রায় নিশ্চিতভাবে query-তে: index নেই, N+1, বা pagination নেই। Kubernetes আর ছয়টি service এগুলোর কোনোটিই সারাবে না, বরং অর্ডারের ইতিহাস এখন কয়েকটি service থেকে network দিয়ে জোড়া লাগাতে হবে, আর ছয়টি database-এর backup, migration, monitoring দুজনের ঘাড়ে। Kafka মানে আরেকটি জটিল ব্যবস্থা চালানো। পাঁচ মিনিটের cache সব পাতায় মানে দাম আর stock পুরোনো দেখানো, আর invalidation-এর কোনো পরিকল্পনা নেই।
বিকল্প: route ধরে p95 আর query log; ধীর query-তে EXPLAIN ANALYZE আর composite index; N+1 থাকলে include বা join; cursor pagination; তারপর load test দিয়ে আগে-পরে মাপা। কোড modular monolith-এ গুছানো, যাতে পরে দরকার হলে সীমা তৈরি থাকে। Cache শুধু মাপা ভারী আর জনপ্রিয় পাতায়, লেখার পরে DEL-সহ।
-
ফল অনুমান করুন: ঈদের queue-এর মডেলে (স্বাভাবিক সময়ে সেকেন্ডে ৮০, t = ১০ থেকে ১৯ সেকেন্ডে ৬০০, queue-এর সীমা ৩,০০০, ৬০ সেকেন্ড চলে) worker-এর গতি ২০০ থেকে বাড়িয়ে ৩০০ করা হলো। সর্বোচ্চ queue, মোট ফেরত (429) আর মোট শেষ কত হবে? Queue কখন খালি হবে?
ঢেউয়ে প্রতি সেকেন্ডে ৬০০ − ৩০০ = ৩০০ জমে। t = ১৮-এর শেষে queue ২,৭০০ (৯ সেকেন্ড × ৩০০)। t = ১৯-এ জায়গা মাত্র ৩০০, তাই ৩০০ জন ফেরত পান; queue ৩,০০০, কাজের পরে আবার ২,৭০০. সর্বোচ্চ queue ২,৭০০, মোট ফেরত ৩০০. মোট আগমন ৫০ × ৮০ + ১০ × ৬০০ = ১০,০০০, তাই মোট শেষ ৯,৭০০. ঢেউয়ের পরে প্রতি সেকেন্ডে ৩০০ − ৮০ = ২২০ কমে: ২,৭০০ ÷ ২২০ ≈ ১২.৩, তাই t = ৩২-এ খালি। কোড বদলে চালালে ঠিক এই সংখ্যাই আসে:
max queue: 2700 total done: 9700 rejected: 300. শিক্ষা: worker-এর গতি দেড় গুণ হলে ফেরত চার ভাগের এক ভাগ, কিন্তু সেই গতি database-কে সত্যিই নিতে হবে; তাই আগে load test.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি ধীর অ্যাপ বড় করার কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Graph বা log থেকে লক্ষণ মাপা (কোন route, কখন, p95 কত)
- Query plan আর profile দেখে বাধাটি খুঁজে বের করা
- সবচেয়ে সস্তা যে পরিবর্তন বাধাটি সারায়, সেটি বেছে নেওয়া
- আগে আর পরে একই load test চালিয়ে মিলিয়ে দেখা
- Production-এর graph আর SLO-তে ফল দেখা
- নতুন দাম, ঝুঁকি আর এখনো যা করা হয়নি তা লিখে রাখা
একটি অ্যাপের DAU ২০,০০০, প্রতিজন দিনে ৩০টি অনুরোধ করেন, আর peak factor ৫. Peak-এ মোটামুটি কত req/s?
- প্রায় ৬.৯
- প্রায় ৩৫
- প্রায় ১৭৪
- প্রায় ৬০০
একটি database প্রতি সেকেন্ডে ৪০০টি SQL পায়, আর প্রতিটি গড়ে ২৫ ms ধরে connection ব্যবহার করে। Little-এর সূত্রে গড়ে কতটি connection ব্যস্ত থাকে?
- ১০
- ১৬
- ২৫
- ৪০০
একটি ক্রেতা নতুন ঠিকানা সংরক্ষণ করার সাথে সাথে তালিকার পাতায় গেলেন, কিন্তু নতুন ঠিকানাটি দেখলেন না। লেখা primary-তে যায়, আর সব পড়া read replica থেকে। সবচেয়ে সম্ভাব্য কারণ আর উপযুক্ত সমাধান কোনটি?
- Transaction রোলব্যাক হয়েছে; প্রতিটি লেখা দুবার পাঠানো
- Browser cache; সব পাতায় no-store দেওয়া
- Replication lag; লেখার পরে কিছুক্ষণ সেই ক্রেতার পড়া primary থেকে
- Index নেই; ঠিকানার table-এ index দেওয়া
একটি Node অ্যাপকে একটি process থেকে load balancer-এর পেছনে তিনটি process-এ নেওয়ার পরে ক্রেতারা মাঝে মাঝে হঠাৎ logout হচ্ছেন। Session রাখা আছে process-এর মেমরিতে। সবচেয়ে টেকসই সমাধান কোনটি?
- Session cookie-র মেয়াদ বাড়ানো
- Load balancer-এ sticky session চালু করে সমস্যা শেষ ধরা
- আবার একটি process-এ ফিরে যাওয়া
- Session Redis বা database-এর মতো shared store-এ রাখা
Peak-এ database-এ cache করা যায় এমন ২,০০০টি পড়া আসে প্রতি সেকেন্ডে, আর database আরামে সেকেন্ডে ৫০০টি নিতে পারে। Cache-এর hit rate অন্তত কত হতে হবে?
- ২৫%
- ৭৫%
- ৯০%
- ৯৯%
একটি ১,০০০ ব্যবহারকারীর দোকানে “আমার অর্ডার” পাতা ধীর, আর query log-এ দেখা যায় প্রতিটি অনুরোধে seq scan. দলের প্রথম কাজ কোনটি হওয়া উচিত?
- EXPLAIN ANALYZE দেখে উপযুক্ত composite index দেওয়া, তারপর আগে-পরে মাপা
- আরও দুটি app server যোগ করা
- পাতাটিকে ৫ মিনিটের জন্য cache করা
- অর্ডারের অংশকে আলাদা microservice বানানো
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- প্রতিটি ধাপে চার প্রশ্ন: লক্ষণ, পরিবর্তন, দাম আর ঝুঁকি, এখনো যা করিনি। লক্ষণ মানে graph বা মাপ, অনুভূতি নয়।
- হিসাব: DAU × অনুরোধ ÷ ৮৬,৪০০ × peak factor; connection = SQL/s × query-র সময়; hit rate ≥ ১ − budget ÷ peak; storage = দৈনিক সারি × আকার × ৩৬৫.
- Database আগে: index আর N+1 সারানো প্রায়ই নতুন server-এর চেয়ে অনেক বড় লাভ দেয়; index ছাড়া cache সমস্যা লুকায়।
- কয়েকটি process মানে stateless: session Redis-এ, ফাইল object storage-এ, pool-এর যোগফল
max_connections-এর নিচে; replica মানে read-after-write-এর যত্ন। - ঢেউ সামলায় queue আর rate limit; microservice আসে শেষে, শুধু যেখানে ভার আর দল আলাদা; partition আর archive আগে, sharding অনেক পরে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৮৮ · ক্যাপস্টোন — একটি পূর্ণ অনলাইন বাজার: নকশা থেকে deploymentপরবর্তী পর্বপুরো বাজারঘর নিজে বানানো: milestone, test, build আর deploy-এর পরিকল্পনা।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ; পরের পর্ব ৮৮ · ক্যাপস্টোন।
- পর্ব ৪২ · N+1 সমস্যাপেছনে ফিরেQuery গোনা, include আর EXPLAIN দেখে index.
- পর্ব ৪৪ · Cache আর Redisপেছনে ফিরেCache-aside, race, stampede আর fail open.
- পর্ব ৮২ · কোথায় চালাবেনপেছনে ফিরেVPS, pm2 cluster, reverse proxy আর zero-downtime reload.
- System Design · Estimationগভীরে যানQPS, storage আর server-এর সংখ্যার পূর্ণ পদ্ধতি।
- System Design · Replicationগভীরে যানLeader-follower, lag আর read-your-writes.
- System Design · Monolith বনাম microservicesগভীরে যানService boundary, strangler fig আর distributed monolith.