পাঠ ১৬ · ৫১-এর মধ্যে · মডিউল ৪
Home / Courses / System Design / রেপ্লিকেশন

রেপ্লিকেশন — Master-Slave ও Multi-Master

Replication — master-slave & multi-master
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • রেপ্লিকেশনের মূল উদ্দেশ্য — এভেইলেবিলিটি, রিড-স্কেলিং, ডিউরেবিলিটি
  • Master-Slave বনাম Multi-Master আর্কিটেকচারের ট্রেড-অফ
  • Synchronous বনাম Asynchronous রেপ্লিকেশন এবং তাদের স্পিড/সেফটি ট্রেড-অফ
  • Python দিয়ে রেপ্লিকেশন ল্যাগ সিমুলেট করে দেখা — রাইটের ঠিক পরে রিপ্লিকা কেন স্টেল ডেটা দেয়

১ · রেপ্লিকেশন কেন দরকার

রেপ্লিকেশনReplicationএকই ডেটার একাধিক কপি একাধিক মেশিনে সংরক্ষণ করা — একটি মেশিন ব্যর্থ হলেও ডেটা হারিয়ে না যায়, এবং রিড রিকোয়েস্ট একাধিক মেশিনে ভাগ করে দেওয়া যায়। একটিমাত্র ডেটাবেস সার্ভারের দুটি বড় সমস্যা আছে — সেটি ক্র্যাশ করলে পুরো সিস্টেম ডাউন হয়ে যায় (একক ব্যর্থতার বিন্দু), এবং সব রিড ও রাইট রিকোয়েস্ট একটিমাত্র মেশিনে চাপ তৈরি করে। রেপ্লিকেশন একই ডেটার একাধিক কপি রেখে এই দুটো সমস্যাই সমাধান করার চেষ্টা করে।

২ · Master-Slave (Primary-Replica) রেপ্লিকেশন

এই মডেলে একটি প্রাইমারি (মাস্টার) সার্ভার সব WRITE গ্রহণ করে, এবং একাধিক রিপ্লিকা (স্লেভ) সার্ভার প্রাইমারির পরিবর্তনগুলো কপি করে নিয়ে শুধু READ সার্ভ করে।

সুবিধা
রিড লোড একাধিক রিপ্লিকার মধ্যে ভাগ করা যায় — উচ্চ read:write অনুপাতের (L02) সিস্টেমে খুবই কার্যকর।
সীমাবদ্ধতা
সব রাইট একটিমাত্র প্রাইমারিতে যায় — রাইট থ্রুপুট একটি মেশিনের ক্ষমতায় বটলনেক হয়ে থাকে।
রেপ্লিকেশন ল্যাগ
প্রাইমারি ও রিপ্লিকার মধ্যে সিঙ্কে সময় লাগে — এই ফাঁকে একটি সাময়িক কনসিস্টেন্সি গ্যাপ তৈরি হয়।

৩ · Multi-Master রেপ্লিকেশন

এই মডেলে একাধিক নোড রাইট গ্রহণ করতে পারে — কোনো একক প্রাইমারি নেই। এটি রাইট এভেইলেবিলিটি বাড়ায় (একটি নোড ডাউন থাকলেও অন্য নোডে লেখা যায়), কিন্তু একটি নতুন সমস্যা তৈরি করে — যদি দুটো ভিন্ন নোডে একই ডেটার উপর একসাথে ভিন্ন রাইট হয়, তাহলে সেই কনফ্লিক্ট কীভাবে মেটানো হবে? এই প্রশ্নের উত্তর পরে L30 (ভেক্টর ক্লক) ও L34 (CRDT)-এ বিস্তারিত দেখব।

ক্লায়েন্ট Client WRITE প্রাইমারি Primary (Master) async replicate() রিপ্লিকা ১ Replica 1 (READ) রিপ্লিকা ২ Replica 2 (READ) অন্যান্য READ clients
সব WRITE প্রাইমারিতে যায়; রিপ্লিকাগুলো একটু পরে (async) সিঙ্ক হয় এবং READ ট্রাফিক সার্ভ করে।

৪ · Sync বনাম Async রেপ্লিকেশন

Synchronous রেপ্লিকেশনSynchronous Replicationপ্রাইমারি ক্লায়েন্টকে "সফল" বলার আগে অন্তত একটি (বা সব) রিপ্লিকার কাছ থেকে লেখা সম্পন্ন হওয়ার নিশ্চয়তা (ack) পাওয়া পর্যন্ত অপেক্ষা করে — নিরাপদ কিন্তু ধীর। -এ প্রাইমারি ক্লায়েন্টকে "সফল" জানানোর আগে রিপ্লিকা থেকে নিশ্চয়তা পাওয়া পর্যন্ত অপেক্ষা করে — নিরাপদ (কোনো ডেটা হারানোর ঝুঁকি কম) কিন্তু ধীর। অন্যদিকে Asynchronous রেপ্লিকেশনAsynchronous Replicationপ্রাইমারি লেখার সাথে সাথেই ক্লায়েন্টকে "সফল" জানিয়ে দেয়, রিপ্লিকাকে ব্যাকগ্রাউন্ডে পরে সিঙ্ক করে — দ্রুত কিন্তু সাময়িক ইনকনসিস্টেন্সি ও প্রাইমারি ক্র্যাশ করলে ডেটা হারানোর সামান্য ঝুঁকি থাকে। -এ প্রাইমারি সাথে সাথেই "সফল" জানিয়ে দেয়, রিপ্লিকা পরে ব্যাকগ্রাউন্ডে সিঙ্ক হয় — দ্রুত কিন্তু ঝুঁকিপূর্ণ, কারণ প্রাইমারি ক্র্যাশ করলে সিঙ্ক না হওয়া ডেটা হারিয়ে যেতে পারে।

৫ · কোড দিয়ে প্রমাণ — রেপ্লিকেশন ল্যাগ

নিচে একটি প্রাইমারি dict ও একটি রিপ্লিকা dict সিমুলেট করা হলো। write() সাথে সাথেই প্রাইমারি আপডেট করে, কিন্তু রিপ্লিকা আপডেট হয় শুধুমাত্র একটি আলাদা, পরবর্তী replicate() কল হলে (async রেপ্লিকেশনের মতো) — দেখা যাক রাইটের ঠিক পরে রিপ্লিকা থেকে পড়লে কী পাওয়া যায়।

Python
primary = {}
replica = {}
pending_replication = []  # প্রাইমারিতে লেখা হয়েছে কিন্তু রিপ্লিকায় এখনও যায়নি

def write(key, value):
    primary[key] = value
    pending_replication.append((key, value))
    print(f"[WRITE] প্রাইমারিতে লেখা হলো: {key} = {value}")

def replicate():
    while pending_replication:
        key, value = pending_replication.pop(0)
        replica[key] = value
    print("[REPLICATE] সব পেন্ডিং পরিবর্তন রিপ্লিকায় সিঙ্ক হয়ে গেছে")

def read_from_primary(key):
    return primary.get(key, "❌ পাওয়া যায়নি")

def read_from_replica(key):
    return replica.get(key, "❌ পাওয়া যায়নি (এখনও replicate() হয়নি)")

# ধাপ ১: প্রাইমারিতে লেখা
write("balance:user42", 500)

print("\n--- replicate() চালানোর আগে ---")
print("প্রাইমারি থেকে read:", read_from_primary("balance:user42"))
print("রিপ্লিকা থেকে read :", read_from_replica("balance:user42"))

# ধাপ ২: এখন async replicate() চালানো হলো
print()
replicate()

print("\n--- replicate() চালানোর পর ---")
print("প্রাইমারি থেকে read:", read_from_primary("balance:user42"))
print("রিপ্লিকা থেকে read :", read_from_replica("balance:user42"))

    
লক্ষ্য করুন — replicate() চালানোর আগে প্রাইমারি থেকে সঠিক মান (500) পাওয়া গেলেও রিপ্লিকা থেকে "পাওয়া যায়নি" বার্তা আসে (স্টেল/অনুপস্থিত ডেটা)। replicate() চালানোর পর দুটো থেকেই একই সঠিক মান পাওয়া যায়। এটাই বাস্তব async master-slave সিস্টেমে রেপ্লিকেশন ল্যাগ — একটি সাময়িক কিন্তু বাস্তব কনসিস্টেন্সি গ্যাপ যা ইঞ্জিনিয়ারদের হিসেবে রাখতে হয় (উদাহরণ: "লিখেই সাথে সাথে একটি রিড-রিপ্লিকা থেকে নিজের ডেটা পড়লে পুরনো তথ্য দেখা যেতে পারে")।
মূল কথা · Key takeaway

রেপ্লিকেশন এভেইলেবিলিটি ও রিড-স্কেলিং দেয়, কিন্তু বিনামূল্যে নয় — sync রেপ্লিকেশনে স্পিড ছেড়ে সেফটি পাওয়া যায়, async রেপ্লিকেশনে স্পিড পাওয়া যায় কিন্তু একটি সাময়িক কনসিস্টেন্সি গ্যাপ (রেপ্লিকেশন ল্যাগ) মেনে নিতে হয়। এই ট্রেড-অফ সরাসরি L04-এর CAP থিওরেম ও কনসিস্টেন্সি মডেলের সাথে যুক্ত।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ একটি ইউজার তার প্রোফাইল আপডেট করার সাথে সাথেই সেই একই আপডেট করা তথ্য দেখতে চায় — কিন্তু আপনার সিস্টেম async master-slave রেপ্লিকেশন ব্যবহার করে। এই সমস্যার কী সমাধান হতে পারে?

একটি সাধারণ প্যাটার্ন হলো "read your own writes" — যে ইউজার সবেমাত্র লিখেছে, তার পরবর্তী কিছু সময়ের রিড রিকোয়েস্ট সাময়িকভাবে প্রাইমারি থেকে সার্ভ করা (রিপ্লিকা থেকে নয়), অথবা রাইট সম্পন্ন হওয়ার সাথে ক্লায়েন্ট-সাইডে একটি "স্টিকি" মার্কার রাখা যতক্ষণ না নিশ্চিত হওয়া যায় রিপ্লিকেশন ক্যাচ-আপ করেছে। এটি sync রেপ্লিকেশনের পুরো খরচ না নিয়েও একটি ভালো ইউজার এক্সপেরিয়েন্স দেয়।

প্র ০২ Multi-master রেপ্লিকেশন একক প্রাইমারির রাইট-বটলনেক সমস্যা সমাধান করে, তাহলে সবসময় multi-master ব্যবহার না করার কারণ কী?

Multi-master একটি নতুন, প্রায়ই আরও জটিল সমস্যা তৈরি করে — কনফ্লিক্ট রেজোলিউশন। দুটো ভিন্ন নোডে একই রেকর্ডে একসাথে ভিন্ন রাইট হলে কোনটি "জিতবে" তা ঠিক করতে অতিরিক্ত লজিক (vector clocks, L30; CRDT, L34) দরকার হয়, যা সিস্টেমের জটিলতা ও অপারেশনাল খরচ বাড়ায়। তাই বেশিরভাগ সিস্টেম যেখানে সম্ভব সরল master-slave ব্যবহার করে, শুধু যেখানে রাইট এভেইলেবিলিটি সত্যিই সংকটজনক (যেমন মাল্টি-রিজিয়ন ডেপ্লয়মেন্ট) সেখানেই multi-master বেছে নেয়।

প্র ০৩ রেপ্লিকেশন (L16) ও শার্ডিং (L17) — এই দুটো ধারণা কীভাবে আলাদা, যদিও দুটোই "একাধিক ডেটাবেস মেশিন" নিয়ে কাজ করে?

রেপ্লিকেশনে প্রতিটি মেশিনে একই সম্পূর্ণ ডেটার একটি কপি থাকে — উদ্দেশ্য এভেইলেবিলিটি ও রিড-স্কেলিং। শার্ডিং-এ (L17) প্রতিটি মেশিনে ডেটার ভিন্ন ভিন্ন অংশ থাকে — উদ্দেশ্য বিশাল ডেটাসেটকে ভাগ করে স্টোরেজ ও রাইট থ্রুপুট স্কেল করা। বাস্তব বড় সিস্টেমে সাধারণত দুটোই একসাথে ব্যবহৃত হয় — ডেটা শার্ড করা হয়, এবং প্রতিটি শার্ড নিজেও রেপ্লিকেটেড থাকে।

অনুশীলন

  1. চিন্তা করুন: একটি নিউজ ওয়েবসাইট (উচ্চ read:write অনুপাত, খুব কম রাইট) ও একটি স্টক-ট্রেডিং সিস্টেম (রাইট-এর সঠিকতা অত্যন্ত গুরুত্বপূর্ণ) — কোনটির জন্য async master-slave এবং কোনটির জন্য sync রেপ্লিকেশন বেশি উপযুক্ত মনে হয়?

    নিউজ ওয়েবসাইটের জন্য async master-slave যথেষ্ট — কিছু সেকেন্ডের রেপ্লিকেশন ল্যাগ (একজন পাঠক সামান্য পুরনো ভিউ কাউন্ট দেখল) তেমন ক্ষতিকর নয়, আর দ্রুত রাইট ভালো ইউজার এক্সপেরিয়েন্স দেয়। স্টক-ট্রেডিং সিস্টেমে sync রেপ্লিকেশন (অন্তত একটি রিপ্লিকার নিশ্চয়তা পাওয়া পর্যন্ত অপেক্ষা) বেশি নিরাপদ, কারণ একটি ট্রেড "সফল" বলে জানানোর পর তা হারিয়ে যাওয়া আর্থিকভাবে মারাত্মক ক্ষতির কারণ হতে পারে।

  2. কোড পরিবর্তন করুন: উপরের কোড সেলে write("balance:user42", 500)-এর পরপরই, replicate() কল করার আগে, আরেকটি write("balance:user99", 200) যোগ করুন। replicate() চালানোর পর replica dict-এ কী কী key/value থাকবে বলে মনে হয়?

    replicate() চালানোর পর replica-তে উভয় এন্ট্রিই থাকবে — {"balance:user42": 500, "balance:user99": 200} — কারণ pending_replication লিস্টে দুটো লেখাই জমা ছিল, এবং replicate() লিস্ট খালি না হওয়া পর্যন্ত সবগুলো এন্ট্রি (FIFO ক্রমে) রিপ্লিকায় প্রয়োগ করে। এটি দেখায় async রেপ্লিকেশন একটি ব্যাচে একাধিক পেন্ডিং পরিবর্তন একসাথে সিঙ্ক করতে পারে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
ডেটাবেস ইনডেক্সিং ও কোয়েরি অপ্টিমাইজেশন