পর্ব ৩: ২৬ মিনিটে ছোট, সস্তা, highly available database, আর AI কেন পেল নিজস্ব PostgreSQL
একটা বড় MySQL node আমরা ২৬ মিনিটের window-তে সরিয়ে আনি standby-সহ সস্তা Standard cluster-এ, read ভাগ করি standby-তে, আর AI-কে দিই pgvector-সহ নিজস্ব PostgreSQL। আসল পরীক্ষার সন্ধ্যা দেখাল, database কখনোই bottleneck ছিল না।
৫ অক্টোবর রাত ২২:৩৩-এ আমরা দুটো load balancer-ই একটা ফাঁকা tag-এর দিকে ঘুরিয়ে দিলাম। যে-ই সাইটে ঢুকল, সে maintenance page দেখল। পর্দার আড়ালে প্রতিটা student, উত্তর আর ফলাফল তখন একটা নতুন database-এ কপি হচ্ছিল, আর business-এর সঙ্গে আমাদের ঠিক করা সময়সীমা ছিল ২৬ মিনিট।
database ধীর, এমন অভিযোগ কেউ করেনি। সমস্যা ছিল তার গড়নে। সেটা ছিল একটাই বড় MySQL node, কোনো standby ছাড়া; ফলে যেদিন ওই মেশিন বিকল হতো, সেদিনই পুরো platform বসে যেত। তাছাড়া কাজের তুলনায় সেটা ছিল বেশি বড়, আর বেশি দামি।
এই লেখা NovaCommerce-এর data layer নিয়ে। NovaCommerce এমন একটা platform, যেটা কোর্স বিক্রি করে আর সময়-বাঁধা online পরীক্ষা নেয়। এতে আছে ২৬ মিনিটের সেই স্থানান্তর, যে client-টাকে আমরা ভুলে গিয়েছিলাম, read আর write কীভাবে আলাদা করলাম, AI কেন নিজস্ব PostgreSQL পেল, আর ৭ অক্টোবরের আসল পরীক্ষার সন্ধ্যা এসব নিয়ে কী বলল। এটি দ্বিতীয় পর্বের পরের ধাপ, যেখানে application layer scale করতে শিখেছিল।
এটি "From One Server to Exam-Day Ready" নামের পাঁচ পর্বের case study-র তৃতীয় পর্ব। NovaCommerce একটি কাল্পনিক নাম; architecture, সংখ্যা আর ভুলগুলো সবই বাস্তব।
ভুল ধরনের বড়
পুরোনো database ছিল একটাই Managed MySQL "Advanced" node, ৮ vCPU আর ৩২ GB RAM নিয়ে। বড় ছিল ঠিক সেভাবে, যেভাবে একটামাত্র বিশাল সেতু বড় হয়। চোখে পড়ার মতো, আর নদী পেরোনোর একমাত্র পথ।
প্রকল্পের শুরুতেই আমরা লিখে রেখেছিলাম, আমাদের কাছে High Availability মানে কী। তার একটা লাইন ছিল: node বিকল হলেও database টিকে থাকবে। একটা node যত বড়ই হোক, এই লাইন পূরণ করতে পারে না। আকার আর availability আলাদা দুটো প্রশ্ন; পুরোনো সেটআপ শুধু প্রথমটার উত্তর দিয়েছিল।
data ছিল প্রায় ১১ GB। তাই আমরা গেলাম Managed MySQL "Standard" ৮.৪-এ, প্রতি node-এ ৪ vCPU আর ১৬ GB, মোট দুটো node: একটা primary, আর একটা standby, যেটা primary বিকল হলে দায়িত্ব নেয়। নতুন cluster-এ buffer pool ৭ GB, max connections ১,৬০১, আর storage autoscale চালু। অবস্থা: Implemented, ৫ অক্টোবর।
আগে
পরে
Plan
Advanced, একটাই node
Standard ৮.৪, দুটো node (primary + standby)
আকার
৮ vCPU / ৩২ GB
প্রতি node-এ ৪ vCPU / ১৬ GB
একটা node বিকল হলে
database বন্ধ
standby দায়িত্ব নেয়
মাসিক খরচ
বেশি
কম; primary + standby মিলিয়ে list price-এ প্রায় ৩৮৯ ডলার
একই সঙ্গে সস্তা আর highly available, এমন সুযোগ খুব কম আসে, তাই আমরা সেটা নিলাম। আজ data layer পুরোটা দেখতে এমন। লেখার বাকি অংশে আমরা এর ভেতর দিয়ে হাঁটব।
write যায় primary-তে, read ভাগ হয় standby আর primary-র মধ্যে, আর AI, cache ও file-এর প্রত্যেকে থাকে নিজস্ব service-এ।
২৬ মিনিটের cutover, ধাপে ধাপে
সময়সীমা আগে থেকে ঠিক করা ছিল, business-এর সম্মতিসহ। ৫ অক্টোবর আমরা এই ক্রমে এগিয়েছি।
queue drain করা হলো আর scheduler বন্ধ হলো, যাতে copy-র মাঝখানে কোনো job write করতে না পারে।
পুরোনো database-এ ৩০ সেকেন্ড ধরে একটাও write নেই। এটাই ছিল গেট: পুরোনো দিক পুরোপুরি চুপ না হওয়া পর্যন্ত copy শুরু হয়নি।
mysqldump দিয়ে ৪টা parallel stream-এ copy। সময় লেগেছে ১০ মিনিট।
সংখ্যা মেলানো: ৩৪১টি table আর ১৮,৭২৭,২৪৪টি row পুরোনো ও নতুনে হুবহু মিলেছে।
নতুন database-এ application test হলো। তারপর নতুন golden snapshot, দুটো pool-এর rollout আর একটা নতুন worker।
২২:৫৯। load balancer ফিরল, শুরুর ২৬ মিনিট পর।
window-র ভেতরে সাতটি পরিকল্পিত ধাপ, আর window বন্ধ হওয়ার ২৫ মিনিট পর একটা চমক।
কপিটা যে ঠিক, তা কীভাবে প্রমাণ করলাম
row-এর সংখ্যা মেলা দুর্বল পরীক্ষা, কারণ দুটো table-এ row সমান থেকেও ভেতরের data আলাদা হতে পারে। তাই আমরা সংখ্যার বাইরেও যাচাই করেছি, আর সাইট ফেরার পরও করে গেছি।
যাচাই
ফল
পুরোনো বনাম নতুনে table ও row
৩৪১টি table, ১৮,৭২৭,২৪৪টি row, হুবহু মিল
প্রতিটা table-এর বিষয়বস্তু
window-র পর সব table-এ full-content checksum
সবচেয়ে বড় results table
column ধরে ধরে তুলনা: ২৭ লাখ ৬০ হাজার (২.৭৬ মিলিয়ন) row হুবহু এক
app user-এর অধিকার
CREATE TABLE প্রত্যাখ্যাত, যেমনটা হওয়া উচিত
live হওয়ার প্রথম ৪ মিনিট
প্রায় ২,৯০০ request, 5xx শূন্য, ১১০টা notification job, ব্যর্থ শূন্য
যে client-টাকে আমরা ভুলে গিয়েছিলাম
২৩:২৪-এ, সাইট ফেরার ২৫ মিনিট পর, এমন কিছু পেলাম যার পরিকল্পনা আমাদের ছিল না। একটা আলাদা পুরোনো server-এ চলা একটা legacy admin panel তখনও পুরোনো database-এ write করে যাচ্ছিল।
যা যা মনে ছিল, সবই আমরা বন্ধ করেছিলাম: web node, worker, scheduler। বন্ধ করা হয়নি এমন একটা জিনিস, যার অস্তিত্বই আমরা ভুলে গিয়েছিলাম। ব্যাপারটা বাসা বদলের মতো। ডাকঘর আপনার চিঠি নতুন ঠিকানায় পাঠিয়ে দেয়, কিন্তু যে বন্ধু এখনও পুরোনো ঠিকানা জানে, সে পুরোনো ঠিকানাতেই চিঠি ফেলে যায়।
ততক্ষণে পুরোনো database-এ ৪৮টা row জমে গেছে। আমরা সেগুলো conditional upsert দিয়ে নতুন database-এ মিলিয়ে দিলাম: row insert হবে, অথবা update হবে কেবল তখনই, যখন পুরোনো কপিটাই বেশি নতুন। ৪৪টা row merge হলো। বাকি ৪টার ক্ষেত্রে নতুন database-এ আগে থেকেই আরও নতুন সংস্করণ ছিল, ওগুলো আমরা রেখে দিলাম। তারপর legacy panel-কে নতুন database-এ ঘুরিয়ে দিলাম আর পুরোনো cluster-এর app password বদলে ফেললাম, যাতে আর কিছুই সেখানে write করতে না পারে। অবস্থা: Implemented।
merge-এর মতোই গুরুত্বপূর্ণ ছিল password বদল। এতে ভুলে যাওয়া কোনো client নিঃশব্দে এমন database ভরতে থাকে না, যেটা কেউ পড়ে না; বরং সঙ্গে সঙ্গে ব্যর্থ হয় আর ধরা পড়ে। পুরোনো cluster ৪৮ ঘণ্টা frozen অবস্থায় ফেরার পথ হিসেবে রাখা ছিল, তারপর মুছে দিয়েছি।
শিক্ষাটা গল্প নয়, একটা checklist:
cutover-এর আগে database-এর প্রতিটা বাইরের client-এর তালিকা করুন। পুরোনো cluster-এর trusted source পড়ুন, আর প্রতিটা এন্ট্রির জন্য জিজ্ঞেস করুন: এটা কার?
প্রতিটা client-কে application-এর একই window-তে সুইচ করুন।
সুইচের পর পুরোনো password বদলান, যাতে যে client আপনি মিস করেছেন সে সঙ্গে সঙ্গে ব্যর্থ হয়।
পুরোনো cluster কিছুদিন frozen রেখে দিন, ফেরার পথ হিসেবে।
standby থেকে read: read/write split
read replica কেন নয়
"read ভারী" শুনলেই পাঠ্যবইয়ের উত্তর হলো read-only replica node। আমরা এটা Considered করেছিলাম। এই platform-এ read-only node কমপক্ষে primary-র সমান বড় হতে হয়, ফলে খরচ প্রায় আরেকবার primary-র সমান। অথচ আমরা ইতিমধ্যেই একটা standby-র দাম দিচ্ছি, যেটা বিকলের অপেক্ষায় বসে আছে।
তাই আমরা সস্তা কিছু Implemented করলাম: standby-ই reader হিসেবেও কাজ করে, তার replica hostname দিয়ে। read ভাগ হয় standby আর primary-র মধ্যে, একটা আটকালে অন্যটা সামলায়। একই hardware, বিলে নতুন কোনো লাইন নেই।
বিকল্প
বাড়তি খরচ
অবস্থা
মন্তব্য
সবকিছু primary-তে
নেই
Baseline
সবচেয়ে সহজ, কিন্তু প্রতিটা read primary-কেই করতে হয়
আলাদা read-only replica node
primary-র সমান আরও একবার
Considered
কমপক্ষে primary-র সমান বড় হতে হয়
standby আর primary মিলে read
নেই
Implemented, ৬ অক্টোবর
read সামান্য পিছিয়ে থাকতে পারে; sticky read সেটা সামলায়
নিয়মগুলো সহজ কথায়
৬ অক্টোবর ০০:১৩-এ আমরা split চালু করেছি। খসড়া আকারে Laravel-এর database configuration বলছে এটুকু:
read যায় standby-তে, আর primary দ্বিতীয় read host হিসেবে তালিকায় থাকে। একটা সাড়া না দিলে অন্যটা দেয়।
Sticky। write-এর পর একই request পরের read-গুলোও primary থেকে পড়ে, যাতে student নিজের দেওয়া উত্তরটা দেখতে পায়। একটা ছোট middleware পরের request-টাকেও primary-তেই রাখে। ভাবুন, whiteboard-এ একটা নোট লিখেছেন: সেটা পড়তে আপনি whiteboard-এই ফিরবেন, এমন photocopy-তে নয় যেটা হয়তো এখনও ছাপা হচ্ছে।
২ সেকেন্ডের connect timeout। ধীর standby দ্রুত primary-তে fallback করে, কোনো student তার জন্য বসে থাকে না।
worker কখনও standby ব্যবহার করে না। সে primary-তেই থাকে, কারণ job-এর দরকার সর্বশেষ data।
সত্যিই কি ভাগ হচ্ছে?
config-এর ওপর ভরসা না করে আমরা একটা live node-এ যাচাই করলাম। ৮টা নতুন connection খুলেছি, দুবার। প্রথমবার দুই server-এর মধ্যে ভাগ হলো ৬ আর ২, দ্বিতীয়বার ৪ আর ৪। primary জানাল read_only=0, standby জানাল read_only=1; অর্থাৎ দুটোই কাজে লাগছে, আর কোনটা কোনটা তাও আমরা জানি।
একটা ফাঁদ
production কোনো class খোঁজে authoritative class map থেকে। একদম নতুন একটা class, যেমন নতুন middleware, যদি map-এ না থাকে, তাহলে application-এর কাছে সেটার অস্তিত্বই নেই, আর request 500 ফেরত দেয়। deploy-এর অংশ হিসেবে class map-এ যোগ করে নিন।
নির্দিষ্ট আকার, সীমিত অধিকার, tag দিয়ে access
সাপ্তাহিক resize: Considered। একটা ভাবনা ছিল পরীক্ষার দিনের আগে database বড় করে, পরে আবার ছোট করে ফেলা। business বেছে নিয়েছে নির্দিষ্ট ৪ vCPU / ১৬ GB। নিচের পরীক্ষার সন্ধ্যার সংখ্যাগুলো সেই সিদ্ধান্তকে সমর্থন করে।
Least privilege: Implemented। app-এর database user-এর আছে SELECT, INSERT, UPDATE আর DELETE, কোনো DDL নেই। সে row বদলাতে পারে, যেটা তাকে পারতেই হবে, কিন্তু কোনো table বানাতে, বদলাতে বা ফেলে দিতে পারে না। cutover-এর সময় আমরা সেটা পরীক্ষা করেছি: CREATE TABLE প্রত্যাখ্যাত হয়েছে।
tag দিয়ে access: Implemented। প্রতিটা database backend tag, worker আর office ও jump address-কে ঢুকতে দেয়। নতুন autoscale হওয়া backend node কোনো হাতে-করা ধাপ ছাড়াই connect করে।
এবার একটা ছোট হিসাব। প্রতিটা backend node ৮০টা php-fpm worker চালায়, আর প্রতিটা একটা করে MySQL connection ধরে রাখতে পারে। মোট যেন অনুমোদিত ১,৬০১-এর নিচে থাকে।
Backend node
প্রতি node-এ php-fpm worker
সবচেয়ে খারাপ অবস্থায় connection
MySQL-এর সীমা
২
৮০
১৬০
১,৬০১
৪
৮০
৩২০
১,৬০১
৬
৮০
৪৮০
১,৬০১
১০ (pool-এর সর্বোচ্চ)
৮০
৮০০
১,৬০১
pool-এর সর্বোচ্চেও আমরা সীমার প্রায় অর্ধেক ব্যবহার করি, ফলে worker আর admin session-এর জন্য জায়গা থাকে।
data layer-এ Valkey
একটা managed Valkey, standby-সহ, cache, session আর queue ধরে রাখে, আর চলে noeviction policy-তে। চতুর্থ পর্বে আরও গভীরে যাব। এখানে শুধু data layer-এর অংশটুকু।
৬ অক্টোবর ০২:২৮-এ একটা backend load test ২,৮৫৪টা application 500 ফেরত দিল। প্রতিটাই Valkey-তে connect করার সময় "Operation timed out", connect timeout ৫ সেকেন্ড। প্রতিটা request নতুন একটা TLS connection খুলত, যার খরচ request প্রতি Valkey-র জন্য প্রায় ৫.৪ ms CPU, আর MySQL-এর জন্য প্রায় ৩.৩ ms। ৪ GB-র Valkey দ্রুত connection নিতে পারছিল না।
Implemented: আমরা সেটাকে in place resize করলাম ৮ GB আর ২টা node-এ। data অক্ষত, host অপরিবর্তিত। পুনরায় test চলল ২টা backend node দিয়ে শুরু করে, pool-কে বাড়তে দিয়ে, ২৫ থেকে ৫০০ request/s পর্যন্ত: application error শূন্য, backend 5xx শূন্য। পরীক্ষার সন্ধ্যায় এটা তার মেমরির প্রায় ১২.৫% ব্যবহার করে। খরচ বেড়েছে, কিন্তু একটা আসল ব্যর্থতা দূর হয়েছে।
AI কেন নিজস্ব PostgreSQL পেল
NovaCommerce-এ AI study feature আছে: adaptive learning, weakness analysis আর predicted questions। এগুলো কাজ করে embedding নিয়ে; embedding হলো লম্বা একসারি সংখ্যা, যা একটা প্রশ্নের অর্থ ধরে রাখে। সবচেয়ে কাছাকাছিগুলো খুঁজে বের করার নাম vector similarity search।
এই embedding আমরা রাখি আলাদা একটা Managed PostgreSQL ১৭-এ, pgvector extension-সহ, ২ vCPU আর ৪ GB। MySQL-এ নয়। তিনটা কারণ:
query-র গড়ন আলাদা। vector search মানে বড় vector, approximate-nearest-neighbour index আর CPU-ভারী query। পরীক্ষার traffic মানে ছোট ছোট read আর write।
পরীক্ষার write-এর সঙ্গে প্রতিযোগিতা নয়। ভারী similarity query কখনও এমন student-এর CPU কেড়ে নেবে না, যে উত্তর save করছে। দুটো server মানে দুটো আলাদা বাজেট।
উপযুক্ত যন্ত্র। এই কাজের জন্য MySQL-এ first-class vector index নেই।
ভিন্ন গড়নের আর ভিন্ন ব্যর্থতা-মূল্যের দুটো workload, তাই আলাদা আকারের দুটো database।
যে downsize আমরা ফিরিয়ে নিয়েছি
PostgreSQL node ছোটই, আমরা আরও ছোট করার চেষ্টা করেছিলাম: ১ vCPU আর ২ GB। Tested, then reverted। পরীক্ষার সময় যে write আসে, তার কারণে আবার ২ vCPU আর ৪ GB-তে ফিরিয়ে এনেছি। সামান্য সাশ্রয়ের জন্য এমন একটা node রাখা পোষায় না, যেটা সবচেয়ে জরুরি এক ঘণ্টায় হাঁপায়। হিসাব করুন পরীক্ষার ঘণ্টার জন্য, শান্ত দিনের জন্য নয়।
এই পরামর্শের সাধারণ রূপ, যেমন কোন index type বেছে নেবেন বা filter-সহ vector search ঠিক রাখবেন কীভাবে, তার জন্য দেখুন আমাদের field guide The Data Tier Under Load। এই case study শুধু আমরা কী করেছি, সেটুকুতেই থাকছে।
Backup আর recovery
standby বাঁচায় বিকল মেশিন থেকে। ভুল থেকে বাঁচায় না: কেউ একটা table মুছে দিলে standby-ও কিছুক্ষণ পরে সেটা মুছে ফেলে। সেই কাজের জন্যই backup। আমরা দুটোই রাখি, আর দুটো আলাদা প্রশ্নের উত্তর দেয়।
কী ভুল হতে পারে
কী আমাদের রক্ষা করে
অবস্থা
একটা MySQL node বিকল
standby দায়িত্ব নেয় (provider-এর failover)
Implemented
data হারায় বা নষ্ট হয়
managed database-এর daily backup (provider)
Implemented
cutover ভুল পথে যায়
পুরোনো cluster ৪৮ ঘণ্টা frozen রাখা, তারপর মুছে ফেলা
Implemented
একটা web node হারায় বা rollout ব্যর্থ হয়
web node-এর golden snapshot; শেষ ৩টি ভালো image রাখা
Implemented
worker হারায়
worker-এর একটা snapshot; এটা এখনও single point of failure
Snapshot: Implemented, standby worker: Planned
database bottleneck ছিল না
database আমরা সরিয়েছি ধীর বলে নয়; সরিয়েছি availability আর খরচের জন্য। কোথায় টাকা খরচ করব না, সেটা ঠিক করেছে measurement: ৭ অক্টোবর পরপর দুটো পরীক্ষার আসল সন্ধ্যায় database ছিল পুরো system-এর সবচেয়ে শান্ত অংশ।
মাপ, ৭ অক্টোবরের পরীক্ষার সন্ধ্যা
মান
MySQL CPU, সর্বোচ্চ / গড়
২৪.৫% / ১৩.৪%
MySQL-এ একসঙ্গে চলমান query, সর্বোচ্চ
৫
MySQL lock wait
শূন্য
Valkey memory
১২.৫%
Backend peak
প্রায় ৬৯ request/s
Backend node CPU, ২ মিনিটের গড়, সর্বোচ্চ
৪৭% (একটা node এক মিনিটের জন্য ৮৬% ছুঁয়েছিল)
bottleneck ছিল প্রতি node-এর backend CPU, আর database-এর হাতে ছিল প্রায় ৬ গুণ headroom। আমাদের capacity model বলছে MySQL সীমা ছুঁতে শুরু করে প্রায় ৪৫০ request/s-এ, সেদিনের ৬৯ request/s peak-এর বিপরীতে। এই model এসেছে একটাই multiple-choice পরীক্ষা থেকে। PDF upload-সহ written পরীক্ষা worker-কে অনেক বেশি চাপে ফেলে, তাই সেগুলো আলাদা করে মাপতে হবে।
টাকার হিসাবে তিনটা data store-এর খরচ list price-এ মাসে প্রায় ৬৮৯ ডলার: MySQL প্রায় ৩৮৯, Valkey প্রায় ২৪০ আর PostgreSQL ৬০ ডলার। backend pool ২টা node-এ থাকলে মাসের প্রায় ৯৯০ ডলারের বিলের এটা প্রায় ৭০%। তাই কোনো store বড় করি মাপার পরেই।
যা শিখলাম
একটা বড় node মানে high availability নয়। Availability-র জন্য লাগে দায়িত্ব নিতে পারে এমন দ্বিতীয় node, আর দামের হিসাবে একজোড়া ছোট মেশিন একটা দানবের চেয়ে ভালো হতে পারে।
cutover-এ copy-র আগে চাই একটা গেট (শূন্য write), copy-র মধ্যে সংখ্যা মেলানো, আর পরে checksum।
cutover-এর আগে পুরোনো database-এর প্রতিটা client-এর তালিকা করুন, আর পরে পুরোনো password বদলান, যাতে মিস হওয়া client সঙ্গে সঙ্গে স্পষ্টভাবে ব্যর্থ হয়।
replica কেনার আগে standby-কে reader বানান। সঙ্গে sticky read আর ছোট connect timeout রাখুন, আর job রাখুন primary-তে।
app user-কে ততটুকু অধিকারই দিন যতটুকু দরকার। DDL নয়।
AI-কে নিজস্ব PostgreSQL দিন, আর তার আকার ঠিক করুন পরীক্ষার ঘণ্টা ধরে।
খরচ করার আগে মাপুন। database-এর ছিল ৬ গুণ headroom; কাজটা ছিল backend-এ।
database এখন শান্ত, তাই পরের প্রশ্নগুলো ব্যাকগ্রাউন্ডে যা চলে তা নিয়ে: একমাত্র worker, queue, log আর monitoring। চতুর্থ পর্বে সেগুলো: Valkey, worker, logging ও reliability।