পাঠ ৩০ · ৫১-এর মধ্যে · মডিউল ৮
Home / Courses / System Design / ভেক্টর ক্লক ও কজালিটি

ভেক্টর ক্লক ও কজালিটি ট্র্যাকিং

Vector clocks & causality tracking
৯ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • কেন ফিজিক্যাল টাইমস্ট্যাম্প ডিস্ট্রিবিউটেড সিস্টেমে ইভেন্টের ক্রম নির্ধারণের জন্য যথেষ্ট নয়
  • Vector Clock-এর তিনটি অপারেশন — local event, send, receive
  • দুটি ভেক্টর ক্লক তুলনা করে happened-before বনাম concurrent নির্ণয় করা
  • Python দিয়ে একটি সম্পূর্ণ ধাপে-ধাপে ২-নোড worked example রিপ্রোডিউস করা

১ · সমস্যা — গ্লোবাল ক্লক ছাড়া ক্রম নির্ধারণ

একটি একক মেশিনে ইভেন্টের ক্রম নির্ধারণ সহজ — একটি ঘড়ি, একটি টাইমলাইন। কিন্তু ডিস্ট্রিবিউটেড সিস্টেমে প্রতিটি নোডের নিজস্ব ফিজিক্যাল ঘড়ি আছে, এবং সেগুলো কখনোই নিখুঁতভাবে সিঙ্ক্রোনাইজড নয় (clock skew)। তাই নোড A-এর timestamp ১০০ ও নোড B-এর timestamp ১০১ থেকে এটা বলা যায় না যে A-এর ইভেন্টটি সত্যিই B-এর আগে ঘটেছিল — B-এর ঘড়ি হয়তো এগিয়ে আছে। আমাদের এমন একটি পদ্ধতি দরকার যা প্রকৃত কজালিটি (causality) — কোন ইভেন্ট কোনটির কারণ — ট্র্যাক করে, ফিজিক্যাল সময়ের ওপর নির্ভর না করে।

২ · Vector Clock — প্রতিটি নোডের নিজস্ব গণনা

Vector ClockVector Clockপ্রতিটি নোড একটি ভেক্টর (প্রতি নোডের জন্য একটি কাউন্টার) রাখে; লোকাল ইভেন্টে নিজের এন্ট্রি বাড়ে, মেসেজ পাঠালে পুরো ভেক্টর সংযুক্ত হয়, রিসিভ করলে elementwise-max নিয়ে নিজের এন্ট্রি বাড়ানো হয় — এভাবে দুটি ইভেন্টের মধ্যে প্রকৃত কার্যকারণ সম্পর্ক ট্র্যাক করা যায়। -এ প্রতিটি নোড $N$-দৈর্ঘ্যের একটি কাউন্টার-ভেক্টর রাখে (একটি এন্ট্রি প্রতি নোডের জন্য)। তিনটি নিয়ম:

Local Event
নোড নিজের এন্ট্রিটি ১ বাড়ায়। বাকি এন্ট্রি অপরিবর্তিত থাকে।
Send
মেসেজ পাঠানোর সময় পাঠানো নোডের বর্তমান পুরো ভেক্টরটি মেসেজের সাথে সংযুক্ত হয়।
Receive
নতুন ভেক্টর = নিজের ও প্রাপ্ত ভেক্টরের elementwise-max, তারপর নিজের এন্ট্রি ১ বাড়ানো হয়।
তুলনা করার নিয়ম

VC1 "happened-before" VC2 হয় যদি VC1-এর প্রতিটি এলিমেন্ট VC2-এর সংশ্লিষ্ট এলিমেন্টের ≤ হয়, এবং অন্তত একটি এলিমেন্ট কঠোরভাবে < হয়। যদি এমন হয় যে কোনো ভেক্টরই অন্যটিকে সম্পূর্ণভাবে dominate না করে (একটি এলিমেন্টে VC1 বড়, আরেকটিতে VC2 বড়) — তাহলে ইভেন্ট দুটি concurrent — অর্থাৎ তাদের মধ্যে কোনো কার্যকারণ সম্পর্ক নেই, তারা স্বাধীনভাবে ঘটেছে। concurrent মানেই একটি প্রকৃত conflict, যেমন Dynamo-স্টাইল সিস্টেমে দুটি ভিন্ন ক্লায়েন্ট একই কী-তে একসাথে ভিন্ন মান লিখলে।

৩ · Worked Example — ধাপে ধাপে

দুটি নোড A ও B কল্পনা করুন (index 0 = A, index 1 = B), উভয়ে [0, 0] দিয়ে শুরু করে।

  • ধাপ ১: A একটি লোকাল ইভেন্ট করে — A-এর ক্লক হয় [1, 0]।
  • ধাপ ২: B (স্বাধীনভাবে, A-এর থেকে সম্পূর্ণ বিচ্ছিন্নভাবে) একটি লোকাল ইভেন্ট করে — B-এর ক্লক হয় [0, 1]।
  • তুলনা ১: [1,0] বনাম [0,1] — কেউ কাউকে dominate করে না (১>০ কিন্তু ০<১) → CONCURRENT।
  • ধাপ ৩: A এখন B-কে একটি মেসেজ পাঠায়, নিজের ক্লক [1,0] সংযুক্ত করে।
  • ধাপ ৪: B মেসেজটি receive করে — নতুন ক্লক = elementwise-max([0,1], [1,0]) = [1,1], তারপর B নিজের এন্ট্রি (index ১) বাড়ায় → [1,2]।
  • তুলনা ২: A-এর ক্লক [1,0] বনাম B-এর receive-পরবর্তী ক্লক [1,2] — ১≤১ এবং ০≤২, এবং অন্তত একটি কঠোরভাবে ছোট (০<২) → A-এর ইভেন্ট B-এর receive-পরবর্তী স্টেটের happened-before।
Node A শুরু [0,0] local event [1,0] Node B শুরু [0,0] local event [0,1] receive(A→B) max→[1,1] +1 own →[1,2] message [1,0]
A-এর local event [1,0] ও B-এর local event [0,1] concurrent। A থেকে B-তে মেসেজ [1,0] পৌঁছানোর পর B-এর receive-পরবর্তী ক্লক [1,2] — যেখানে A-এর [1,0] happened-before।

৪ · Python দিয়ে যাচাই করা

নিচের কোডে local_event, receive, ও compare ফাংশন দিয়ে উপরের ধাপে-ধাপে উদাহরণটি হুবহু পুনর্গঠন করা হয়েছে।

Python
def local_event(vc, idx):
    new_vc = vc[:]
    new_vc[idx] += 1
    return new_vc

def receive(vc, idx, other_vc):
    merged = [max(a, b) for a, b in zip(vc, other_vc)]
    merged[idx] += 1
    return merged

def compare(vc1, vc2):
    if vc1 == vc2:
        return "equal"
    le = all(a <= b for a, b in zip(vc1, vc2))   # vc1 এর প্রতিটি এলিমেন্ট <= vc2
    ge = all(a >= b for a, b in zip(vc1, vc2))   # vc1 এর প্রতিটি এলিমেন্ট >= vc2
    if le:
        return "before"       # vc1 happened-before vc2
    if ge:
        return "after"        # vc1 happened-after vc2
    return "concurrent"


# index 0 = A, index 1 = B
A = [0, 0]
B = [0, 0]

A = local_event(A, 0)   # A করে একটি local event -> [1,0]
B = local_event(B, 1)   # B (স্বাধীনভাবে) করে একটি local event -> [0,1]

print("A:", A)
print("B:", B)

result1 = compare(A, B)
print("compare(A, B) ->", result1)

# A এখন B-কে মেসেজ পাঠায়, নিজের ক্লক [1,0] সংযুক্ত করে
message_from_A = A[:]
B_after_receive = receive(B, 1, message_from_A)   # max([0,1],[1,0])=[1,1] তারপর index1 +1 -> [1,2]

print()
print("A-এর পাঠানো মেসেজের ক্লক:", message_from_A)
print("B receive করার পর ক্লক:  ", B_after_receive)

result2 = compare(A, B_after_receive)
print("compare(A, B_after_receive) ->", result2)

print()
print("প্রত্যাশিত সিকোয়েন্স মিলছে কি:",
      result1 == "concurrent" and result2 == "before")

    
কোডটি কী প্রমাণ করছে

প্রথম তুলনায় compare(A, B) রিটার্ন করে "concurrent" — কারণ [1,0] ও [0,1]-এর মধ্যে একটিও অন্যটিকে dominate করে না। A থেকে B-তে মেসেজ পাঠানোর পর ও B রিসিভ করার পর, দ্বিতীয় তুলনায় compare(A, B_after_receive) রিটার্ন করে "before" — কারণ B-এর receive অপারেশন A-এর ভেক্টর absorb করে নিয়েছে, ফলে A-এর ঘটনাটি এখন B-এর নতুন স্টেটের কার্যকারণগতভাবে "আগে" প্রমাণিত হয়। এটাই ঠিক সেই ফলাফল যা ব্রিফের worked example অনুযায়ী প্রত্যাশিত — প্রথমে concurrent, তারপর before।

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

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

প্র ০১ Vector Clock দুটি ইভেন্ট concurrent শনাক্ত করার পর, সিস্টেম সেই conflict-টি স্বয়ংক্রিয়ভাবে সমাধান করে দেয় কি?

না — Vector Clock শুধু সনাক্ত করে যে দুটি লেখা concurrent (তাই একটি সত্যিকারের conflict), কিন্তু কোনটি "সঠিক" তা নিজে সিদ্ধান্ত নেয় না। এই সিদ্ধান্ত অ্যাপ্লিকেশনের ওপর ছেড়ে দেওয়া হয় — যেমন উভয় মান ব্যবহারকারীকে দেখিয়ে ম্যানুয়ালি বেছে নিতে বলা (Amazon-এর শপিং কার্টের ঐতিহাসিক পদ্ধতি), অথবা একটি নির্দিষ্ট নিয়ম (Last-Write-Wins) প্রয়োগ করা। L34-এ আমরা দেখব CRDT কীভাবে conflict স্বয়ংক্রিয়ভাবে, সামঞ্জস্যপূর্ণভাবে মার্জ করে দেয় — এমনকি ম্যানুয়াল হস্তক্ষেপ ছাড়াই।

প্র ০২ একটি ক্লাস্টারে নোডের সংখ্যা N হলে ভেক্টর ক্লকের আকার ঠিক কত হবে, এবং N অনেক বড় (যেমন কয়েক হাজার) হলে এটি কেন একটি বাস্তব সীমাবদ্ধতা হয়ে দাঁড়ায়?

ভেক্টর ক্লকের আকার ঠিক N (নোডের সংখ্যা)-এর সমান, কারণ প্রতিটি নোডের জন্য একটি এন্ট্রি লাগে। N যদি কয়েক হাজার হয়, তাহলে প্রতিটি মেসেজের সাথে হাজার-এন্ট্রি একটি ভেক্টর পাঠানো ও স্টোর করা যথেষ্ট ওভারহেড তৈরি করে — স্টোরেজ ও নেটওয়ার্ক ব্যান্ডউইথ উভয়ই ব্যয় হয়। বাস্তব বড় সিস্টেমে এই কারণে dotted version vector বা সীমিত সংখ্যক সাম্প্রতিক নোডের এন্ট্রি রাখার মতো অপ্টিমাইজেশন ব্যবহার করা হয়।

প্র ০৩ যদি A ও B-এর মধ্যে কখনো কোনো মেসেজ আদান-প্রদান না হতো (শুধু স্বাধীনভাবে লোকাল ইভেন্ট চলতেই থাকত), তাহলে তাদের ক্লকের তুলনার ফলাফল কী হতো, এবং কেন এটাই স্বাভাবিক?

কোনো মেসেজ আদান-প্রদান না হলে প্রতিটি নোডের ভেক্টরে শুধু নিজের এন্ট্রিই বাড়তে থাকবে, অন্যের এন্ট্রি সবসময় ০-তেই থেকে যাবে (যেমন A-এর ক্লক [5,0], B-এর [0,3])। তুলনা করলে সবসময় concurrent পাওয়া যাবে — এবং এটাই সঠিক, কারণ যতক্ষণ দুটি নোডের মধ্যে কোনো তথ্য প্রবাহিত না হয়, ততক্ষণ তাদের ইভেন্টের মধ্যে বাস্তবে কোনো কার্যকারণ সম্পর্কই তৈরি হয়নি — তারা সত্যিকার অর্থেই একে অপরের থেকে স্বাধীন।

অনুশীলন

  1. কোড বাড়ান: উপরের কোডের পরে, B (এখন [1,2]) A-কে একটি রিপ্লাই মেসেজ পাঠায়, এবং A সেটি receive করে। A-এর নতুন ক্লক কী হবে হাতে হিসাব করুন, তারপর কোডে receive(A, 0, B_after_receive) চালিয়ে মিলিয়ে দেখুন।

    A-এর বর্তমান ক্লক [1,0], B-এর পাঠানো ক্লক [1,2]। elementwise-max([1,0],[1,2]) = [1,2], তারপর A নিজের এন্ট্রি (index ০) বাড়ায় → [2,2]। এখন compare(B_after_receive, A_new) চেক করলে [1,2] বনাম [2,2] — ১≤২, ২≤২, অন্তত একটি কঠোরভাবে ছোট → B-এর receive-ইভেন্ট A-এর এই নতুন স্টেটের happened-before, যা স্বাভাবিক কারণ A এইমাত্র B-এর তথ্য absorb করেছে।

  2. চিন্তা করুন: একটি ৩-নোড সিস্টেমে (A, B, C) যদি A ও B একে অপরকে মেসেজ পাঠায় কিন্তু C সম্পূর্ণ বিচ্ছিন্ন থাকে, তাহলে C-এর ইভেন্টগুলো A ও B-এর ইভেন্টগুলোর সাথে তুলনা করলে কী ফলাফল আসবে?

    সবসময় concurrent — কারণ C-এর কোনো ইভেন্টের তথ্য কখনো A বা B-এর ভেক্টরে প্রবেশ করেনি (এবং উল্টোটাও), তাই কোনো কার্যকারণ সম্পর্ক তৈরি হয়নি। এটি দেখায় Vector Clock শুধু ফিজিক্যাল সময়ের নৈকট্য দিয়ে নয়, বরং প্রকৃত তথ্য-প্রবাহ (কে কার তথ্য "দেখেছে") দিয়ে কজালিটি সংজ্ঞায়িত করে।

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

আগের পাঠ
ইভেন্ট সোর্সিং ও CQRS