ট্রাফিক স্পাইকে autoscaling: আসলে কী scale করে, আর কী করা উচিত নয়
পূর্বঘোষিত স্পাইক চূড়ায় ওঠে সেকেন্ডে, আর নতুন সার্ভার তৈরি হতে লাগে মিনিট। জানুন হঠাৎ চাপে কী scale করে, reactive নিয়মের চেয়ে pre-scaling কেন ভালো, SSR ও PHP-FPM স্তর কীভাবে সাজাবেন আর load test কীভাবে করবেন।
Author
Anichur Rahaman
3 সপ্তাহ আগে11 min read2 views
সেকেন্ডে ২০ request, একদম স্থির। ফ্ল্যাশ সেল খোলার আগের রাত ১১টা ৫৯ মিনিটে একটা অনলাইন স্টোরের traffic এটুকুই, আর platform লিড dashboard-এর দিকে তাকিয়ে আছেন। autoscaling নিয়ম সাজানো আছে: পাঁচ মিনিট ধরে CPU ৭০%-এর ওপরে থাকলে একটা server যোগ হবে। (এটা একটা কাল্পনিক দৃশ্য, কোনো আসল দোকান নয়।)
মাঝরাতে সেল খুলল, আর প্রায় হাজারখানেক ক্রেতা একই মিনিটে হুড়মুড় করে ঢুকল। লোড লাফিয়ে উঠল সেকেন্ডে ৪৪০ request-এ। রাত ১২টা ০১-এ dashboard তখনো সবুজ, কারণ পাঁচ মিনিটের গড় প্রায় নড়েইনি। ১২টা ০৩-এ checkout timeout করছে। নতুন server যোগ হলো ১২টা ০৬-এ, ততক্ষণে ক্রেতারা প্রতিযোগীর দোকানে চলে গেছেন।
autoscaling সাড়া দেয় লোডের পরে, আর পূর্বঘোষিত স্পাইক আসে সেই সাড়ার আগেই। এই লেখায় দেখব হঠাৎ চাপে আসলে কী scale করে, কাকে scale করতে বলা উচিত নয়, আর সেটা event-এর মাঝখানে নয়, আগেই কীভাবে যাচাই করবেন।
নিচের সংখ্যাগুলো আমার নিজের test থেকে নেওয়া, একটাই Laravel ও Next.js সেটআপে। এগুলো পদ্ধতি দেখানোর জন্য, সবার জন্য খাটে এমন বেঞ্চমার্ক নয়। নিজের system-এ নিজে চালিয়ে দেখুন।
এটি "হাই-ভলিউম system-এর ইঞ্জিনিয়ারিং" সিরিজের পাঁচ পর্বের প্রথম পর্ব। এখানে এজ থেকে অ্যাপ্লিকেশন পর্যন্ত request-এর পথ নিয়ে কথা হচ্ছে। queue, data স্তর, observability আর নিরাপদ deploy আসবে ২ থেকে ৫ নম্বর পর্বে।
স্পাইক মানে ব্যস্ত দিন নয়
দুই ধরনের traffic-এর জন্য দুই রকম সমাধান লাগে। ধীরে ধীরে বাড়া লোড কোনো কন্ট্রোল লুপকে টের পেয়ে সাড়া দেওয়ার সময় দেয়। হঠাৎ সিঁড়ি-লাফ সেই সময় দেয় না। এক মিনিটের মধ্যে লোড যদি সেকেন্ডে ২০ request থেকে ৪৪০-এ ওঠে, পাঁচ মিনিটের গড় CPU দেখে চলা কোনো নিয়ম তখনো কিছুই টের পায়নি।
সুখবর হলো, পূর্বঘোষিত স্পাইক আগে থেকেই আন্দাজ করা যায়। শুরুর সময় আপনি জানেন, কত মানুষ আসবে মোটামুটি জানেন, আর আগের event-এর request মিক্স প্রায়ই আবার চালিয়ে দেখা যায়। এই আগাম জানাটাই আপনার সবচেয়ে বড় হাতিয়ার, আর এই লেখার বেশিরভাগ অংশ সেটা কাজে লাগানোর কথা।
request-এর পথ: এজ থেকে app পর্যন্ত
কী scale করবেন ঠিক করার আগে একটা request-এর পুরো পথ এঁকে ফেলুন। আমার চালানো সেটআপগুলোতে পথটা এমন: একদম সামনে CDN ও WAF, তারপর একটা managed load balancer, দুটো backend ওয়েব নোড, এক বা একাধিক server-side rendering (SSR) frontend নোড, ব্যাকগ্রাউন্ড জবের জন্য একটা worker নোড, আর সবার পেছনে data স্তর।
এজ থেকে data পর্যন্ত request-এর পথ। প্রতিটি স্তর আলাদা উপায়ে, আলাদা গতিতে scale করে।
প্রতিটি বাক্সের নিজের ব্যর্থতার ধরন আছে, নিজের scale করার উপায়ও আছে। সবকিছুকে এক "server" ভাবলে যা হয়: যে স্তরটা কখনো সমস্যাই ছিল না, সেখানেই RAM বাড়ানো হয়।
এজ আর load balancer
প্রথমে সামনে একটা CDN বসান। স্ট্যাটিক ফাইল, ছবি, আর সব গেস্টের কাছে একই রকম দেখায় এমন পেজ স্পাইকের সময় যেন আপনার server পর্যন্ত না-ই পৌঁছায়। একই স্তরের WAF আর বট-রুল স্ক্র্যাপারদেরও আটকে রাখে, যাতে ক্রেতাদের জন্য রাখা ক্ষমতা তারা খেয়ে না ফেলে।
CDN-এর পেছনে health check আর connection draining সহ একটা managed load balancer ব্যবহার করুন। Health check অসুস্থ নোডকে নিজে থেকেই সরিয়ে দেয়। Draining নোডকে বিদায় নেওয়ার আগে চলমান request-গুলো শেষ করতে দেয় — এর ফলেই deploy আর scale-in ব্যবহারকারীর চোখে অদৃশ্য থাকে।
দুটো খুঁটিনাটি যাচাই করা দরকার। প্রথমত, ব্যালান্সিং অ্যালগরিদম। অনেক cloud load balancer ডিফল্টে round robin চালায়, যা ধরে নেয় প্রতিটি request-এর খরচ সমান। checkout আর সার্চ কিন্তু তা নয়। "Least outstanding requests" বা "least connections" মোড পরের request পাঠায় সেই নোডে, যার হাতে কাজ সবচেয়ে কম। যেমন AWS-এ Application Load Balancer-এর ডিফল্ট round robin, আর least outstanding requests একটা target group-এর সেটিং। দ্বিতীয়ত, load balancer নিজেও এমন একটা সার্ভিস, যাকে scale করতে হয়। AWS-এর ডকুমেন্টেশন বলছে, একটা Application Load Balancer প্রায় পাঁচ মিনিটে নিজের ক্ষমতা দ্বিগুণ করতে পারে; আর যে event-এ পাঁচ মিনিটের কম সময়ে traffic দ্বিগুণের বেশি হবে, তার জন্য আগাম ক্ষমতা সংরক্ষণের (reserved capacity) ব্যবস্থা আছে (capacity reservation গাইডটি দেখুন)। অন্য প্রোভাইডারদেরও একই ধরনের সীমা থাকে, তাই নিজেরটা জিজ্ঞেস করে নিন।
আমার load test-এ load balancer কখনোই বাধা হয়নি: সর্বোচ্চ রেটেও CPU ৮%-এর নিচে ছিল, এরর শূন্য। এটাই সাধারণ চিত্র। আগে পথের আরও ভেতরে খুঁজুন।
Stateless ওয়েব নোড: এটাই প্রবেশের টিকিট
দুটো ওয়েব নোড load balancer-এর পেছনে বসানো যায় তখনই, যখন যেকোনো নোড যেকোনো request সামলাতে পারে। এই বৈশিষ্ট্যের নাম stateless। শুরু থেকে ঠিক করা সস্তা, পরে জোড়াতালি দিয়ে ঠিক করা যন্ত্রণার। দ্বিতীয় নোড বসানোর আগে এই চেকলিস্ট মিলিয়ে নিন।
সেশন Redis বা Valkey-তে, নোডের ডিস্কের ফাইলে নয়।
অ্যাপ্লিকেশন cache Redis বা Valkey-তে, যাতে সব নোড একই cache ব্যবহার করে।
আপলোড আর তৈরি-হওয়া ফাইল অবজেক্ট স্টোরেজে (S3-সামঞ্জস্যপূর্ণ), লোকাল ডিস্কে কখনোই নয়।
একটাই scheduler। Cron ধাঁচের জব ঠিক একটা নোডে চলবে, নইলে প্রতিটি টাস্ক দুবার চলবে।
একই বিল্ড। প্রতিটি নোড একই ইমেজ চালাবে, কনফিগারেশন আসবে এনভায়রনমেন্ট থেকে।
প্রতিটি স্তরে আপলোড সীমা মিলিয়ে রাখা। প্রতিটি প্রক্সির নিজস্ব বডি লিমিট আছে (nginx-এর client_max_body_size, PHP-র post_max_size ও upload_max_filesize)। একটা সেটআপে হোস্ট প্রক্সির ১ MB ডিফল্ট চুপচাপ আপলোড ফিরিয়ে দিচ্ছিল, যতক্ষণ না সব স্তর মেলানো হলো।
StoreConsole-এর মতো self-hosted platform এই কারণেই সেশন আর cache Redis-এ রাখে, যাতে নোডগুলো একে অন্যের বদলি হতে পারে।
SSR স্তর: একটা Node প্রসেস মানে একটা কোর
এখানেই আমি সবচেয়ে বেশিবার চমকে গেছি। Next.js server পেজ রেন্ডার করে একটা Node.js প্রসেসে, আর Node আপনার JavaScript চালায় একটাই থ্রেডে। মেশিন যত বড়ই হোক, একটা প্রসেস রেন্ডারিংয়ে মোটামুটি একটা কোরই ব্যবহার করতে পারে।
আগের একটা পিকের আসল request মিক্স k6 দিয়ে একটা মাত্র frontend প্রসেসের ওপর চালিয়ে দেখেছিলাম। সেটা থেমে গেল সেকেন্ডে ২২০ থেকে ২৫০ request-এ। তার পর থেকে p95 latency প্রায় ২৬০ ms থেকে লাফিয়ে প্রায় ৩ সেকেন্ডে উঠল, আর p99 পৌঁছাল ৬.৪ সেকেন্ডে। পুরো সময় হোস্টে দুইয়ের বেশি কোর বসে ছিল। আসল event-এর পিক মিনিটে দরকার ছিল প্রায় সেকেন্ডে ৪৪০ request — অর্থাৎ একটা প্রসেস ঠিক সেই মুহূর্তেই ফেল করত, যখন তাকে সবচেয়ে বেশি দরকার।
সমাধান সোজা: একই ইমেজ থেকে কয়েকটা একই রকম frontend কন্টেইনার চালানো (আমার ক্ষেত্রে তিনটা), সামনে nginx-এ least_conn, যা প্রতিটি request পাঠায় সবচেয়ে কম সক্রিয় কানেকশনওয়ালা server-এ। তারপর টার্গেট রেটে আবার test। Next.js-এর বেলায় আরও একটা কথা: ডিফল্টে প্রতিটি ইনস্ট্যান্স নিজের লোকাল cache রাখে, তাই একাধিক ইনস্ট্যান্স চালালে ফ্রেমওয়ার্কের ডকুমেন্টেশনে বলা পথে একটা shared cache store ঠিক করে দিন।
হিসাবটা
এখানকার ক্যাপাসিটি প্ল্যানিং সরল গুণ। প্রতি প্রসেসের মাপা সীমাকে প্রসেসের সংখ্যা দিয়ে গুণ করুন, তারপর প্রত্যাশিত পিকের সঙ্গে হেডরুমসহ মিলিয়ে দেখুন।
বিষয়
মান (একটা সেটআপ)
মন্তব্য
পিক মিনিটে দরকার
~440 req/s
আগের আসল event আবার চালিয়ে পাওয়া
একটা SSR প্রসেস, মাপা
220-250 req/s
এর পরে p95 ~260 ms থেকে ~3 s হয়ে গেছে
তিনটা SSR প্রসেস
~660-750 req/s
পিকের প্রায় দেড় গুণ, হেডরুম হিসেবে
Load balancer CPU
৮% বা কম
বাধা ছিল না
PHP-FPM: পুল সাইজ ভেবেচিন্তে ঠিক করুন
backend ওয়েব স্তরের সমস্যা উল্টো ধরনের। PHP-FPM একটা worker প্রসেসের পুল চালায়, আর প্রতিটি একসঙ্গে একটাই request সামলায়। পুল ছোট হলে request লাইনে দাঁড়িয়ে থাকে। বেশি বড় হলে নোডের মেমোরি ফুরিয়ে যায়, সোয়াপিং শুরু হয় বা প্রসেস কিল হয়।
কাজ চলার মতো একটা মোটামুটি নিয়ম: যত busy worker লাগবে ≈ সেকেন্ডে request × প্রতি request-এর গড় সময়। একটা কাল্পনিক উদাহরণ: সেকেন্ডে ২০০ request, প্রতিটি ১৫০ ms হলে প্রায় ৩০টা busy worker লাগে, তাই ৬০-র পুল ধীর request-এর জন্য জায়গা রাখে। তারপর মেমোরি মেলান: একটা worker প্রায় ১০০ MB নিলে ৬০টায় লাগে প্রায় ৬ GB, কাজেই নোডে এর চেয়ে বেশি দরকার।
একটা সেটআপে আমি এই মানগুলো ব্যবহার করেছিলাম: pm.max_children=60, start_servers=30, min_spare_servers=20, max_spare_servers=30 আর max_requests=1000, সঙ্গে nginx থেকে FPM-এ keepalive (keepalive 16 ও fastcgi_keep_conn on)। ৩০টা দিয়ে শুরু করা জরুরি: ঢেউ আসার সময় চাপের মধ্যে fork করতে হয় না, পুল আগে থেকেই গরম থাকে।
আরও দুটো অভ্যাসে লাভ হয়। প্রোডাকশনে validate_timestamps=0 দিয়ে OPcache চালু রাখুন, আর deploy-এর সময় worker রিস্টার্ট করুন। আর fastcgi_next_upstream error timeout http_500 http_503 সঙ্গে fastcgi_next_upstream_tries 2 ভেবে দেখুন; এতে FPM-এর ক্ষণস্থায়ী ঝামেলা চুপচাপ রিট্রাইয়ে সামলে গিয়েছিল। এটা শুধু idempotent request-এর জন্য নিরাপদ। payment-এর POST রিট্রাই করা সমাধান নয়, বাগ।
Reactive autoscaling কেন দেরিতে পৌঁছায়
এবার আসল কথা। Reactive autoscaling একটা metric দেখে, থ্রেশহোল্ডের জন্য অপেক্ষা করে, মেশিন চালু করে, তারপর সেটা বুট হওয়া, load balancer-এ যোগ দেওয়া আর cache গরম হওয়ার জন্য অপেক্ষা করে। বাস্তবে এতে সবচেয়ে ভালো ক্ষেত্রেও এক থেকে তিন মিনিট লাগে। ডিফল্ট সেটিং দেরি আরও বাড়াতে পারে: যেমন AWS EC2 Auto Scaling-এ ডিফল্ট cooldown ৩০০ সেকেন্ড, আর basic monitoring ইনস্ট্যান্সের metric দেয় পাঁচ মিনিটে একবার, যতক্ষণ না আপনি টাকা দিয়ে এক মিনিটের detailed monitoring নেন।
পূর্বঘোষিত স্পাইক চূড়ায় ওঠে সেকেন্ডে। কাজেই নতুন মেশিন পৌঁছায় তখন, যখন মানুষ চলে গেছে — বা আরও খারাপ, এরর দেখে ফেলার পর। একটা ভার্চুয়াল মেশিনের RAM-ও রিবুট ছাড়া বাড়ানো যায় না, তাই "যেখানে আছে সেখানেই বড় করা"-ও কাজ করে না।
Reactive scaling লোডের পিছু নেয়। Pre-scaling লোড আসার আগেই তৈরি থাকে। উদাহরণমূলক টাইমলাইন।
জানা event-এর জন্য pre-scale করুন
পূর্বঘোষিত event-এ শুরুর আগে নোডের ন্যূনতম সংখ্যা বাড়িয়ে নিন, শেষে আবার কমান। শুনতে যত খরচ মনে হয়, আসলে তত নয়: কয়েক ঘণ্টার জন্য একটা বাড়তি নোডের খরচ একটা ব্যর্থ সেলের ক্ষতির তুলনায় সামান্য। কোনো নিয়মের চেয়েও এটা বেশি ভরসাযোগ্য, কারণ সময়মতো কোনো metric সীমা পেরোবে কি না তার ওপর নির্ভর করে না।
অপ্রত্যাশিত ঘটনার জন্য reactive নিয়ম রাখুন নিরাপত্তা-জাল হিসেবে, মূল পরিকল্পনা হিসেবে নয়। আর যা দ্রুত scale করা যায়, সেটাই scale করুন। কন্টেইনারের ভেতরের প্রসেস চালু হয় সেকেন্ডে, মেশিন লাগে মিনিট — তাই আমি queue worker scale করি প্রসেস দিয়ে, মেশিন দিয়ে নয় (এই সিরিজের ২ নম্বর পর্বে সেটা আছে)।
পদ্ধতি
সাড়া দিতে সময়
কোথায় সেরা
Pre-scale (ন্যূনতম সংখ্যা বাড়ানো)
event-এর আগেই তৈরি
পূর্বঘোষিত স্পাইক
Reactive VM autoscaling
১-৩ মিনিট বা বেশি
ধীরে বাড়া লোড, অপরিকল্পিত traffic
প্রসেস-স্তরের scaling
কয়েক সেকেন্ড
কন্টেইনারের ভেতরের queue worker
কোন উপায় কখন: তিনটা প্রশ্নই ঠিক করে দেয় pre-scaling, reactive scaling, প্রসেস-স্তরের scaling নাকি এজের সুরক্ষা।
আসল event-এর মতো করে load test করুন
ওপরের কোনো সংখ্যাই আন্দাজে আসেনি। এসেছে event-এর মতো দেখতে test থেকে। শুধু হোমপেজে হাতুড়ি পেটালে প্রায় কিছুই প্রমাণ হয় না। এই ক্রমটা মেনে চলুন।
আসল request মিক্স তুলে নিন আগের event-এর access log থেকে: কোন URL, কী অনুপাতে, ব্যবহারকারী কতক্ষণ থেমে থেমে করে।
k6 (বা অনুরূপ টুল) দিয়ে সেটা আবার চালান প্রত্যাশিত রেটে, তারপর তার দেড় গুণে।
স্ক্রিপ্টে abort threshold বসান, যেমন p95 ৩ সেকেন্ডের বেশি, বা যেকোনো 5xx বা timeout, যাতে ব্যর্থ test নিজেই থামে, শেয়ার্ড system-এর ক্ষতি না করে।
একটা guard script যোগ করুন, যা নোডগুলোর CPU দেখে, সীমা ছুঁলে রান থামিয়ে দেয়।
নিজের metric cloud প্রোভাইডারের metric-এর সঙ্গে মেলান। আমার test-এ দুটো কয়েক শতাংশের মধ্যে মিলেছে, মানে dashboard ভরসাযোগ্য ছিল।
একবারে একটাই জিনিস বদলান, আর প্রতিটি ফিক্সের পর টার্গেট রেটে আবার চালান।
test সীমা কোথায় দেখিয়ে না দেওয়া পর্যন্ত হার্ডওয়্যার আপগ্রেড করবেন না। আমার ক্ষেত্রে মেশিনগুলো ঠিকই ছিল। সীমা ছিল সিঙ্গেল-থ্রেডেড প্রসেস, আর সমাধানে কনফিগারেশন ছাড়া কোনো খরচ হয়নি।
একই মাঝরাত, এবার প্রস্তুত
প্রথম দৃশ্যটা আবার চালান, এবার প্রস্তুতি সেরে। রাত ১১টায় ওয়েব নোডের ন্যূনতম সংখ্যা দুই থেকে তিন হলো, আর SSR স্তরে তিনটা প্রসেস চলছে least_conn-এর পেছনে। cache গরম, FPM পুলে আগে থেকেই ৩০টা worker।
মাঝরাতে সেকেন্ডে ৪৪০ request এল। মাপা সীমা প্রায় ৬৬০, তাই p95 স্বাভাবিক মানের কাছেই থাকল, আর autoscaling নিয়ম একবারও চলল না, কারণ তার দরকারই পড়ল না। রাত ১২টা ৩০-এ ন্যূনতম সংখ্যা আবার নামল। পুরো event-এর খরচ ছিল কয়েক ঘণ্টার একটা বাড়তি নোড।
একটা Node.js SSR প্রসেস প্রায় একটা কোর ব্যবহার করে। একই রকম কয়েকটা প্রসেস least_conn-এর পেছনে চালান, আর টার্গেট রেটে test করুন।
PHP-FPM পুলের সাইজ ঠিক করুন সেকেন্ডে request, request-এর সময় আর প্রতি worker-এর মেমোরি থেকে, আর পুল আগে থেকে গরম রাখুন।
আসল request মিক্স, abort threshold আর guard script দিয়ে load test করুন; হার্ডওয়্যার কেনার আগে মাপা বাধাটা ঠিক করুন।
আনিছুর রহমান একজন সফটওয়্যার আর্কিটেক্ট এবং StoreConsole-এর নির্মাতা। বাড়তে থাকা ব্যবসার জন্য তিনি কমার্স ও ERP সিস্টেম ডিজাইন করেন — বিশেষ মনোযোগ event-driven আর্কিটেকচার, ডেটার নির্ভুলতা আর নিজের সার্ভারে চালানো সিস্টেমের ওপর।