One self-hosted console to run your entire business — commerce, ERP, HRM, CRM & manufacturing

চাপের মুখে data tier: PostgreSQL, কানেকশন পুলিং, Redis আর pgvector

ট্রাফিকের চাপ এলে প্রথম ইচ্ছেটা হয় ডেটাবেস বড় করা, আর সেটা প্রায়ই ভুল। জানুন কীভাবে write throughput দেখে সাইজ ঠিক করবেন, PgBouncer দিয়ে কানেকশন pool করবেন, Redis বা Valkey ভূমিকা ধরে ভাগ করবেন, আর checkout ধীর না করে pgvector search চালাবেন।

Author

Anichur Rahaman

2 সপ্তাহ আগে12 min read1 views
চাপের মুখে data tier: PostgreSQL, কানেকশন পুলিং, Redis আর pgvector

ফ্ল্যাশ সেলের সকাল ৯:০২। এই কাল্পনিক দৃশ্যে অন-কল ইঞ্জিনিয়ার dashboard-এ দেখছেন, ১,০০০টার মধ্যে ৯৪০টা database connection ব্যবহার হচ্ছে, checkout তিন সেকেন্ডের বেশি সময় নিচ্ছে। কেউ বলল, দ্বিতীয় ঢেউ আসার আগেই database-কে পরের সাইজে তুলে ফেলা হোক।

সেটা হবে আন্দাজে কাজ। ওই ৯৪০ connection-এর বেশিরভাগই idle, দুই request-এর মাঝখানে PHP process ধরে রেখেছে, আর database-এর CPU আছে ৩৫ শতাংশের কাছাকাছি। আসল লাইনটা হলো এগারোটা worker, যারা একই কয়েকটা সারিতে write-এর জন্য অপেক্ষা করছে। connection-এর সংখ্যা লক্ষণ মাত্র, আর সাইজ বাড়ানো মানে লক্ষণের চিকিৎসা।

এর আসল রূপ আমি দেখেছি। একটা platform-এ প্রায় ১,০০০ user একই মিনিটে কাজ করবে এমন নির্ধারিত চাপের প্রস্তুতি নিয়েছিলাম; সেখানে managed database (১৬ GB RAM, ৪ vCPU, প্রায় ১,০০০ connection-এর সীমা) কখনোই বাধা ছিল না। বাধা ছিল মাত্র একটা serial worker। এই লেখায় data tier নিয়ে আমি এখন যেভাবে ভাবি সেটাই বলব: কী মাপতে হবে, কীভাবে pool করতে হবে, replica কী পারে আর কী পারে না, Redis বা Valkey-কে কীভাবে পরিষ্কার দায়িত্ব দিতে হবে, আর transactional database-এ vector query না চাপিয়ে কীভাবে AI search যোগ করা যায়।

এটি "হাই-ভলিউম system-এর ইঞ্জিনিয়ারিং" সিরিজের তৃতীয় পর্ব। প্রথম পর্বে আছে edge ও web tier, দ্বিতীয় পর্বে queue ও worker। এবার আমরা এক স্তর নিচে, data-র কাছে নামছি।

connection-এর সংখ্যা নয়, মাপুন write throughput

"১,০০০ max connections" লেখা দেখলে বোঝা যায় কতজন ক্লায়েন্ট কানেক্ট করতে পারবে, server কতটা কাজ করতে পারবে তা বোঝা যায় না। চার vCPU একই মুহূর্তে মাত্র কয়েকটা query চালাতে পারে। বাকিরা কানেক্টেড থাকুক বা না থাকুক, অপেক্ষাই করে।

তাই রিলেশনাল database-কে আসলে যা আটকে দেয় সেগুলো মাপুন: write throughput (প্রতি সেকেন্ডে commit আর WAL-এর পরিমাণ), CPU, ডিস্কের latency আর lock-এর অপেক্ষা। connection-এর সংখ্যা শুধু অন্য সমস্যার লক্ষণ — যেমন ধীর query connection আটকে রাখছে।

আমার নিয়ম সোজা: load test প্রমাণ না করা পর্যন্ত database আপগ্রেড করবেন না। ওই সেটআপে সমাধান ছিল web আর worker node আলাদা করা এবং প্রায় ১২টা worker process চালানো। database বাড়তি চাপ অনায়াসে সামলেছে। প্রায় ২০টা worker ছাড়ালে job শুধু database write-এর লাইনে দাঁড়িয়ে থাকত — আসল সীমা ওটাই, connection নয়। এই সংখ্যাগুলো একটা নির্দিষ্ট সেটআপের, সর্বজনীন benchmark নয়; নিজের test নিজে চালান।

Data tier-এর মানচিত্র: PostgreSQL primary ও replica-র সামনে PgBouncer, ভূমিকা অনুযায়ী ভাগ করা Redis বা Valkey, আলাদা pgvector database, আর compute-এর একই region-এ object storage
একটা tier, কয়েকটা store — প্রতিটির একটাই কাজ আর নিজস্ব সীমা।

connection pool করুন

PHP অনেক ছোট ছোট connection খোলে: প্রতিটা request, এমনকি প্রতিটা queue job-ও কানেক্ট করে আবার ছেড়ে দেয়। PostgreSQL প্রতি connection-এর জন্য আলাদা process চালায়, তাই এক হাজার ক্লায়েন্ট কাজের একটা query চালানোর আগেই অনেক মেমরি খেয়ে ফেলতে পারে। PgBouncer-এর মতো pooler অ্যাপ্লিকেশন আর database-এর মাঝখানে বসে। শত শত ক্লায়েন্ট connection ভাগ করে ব্যবহার করে কয়েক ডজন আসল server connection।

একটা কাল্পনিক হিসাব। দুটো web node-এ প্রতিটায় ৬০টা PHP-FPM child থাকলে ১২০টা connection খুলতে পারে, ১২টা worker process যোগ করে আরও ১২, আর scheduler ও কয়েকটা admin session মিলিয়ে প্রায় ৮। মোট প্রায় ১৪০ connection, অর্থাৎ ৪ vCPU-র জন্য ১৪০টা PostgreSQL process-এর কাড়াকাড়ি। PgBouncer-কে transaction mode-এ বসিয়ে ২০টা server connection-এর pool দিলে একই ১৪০ ক্লায়েন্ট ভাগ করে নেয় ওই ২০টা। গড়ে একটা transaction যদি ৫ ms connection ধরে রাখে, ২০টা connection সর্বোচ্চ ২০ ÷ ০.০০৫ = ৪,০০০ transaction প্রতি সেকেন্ডে টানতে পারে। CPU আপনাকে এর অনেক নিচেই আটকে দেবে, আর ঠিক সেটাই লক্ষ্য: pool আসল সীমাটাকে connection-এর ওঠানামার নিচে লুকিয়ে না রেখে সামনে এনে দেয়।

PgBouncer-এর তিনটা pool mode আছে, আর কোনটা বেছে নিচ্ছেন তার ওপর নির্ভর করে app কী করতে পারবে।

Pool modeserver connection আটকে থাকেকোথায় সেরাসতর্কতা
Sessionক্লায়েন্ট connection-এর পুরো সময়দীর্ঘস্থায়ী listener, session state লাগে এমন টুলসবচেয়ে কম সাশ্রয়; idle ক্লায়েন্টও server connection ধরে রাখে
Transactionএকটা transaction-এর জন্যweb request ও queue job; high volume-এ সাধারণ পছন্দsession state-নির্ভর যেকোনো কিছু ভেঙে যায়
Statementএকটা statement-এর জন্যসরল, শুধু autocommit-ভিত্তিক কাজএকাধিক statement-এর transaction চলে না

লোডের মুখে থাকা web app-এর জন্য transaction mode-ই সবচেয়ে বড় লাভ দেয়। তবে সুইচ করার আগে এর কয়েকটা সীমাবদ্ধতা জেনে নেওয়া ভালো।

Transaction mode-এ কী ভাঙে

একই ক্লায়েন্টের পরের transaction অন্য server connection-এ চলে যেতে পারে, তাই session-এ রাখা state ভরসাযোগ্য নয়:

  • Session-level setting। সাধারণ SET পরের transaction-এ থাকে না; transaction-এর ভেতরে SET LOCAL থাকে।
  • Session-level advisory lock আর LISTEN/NOTIFY। এগুলোর জন্য direct connection বা session mode ব্যবহার করুন।
  • Temporary table আর WITH HOLD cursor যা transaction পেরিয়ে টিকে থাকে।
  • Prepared statement। PgBouncer-এর পুরনো ভার্সন transaction mode-এ এগুলো সামলাতে পারত না। ১.২১ থেকে max_prepared_statements অপশন শূন্যের বেশি করলে PgBouncer এগুলো ট্র্যাক করতে পারে। ধরে না নিয়ে নিজের ভার্সন আর ড্রাইভার যাচাই করুন।

বাস্তব সমাধান হলো দুটো connection পথ: app-এর জন্য pooled পথ, আর migration, schema টুল ও listener-এর জন্য direct পথ। pool size ও timeout-সহ সব অপশন আছে PgBouncer configuration reference-এ।

Replica: কী ঠিক করে, আর কী primary-তেই থাকবে

Read replica হলো primary-র একটা কপি, যেটা সামান্য দেরিতে primary-কে অনুসরণ করে। যে কাজ একটু পিছিয়ে থাকলেও চলে, সেখানে এটা চমৎকার: report, export, search listing, dashboard আর analytics query, যেগুলো না হলে checkout-এর সঙ্গে পাল্লা দিত।

তবে এটা সব ক্ষেত্রে গতি বাড়ানোর জাদু নয়। Replication lag স্বাভাবিক, আর write বেশি হলে তা বাড়ে। এগুলো primary-তেই রাখুন:

  • যা write করার পরপরই পড়ে, যেমন checkout-এর পর order পেজ দেখানো।
  • stock রিজার্ভেশন, payment, authentication — টাকা বা অ্যাক্সেস নিয়ে যা-ই সিদ্ধান্ত নেয়।
  • যে transaction-এ write-ও আছে, তার ভেতরের যেকোনো query।

বাস্তবে এটা একটা routing নিয়ম, যা এক পাতায় লিখে ফেলা যায়।

তিনটি সিদ্ধান্তের flowchart: session state লাগলে direct connection, write করলে বা নিজের write পড়লে primary, lag সহনীয় হলে replica, নইলে primary
কোন query কোথায় চলবে, আর তা ঠিক করে দেওয়া তিনটা প্রশ্ন।

Index আর N+1 query: সবচেয়ে সস্তা ক্ষমতা

হার্ডওয়্যার বাড়ানোর আগে খুঁজে বের করুন কোন query সবচেয়ে বেশি সময় খায়। PostgreSQL-এর pg_stat_statements এক্সটেনশন মোট সময় ধরে query-র র‍্যাংকিং দেয়, আর ANALYZE ও BUFFERS অপশনসহ EXPLAIN দেখায় একটা query কয়েকটা পেজ পড়ছে, না দশ লাখ।

অপচয়ের বেশিরভাগ আসে দুটো সমস্যা থেকে:

  • Index নেই বা ভুল index। যে কলামে মানানসই index নেই, সেখানে filter আর sort মিলিসেকেন্ডের lookup-কে পুরো table scan বানিয়ে দেয়। আপনার আসল filter আর ordering-এর সঙ্গে মেলে এমন composite index বানান।
  • N+1 query। লিস্ট পেজে একটা query লিস্টের জন্য, তারপর প্রতিটা সারির জন্য আরেকটা। পঞ্চাশ সারি মানে একান্নবার database-এ যাওয়া। যে relation দেখাচ্ছেন, সেগুলো eager-load করুন।

Redis ও Valkey: একটা server, কয়েকটা কাজ

নাম নিয়ে দু-কথা। ২০২৪ সালের মার্চে Redis source-available লাইসেন্সে চলে যায়, আর Linux Foundation চালু করে Valkey — Redis 7.2.4 থেকে নেওয়া একটা fork, যেটা অনুমতিমূলক BSD লাইসেন্সেই আছে। Valkey একই protocol বোঝে, তাই নিচের সবকিছু দুটোর ক্ষেত্রেই খাটে।

সবচেয়ে সাধারণ ভুল হলো cache, session, API token আর queue একই keyspace-এ রাখা। তখন একটা flush বা একবার মেমরির চাপ সবগুলোকে নষ্ট করে। প্রতিটা ভূমিকাকে আলাদা logical database বা আলাদা instance দিন, সঙ্গে আলাদা key prefix। Cache নিরাপদে খালি করা যায়। Queue যায় না।

ভূমিকাহারালে কী হয়Eviction policyনিয়ম
Cacheকিছুক্ষণ ধীর; data আবার তৈরি হয়allkeys-lruসবকিছুর TTL থাকে; evict হলে সমস্যা নেই
Session ও API tokenমানুষ logout হয়ে যায়volatile-ttl বা volatile-lruএমন size রাখুন যেন eviction কখনো না ঘটে
Queuejob হারিয়ে যায়noevictionকাজ চুপচাপ ফেলে দেওয়ার বদলে write সশব্দে ব্যর্থ হয়; মেমরিতে alert রাখুন

প্রতিটা policy-র বিবরণ আছে Redis eviction ডকুমেন্টেশনে। দুটো কথা সবচেয়ে জরুরি। allkeys-lru হলে Redis মেমরির সীমা ধরে রাখতে যেকোনো key সরিয়ে দিতে পারে — cache-এর জন্য ঠিক এটাই চাই, queue-এর জন্য ঠিক এটাই চাই না। noeviction হলে server ভরে গেলে data মুছে ফেলার বদলে নতুন write ফিরিয়ে দেয়; ফলে queue ঠিক থাকে, আর আপনি জানতে পারেন error ও alert থেকে।

কখনো FLUSHDB নয়

আপনার runbook আর app — দুই জায়গাতেই FLUSHDB ও FLUSHALL নিষিদ্ধ করুন। Cache পরিষ্কার করতে হলে prefix ধরে মুছুন। তাহলে ভুল করে, সবচেয়ে খারাপ মুহূর্তেও, cache flush কখনো queue-র job মুছতে পারবে না।

সঠিক আকারে বানান

ওই একই platform-এ managed Valkey instance ছিল ১৬ GB, ব্যবহার হচ্ছিল প্রায় ৬২ MB। বাস্তব লোডে আসল মেমরির ব্যবহার দেখুন, burst-এর সময় queue-র জন্য উদার headroom রাখুন, আর সেই মাপে নামিয়ে আনুন।

AI search-এর জন্য pgvector, নিজস্ব database-এ

আপনার স্টোর বা ERP-তে যদি AI search, recommendation বা ডকুমেন্ট খুঁজে আনা অ্যাসিস্ট্যান্ট থাকে, তাহলে আপনি embedding রাখছেন: অর্থ বোঝানো লম্বা সংখ্যার সারি। pgvector এক্সটেনশন PostgreSQL-এ vector টাইপ আর nearest-neighbour search যোগ করে, তাই শুরুতে আলাদা vector product লাগে না।

আমার ডিজাইনের নিয়ম হলো এটা চালানো আলাদা একটা PostgreSQL database-এ, নিজস্ব connection আর নিজস্ব migration পথ দিয়ে। সর্বশেষ যে সেটআপ বানিয়েছি তাতে ছিল PostgreSQL 18-এ pgvector 0.8; PostgreSQL 18 প্রকাশিত হয় ২০২৫ সালের সেপ্টেম্বরে। Vector query CPU আর মেমরিতে ভারী, আর index তৈরি আরও ভারী। আলাদা database-এ AI-র কাজ checkout-কে ধীর করতে পারে না, আর আপনি নিজের সময়সূচিতে সেটার size, backup ও আপগ্রেড ঠিক করতে পারেন।

product বদল থেকে queue-তে job, embedding বানানো worker, pgvector-এ সংরক্ষণ আর filter-সহ nearest-neighbour query পর্যন্ত প্রবাহ
Embedding বানায় worker, পরিবর্তনের পরে — web request-এর ভেতরে কখনো নয়।

Embedding বানান worker দিয়ে

Embedding বানানো মানে একটা model-কে ডাকা, যেটা ধীর এবং ব্যর্থ হতে পারে। কাজটা করুন database commit-এর পরে চালু হওয়া queue job দিয়ে, web request-এ নয়। Job-কে idempotent রাখুন: মূল লেখার একটা hash রাখুন এবং কিছু না বদলালে কাজ বাদ দিন; প্রতিটা সারির সঙ্গে model-এর ভার্সন রাখুন, যাতে পরে ব্যাকগ্রাউন্ডে আবার embed করা যায়। এটা দ্বিতীয় পর্বের সেই একই worker-শৃঙ্খলা।

HNSW না IVFFlat

pgvector দুই ধরনের approximate index দেয়, আর দুটোই সামান্য নির্ভুলতার বিনিময়ে অনেক গতি আনে।

HNSWIVFFlat
গতি ও recallসাধারণত ভালো ভারসাম্যভালো, কতগুলো list probe করছেন তার ওপর নির্ভর করে
তৈরির সময় ও মেমরিতৈরি ধীর, মেমরি বেশি লাগেতৈরি দ্রুত, মেমরি কম লাগে
আগে data লাগে কিনা, খালি table-এও শুরু করা যায়হ্যাঁ, প্রতিনিধিত্বমূলক data লোড করার পরে বানান
সাধারণ ব্যবহারবেশিরভাগ live search-এর ডিফল্টখুব বড়, কম বদলানো সেট, যেখানে তৈরির খরচ গুরুত্বপূর্ণ

বেশিরভাগ কমার্স ও ERP কাজে আমি শুরু করি HNSW দিয়ে। দুটোর টিউনিং অপশন আছে pgvector প্রজেক্টের ডকুমেন্টেশনে।

Filter-এ এসেই search নীরবে ভাঙে

বাস্তব query কখনো শুধু "এই লেখার সবচেয়ে কাছেরটা" হয় না। হয় "সবচেয়ে কাছের, stock-এ আছে, এই স্টোরের, এই ভাষার"। Approximate index-এ filter প্রয়োগ হয় index scan হয়ে যাওয়ার পরে। কোনো filter যদি ১০% সারির সঙ্গে মেলে আর hnsw.ef_search-এর ডিফল্ট ৪০ হয়, তাহলে গড়ে মাত্র চারটা মিলে-যাওয়া সারি পাবেন — দশটা চাইলেও।

২০২৪-এর শেষে প্রকাশিত pgvector 0.8.0 ঠিক এই সমস্যার জন্য iterative index scan এনেছে। Filter পেরিয়ে খুব কম সারি টিকলে scan চলতে থাকে, যতক্ষণ না যথেষ্ট সারি মেলে বা একটা সীমায় পৌঁছায়।

Settingকী করে
hnsw.iterative_scanHNSW-এর জন্য iterative scan চালু করে: strict_order দূরত্বের সঠিক ক্রম রাখে, relaxed_order সামান্য অক্রম মেনে ভালো recall দেয়
hnsw.max_scan_tuplesiterative scan সর্বোচ্চ কতগুলো index entry দেখতে পারবে তার সীমা
ivfflat.iterative_scanIVFFlat-এর জন্য একই ধারণা, relaxed ordering সহ
ivfflat.max_probesiterative scan-এ সর্বোচ্চ কতগুলো list probe হবে তার সীমা

Object storage রাখুন compute-এর পাশেই

ফাইলও state। product-এর ছবি, invoice আর export থাকবে S3-compatible object storage-এ, web node-এর ডিস্কে নয়; নইলে load balancer-এর পেছনে দুটো node চালানো যায় না।

Bucket রাখুন server-এর একই region-এ। একটা load test-এ ভিন্ন region-এর bucket প্রতিটা তৈরি-হওয়া ফাইলের upload-এ আনুমানিক ৫০ থেকে ৮০ ms যোগ করেছিল। ফাইল আগে লোকাল ডিস্কে নামানোর বদলে সরাসরি storage থেকে stream করায় একটা job-এর সর্বোচ্চ মেমরিও প্রায় ২৮৮ MB থেকে প্রায় ১২৮ MB-তে নেমেছিল।

event-এর আগে data tier-এর চেকলিস্ট

  1. লক্ষ্য rate-এ load test চালান আর database-এর CPU, ডিস্ক latency, lock-এর অপেক্ষা ও প্রতি সেকেন্ডে commit লিখে রাখুন। তারপরই ঠিক করুন size বাড়াবেন কি না।
  2. app-এর সামনে PgBouncer transaction mode-এ বসান, আর migration ও listener-এর জন্য direct connection আলাদা রাখুন।
  3. Prepared statement সাপোর্টের জন্য আপনার PgBouncer ভার্সন আর ড্রাইভার যাচাই করুন।
  4. pg_stat_statements দিয়ে query র‍্যাংক করুন, সবচেয়ে ভারীগুলো ঠিক করুন, আর সবচেয়ে ব্যস্ত পেজের N+1 সরান।
  5. লিখে ফেলুন কোন read replica-তে যেতে পারে, আর নিশ্চিত করুন checkout, stock ও payment primary-তেই থাকছে।
  6. Redis বা Valkey ভূমিকা অনুযায়ী আলাদা prefix-সহ ভাগ করুন, প্রতিটার eviction policy ঠিক করুন, আর FLUSHDB নিষিদ্ধ করুন।
  7. Queue-র মেমরিতে alert রাখুন, যেন noeviction নীরব ব্যর্থতায় না গড়ায়।
  8. AI search চালান নিজস্ব PostgreSQL database-এ, filter-সহ query test করুন, আর recall কমলে iterative scan চালু করুন।
  9. নিশ্চিত করুন object storage আপনার compute-এর একই region-এ।

আবার ৯:০২-এ ফিরি। Pooler transaction mode-এ থাকলে ৯৪০ ক্লায়েন্ট connection নেমে আসে ২০টা server connection-এ, আর dashboard-এ ভয়ধরানো সংখ্যাটা আর দেখা যায় না। সেলস report চলে replica-তে, cache আর queue থাকে আলাদা keyspace-এ, আর অন-কল ইঞ্জিনিয়ার জিজ্ঞেস করেন "কোনটা saturated?", "পরের সাইজ কত?" নয়। Load test আগেই উত্তর দিয়ে রেখেছে: write, আর database আছে স্বস্তিদায়ক margin-এ।

Data tier ঠিকঠাক হলেও event-এর দিন সে কী করছে তা দেখতে পাওয়া চাই। চতুর্থ পর্বে সেটাই: কাজে লাগার মতো log, metric আর trace।

সংক্ষেপে

  • database-এর size ঠিক করুন write throughput আর মাপা saturation দিয়ে, connection-এর সীমা দিয়ে নয়; load test প্রয়োজন না দেখালে কখনো আপগ্রেড নয়।
  • app-এর জন্য PgBouncer transaction mode ব্যবহার করুন, আর জানুন এটা কী ভাঙে: session state, session-level lock, LISTEN/NOTIFY আর পুরনো prepared-statement সেটআপ।
  • Replica সামলায় lag সহনীয় read; যা write-এর পরে পড়ে, বা টাকা ও stock ছোঁয়, তা থাকে primary-তে।
  • Cache, session আর queue-কে আলাদা Redis বা Valkey database ও prefix দিন — cache-এ allkeys-lru, queue-তে noeviction — আর পুরো database কখনো flush করবেন না।
  • pgvector রাখুন নিজস্ব PostgreSQL database-এ, embedding বানান worker-এ, শুরুতে HNSW বেছে নিন, আর filter-সহ search-এ iterative scan ব্যবহার করুন।
  • Cache-এর মেমরি সঠিক মাপে বানান, আর object storage রাখুন compute-এর একই region-এ।

আনিছুর রহমান একজন সফটওয়্যার আর্কিটেক্ট এবং StoreConsole-এর নির্মাতা। বাড়তে থাকা ব্যবসার জন্য তিনি কমার্স ও ERP সিস্টেম ডিজাইন করেন — বিশেষ মনোযোগ event-driven আর্কিটেকচার, ডেটার নির্ভুলতা আর নিজের সার্ভারে চালানো সিস্টেমের ওপর।

About the Author

Anichur Rahaman

Continue Reading