পাঠ ৩৪ · ৫১-এর মধ্যে · মডিউল ৮
Home / Courses / System Design / CRDT পরিচিতি

CRDT — কনফ্লিক্ট-ফ্রি রেপ্লিকেটেড ডেটা টাইপ পরিচিতি

Introduction to CRDTs
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • মাল্টি-মাস্টার রেপ্লিকেশনে কনকারেন্ট রাইট কনফ্লিক্ট কোথা থেকে আসে
  • CRDT-এর মূল গ্যারান্টি — merge কমিউটেটিভ, অ্যাসোসিয়েটিভ, আইডেম্পোটেন্ট কেন এত গুরুত্বপূর্ণ
  • G-Counter এর গঠন ও কাজ, এবং LWW-Register-এর সরলতা বনাম তার লিমিটেশন
  • Python-এ একটি G-Counter বানিয়ে যাচাই করা যে merge সত্যিই কমিউটেটিভ

১ · সমস্যা — কনকারেন্ট রাইট কে জেতে?

L16-এ দেখেছি মাল্টি-মাস্টার রেপ্লিকেশনে একাধিক নোড লেখা গ্রহণ করতে পারে — এটি রাইট অ্যাভেইলেবিলিটি বাড়ায়, কিন্তু একটি নতুন সমস্যা তৈরি করে: যদি রেপ্লিকা A ও রেপ্লিকা B একই সময়ে (নেটওয়ার্ক পার্টিশনের কারণে একে অপরকে না জেনে) একই ডেটায় ভিন্ন ভিন্ন আপডেট করে, তাহলে যখন তারা পরে সিঙ্ক হবে — কার আপডেট জিতবে?

একটি সহজ সমাধান হলো ম্যানুয়াল কনফ্লিক্ট রেজোলিউশন (ইউজারকে জিজ্ঞেস করা) বা একটি কেন্দ্রীয় সমন্বয়ক — কিন্তু এতে অ্যাভেইলেবিলিটি ও অটোমেশন দুটোই ক্ষতিগ্রস্ত হয়। CRDT একটি গাণিতিকভাবে গ্যারান্টিযুক্ত বিকল্প দেয়।

২ · CRDT — merge যেভাবে সবসময় একই ফলাফলে "কনভার্জ" করে

CRDTConflict-free Replicated Data Typeএমন একটি ডেটা স্ট্রাকচার যার merge অপারেশন কমিউটেটিভ (ক্রম গুরুত্বপূর্ণ নয়), অ্যাসোসিয়েটিভ (গ্রুপিং গুরুত্বপূর্ণ নয়) ও আইডেম্পোটেন্ট (একই মার্জ একাধিকবার চালালেও ফলাফল বদলায় না) — ফলে কনকারেন্ট আপডেট সবসময় একই সামঞ্জস্যপূর্ণ ফলাফলে কনভার্জ করে। -এর মূল আইডিয়া হলো ডেটা স্ট্রাকচারটি এমনভাবে ডিজাইন করা যাতে তার merge() ফাংশন সবসময় নিরাপদ হয় — কোন রেপ্লিকা "আগে" বা "পরে" মার্জ হলো তাতে কিছু যায় আসে না, ফলাফল সবসময় একই থাকবে।

কমিউটেটিভ
merge(A, B) = merge(B, A) — কোন রেপ্লিকার স্টেট আগে মার্জ হলো তা গুরুত্বপূর্ণ নয়।
অ্যাসোসিয়েটিভ
merge(merge(A,B),C) = merge(A, merge(B,C)) — কীভাবে গ্রুপ করে মার্জ করা হলো তাতে কিছু যায় আসে না।
আইডেম্পোটেন্ট
merge(A, A) = A — একই আপডেট (যেমন নেটওয়ার্ক রিট্রাইয়ের কারণে) দুবার প্রয়োগ হলেও কোনো ক্ষতি নেই।

৩ · G-Counter — একটি গ্রো-অনলি কাউন্টার CRDT

সবচেয়ে সহজ CRDT হলো G-Counter (grow-only counter) — যেমন একটি "মোট ভিউ কাউন্ট" যা শুধু বাড়ে, কখনো কমে না। প্রতিটি রেপ্লিকা নিজের ইনক্রিমেন্ট গোনে একটি পৃথক কাউন্টারে (একটি dict-এর মতো {replica_id: count}), যাতে দুটি রেপ্লিকা একে অপরের কাউন্টার সরাসরি না ছুঁয়ে স্বাধীনভাবে ইনক্রিমেন্ট করতে পারে।

  • increment(replica_id) — শুধু সেই রেপ্লিকার নিজস্ব কাউন্টার ১ বাড়ায়।
  • merge(state1, state2) — প্রতিটি replica_id-এর জন্য দুই স্টেটের elementwise max নেওয়া হয় (নিরাপদ, কারণ একটি রেপ্লিকার নিজস্ব কাউন্টার শুধু বাড়ে, তাই max নেওয়া কখনো তথ্য হারায় না)।
  • value() — সব রেপ্লিকার কাউন্ট যোগ করে মোট ভ্যালু বের করা হয়।
Replica 1 {r1: 3} Replica 2 {r2: 2} merge() elementwise max {r1:3, r2:2} value = 5
যে ক্রমেই merge করা হোক (merge(s1,s2) বা merge(s2,s1)) — চূড়ান্ত স্টেট ও ভ্যালু সবসময় একই।
Python
def increment(state, replica_id, amount=1):
    new_state = dict(state)
    new_state[replica_id] = new_state.get(replica_id, 0) + amount
    return new_state

def merge(state1, state2):
    keys = set(state1) | set(state2)
    return {k: max(state1.get(k, 0), state2.get(k, 0)) for k in keys}

def value(state):
    return sum(state.values())

# দুইটি রেপ্লিকা, প্রতিটি স্বাধীনভাবে ইনক্রিমেন্ট করছে
s1 = {}
s1 = increment(s1, "replica_1")
s1 = increment(s1, "replica_1")
s1 = increment(s1, "replica_1")   # replica_1 নিজে ৩ বার ইনক্রিমেন্ট করল

s2 = {}
s2 = increment(s2, "replica_2")
s2 = increment(s2, "replica_2")   # replica_2 নিজে ২ বার ইনক্রিমেন্ট করল

print(f"replica_1 এর স্টেট (merge-এর আগে): {s1}")
print(f"replica_2 এর স্টেট (merge-এর আগে): {s2}")

merged_1_then_2 = merge(s1, s2)
merged_2_then_1 = merge(s2, s1)

print(f"\nmerge(s1, s2) = {merged_1_then_2}  → value = {value(merged_1_then_2)}")
print(f"merge(s2, s1) = {merged_2_then_1}  → value = {value(merged_2_then_1)}")

assert merged_1_then_2 == merged_2_then_1, "কমিউটেটিভিটি ভেঙে গেছে!"
expected_total = 3 + 2
assert value(merged_1_then_2) == expected_total

print(f"\nদুই দিক থেকেই merge একই ফলাফল দিল (commutative) — মোট = {value(merged_1_then_2)} "
      f"(প্রত্যাশিত {expected_total} এর সাথে মিলেছে)")

    
লক্ষ্য করুন — merge(s1, s2) ও merge(s2, s1) দুটোই ঠিক একই ডিকশনারি {'replica_1': 3, 'replica_2': 2} দেয়, এবং value() উভয় ক্ষেত্রেই ৫ — যা মোট ইনক্রিমেন্ট সংখ্যার (৩+২) সমান। কোনো কেন্দ্রীয় সমন্বয়ক বা "কে আগে লিখেছে" জানার প্রয়োজন হয়নি।

৪ · LWW-Register — সরল কিন্তু নিখুঁত নয়

সব ডেটার জন্য G-Counter-এর মতো "শুধু বাড়ে" মডেল খাটে না — কখনো কখনো আমাদের একটি সাধারণ ভ্যালু (যেমন "ইউজারের প্রোফাইল নাম") সেট করতে হয়, যেখানে নতুন ভ্যালু পুরনোটিকে সম্পূর্ণ প্রতিস্থাপন করে। এর জন্য LWW-RegisterLast-Write-Wins Registerএকটি CRDT যেখানে প্রতিটি লেখাকে একটি টাইমস্ট্যাম্প দিয়ে ট্যাগ করা হয়, এবং merge সবসময় সবচেয়ে বেশি (পরে) টাইমস্ট্যাম্পযুক্ত ভ্যালুটি রাখে। ব্যবহৃত হয় — প্রতিটি রাইট একটি টাইমস্ট্যাম্প বহন করে, merge সবসময় সবচেয়ে নতুন টাইমস্ট্যাম্পের ভ্যালু রাখে।

LWW-Register-এর লুকানো খরচ

LWW-Register সহজ ও কার্যকর, কিন্তু এর একটি সমস্যা আছে — যদি দুটি রেপ্লিকা সত্যিই একই সময়ে কনকারেন্টলি ভিন্ন ভ্যালু লেখে, LWW নীরবে একটি লেখা বাতিল করে দেবে, এমনকি যদি সেটি ব্যবহারকারীর জন্য গুরুত্বপূর্ণ হয়। এটি L30-এর ভেক্টর ক্লকের বিপরীত দর্শন — ভেক্টর ক্লক অন্তত জানিয়ে দেয় যে দুটি ইভেন্ট কনকারেন্ট ছিল (মানুষ বা অ্যাপ লজিক তখন সিদ্ধান্ত নিতে পারে), যেখানে LWW স্বয়ংক্রিয়ভাবে সিদ্ধান্ত নিয়ে নেয় — সুবিধাজনক কিন্তু কখনো কখনো ডেটা নীরবে হারানোর ঝুঁকি নিয়ে।

মূল কথা · Key takeaway

CRDT কোনো জাদু নয় — এটি এমন ডেটা স্ট্রাকচার বেছে নেওয়ার একটি শৃঙ্খলা, যাদের merge গাণিতিকভাবে নিরাপদ। যেসব ডেটার প্রকৃতি এই merge-বান্ধব কাঠামোতে ফিট করে (কাউন্টার, সেট, সাধারণ রেজিস্টার), সেখানে CRDT কেন্দ্রীয় সমন্বয়ক বা ম্যানুয়াল কনফ্লিক্ট রেজোলিউশন ছাড়াই মাল্টি-মাস্টার অ্যাভেইলেবিলিটি দেয় — বিনিময়ে কখনো কখনো (LWW-এর ক্ষেত্রে) নীরব ডেটা-লস বা (G-Counter-এর ক্ষেত্রে) সীমিত অপারেশন সেটের ট্রেড-অফ মেনে নিতে হয়।

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

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

প্র ০১ G-Counter-এ merge কেন elementwise max ব্যবহার করে, elementwise sum নয়?

কারণ merge আইডেম্পোটেন্ট হতে হবে — একই স্টেট দুবার merge করলে ফলাফল বদলানো উচিত নয়। যদি sum ব্যবহার করা হতো, একই আপডেট দুবার merge হলে (নেটওয়ার্ক রিট্রাই বা একই মেসেজ দুবার ডেলিভারির কারণে, যা বাস্তবে ঘটতেই পারে) মোট ভ্যালু ভুলভাবে দ্বিগুণ হয়ে যেত। max নেওয়া নিরাপদ কারণ প্রতিটি রেপ্লিকার নিজস্ব কাউন্টার শুধু বাড়ে (কখনো কমে না) — তাই একই স্টেট বারবার merge করলেও max অপরিবর্তিত থাকে (merge(A,A) = A)।

প্র ০২ একটি "PN-Counter" (যা বাড়তেও পারে, কমতেও পারে — যেমন একটি "লাইক কাউন্ট" যেখানে আনলাইকও সম্ভব) কীভাবে G-Counter-এর ধারণা দিয়ে বানানো যেতে পারে?

একটি PN-Counter আসলে দুটি আলাদা G-Counter দিয়ে বানানো হয় — একটি "P" (positive, ইনক্রিমেন্ট গোনে) এবং একটি "N" (negative, ডিক্রিমেন্ট গোনে)। চূড়ান্ত ভ্যালু হলো value(P) - value(N)। যেহেতু P ও N দুটোই আলাদাভাবে গ্রো-অনলি (শুধু বাড়ে), প্রতিটি স্বাধীনভাবে G-Counter-এর মতো elementwise-max দিয়ে নিরাপদে merge করা যায় — এবং তাদের বিয়োগফল একটি সম্পূর্ণ কার্যকরী কনফ্লিক্ট-ফ্রি বাড়া-কমা কাউন্টার তৈরি করে।

প্র ০৩ কেন CRDT গসিপ প্রোটোকলের (L32) সাথে স্বাভাবিকভাবে ভালো মেলে — দুটো একসাথে ব্যবহার করার কী সুবিধা?

গসিপ কোনো নির্দিষ্ট ক্রমে বা নির্দিষ্ট সময়ে স্টেট ছড়ায় না — বিভিন্ন নোড বিভিন্ন সময়ে বিভিন্ন ক্রমে একে অপরের সাথে সিঙ্ক করে, এবং একই স্টেট একাধিকবার merge হতে পারে। যেহেতু CRDT-এর merge কমিউটেটিভ, অ্যাসোসিয়েটিভ ও আইডেম্পোটেন্ট, গসিপের এই "যেকোনো ক্রমে, যেকোনো বার" স্বভাবের সাথে এটি নিখুঁতভাবে খাপ খায় — কোন নোড কার সাথে কখন গসিপ করলো তা নিয়ে কোনো চিন্তা না করেই, সব নোড শেষ পর্যন্ত একই সঠিক স্টেটে কনভার্জ করবে এই গ্যারান্টি থাকে।

অনুশীলন

  1. বিস্তৃত করুন: উপরের কোড সেলে একটি তৃতীয় রেপ্লিকা replica_3 যোগ করুন যা ৪ বার ইনক্রিমেন্ট করে, তারপর তিনটি স্টেটকে বিভিন্ন ক্রমে merge করে দেখুন ফলাফল একই থাকে কি না।

    s3 তৈরি করে ৪ বার ইনক্রিমেন্ট করুন, তারপর merge(merge(s1,s2),s3) এবং merge(s1,merge(s2,s3)) দুটোই হিসাব করুন — অ্যাসোসিয়েটিভিটির কারণে দুটোই {'replica_1':3,'replica_2':2,'replica_3':4} দেবে এবং value() = ৯ (৩+২+৪) হবে, যেভাবেই গ্রুপ করে merge করা হোক না কেন।

  2. চিন্তা করুন: একটি শপিং কার্ট সিস্টেমে "কার্টে থাকা আইটেমসমূহ" ডেটার জন্য কি G-Counter, LWW-Register, নাকি একটি "OR-Set" (Observed-Remove Set, শুধু উল্লেখ) জাতীয় CRDT সবচেয়ে উপযুক্ত হবে? কেন?

    একটি শপিং কার্টে আইটেম যোগ ও অপসারণ দুটোই দরকার (শুধু বাড়ে না, তাই G-Counter অনুপযুক্ত), এবং LWW একটি পুরো সেট প্রতিস্থাপন করলে কনকারেন্টলি দুটি ভিন্ন ডিভাইস থেকে যোগ করা দুটি ভিন্ন আইটেম হারিয়ে যেতে পারে। এই কারণেই বাস্তব সিস্টেম (যেমন Amazon-এর বিখ্যাত Dynamo পেপারে বর্ণিত শপিং কার্ট) একটি সেট-ভিত্তিক CRDT (OR-Set-এর মতো) ব্যবহার করে, যা যোগ ও অপসারণ উভয়ই ট্র্যাক করে এবং কনকারেন্ট যোগ-অপসারণকে নিরাপদে মার্জ করতে পারে — কোনো আইটেম নীরবে হারানোর ঝুঁকি ছাড়াই।

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

আগের পাঠ
ব্লুম ফিল্টার