প্রায় প্রতিটা সিস্টেমের চূড়ান্ত bottleneck হলো database. Application server গুলো stateless হলে সহজেই বাড়ানো যায়, কিন্তু state যেখানে থাকে সেটাকে scale করাই system design-এর সবচেয়ে কঠিন অংশ। এই কারণে ডেটা সংরক্ষণের সিদ্ধান্তগুলো architecture-এর সবচেয়ে ব্যয়বহুল ও কঠিনভাবে বদলানো সিদ্ধান্ত।
SQL vs NoSQL
RDBMS (SQL) — ডেটা table-এ সারি ও কলামে, আগে থেকে নির্ধারিত schema, এবং SQL দিয়ে শক্তিশালী query (JOIN, aggregation)। এর মূল প্রতিশ্রুতি ACID:
- Atomicity — transaction পুরোটা হবে অথবা কিছুই হবে না।
- Consistency — transaction শেষে database সবসময় বৈধ অবস্থায় থাকবে (constraint ভাঙবে না)।
- Isolation — একসাথে চলা transaction গুলো একে অপরকে বিভ্রান্ত করবে না।
- Durability — commit হয়ে গেলে ডেটা টিকে থাকবে, বিদ্যুৎ চলে গেলেও।
NoSQL — flexible বা schema-less, সাধারণত horizontal scaling-কে মাথায় রেখে তৈরি, এবং প্রায়ই BASE (Basically Available, Soft state, Eventually consistent) মডেল অনুসরণ করে। JOIN সাধারণত নেই — বদলে ডেটা denormalize করে "যেভাবে পড়া হবে সেভাবেই" সংরক্ষণ করা হয়।
কীভাবে বাছবেন
SQL বেছে নিন যখন — ডেটার মধ্যে জটিল সম্পর্ক আছে, transaction-এর নির্ভুলতা অপরিহার্য (আর্থিক, inventory), query-র ধরন আগে থেকে জানা নেই (ad-hoc reporting), অথবা ডেটার আকার একটা বড় মেশিনে ধরে যায়।
NoSQL বেছে নিন যখন — বিপুল write throughput দরকার, ডেটার আকার একক মেশিনের সীমা ছাড়ায়, schema দ্রুত বদলায় বা অসম, access pattern খুব সরল ও আগে থেকে জানা (key দিয়ে পড়া), অথবা যা লাগবে তা হলো সীমাহীন horizontal scale।
একটা সতর্কতা: আধুনিক PostgreSQL/MySQL অবিশ্বাস্যভাবে দ্রুত এবং JSON column-এর মাধ্যমে অনেকটা NoSQL-এর নমনীয়তাও দেয়। "আমাদের scale লাগবে" ভেবে দিনের শুরুতেই NoSQL বেছে নেওয়া একটা সাধারণ ও ব্যয়বহুল ভুল। সন্দেহ হলে relational দিয়ে শুরু করুন।
RDBMS Scaling কৌশল
Relational database একটা মেশিনে চলতে চলতে যখন সীমায় পৌঁছায়, তখন নিচের কৌশলগুলো — সাধারণত এই ক্রমেই — প্রয়োগ করা হয়।
Replication
একই ডেটার একাধিক কপি রাখা। Master-Slave (primary সব write নেয়, replica-রা read সার্ভ করে) সবচেয়ে প্রচলিত, কারণ বেশিরভাগ অ্যাপ read-heavy। Master-Master write-ও ছড়ায় কিন্তু conflict resolution-এর জটিলতা আনে। বিস্তারিত: Availability Patterns।
মনে রাখার মূল বিষয় — replication read scale করে, write নয়, এবং replication lag-এর কারণে replica থেকে পড়লে সামান্য পুরনো ডেটা পেতে পারেন। তাই লেখার পরপরই পড়া দরকার হলে ওই query টা primary-তে পাঠান।
Federation (Functional Partitioning)
Database কে function অনুযায়ী ভাগ করা — একটা database-এ user, আরেকটায় product, আরেকটায় forum। প্রতিটা এখন ছোট, তাই বেশি ডেটা memory/cache-এ ধরে, write ট্রাফিক তিন ভাগে ভাগ হয়, আর প্রতিটাকে আলাদা করে scale করা যায়।
- অসুবিধা: দুই database-এর table-এর মধ্যে JOIN করা যায় না, তাই application-এ ডেটা জোড়া লাগাতে হয়। একাধিক database জুড়ে transaction-ও কঠিন।
- এটাই বাস্তবে microservice-এ যাওয়ার প্রথম ধাপ, এবং sharding-এর চেয়ে অনেক সহজ — তাই আগে এটা বিবেচনা করুন।
Sharding (Horizontal Partitioning)
একই table-এর সারিগুলোকে একাধিক database-এ ভাগ করা — যেমন user ID-র ভিত্তিতে user 1–1M একটা shard-এ, 1M–2M আরেকটায়। এটাই write scale করার প্রকৃত উপায়, এবং সবচেয়ে জটিল।
Shard key নির্বাচনই সবচেয়ে গুরুত্বপূর্ণ সিদ্ধান্ত। ভালো shard key ডেটা ও ট্রাফিক সমানভাবে ছড়ায় এবং বেশিরভাগ query একটামাত্র shard-এ সীমাবদ্ধ রাখে। খারাপ key দিলে hotspot তৈরি হয় (একটা shard-এ ৮০% ট্রাফিক) — যা sharding-এর পুরো উদ্দেশ্যই ব্যর্থ করে।
সাধারণ কৌশল: range-based (সহজ, কিন্তু hotspot-প্রবণ), hash-based (সমান বণ্টন, কিন্তু range query অসম্ভব), directory-based (একটা lookup table shard ঠিক করে — নমনীয়, কিন্তু lookup নিজেই একটা bottleneck ও SPOF)।
খরচ: cross-shard JOIN ও aggregation অত্যন্ত ব্যয়বহুল; distributed transaction প্রায় অসম্ভব; shard পুনর্বণ্টন (resharding) একটা বড় অপারেশন — তাই consistent hashing বা প্রচুর virtual shard আগে থেকে তৈরি রাখা বুদ্ধিমানের কাজ। Sharding-কে শেষ অস্ত্র হিসেবে দেখুন, প্রথম নয়।
Denormalization
Normalization ডেটার পুনরাবৃত্তি এড়ায়, কিন্তু পড়ার সময় ব্যয়বহুল JOIN বাধ্যতামূলক করে। Denormalization হলো ইচ্ছাকৃতভাবে কিছু ডেটা একাধিক জায়গায় রেখে দেওয়া, যাতে read-এ JOIN না লাগে — যেমন orders table-এ user_name কপি করে রাখা, বা comment সংখ্যা গুনে না রেখে একটা counter column-এ রাখা।
Trade-off: read দ্রুত হয়, কিন্তু write জটিল ও ধীর হয় (একই ডেটা একাধিক জায়গায় আপডেট করতে হয়) এবং ডেটা অসামঞ্জস্যপূর্ণ হওয়ার ঝুঁকি তৈরি হয়। Read:write অনুপাত যত বেশি, denormalization তত বেশি যুক্তিযুক্ত। Sharded সিস্টেমে এটা প্রায় অনিবার্য, কারণ cross-shard JOIN সম্ভব নয়। Materialized view এই কাজটা database-এর ভেতরেই করার একটা পরিচ্ছন্ন উপায়।
SQL Tuning
Architecture বদলানোর আগে সবচেয়ে সস্তা ও কার্যকর কাজটা প্রায়ই এটাই। মাপুন, তারপর ঠিক করুন — অনুমান করে নয়।
- প্রথমে মাপুন: slow query log চালু করুন,
EXPLAIN/EXPLAIN ANALYZEদিয়ে query plan দেখুন। খুঁজুন full table scan, ভুল index ব্যবহার, ও ভুল row estimate। - Index:
WHERE,JOIN,ORDER BY-তে ব্যবহৃত column-এ index দিন। একাধিক column-এ query হলে composite index দিন (ক্রম গুরুত্বপূর্ণ)। মনে রাখুন index পড়া দ্রুত করে কিন্তু প্রতিটা write ধীর করে ও জায়গা নেয় — তাই অপ্রয়োজনীয় index বাদ দিন। - Query ঠিক করুন:
SELECT *এড়িয়ে দরকারি column নিন। N+1 query সমস্যা খুঁজে বের করুন (ORM-এর সবচেয়ে সাধারণ ফাঁদ) — একটাJOINবা batch fetch দিয়ে বদলান। বিশালOFFSET-এর বদলে keyset (cursor) pagination ব্যবহার করুন। - Schema: উপযুক্ত সবচেয়ে ছোট data type বাছুন, বড় VARCHAR-এর বদলে ENUM/lookup, আর খুব বড় table-এ partitioning বিবেচনা করুন।
- Connection pooling ব্যবহার করুন — প্রতিটা নতুন database connection ব্যয়বহুল।
NoSQL-এর ধরনসমূহ
Key-Value Store
সবচেয়ে সরল মডেল: একটা key, একটা value (যা database-এর কাছে অস্বচ্ছ blob)। O(1) সময়ে read/write, অসাধারণ throughput, সহজে shard করা যায়।
- সীমাবদ্ধতা: value-র ভেতরের কিছু দিয়ে query করা যায় না, শুধু key দিয়েই খোঁজা যায়।
- ব্যবহার: cache, session store, rate limiter, feature flag, leaderboard, shopping cart।
- উদাহরণ: Redis, Memcached, DynamoDB, etcd।
Document Store
Key-value-র উন্নত রূপ — value টা একটা structured document (JSON/BSON/XML), এবং database সেটার ভেতরটা বোঝে। ফলে document-এর field দিয়ে query ও index করা যায়।
- সুবিধা: flexible schema (প্রতিটা document আলাদা গঠনের হতে পারে), object-oriented কোডের সাথে স্বাভাবিকভাবে মেলে, একটা entity-র সব ডেটা একসাথে থাকায় read দ্রুত।
- সীমাবদ্ধতা: জটিল JOIN দুর্বল বা অনুপস্থিত; schema-হীনতা মানে বৈধতার দায় application-এর ঘাড়ে।
- ব্যবহার: content management, product catalog, user profile, event log।
- উদাহরণ: MongoDB, CouchDB, Firestore, Elasticsearch।
Wide Column Store
ডেটা row key ও column family দিয়ে সংগঠিত; প্রতিটা row-তে ভিন্ন ভিন্ন column থাকতে পারে (লক্ষ লক্ষ column পর্যন্ত)। ডিজাইনই করা হয়েছে বিশাল write throughput ও পাহাড়সমান ডেটার জন্য, এবং একাধিক datacenter-এ ছড়ানোর জন্য।
- সুবিধা: অসাধারণ write performance ও linear scalability, কোনো single point of failure নেই (masterless), tunable consistency।
- সীমাবদ্ধতা: query pattern আগে থেকে জেনে schema বানাতে হয় — এখানে ডেটা মডেল query-কে অনুসরণ করে, উল্টোটা নয়। Ad-hoc query কার্যত অসম্ভব।
- ব্যবহার: time-series ও IoT ডেটা, message history, activity feed, বিশাল আকারের logging।
- উদাহরণ: Cassandra, HBase, ScyllaDB, Bigtable।
Graph Databases
ডেটাকে node (entity) ও edge (সম্পর্ক) হিসেবে সংরক্ষণ করে, যেখানে সম্পর্ক নিজেই প্রথম শ্রেণির নাগরিক। যে query গুলো SQL-এ বহুস্তরের recursive JOIN দাবি করে, এখানে সেগুলো স্বাভাবিক ও দ্রুত।
- সুবিধা: গভীর সম্পর্ক traverse করা অত্যন্ত দ্রুত ("আমার বন্ধুর বন্ধুরা যারা এই শহরে থাকে"), সম্পর্কের মডেল স্বজ্ঞাত।
- সীমাবদ্ধতা: horizontal scaling কঠিন (graph কে ভাগ করা কঠিন), তুলনামূলকভাবে বিশেষায়িত ও কম প্রচলিত।
- ব্যবহার: social network, recommendation engine, fraud detection, knowledge graph, network topology।
- উদাহরণ: Neo4j, Amazon Neptune, ArangoDB।
পূর্ববর্তী: Load Balancers · পরবর্তী: Caching