পর্ব ৫: মাপুন, বদলান, আবার মাপুন: load test, পরীক্ষার এক আসল রাত আর চূড়ান্ত আর্কিটেকচার
সাতটা পরিমাপ, পরীক্ষার এক আসল সন্ধ্যা আর একটা scale-in ভুল: load test, capacity model আর সৎ একটা soak পরিকল্পনা কীভাবে NovaCommerce-এর চূড়ান্ত আর্কিটেকচার গড়ল, তার খরচ কত, আর Solutions Architect-এর checklist। পঞ্চম পর্বে সিরিজ শেষ।
৭ অক্টোবরের সন্ধ্যায় NovaCommerce-এ ৬৬১ জন শিক্ষার্থী একটা পরীক্ষা শুরু করল, চূড়ায় প্রতি মিনিটে ৮৬ জন করে, আর তার পরের পরীক্ষাটায় বসল আরও ৪৩৯ জন। backend সর্বোচ্চ উঠল প্রায় ৬৯ request/s-এ, আর দুই মিনিটের গড়ে CPU পৌঁছাল ৪৭%-এ। database তার CPU-র সিকিভাগেরও কম ব্যবহার করল। একটা background job-ও ব্যর্থ হয়নি।
তারপর পরীক্ষা চলাকালেই কেউ একজন pool-এর minimum কমিয়ে দিলেন। একটা চালু node কোনো drain ছাড়াই হঠাৎ উধাও হয়ে গেল, আর দুই মিনিটে প্রায় ৩৬০টা request ব্যর্থ হলো।
সেই সন্ধ্যার দুটো অংশই ছিল test-এর ফল: শান্ত অংশটা এসেছে আগের কয়েক দিনের ছোট একসারি test থেকে, আর বিশ্রী অংশটা এসেছে এমন একটা জায়গা থেকে, যেটা তখনো কোনো test-এ ধরা পড়েনি। এই পর্বে পুরো যাত্রাটা ক্রমানুসারে দেখব (baseline, load test, bottleneck, optimization, retest, scaling, soak, চূড়ান্ত ফল), তারপর চূড়ান্ত আর্কিটেকচার, তার খরচ, আর প্রতিটি architecture review-তে আমি এখন যে checklist ব্যবহার করি সেটা।
এটি "এক সার্ভার থেকে পরীক্ষার দিনের প্রস্তুতি" (From One Server to Exam-Day Ready) নামের পাঁচ পর্বের কেস স্টাডির পঞ্চম পর্ব। NovaCommerce একটি কাল্পনিক নাম; আর্কিটেকচার, সংখ্যা আর ভুলগুলো সব আসল।
যে চক্রটা আমরা বারবার ঘুরেছি
এই সিরিজের প্রতিটি পরিবর্তন এসেছে একটাই চক্র থেকে: মাপুন, bottleneck খুঁজে বের করুন, কারণ বুঝুন, নকশা বদলান, আবার test করুন, আবার মাপুন। কারণ না বুঝে করা পরিবর্তন আসলে আন্দাজ, আর যে আন্দাজ একটা server হয়ে দাঁড়ায়, তার খরচ প্রতি মাসে গুনতে হয়। বাগানের পাইপে একটা মোচড় থাকলে পানি কম আসে, তখন কল আরও খুলে লাভ নেই; পাইপ ধরে হাঁটতে হয় যতক্ষণ না মোচড়টা মেলে। আমাদের মোচড় সরেছে তিনবার, আর প্রতিবার সেটা ছিল আলাদা ধরনের সমস্যা।
সাতটা পরিমাপ, প্রতিটার পেছনে একটা করে সংখ্যা, আর একটা ড্যাশ-করা কার্ড সেই test-এর জন্য, যেটা এখনো planned।
Baseline আর load test: bottleneck সরেছে তিনবার
Baseline: CPU graph নয়, একটা status code
production-এ আমরা প্রথম যেটা মেপেছি, সেটা CPU নয়। ২৫ সেপ্টেম্বর পরীক্ষা চলাকালে শিক্ষার্থীরা HTTP 429 (too many requests) পাচ্ছিল। API rate limiter চলছিল IP address ধরে, কারণ token-ভিত্তিক শিক্ষার্থীদের জন্য ডিফল্ট auth guard ফাঁকা ছিল। একটা স্কুল বা মোবাইল অপারেটর শত শত শিক্ষার্থীকে একটাই NAT IP-র পেছনে বসিয়ে দেয়, ফলে পুরো ভবনের সবাই একটাই বালতি ভাগ করে নিচ্ছিল। 429 মানে application বলছে "না", কোনো server হাঁপিয়ে ওঠেনি, তাই বাড়তি node কিছুই বদলাতে পারত না। Implemented: limiter এখন শিক্ষার্থী বা instructor-এর account ধরে চলে, আর IP ধরে চলে শুধু গেস্টদের জন্য।
Load test: ২,৮৫৪টা error, একটাই বাক্য
৬ অক্টোবর ০২:২৮-এ একটা backend load test ফেরত দিল ২,৮৫৪টা application 500, প্রতিটাই RedisException: Operation timed out, connect করার সময় (connect timeout ৫ সেকেন্ড)। কারণটা এই: Valkey (৪ GB, primary আর standby) লোডের মুখে যথেষ্ট দ্রুত connection নিতে পারছিল না, আর প্রতিটি request তার সঙ্গে নতুন করে একটা TLS connection খুলছিল। এতে request প্রতি CPU লাগছিল Valkey-র TLS connect-এ প্রায় ৫.৪ ms, আর MySQL-এ প্রায় ৩.৩ ms। আরও web node যোগ করলে একই Valkey-র কাছে শুধু আরও connection যেত।
Implemented: Valkey জায়গাতেই (in place) বড় করা হলো ৮ GB-তে, দুটো node-এ (primary আর standby); data অক্ষত, host একই। খরচ বাড়ল, কিন্তু একটা আসল ব্যর্থতা দূর হলো।
Retest: দুটো backend node দিয়ে শুরু করে, pool-কে বাড়তে দিয়ে, সেকেন্ডে ২৫ থেকে ৫০০ request পর্যন্ত ধীরে ধীরে চাপ বাড়িয়ে পেলাম ০টা application error আর ০টা backend 5xx। load balancer-এ ১৪৩টা error এসেছিল, তবে শুধু সেই সময়, যখন node-গুলো CPU-তে ঠাসা। এটা প্রত্যাশিত "ক্ষমতা শেষ" সংকেত, bug নয়; আর এটাই দেখিয়ে দিল একটা node-এর ক্ষমতা কোথায় শেষ।
একটা node আসলে কতটা পারে
backend test থেকে আমরা সেই সংখ্যাটাও পেলাম, যার ওপর পরের প্রতিটা সিদ্ধান্ত দাঁড়িয়ে আছে। পুরো CPU-তে একটা ৪ vCPU / ৮ GB backend node আসল API মিক্সের সেকেন্ডে প্রায় ৬৫ থেকে ৭০ request সামলাল, অর্থাৎ request প্রতি প্রায় ৫৯ ms CPU। সংখ্যা দুটো মেলে: ৪ vCPU মানে সেকেন্ডে ৪,০০০ ms CPU, আর ৪,০০০-কে ৫৯ দিয়ে ভাগ করলে প্রায় ৬৮। autoscale প্রথম বাড়তি node যোগ করল CPU ৯৯%-এ পৌঁছানোর প্রায় ৭ মিনিট পরে।
অর্থাৎ পরপর তিনটা bottleneck: একটা নিয়ম (limiter), তারপর data স্তর (Valkey connection), আর তারপরই শুধু backend CPU। প্রতিটার সমাধান আলাদা, আর শুধু শেষটাই বাড়তি server দিয়ে মেটে।
Scaling test: নতুন server আসে কি, আর কখন?
Frontend: দুটো node, সেকেন্ডে ৩৫৬ request
frontend-কে pool হিসেবে নতুন করে সাজানোর পর (প্রতি node-এ nginx-এর পেছনে দুটো একই রকম Next.js container), দুটো node সামলাল সেকেন্ডে প্রায় ৩৫৬ request, p95 ০.০৫ সেকেন্ড, ০টা error, CPU ৫৫ থেকে ৬০%-এ; পুরোনো server-এর সঙ্গে মিলিয়ে দেখলাম ১২টা আসল পেজ। এই headroom নিজের দাম নিজেই তুলে দিল: pool dedicated-CPU droplet ছেড়ে চলে এল shared AMD ২ vCPU / ৪ GB droplet-এ। Implemented.
১৬ মিনিটের autoscale test
৫ অক্টোবর, ২১:১৭ থেকে ২১:৩৩ পর্যন্ত k6 চালাল আসল পরীক্ষা-চূড়ার request মিক্স (শুধু GET, কিছুই লেখা হয়নি): সেকেন্ডে ৩০০ থেকে ৪৫০ request, তারপর ছয় মিনিট ধরে প্রায় ৭৫০।
pool-এর গড় ৭৫%-এ পৌঁছালে ২১:২৯-এ backend ২ থেকে ৩ node-এ উঠল। frontend উঠল ২১:৩০-এ, ৭১%-এ। নতুন node নিজে নিজেই load balancer-এ যোগ দিল।
০টা server error, ০টা timeout, সামগ্রিক p95 ১৩২ থেকে ১৫৩ ms। তৃতীয় node-এর অপেক্ষায় backend যখন ৯০%-এর কাছাকাছি, তখন API p95 ছিল ৩২০ থেকে ৭০৫ ms।
প্রায় ৪% request পেল 429, কারণ পুরো লোড এসেছিল একটাই test IP থেকে: per-IP limit নিজের কাজটাই করেছে।
শিক্ষাটা ছিল সময়ে। DigitalOcean-এর CPU metric আসল লোডের চেয়ে ৫ থেকে ৮ মিনিট পিছিয়ে থাকে, তাই scaling শুরু হয় দেরিতে, তারপর ছাড়িয়ে যায়: লোড থেমে যাওয়ার পরেও backend ক্ষণিকের জন্য ৪ node-এ উঠেছিল। প্রথম বাড়তি backend node এল test শুরুর বারো মিনিট পরে, আর পরীক্ষা বারো মিনিট অপেক্ষা করে না। তাই নির্ধারিত পরীক্ষার আগে আমরা pre-scale করি, বড় পরীক্ষার প্রায় ৪৫ মিনিট আগে pool-এর minimum বাড়িয়ে দিই, আর reactive autoscaling রাখি নিরাপত্তা-জালের মতো। এই যুক্তির সাধারণ রূপটা আছে ট্রাফিক স্পাইকে autoscaling লেখায়।
Rollout probe
Tested, then fixed: আমাদের প্রথম backend template rollout-এ একটা ছোট 503 ঝলক দেখা গেল, কারণ DigitalOcean পুরোনো droplet মুছে ফেলছিল, অথচ load balancer তখনো সেগুলোতে request পাঠাচ্ছিল। guarded rollout শুধু image বদলায়, নতুন node-গুলো /lb-health-এ সাড়া না দেওয়া পর্যন্ত অপেক্ষা করে, balancer সেগুলোকে গ্রহণ করার জন্য আরও প্রায় ৪০ সেকেন্ড সময় দেয়, আর আগে drain করে পুরোনো node-গুলোকে। ৭ অক্টোবর দুটো pool-এর পুরো rollout চলল আসল পেজে প্রতি ২ সেকেন্ডের uptime probe-এর নজরে: ২১৯টা probe, ০টা error।
Scaling কোথায় কাজে এল, কোথায় এল না
কাজ শুরুর আগে এই টেবিলটা দেখতে পেলে ভালো হতো। test বা production যে সমস্যাই ধরিয়ে দিয়েছে, প্রতিটার জন্য এখানে একটাই প্রশ্ন: বাড়তি server কি এটা মেটাত?
ধরা-পড়া সমস্যা
বাড়তি server?
যা দিয়ে মিটল
Status
প্রতি node-এ সেকেন্ডে ৬৫ থেকে ৭০ request-এ backend CPU ঠাসা
হ্যাঁ
Autoscale pool (২ থেকে ১০ node) আর pre-scaling
Implemented
পুরো স্কুল 429 পাচ্ছে (২৫ সেপ্টেম্বর)
না
limiter চলে শিক্ষার্থীর account ধরে, IP ধরে শুধু গেস্টের জন্য
Implemented
route throttle তখনো IP ধরে গোনা হচ্ছিল: একটা পরীক্ষায় একটা dashboard endpoint-এর ৭৪% call 429 পেল (৭ অক্টোবর)
না
throttle গোনা হয় শিক্ষার্থী ধরে, route ধরে; এরপর এমন 429 ০টা
Implemented
২,৮৫৪টা Valkey connection timeout (৬ অক্টোবর)
না
Valkey জায়গাতেই ৮ GB x ২ করা হলো
Implemented
merit card (০.৩৪ সেকেন্ডের job) প্রায় ১০ সেকেন্ডের AI job-এর পেছনে ১০ থেকে ১১৫ মিনিট আটকে ছিল (৬ অক্টোবর)
না
AI job-এর জন্য আলাদা queue lane; merit card তৈরি হয় প্রায় ১ মিনিটে
Implemented
backend প্রতিটা শিক্ষার্থীর জন্য শুধু একটা frontend IP দেখে
না
signed header-এ আসল শিক্ষার্থী IP পাঠানো
Planned
ছয়টা সমস্যার মধ্যে ক্ষমতার সমস্যা ছিল একটাই। বাকি পাঁচটা: দুটো নিয়ম, একটা connection-সীমা, একটা queue-সাজানো আর একটা হারানো header। data স্তরে এই ধরনটা নিয়ে সাধারণ আলোচনা আছে লোডের মুখে data স্তর লেখায়।
Soak test: কী চালিয়েছি, কী চালাইনি
load test জানতে চায় একটা system কতটা নিতে পারে। soak test জানতে চায় কতক্ষণ নিতে পারে: ইঞ্জিন এক মিনিট পুরো গতিতে ঘুরলে তিন ঘণ্টা চলার কথা তেমন কিছু জানা যায় না, কারণ ধীরগতির সমস্যাগুলো দেরিতে ধরা দেয় (memory ধীরে ধীরে বাড়া, connection leak, disk ভরে যাওয়া, queue জমতে থাকা)। "আমরা soak test করেছি" কথাটা বলা সহজ, রক্ষা করা কঠিন, তাই সোজা বলি: আমরা এখনো কোনো আনুষ্ঠানিক soak test চালাইনি।
প্রমাণ
কী ঢাকে
Status
ছোট টানা run: ২৫ থেকে ৫০০ request/s চড়াই, সেকেন্ডে প্রায় ৭৫০ request পর্যন্ত ১৬ মিনিটের autoscale test, পুরো rollout জুড়ে ২ সেকেন্ডের uptime probe
কয়েক মিনিটের টানা লোড: চড়াইয়ে ০টা application error, autoscale test-এ ০টা server error
Tested
৭ অক্টোবরের পরীক্ষার সন্ধ্যা: প্রায় দুই ঘণ্টা, পরপর দুটো পরীক্ষা
স্থির CPU, ০টা ব্যর্থ job, একটা scale-in ভুল
Observed in production
পরের বড় load test-এর সঙ্গে কয়েক ঘণ্টার soak, সম্মত একটা maintenance window-এ
memory বৃদ্ধি, connection leak, queue-এর জমে থাকা
Planned
পরীক্ষার সন্ধ্যাটাই আমাদের কাছে এ জিনিসের সবচেয়ে কাছাকাছি, তবে সেটা পর্যবেক্ষণ, নিয়ন্ত্রিত test নয়। একটা সংশ্লিষ্ট যাচাই অবশ্য টিকে আছে: ২৪ ঘণ্টায় backend node-গুলো disk-এ লিখেছে ০টা ফাইল, তাই সেখানে disk soak-এর ঝুঁকি নয়। frontend node-এ nginx log এখনো লোকালি জমে, দিনে প্রায় ৪৪০ MB, আর সে কারণেই frontend-এ Fluent Bit Planned তালিকায়।
আসল পরীক্ষা: ৭ অক্টোবর, production-এর প্রমাণ
আসল শিক্ষার্থীর বিকল্প নেই। পরপর দুটো পরীক্ষা চলল, আর প্রতি মিনিটের digest আমাদের দেখাল: কতজন শুরু ও জমা দিল, প্রতি node-এ request ও 5xx, pool CPU, worker-এর লোড আর ব্যর্থ job।
পরিমাপ
ফল
পরীক্ষা A (২০ মিনিট)
৬৬১ জন শুরু করল, ৬৩৮ জন জমা দিল (৯৬.৫%); শুরুর চূড়া প্রতি মিনিটে ৮৬
পরীক্ষা B (২৫ মিনিট)
৪৩৯ জন শুরু করল, ৪২৫ জন জমা দিল (৯৬.৮%)
Frontend
পরীক্ষা A-তে সর্বোচ্চ ১,৭৪১ জন unique visitor, এক মিনিটে চূড়া ৪০৩; সন্ধ্যা ৬টা থেকে ৮টায় প্রায় ৭,৩২,০০০ request; CPU সর্বোচ্চ ২৪%
Backend
চূড়া সেকেন্ডে প্রায় ৬৯ request (প্রতি মিনিটের log থেকে; provider-এর দুই মিনিটের গড়ে ৬৩); দুই মিনিটের গড়ে CPU সর্বোচ্চ ৪৭%; একটা node এক মিনিট ৮৬%-এ
MySQL
CPU সর্বোচ্চ ২৪.৫% (গড় ১৩.৪%), একসঙ্গে সর্বোচ্চ ৫টা query চলমান, ০টা lock wait
Valkey
memory ১২.৫%
Worker
CPU ৪৬%, ০টা ব্যর্থ job
তিনটা জিনিস চোখে পড়ে।
bottleneck হলো প্রতি node-এর backend CPU, আর database-এর হাতে ছিল প্রায় ৬ গুণ headroom। নিচের model-এ MySQL-এর সীমা প্রায় ৪৫০ request/s, আর সেই রাতে ছিল ৬৯।
test-এর সংখ্যা সন্ধ্যাটাকে আগেই বলে দিয়েছিল। সেকেন্ডে ৬৩ request, প্রতিটায় ৫৯ ms CPU, মানে সেকেন্ডে প্রায় ৩.৭ CPU-সেকেন্ড। দুটো ৪ vCPU node-এ মোট ৮ vCPU, তাই গড় হওয়ার কথা প্রায় ৪৬%। dashboard দেখাল ৪৭%।
গড় সেই node-কে আড়াল করে, যেটা কষ্ট পাচ্ছে। দুটো backend node-এর মধ্যে traffic ভাগ হলো ৬৫/৩৫, কারণ frontend proxy-গুলোর দীর্ঘস্থায়ী keep-alive connection traffic আটকে রাখে। dashboard বলছিল ৪৭%, অথচ একটা node এক মিনিট ছুঁয়েছিল ৮৬%।
Scale-in-এর ভুল
একটা পরীক্ষার মাঝখানে কেউ backend pool-এর minimum কমিয়ে দিলেন। DigitalOcean একটা চালু node কোনো drain ছাড়াই সরিয়ে ফেলল, আর দুই মিনিটে প্রায় ৩৬০টা request ব্যর্থ হলো। আগের test আমাদের শিখিয়েছিল, scale-out ধীর আর দেরিতে হয়। পরীক্ষাটা শেখাল বাকি অর্ধেক: scale-in দ্রুত, আর বিদায় না জানিয়েই চলে যায়। Implemented as a runbook rule: পরীক্ষার আগে minimum বাড়ান, পরে কমান, আর কোনো node drain করার জন্য autoscale scale-in-এর ওপর ভরসা করবেন না।
Capacity model: কী বলে, আর কোথায় থামে
পরীক্ষার পর আসল data থেকে আমি একটা ছোট model বানিয়েছি, যাতে পরের পরীক্ষা ভয়ের বদলে অঙ্ক দিয়ে সাজানো যায়। এর চারটা মাপা input: একজন শিক্ষার্থী পরীক্ষা খুললে প্রায় ১৫টা request, তারপর মিনিটে প্রায় ১.৫টা (সেকেন্ডে ০.০২৫); সাইটের স্বাভাবিক traffic সেকেন্ডে প্রায় ৩০ request; একটা backend node-এর নিরাপদ লোড সেকেন্ডে ৫০ request (CPU ৭৫%); আর অসমভাবে ভাগ হওয়ার জন্য ৩০% margin।
সহজ কথায়: যতগুলো node থাকবে তাকে ৫০ দিয়ে গুণ করুন, margin-এর জন্য ১.৩ দিয়ে ভাগ করুন, আর স্বাভাবিক traffic-এর ৩০ বাদ দিন। যা থাকে, সেটাই শিক্ষার্থীরা ব্যবহার করতে পারে সেই মুহূর্তে, যখন শেষ জন ঢুকছে। প্রতিটি শিক্ষার্থীর খরচ: খোলার সময়ের ১৫টা request, যা ঢোকার সময়সীমা জুড়ে ছড়ানো, আর উপস্থিত থাকার জন্য ০.০২৫।
ধরুন ৪টা node। ৪ x ৫০ / ১.৩ প্রায় ১৫৪, তা থেকে ৩০ বাদ দিলে থাকে প্রায় ১২৪। শিক্ষার্থীরা যদি ১০ মিনিট ধরে ঢোকে, প্রত্যেকের খরচ ১৫ / ৬০০ + ০.০২৫ = সেকেন্ডে ০.০৫ request, তাই ১২৪ / ০.০৫ হয় প্রায় ২,৫০০ জন; টেবিলে গোল করে নিচের দিকে ২,৪০০ লেখা হয়েছে। সবাই যদি ২ মিনিটের মধ্যে ঢোকে, প্রত্যেকের খরচ ১৫ / ১২০ + ০.০২৫ = ০.১৫, ফল প্রায় ৮০০।
Backend node
প্রায় ১০ মিনিটে ঢুকলে
প্রায় ২ মিনিটের মধ্যে সবাই ঢুকলে
নিরাপদ backend লোড (হিসাব করে পাওয়া)
২
প্রায় ৯০০
প্রায় ৩০০
সেকেন্ডে প্রায় ৭৭ request
৪
প্রায় ২,৪০০
প্রায় ৮০০
সেকেন্ডে প্রায় ১৫৪ request
৬
প্রায় ৪,০০০
প্রায় ১,৩০০
সেকেন্ডে প্রায় ২৩১ request
৮ থেকে ১০
৫,৫০০ বা তার বেশি
১,৮০০ থেকে ২,৩০০
সেকেন্ডে প্রায় ৩০৮ থেকে ৩৮৫ request
১০ node পর্যন্ত পুরো পথে সীমা backend node-ই; MySQL-এর রেখা ১০ node-এর সীমারও ওপরে।
সেই সন্ধ্যা প্রথম সারির সঙ্গে মেলে: প্রতি মিনিটে শুরুর চূড়া ৮৬, আর ওই সারি অনুমতি দেয় মিনিটে প্রায় ৯০। model যেখানে থামে:
এটা মাত্র একটা multiple-choice পরীক্ষা থেকে নেওয়া। PDF আপলোডসহ লিখিত পরীক্ষা worker-এর ওপর অনেক বেশি চাপ দেয়, তাই তার আলাদা মাপ দরকার।
৩ node-এর ওপরের সারিগুলো আন্দাজ-বাড়ানো (extrapolated); test-এ আমরা সবচেয়ে বেশি দেখেছি ৩ node, ক্ষণিকের জন্য ৪। Planned: maintenance window-এ পুরো একটা load test।
node গোনা হয় তখনই, যখন সেটা সত্যিই আছে। metric ৫ থেকে ৮ মিনিট পিছিয়ে থাকে, আর CPU ৯৯%-এ পৌঁছানোর প্রায় ৭ মিনিট পরে আসে প্রথম বাড়তি node; তাই টেবিলটা pre-scaling-এর দিকনির্দেশ, reactive scaling-এর প্রতিশ্রুতি নয়।
এটা শুধু backend-এর request-পথ ঢাকে। worker, Valkey আর AI provider-এর rate limit-এর নিজস্ব সীমা আছে।
চূড়ান্ত আর্কিটেকচার, স্তরে স্তরে
পূর্ণ বাক্সগুলো আজ চালু; ড্যাশ-করা পটিটা planned।
প্রতিটা বাক্স আছে কারণ কোনো test বা আসল পরীক্ষা সেটা চেয়েছে; ড্যাশ-করা পটিটা সেই কাজ, যা আমরা এখনো করিনি।
Edge. দুটো load balancer, প্রতি স্তরের জন্য একটা। Rejected: দুই স্তরের জন্য একটাই balancer, কারণ DigitalOcean-এর balancer host বা path দেখে route করতে পারে না।
Frontend pool. ২ থেকে ১০ node, nginx-এর পেছনে দুটো Next.js container (প্রতি vCPU-তে একটা), CPU ৭০%-এ scale করে।
Backend pool. একটাই golden snapshot থেকে ২ থেকে ১০ node, শুধু nginx আর php-fpm, CPU target ৫৫% (শুরু হয়েছিল ৭০%-এ)। web node-এ কোনো job নেই, তাই বাড়তি node কখনো একই job দুবার চালায় না।
Data. MySQL Standard 8.4 (৪ vCPU / ১৬ GB, primary আর standby, read যায় standby-তে, sticky = true); দুই node-এর ৮ GB Valkey; আর আলাদা করে রাখা PostgreSQL with pgvector (২ vCPU / ৪ GB), যাতে vector query কখনো পরীক্ষার write-এর সঙ্গে প্রতিযোগিতায় না নামে।
Worker. একটা স্থির ৪ vCPU / ৮ GB droplet: queue lane (আলাদা AI lane-সহ), scheduler, WebSockets আর সব SMS, কারণ SMS gateway শুধু একটা IP whitelist করে। এটা একটা জানা single point of failure।
ফাইল, log, নজরদারি। CDN-এর পেছনে Spaces; backend log Fluent Bit হয়ে OpenSearch-এ; আমাদের ops console প্রতিটা স্তর read-only ভঙ্গিতে নমুনা নেয় আর guarded deploy চালায়।
আগে আর এখন
ক্ষেত্র
আগে (৫ অক্টোবর পর্যন্ত)
এখন
Frontend
একটা ৮ vCPU / ১৬ GB droplet, একটা Next.js container, load balancer নেই
২ থেকে ১০ node-এর load-balanced pool, প্রতি node-এ দুটো container
Backend
দুটো স্থির droplet, ID ধরে target করা, তাই নতুন server কখনো যোগ দিতে পারত না
balancer target করে একটা tag; একটা snapshot থেকে ২ থেকে ১০ node-এর pool, পরীক্ষার আগে pre-scaled
MySQL
একটা Advanced node, ৮ vCPU / ৩২ GB, standby নেই
Standard ৪ vCPU / ১৬ GB, primary আর standby
"Standby" server
দুটো বন্ধ-করা droplet, মাসে ১১২ ডলার, চালু করতে মিনিট লাগে
সরানো হয়েছে; আসল standby আছে MySQL আর Valkey-র ভেতরে (Valkey এখন ৮ GB, আগে ছিল ৪)
টাকাটা কী কিনে দেয়
আইটেম (মাসিক, DigitalOcean list price)
খরচ
আগে: পুরো web স্তর (১৬ GB frontend, ২টা backend, worker, ১টা load balancer, ২টা বন্ধ standby)
এখন, data আর log: MySQL জোড়া, Valkey ৮ GB x ২, PostgreSQL, OpenSearch
প্রায় ৩৮৯ + ২৪০ + ৬০ + ২০ ডলার
এখন: snapshot, Spaces
প্রায় ৫ + ৫ ডলার, সঙ্গে ব্যবহারভিত্তিক খরচ
এখন: মোট production, backend pool ২ node-এ
প্রায় ৯৯০ ডলার
মোট দুটো সমান সমান নয়: ৪১৬ ডলার ছিল শুধু web স্তরের, আর ৯৯০ ডলার পুরো production stack-এর। একই list price ধরলে শুধু web স্তরের খরচ এখন প্রায় ২৭২ ডলার। বাকি ৭০৯ ডলার managed MySQL, Valkey, PostgreSQL আর OpenSearch-এর, আর বাড়তি টাকাটা ঠিক এগুলোই কিনে দেয়: node বিকল হলেও টিকে থাকা database, জায়গা ও standby-সহ Valkey, পরীক্ষার write-এর পথ থেকে সরে থাকা vector search, আর মুছে যাওয়া node-কে ছাড়িয়ে টিকে থাকা log। pre-scaling সস্তা অংশ: একটা বাড়তি backend node-এর খরচ ঘণ্টায় প্রায় ০.০৮ ডলার। সিদ্ধান্তগুলোর যুক্তি নিচের checklist-এ আছে।
Planned, এখনো করা হয়নি
Planned: পরের বড় পরীক্ষার আগে maintenance window-এ পুরো একটা load test আর কয়েক ঘণ্টার আনুষ্ঠানিক soak।
Planned: CDN থেকে Next.js-এর static asset পরিবেশন, frontend node-এ Fluent Bit, আর frontend থেকে backend-এ signed real-IP header, যাতে প্রতিটা per-IP নিয়ম ও log নির্ভুল হয়।
Planned: worker-এর high availability: SMS gateway-তে whitelist করা একটা reserved IP, একটা ছোট standby worker, আর বড় পরীক্ষার আগে চালু করা শুধু-পরীক্ষার burst worker।
Considered: সেকেন্ড-স্তরের scaling-এর জন্য Kubernetes (DOKS), যেটা চালানোর ঝামেলা অনেক বেশি। ৩ থেকে ৪ সেকেন্ডে তৈরি warm standby-ও ভাবা হয়েছিল, কিন্তু DigitalOcean pool-এ warm pool নেই, তাই আমরা pre-scale করি আর balancer-এর ভেতরে বাড়তি ক্ষমতা চালু রাখি।
Solutions Architect-এর checklist
architecture review-তে আমি এখন এই checklist ব্যবহার করি। প্রতিটি আইটেম এই প্রকল্পে আমরা করেছি বা এর জন্য দাম দিয়েছি।
Scalability
web node stateless: session, cache আর queue Valkey-তে, upload Spaces-এ, ২৪ ঘণ্টায় disk-এ লেখা ফাইল ০টা।
প্রতিটি স্তর scale করে নিজের একক ধরে: frontend node-এ দুটো Next.js container, backend node-এ php-fpm worker।
জানা লোডের জন্য pre-scale; reactive scaling শুধু নিরাপত্তা-জাল।
Availability
কোনো একটা node সাইট ফেলে দিতে পারে না: দুটো balancer, pool-এর minimum ২, MySQL ও Valkey-র standby।
health check-এর উত্তর দেয় একা nginx, আর node সরানোর আগে drain করা হয় (/lb-health-এ 503)।
পুরোনো system থাকে যতক্ষণ না traffic সত্যিই সরে যায়: DNS বদলের এক ঘণ্টা পরেও সেটা প্রায় ৩৬% request পাচ্ছিল।
Performance
জানুন request প্রতি CPU (৫৯ ms), শুধু request/s নয়।
request প্রতি setup-খরচের দিকে নজর রাখুন: নতুন TLS connection-এ CPU লাগছিল Valkey-তে প্রায় ৫.৪ ms, MySQL-এ ৩.৩ ms।
read ভাগ হয় standby আর primary-র মধ্যে, write যায় primary-তে, আর শিক্ষার্থী তবু নিজের উত্তর সঙ্গে সঙ্গে দেখতে পায়।
Reliability
ধীর job-এর জন্য আলাদা queue lane, যাতে ০.৩৪ সেকেন্ডের job কখনো ১০ সেকেন্ডের job-এর পেছনে না দাঁড়ায়।
বাইরের provider-এর কাছে call ছন্দ মেপে পাঠানো ও আবার চেষ্টা করা হয় (সবাই মিলে মিনিটে ৮টা, ১ থেকে ১৫ মিনিটের backoff, ১২ ঘণ্টা পর্যন্ত), তাই শিক্ষার্থী কখনো error দেখে না।
যেখানে বহু শিক্ষার্থী একটা ঠিকানা ভাগ করে, সেখানে rate limit গোনা হয় শিক্ষার্থী ধরে, IP ধরে নয়।
Observability
পরীক্ষার সময় প্রতি মিনিটের digest: কতজন শুরু ও জমা দিল, প্রতি node-এ request ও 5xx, pool CPU, worker-এর লোড, ব্যর্থ job।
শুধু pool-এর গড় নয়, প্রতিটা node আলাদা দেখুন (৪৭% বনাম ৮৬%)।
log যেন node-এর চেয়ে বেশি টেকে: backend-এর জন্য Fluent Bit থেকে OpenSearch; frontend Planned।
Failure recovery
managed database backup ও failover, golden snapshot, আর rollback-এর জন্য রাখা শেষ ৩টা ভালো image।
প্রতিটা cutover-এ ফেরার পথ রাখা: পুরোনো database ৪৮ ঘণ্টা জমানো, পুরোনো frontend DNS খালি না হওয়া পর্যন্ত রাখা।
single point of failure লিখে রাখা। worker তেমন একটা: সে বিকল হলে SMS, scheduler আর পরীক্ষার প্রক্রিয়াকরণ থেমে যায়, জমা-দেওয়া উত্তর নিরাপদে Valkey-তে অপেক্ষা করে।
Cost
আকার ঠিক করার আগে headroom মাপুন: test-এর পর frontend গেল shared CPU-তে, আর একটা বড় Advanced node-এর জায়গায় এল standby-সহ Standard MySQL (সস্তা এবং highly available)।
যে খরচ কিছু কেনে না তা সরান: দুটো বন্ধ standby মাসে ১১২ ডলার খরচ করত, availability দিত না।
over-provision না করে pre-scale করুন, আর খরচ করুন সেখানে, যেখানে সেটা একটা আসল ব্যর্থতা দূর করে, যেমন বড় Valkey।
Maintainability
pool-এর node কখনো হাতে বদলাবেন না: একটা বদলান, test করুন, snapshot নিন, pool template-কে সেই snapshot-এর দিকে দেখান।
pool update-এ প্রতিটা setting পাঠান, যাতে কিছু চুপচাপ reset না হয়।
নতুন রক্ষাকবচের সঙ্গে আসে এমন test, যা fix ছাড়া ব্যর্থ হয়, যেমন শিক্ষার্থী-ধরা throttle-এর বেলায়।
Future growth
connection-এর অঙ্ক লিখে রাখুন: ১০ node x ৮০ php-fpm worker = ৮০০, যা MySQL-এর ১,৬০১-এর নিচে।
পরের সীমা জানুন: MySQL প্রায় ৪৫০ request/s-এ, সেই রাতের চূড়ার প্রায় ৬ গুণ।
লিখিত পরীক্ষা আলাদা করে মাপুন, আর account-এর সীমা আগেই দেখে নিন: rollout-এর সময় দুটো pool তাদের সর্বোচ্চে পৌঁছাতে পারার আগে droplet limit ২৫ বাড়াতে হয়েছিল।
আমরা যা শিখলাম, আর এখন কোথায় দাঁড়িয়ে
প্রথম bottleneck প্রায়ই সেটা নয়, যেটাকে আপনি ভয় পেয়েছিলেন। আমরা ভাবছিলাম server নিয়ে; প্রথম তিনটা সমস্যা ছিল একটা rate-limit নিয়ম, একটা connection-সীমা আর একটা queue-সাজানো।
লক্ষণ নয়, কারণ সারান। ছয়টা সমস্যার মধ্যে ক্ষমতার সমস্যা ছিল একটাই।
production-কে model-এর সঙ্গে মেলান। সেকেন্ডে ৬৩ request আর ৫৯ ms থেকে হিসাব দাঁড়িয়েছিল প্রায় ৪৬% CPU, দেখলাম ৪৭%।
গড় সেই node-কে আড়াল করে যেটা কষ্ট পাচ্ছে, আর scale-in আসে হঠাৎ। গড়ে ৪৭%, একটা node-এ ৮৬%; পরীক্ষার আগে minimum বাড়ান, পরে কমান।
যা test করেননি, তা সোজা বলুন। আমাদের আছে load test আর একটা আসল সন্ধ্যা; আনুষ্ঠানিক soak Planned।
এই সিরিজ শুরুর সময় platform ছিল একটা frontend droplet, ID দিয়ে বাঁধা দুটো backend droplet আর একটা বিশাল database node। ৭ অক্টোবরের পরীক্ষার সন্ধ্যা চলল একেবারে আলাদা একটা system-এ, আর সেই তফাত গড়ে ওঠেনি শুধু server বাড়ানো দিয়ে। গড়ে উঠেছে সেই একই চক্র দিয়ে, বারবার, এমনকি সেই সময়গুলোতেও, যখন মাপজোখ আমাদের জানিয়ে দিয়েছে যে আমরা ভুল ছিলাম।
মাপুন। bottleneck খুঁজুন। বুঝুন। একটা জিনিস বদলান। test করুন। আবার মাপুন।
ওই চক্রটাই আর্কিটেকচার; pool, standby আর diagram-গুলো তার রেখে-যাওয়া চিহ্ন। এই পাঁচ পর্ব থেকে একটা জিনিস নিয়ে গেলে নিন সেই চক্রটা, আর আপনার পরের পরীক্ষার রাত, sale বা launch-এর আগে সেটা কাজে লাগান।