ভেক্টর ক্লক ও কজালিটি ট্র্যাকিং
এই পাঠে যা শিখবেন
- কেন ফিজিক্যাল টাইমস্ট্যাম্প ডিস্ট্রিবিউটেড সিস্টেমে ইভেন্টের ক্রম নির্ধারণের জন্য যথেষ্ট নয়
- 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$-দৈর্ঘ্যের একটি কাউন্টার-ভেক্টর রাখে (একটি এন্ট্রি প্রতি নোডের জন্য)। তিনটি নিয়ম:
নোড নিজের এন্ট্রিটি ১ বাড়ায়। বাকি এন্ট্রি অপরিবর্তিত থাকে।
মেসেজ পাঠানোর সময় পাঠানো নোডের বর্তমান পুরো ভেক্টরটি মেসেজের সাথে সংযুক্ত হয়।
নতুন ভেক্টর = নিজের ও প্রাপ্ত ভেক্টরের 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।
৪ · Python দিয়ে যাচাই করা
নিচের কোডে local_event, receive, ও compare ফাংশন দিয়ে উপরের ধাপে-ধাপে
উদাহরণটি হুবহু পুনর্গঠন করা হয়েছে।
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
পাওয়া যাবে — এবং এটাই সঠিক, কারণ যতক্ষণ দুটি নোডের মধ্যে কোনো তথ্য প্রবাহিত না হয়, ততক্ষণ তাদের ইভেন্টের
মধ্যে বাস্তবে কোনো কার্যকারণ সম্পর্কই তৈরি হয়নি — তারা সত্যিকার অর্থেই একে অপরের থেকে স্বাধীন।
অনুশীলন
-
কোড বাড়ান: উপরের কোডের পরে, 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 করেছে। -
চিন্তা করুন: একটি ৩-নোড সিস্টেমে (A, B, C) যদি A ও B একে অপরকে মেসেজ পাঠায় কিন্তু C সম্পূর্ণ বিচ্ছিন্ন থাকে, তাহলে C-এর ইভেন্টগুলো A ও B-এর ইভেন্টগুলোর সাথে তুলনা করলে কী ফলাফল আসবে?
সবসময় concurrent — কারণ C-এর কোনো ইভেন্টের তথ্য কখনো A বা B-এর ভেক্টরে প্রবেশ করেনি (এবং উল্টোটাও), তাই কোনো কার্যকারণ সম্পর্ক তৈরি হয়নি। এটি দেখায় Vector Clock শুধু ফিজিক্যাল সময়ের নৈকট্য দিয়ে নয়, বরং প্রকৃত তথ্য-প্রবাহ (কে কার তথ্য "দেখেছে") দিয়ে কজালিটি সংজ্ঞায়িত করে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫১টি পাঠ পরের পাঠ — কোরাম রিড/রাইট দিয়ে replica-দের মধ্যে consistency guarantee করা।
- রেপ্লিকেশন — Master-Slave ও Multi-Master L16 Multi-master রেপ্লিকেশনে কনফ্লিক্ট রেজোলিউশনের প্রয়োজনীয়তা আবার দেখুন।
- CRDT — কনফ্লিক্ট-ফ্রি রেপ্লিকেটেড ডেটা টাইপ L34 Concurrent writes স্বয়ংক্রিয়ভাবে, সামঞ্জস্যপূর্ণভাবে মার্জ করার কৌশল দেখুন।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics ও System Design — সব এক জায়গায়।