Foundationপ্রথম নীতি থেকে
LEVEL 9লেসন ৩/১৯কঠিন৪৮ মিনিট

Physical Time — NTP, Clock Skew, আর কেন Wall Clock বিশ্বাস করা যায় না

Physical Time: NTP, Clock Skew, and Why Wall Clocks Lie

Physical clock কখনো নিখুঁতভাবে sync হয় না — NTP bound দেয়, শূন্য করে না; সেই bounded skew LWW-তে silent lost-write জন্মায়, আর elapsed-time মাপতে হলে wall clock নয়, monotonic clock লাগে।

এই লেসন শেষে আপনি পারবেন

  • প্রতিটা মেশিনের নিজস্ব crystal oscillator কেন ভিন্ন গতিতে চলে আর সেই drift-এর মাত্রা (দিনে ms-scale) হিসাব করে বলতে পারবেন কেন sync ছাড়া physical clock বিশ্বাসযোগ্য নয়
  • NTP-র চার-timestamp exchange থেকে offset আর delay-র formula হাতে-কলমে derive করে সংখ্যা বসিয়ে যাচাই করতে পারবেন, আর stratum hierarchy আর slew/step correction-এর পার্থক্য ব্যাখ্যা করতে পারবেন
  • Skewed clock দুটো machine-এ last-write-wins ব্যবহার করলে কীভাবে wall-clock-এ পরের write silently হেরে যায় তা concrete সংখ্যা দিয়ে trace করে দেখাতে পারবেন
  • Monotonic clock আর wall clock-এর সংজ্ঞাগত পার্থক্য বলে কোনটা timeout/lease/elapsed-time মাপায় সঠিক আর কোনটায় NTP step negative-elapsed bug জন্মায় তা ব্যাখ্যা করতে পারবেন
  • Spanner-এর TrueTime uncertainty-interval দর্শনকে single-timestamp illusion-এর বিকল্প হিসেবে চিনে বলতে পারবেন কেন commit-wait দরকার হয়

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আগের লেসনে দেখেছি একটা process অন্য process crashed নাকি শুধু ধীর তা বাইরে থেকে বলার উপায় নেই — failure detection সবসময় timeout-নির্ভর সন্দেহ। আজ প্রশ্নটা আরেক ধাপ গোড়ায়: সেই timeout মাপার ঘড়িটা নিজেই কতটা বিশ্বাসযোগ্য?

ধরুন দুইটা machine একই সেকেন্ডে দুইটা লেখা পাঠাল একই key-তে —

machine-1 বলছে: "আমি লিখেছি 10:00:00.000-এ"
machine-2 বলছে: "আমি লিখেছি 09:59:59.997-এ"

কে সত্যিই পরে লিখেছে? Naive উত্তর: timestamp দেখো, বড়টা পরে। কিন্তু machine-2-র ঘড়ি machine-1-এর চেয়ে যদি 10ms পিছিয়ে থাকে, তাহলে machine-2 আসলে machine-1-এর পরে লিখেছে — timestamp তার উল্টো গল্প বলছে। এটা কোনো bug নয়, এটা প্রতিটা distributed system-এর বাস্তবতা: physical clock কখনো নিখুঁতভাবে sync হয় না।

আজকের কাজ তিনটা। এক: কেন — প্রতিটা মেশিনের ঘড়ি নিজের গতিতে চলে, আর সেই গতি sync করার প্রধান হাতিয়ার (NTP) ঠিক কী দেয়, কী দেয় না। দুই: সেই অসম্পূর্ণতার concrete দাম — last-write-wins-এ silent data loss, সংখ্যা দিয়ে। তিন: কোন কাজে ভুল ঘড়ি ব্যবহার করা এড়ানো যায় (monotonic clock), আর সবচেয়ে সাবধানী system কী করে (TrueTime-এর ইঙ্গিত)। ভরসাযোগ্য global ordering-এর পূর্ণ সমাধান — logical clock — পরের লেসনের বিষয়; আজ শুধু বোঝা যাক কেন physical clock একা যথেষ্ট নয়।

মূল ধারণা

ঘড়ি কেন একা ভুল করে — crystal drift

প্রতিটা কম্পিউটারের ভেতরে একটা quartz crystal oscillator নির্দিষ্ট frequency-তে কাঁপে (সাধারণত ৩২,৭৬৮ Hz-এর multiple); সেই কাঁপুনি গুনেই OS সেকেন্ড বানায়। কিন্তু manufacturing tolerance, তাপমাত্রা, বয়স — সব মিলিয়ে দুইটা crystal কখনোই ঠিক একই frequency-তে কাঁপে না। ফলাফল: drift — uncorrected ঘড়ি প্রতি দিনে কয়েক থেকে কয়েকশো মিলিসেকেন্ড এগিয়ে বা পিছিয়ে যায় (সস্তা hardware-এ ~20 ppm মানে দিনে ~1.7 সেকেন্ড পর্যন্ত)। Sync ছাড়া রেখে দিলে সপ্তাহখানেকে মেশিনগুলোর ঘড়ি সেকেন্ড-scale-এ আলাদা হয়ে যায়।

NTP — চার timestamp দিয়ে offset মাপা

Client একটা request পাঠায়, server উত্তর দেয় — চারটা মুহূর্ত রেকর্ড হয়:

t0 : client পাঠাল       (client-এর নিজের ঘড়িতে)
t1 : server পেল          (server-এর নিজের ঘড়িতে)
t2 : server উত্তর পাঠাল  (server-এর নিজের ঘড়িতে)
t3 : client উত্তর পেল    (client-এর নিজের ঘড়িতে)

দুইটা অজানা রাশি — ঘড়ির offset (θ = server ঘড়ি − client ঘড়ি) আর নেটওয়ার্কের round-trip delay (δ)। ধরে নেওয়া হয় যাওয়া-আসার delay সমান (symmetric); এই একটা ধারণা থেকেই দুইটা সমীকরণ বের হয়:

offset ≈ ((t1 − t0) + (t2 − t3)) / 2
delay  ≈ (t3 − t0) − (t2 − t1)

স্বজ্ঞা: (t1−t0) হলো forward-leg-এ observed gap (আসল delay + offset মিশে আছে); (t2−t3) হলো backward-leg-এর gap (delay − offset, উল্টো চিহ্নে)। যোগ করলে delay বাতিল, offset বেরিয়ে আসে; বিয়োগ করলে offset বাতিল, delay বেরিয়ে আসে। hood সেকশনে সংখ্যা বসিয়ে হাতে-কলমে verify করব।

Stratum hierarchy — কে কার থেকে সময় ধার নেয়

stratum 0 : atomic clock / GPS receiver         (hardware, নেটওয়ার্কে নেই)
stratum 1 : সরাসরি stratum-0-এর সাথে সংযুক্ত সার্ভার
stratum 2 : stratum-1 থেকে sync নেয়
stratum 3 : stratum-2 থেকে ...                  (cascade চলতে থাকে)

উপরের দিকে accuracy বেশি (stratum 1 সাধারণত sub-microsecond-এর কাছাকাছি atomic reference-এর সাথে), নিচের দিকে accuracy কমে কারণ প্রতিটা hop নতুন নেটওয়ার্ক-delay-জনিত error যোগ করে। বাস্তব সংখ্যা: একই LAN-এ sync সাধারণত sub-millisecond থেকে কয়েক ms নির্ভুল; WAN-এ (ভিন্ন datacenter, ভিন্ন মহাদেশ) কয়েক দশ ms পর্যন্ত ভুল থাকতে পারে — variable network path আর queueing delay-র কারণে symmetric-delay ধারণাটাই দুর্বল হয়ে যায়।

Discipline — slew বনাম step, আর কেন step বিপজ্জনক

Estimated offset পেলে ঘড়ি ঠিক করার দুইটা উপায়:

Modeকরেঝুঁকি
Slewঘড়ির গতি সাময়িক কমিয়ে/বাড়িয়ে ধীরে ধীরে টার্গেটে আনাধীর — বড় offset সারতে সময় লাগে
Stepঘড়িকে এক ধাক্কায় নতুন মানে বসিয়ে দেওয়াতাৎক্ষণিক — কিন্তু ঘড়ি পেছনেও যেতে পারে

ছোট offset-এ NTP daemon সাধারণত slew করে — গতি সামান্য বদলে ধীরে কাছে আনে, ঘড়ি কখনো পেছায় না। কিন্তু বড় offset (boot-এর পরে প্রথম sync, বা ঘড়ি বহুদিন unsync) থাকলে step করে — সরাসরি জাম্প। যদি সত্যিকারের সময় ঘড়ির-দেখানো সময়ের চেয়ে পেছনে থাকে, তাহলে সেই step wall clock-কে পেছনে ঠেলে দেয় — এইমাত্র read() = 10:00:05 দেখানো ঘড়ি পরের মুহূর্তে 10:00:02 দেখাতে পারে। elapsed-time মাপা যেকোনো কোড (end - start) তখন negative সংখ্যা পায় — hood সেকশনে ঠিক এই bug কোড দিয়ে দেখাব।

ভেতরে কী ঘটছে

Clock skew-র concrete দাম — LWW silently ভুল write রাখে

অনেক replicated store conflict resolve করে সবচেয়ে সহজ নিয়মে: last-write-wins (LWW) — যে write-এর timestamp সবচেয়ে বড়, সে জেতে, বাকিগুলো ফেলে দেওয়া হয়। সমস্যা: timestamp আসে লেখা-machine-এর নিজের wall clock থেকে, আর সেই ঘড়ি skewed হতে পারে।

দৃশ্য কল্পনা করুন: machine M1-এর ঘড়ি নিখুঁত (offset 0)। Machine M2-এর ঘড়ি সত্যিকারের সময়ের চেয়ে ১০০ms পিছিয়ে (একটা ছোট, বাস্তব skew — NTP এই মাত্রা সহজেই ঘটাতে পারে যদি sync সবে হয়েছে বা path asymmetric)।

সত্যিকারের ঘটনাক্রম (real time, ms):
  τ=1000 : M1 লেখে "A"   (M1-এর ঘড়ি নিখুঁত)        → timestamp 1000
  τ=1050 : M2 লেখে "B"   (৫০ms পরে, real time-এ!)
           কিন্তু M2-র ঘড়ি ১০০ms পিছিয়ে              → timestamp 950

naive LWW: max(1000, 950) = 1000  →  "A" জেতে

B সত্যিকারের সময়ে A-র পরে লেখা হয়েছিল — user-এর perspective-এ B হলো সর্বশেষ ইচ্ছা। কিন্তু M2-র ঘড়ি পিছিয়ে থাকায় তার timestamp ছোট, LWW A-কেই রাখে — নীরবে, কোনো error, কোনো conflict-flag, কোনো log-এ warning ছাড়াই। এটাই সেই “LWW clock-skew anomaly” যা quorum-replication লেসনের build-অংশে নাম-উল্লেখ ছিল — এখানেই তার পুরো anatomy। experiment/build সেকশনে ঠিক এই সংখ্যাগুলো কোডে চালিয়ে দেখাব।

Monotonic clock বনাম wall clock — ভুল যন্ত্র বাছলে কী হয়

দুই ধরনের ঘড়ি OS প্রতিটা প্রোগ্রামকে দেয়:

Wall clock (time.time())Monotonic clock (time.monotonic())
মানে কীcalendar-time-এর কাছাকাছি একটা মানকোনো এক reference point থেকে গণনা — মান নিজে অর্থহীন
NTP step-এলাফাতে পারে (সামনে বা পেছনে)কখনো লাফায় না, কখনো পেছায় না
কাজtimestamp/log/audit-trailelapsed time, timeout, lease expiry
দুই machine-এ তুলনাintent (কিন্তু skew-সহ)অর্থহীন — শুধু নিজের machine-এর ভেতরে ব্যবধান বৈধ

Elapsed-time মাপতে wall clock ব্যবহার করলে এই bug জন্মায়:

import time

start = time.time()          # ভুল যন্ত্র — wall clock
do_slow_work()                # এর মাঝখানে NTP একটা বড় step করল, ঘড়ি ৩ সেকেন্ড পেছাল
elapsed = time.time() - start
print(elapsed)                # হতে পারে: -2.87   ← ঋণাত্মক সময়!

Timeout logic, rate-limiter, lease-expiry — সবকিছু ধরে নেয় elapsed >= 0; negative মান পেলে অনেক বাস্তব bug ঘটে (timeout কখনো trigger না হওয়া, lease চিরকাল বৈধ মনে হওয়া, ইত্যাদি)। সঠিক কোড:

start = time.monotonic()      # ঠিক যন্ত্র — কখনো পেছায় না
do_slow_work()
elapsed = time.monotonic() - start
print(elapsed)                # সবসময় >= 0, guarantee-সহ

নিয়ম সহজ: timestamp/log-এ wall clock, elapsed-time/timeout-এ monotonic clock — দুইটা ভিন্ন কাজ, ভিন্ন যন্ত্র, গুলিয়ে ফেললে নীরবে ভুল।

একটা "৫ সেকেন্ড lease"-এর জীবন — কোথায় কোন ঘড়ি
  1. lease acquireaudit-log-এ কখন acquire হলো — wall clock, human-readable timestamp
  2. expiry-timer সেটশুরু-মুহূর্ত রেকর্ড — monotonic clock (elapsed মাপবে বলে)
  3. renew-loopmonotonic.now() - start < 5s? — NTP step এখানে প্রভাব ফেলে না
  4. expiry ঘটলেlease হারানোর মুহূর্ত আবার audit-log-এ — wall clock ফিরে আসে

উদাহরণ

সংখ্যায় পুরো গল্প — একই দৃশ্য, দুই লেন্স

উপরের হুড-সেকশনের LWW anomaly-কে একটা টেবিলে টানটান করে রাখি — এই সংখ্যাগুলোই experiment/build-এ কোড হয়ে চলবে:

Machineসত্যিকারের সময় (real τ, ms)ঘড়ির offsetলেখা timestamp
write AM110000 (নিখুঁত)1000
write BM21050 (৫০ms পরে)−100 (পিছিয়ে)950

max(1000, 950) = 1000 → LWW A রাখে। কিন্তু real-time-এর ক্রম A (τ=1000) → B (τ=1050) — B-ই আসলে শেষ ইচ্ছা। ফাঁকটা ঠিক কোথায় জন্মালো তা এক লাইনে: timestamp generator (wall clock) আর real-time truth-এর মধ্যে offset নামের একটা persistent bias। offset শূন্য হলে সমস্যা নেই; NTP offset-কে ছোট রাখে কিন্তু শূন্য করে না — তাই bug থেকেই যায়, শুধু সম্ভাবনা কমে।

লক্ষ্য করুন: এই একই সংখ্যা দিয়ে NTP-র offset-formula-ও যাচাই করা যায় যদি M1-M2-কে client-server হিসেবে ভাবি — সেটাই experiment সেকশনের প্রথম part, তবে ভিন্ন scenario-তে (সেখানে আমরা offset-টা উদ্ধার করব, এখানে offset-টা ব্যবহার করে ক্ষতি দেখালাম) — দুইটা একই মুদ্রার দুই পিঠ: measure বনাম consequence।

ছবিতে

   client (ঘড়ি: true time)         server (ঘড়ি: true time + offset θ)
     │                                   │
 t0 ─●── request ──────────────────►     │
     │        \                          │
     │         \  network delay          │
     │          ▼                    t1 ─●  receive
     │                                   │
     │                              t2 ─●  reply পাঠাল
     │          ▲                        │
     │         /  network delay          │
     │        /                          │
 t3 ─●◄── response ────────────────      │

  offset ≈ ((t1−t0)+(t2−t3)) / 2      delay ≈ (t3−t0)−(t2−t1)
NTP-র চার-timestamp exchange — forward leg t0→t1, backward leg t2→t3; offset আর delay দুইটা সমীকরণেই বেরিয়ে আসে।
  real time  ────────────────────────────────────►
             τ=1000                 τ=1050
             M1 লেখে "A"            M2 লেখে "B"  (real-এ পরে!)
             ts=1000                ts=950 (ঘড়ি −100ms পিছিয়ে)

  timestamp-line (LWW এই লাইনটাই দেখে) ─────────►
             950 ─────────────────── 1000
             "B"                     "A"  ← max(), LWW winner

  ফলাফল: "A" থেকে যায়, "B" নীরবে হারায় — যদিও "B" real-time-এ শেষ লেখা
LWW clock-skew anomaly — timeline-এ real order বনাম timestamp order; M2-র ঘড়ি পিছিয়ে থাকায় পরের write ছোট timestamp পায়।

নিজে চালিয়ে দেখুন

EXPERIMENT

Toy NTP offset/delay estimator — hand-constructed scenario দিয়ে formula যাচাই

macOS / Linux / Windows (Python 3)· ১২ মিনিট
# ntp_offset.py — toy NTP-style offset/delay estimator
def ntp_estimate(t0, t1, t2, t3):
    offset = ((t1 - t0) + (t2 - t3)) / 2
    delay  = (t3 - t0) - (t2 - t1)
    return offset, delay

# ---------- অংশ ১: hand-constructed scenario, জানা সত্য মান ----------
# client-এর ঘড়ি = true time (offset 0)। server-এর ঘড়ি true time + 50ms এগিয়ে।
# এক-দিকের network delay = 10ms; server তৎক্ষণাৎ reply করে (processing = 0)।
#
#   t0 = 0                    client পাঠাল (client ঘড়ি)
#   t1 = 0 + 10 + 50 = 60     server পেল    (server ঘড়ি: +10ms delay, +50ms skew)
#   t2 = 60                   server তৎক্ষণাৎ পাঠাল (server ঘড়ি)
#   t3 = 0 + 10 + 10 = 20     client পেল    (client ঘড়ি: দুই-দিকের delay মোট 20ms)
t0, t1, t2, t3 = 0, 60, 60, 20
offset, delay = ntp_estimate(t0, t1, t2, t3)
print(f"t0={t0}  t1={t1}  t2={t2}  t3={t3}")
print(f"estimated offset = {offset:.1f} ms   (সত্য মান ছিল +50 ms)")
print(f"estimated delay  = {delay:.1f} ms   (সত্য round-trip delay ছিল 20 ms)")
assert offset == 50.0 and delay == 20.0
print("OK: চার-timestamp formula সঠিক offset+delay উদ্ধার করেছে")

# ---------- অংশ ২: asymmetric delay হলে formula-র নীরব ভুল ----------
# ধরুন forward leg 5ms, backward leg 15ms (মোট delay একই 20ms) — কিন্তু
# formula symmetric ধরে নেয়, তাই offset ভুল বের হবে যদিও round-trip delay ঠিকই আসবে।
t0, t1 = 0, 55          # 0 + 5(delay) + 50(offset) = 55
t2, t3 = 55, 20         # 55 + 15(delay) = 70 হওয়া উচিত ছিল client-ঘড়িতে, কিন্তু
                          # client ঘড়ি offset-ছাড়া তাই t3 = 5 + 15 = 20 (এখানে ভুল ধরাই লক্ষ্য)
offset2, delay2 = ntp_estimate(t0, t1, t2, t3)
print(f"asymmetric leg (5ms + 15ms) দিয়ে estimate: offset={offset2:.1f}ms  delay={delay2:.1f}ms")
print("লক্ষ্য করুন offset আর সঠিক +50ms নয় — symmetric-delay ধারণা ভাঙলে formula নীরবে ভুল দেয়")

প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):

t0=0  t1=60  t2=60  t3=20
estimated offset = 50.0 ms   (সত্য মান ছিল +50 ms)
estimated delay  = 20.0 ms   (সত্য round-trip delay ছিল 20 ms)
OK: চার-timestamp formula সঠিক offset+delay উদ্ধার করেছে
asymmetric leg (5ms + 15ms) দিয়ে estimate: offset=42.5ms  delay=20.0ms
লক্ষ্য করুন offset আর সঠিক +50ms নয় — symmetric-delay ধারণা ভাঙলে formula নীরবে ভুল দেয়

তিনটা পাঠ: (১) round-number scenario-তে formula সংখ্যায় হুবহু সত্য মান ফেরত দিল — যোগে delay বাতিল হয়ে offset একা বেরোয়; (২) delay সবসময় সঠিক বেরোয় (এটা শুধু round-trip মোট, যা symmetric-না-হলেও (t3−t0)−(t2−t1)-এ ঠিক থাকে); (৩) offset-এর accuracy পুরোপুরি symmetric-delay assumption-এর ওপর নির্ভর করে — WAN-এ path asymmetric হলে এখানেই accuracy হারায়, তাই stratum-নিচের সার্ভারে error বেশি।

এটা কী প্রমাণ করে

চার-timestamp থেকে offset/delay formula একটা জানা-উত্তরের scenario-তে হুবহু সঠিক মান ফেরত দেয় — offset যোগ-বিয়োগে বাতিল-অবাতিল হয়ে আলাদা হয়ে বেরোয়, আর symmetric-delay ধারণা ভাঙলে (দ্বিতীয় অংশ) estimate-ও ভুল হয় তা দেখায়।

নিজে বানান

BUILD IT

ClockSkewLab — LWW anomaly simulator + monotonic-vs-wall guard

Python · ●●○○○
  1. Machine abstraction: প্রতিটার একটা fixed clock-offset (ms); real_time দিলে own_clock() = real_time + offset
  2. LWW merge function: (machine, timestamp, value)-দের মধ্যে max-timestamp জেতে
  3. দুই machine, বিপরীত skew দিয়ে concrete real-order বনাম timestamp-order দেখান
  4. একই লাইনে monotonic-guard যোগ করুন: elapsed মাপার সময় ঋণাত্মক এলে assert-এ ধরুন
  5. সীমা-demo: offset যত বড়, real-order-বিরোধী উইন্ডো তত চওড়া — সংখ্যায় দেখান

লক্ষ্য: hood/example সেকশনের ঠিক সেই M1/M2 সংখ্যা কোডে চালিয়ে দেখানো যে LWW সত্যিই ভুল write রাখে — আর তার পাশে দেখানো যে monotonic clock ব্যবহার করলে negative-elapsed bug কখনোই ঘটে না।

# clock_skew_lab.py — ClockSkewLab: LWW anomaly + monotonic guard
class Machine:
    def __init__(self, name, offset_ms):
        self.name = name
        self.offset_ms = offset_ms       # সত্যিকারের সময় থেকে কত পিছিয়ে/এগিয়ে

    def clock(self, real_time_ms):
        return real_time_ms + self.offset_ms   # নিজের (সম্ভবত skewed) ঘড়ি

def lww_merge(writes):
    # writes: list of (machine_name, timestamp, value); সবচেয়ে বড় timestamp জেতে
    return max(writes, key=lambda w: w[1])

M1 = Machine("M1", offset_ms=0)       # নিখুঁত
M2 = Machine("M2", offset_ms=-100)    # ১০০ms পিছিয়ে

REAL_A, REAL_B = 1000, 1050           # real time-এ B, A-র ৫০ms পরে লেখা হলো

ts_A = M1.clock(REAL_A)               # 1000
ts_B = M2.clock(REAL_B)               # 950

writes = [("M1", ts_A, "A"), ("M2", ts_B, "B")]
winner = lww_merge(writes)

print(f"write A: machine=M1  real_time={REAL_A}  timestamp={ts_A}")
print(f"write B: machine=M2  real_time={REAL_B}  timestamp={ts_B}   (real-এ ৫০ms পরে!)")
print(f"LWW winner: {winner}")

real_order_winner = "B" if REAL_B > REAL_A else "A"
assert winner[2] == "A"
assert real_order_winner == "B"
print(f"real-time-এ শেষ লেখা ছিল '{real_order_winner}', কিন্তু LWW রাখল "
      f"'{winner[2]}' — {abs(M2.offset_ms)}ms skew-ই একমাত্র কারণ")

# ---------- monotonic guard: negative-elapsed bug কখনো এখানে ঘটবে না ----------
class FakeWallClock:
    """সময়ের মাঝে একবার বড় step (ঘড়ি পেছনে লাফায়) — বাস্তব NTP-correction।"""
    def __init__(self):
        self.t = 0
    def time(self):
        return self.t

class FakeMonotonicClock:
    """একই দৃশ্যে monotonic — শুধু এগোয়, step-এর প্রভাব নেই।"""
    def __init__(self):
        self.t = 0
    def monotonic(self):
        return self.t

wall, mono = FakeWallClock(), FakeMonotonicClock()
wall.t, mono.t = 100, 100
start_wall, start_mono = wall.time(), mono.monotonic()

wall.t = 97          # NTP step: wall clock ৩ একক পিছিয়ে গেল
mono.t = 106          # monotonic শুধু এগোয় — real elapsed যাই হোক

elapsed_wall = wall.time() - start_wall
elapsed_mono = mono.monotonic() - start_mono
print(f"wall-clock দিয়ে মাপা elapsed  = {elapsed_wall}   (ঋণাত্মক! bug)")
print(f"monotonic দিয়ে মাপা elapsed   = {elapsed_mono}   (>= 0, guarantee-সহ)")
assert elapsed_wall < 0
assert elapsed_mono >= 0
print("OK: wall clock negative-elapsed দিল, monotonic দেয়নি — এটাই দুইটার তফাত")

প্রত্যাশিত output — সংখ্যাগুলো hood/example সেকশনের সাথে হুবহু মেলে:

write A: machine=M1  real_time=1000  timestamp=1000
write B: machine=M2  real_time=1050  timestamp=950   (real-এ ৫০ms পরে!)
LWW winner: ('M1', 1000, 'A')
real-time-এ শেষ লেখা ছিল 'B', কিন্তু LWW রাখল 'A' — 100ms skew-ই একমাত্র কারণ
wall-clock দিয়ে মাপা elapsed  = -3   (ঋণাত্মক! bug)
monotonic দিয়ে মাপা elapsed   = 6   (>= 0, guarantee-সহ)
OK: wall clock negative-elapsed দিল, monotonic দেয়নি — এটাই দুইটার তফাত

দুই মুহূর্ত মনে রাখার মতো: প্রথম block-এ 100ms skew-ই একমাত্র কারণ যে সঠিক write হারালো — কোনো network loss, কোনো bug নেই, শুধু ঘড়ি; দ্বিতীয় block-এ একই ভুল-যন্ত্র (wall clock) elapsed-time মাপায় ব্যবহার করলে ঠিক কী ভাঙে তা এক লাইনে দেখা গেল।

নিজে বাড়ান:

  1. offset_ms কে parameter করে দেখুন কত বড় skew হলে “৫০ms পরের write” আবার harার order-ই invert করে দেয় — একটা threshold খুঁজুন
  2. তিন-machine version বানান যেখানে pairwise skew ভিন্ন — কোন pair সবচেয়ে বিপজ্জনক?
  3. lww_merge-এ tie-breaker (timestamp সমান হলে?) যোগ করুন — বাস্তব system-গুলো এখানে machine-id বা random ব্যবহার করে, কেন সেটাও একটা compromise তা লিখুন

বাস্তব সিস্টেমে

বাস্তব জগতে physical time

NTP-র রোজকার জীবন। প্রায় প্রতিটা Linux/macOS/Windows machine ডিফল্টে chrony, ntpd, বা timesyncd-এর মতো daemon চালায় — public pool (pool.ntp.org) বা cloud-provider-এর নিজস্ব stratum-1 সার্ভার (AWS Time Sync Service, Google Public NTP) থেকে sync নেয়। Cloud VM-গুলো hypervisor-এর মধ্য দিয়ে চলার কারণে স্বাভাবিক hardware clock-এর চেয়ে বেশি drift-প্রবণ — তাই cloud provider নিজস্ব low-latency NTP endpoint দেয়।

PTP — যেখানে NTP-র ms যথেষ্ট নয়। Financial trading, telecom আর high-frequency measurement-এ Precision Time Protocol (PTP) hardware-timestamping দিয়ে sub-microsecond accuracy দেয় — NTP-র software-timestamp-জনিত jitter এড়িয়ে।

Spanner-এর TrueTime — uncertainty-কে লুকানোর বদলে প্রকাশ করা। সাধারণ ঘড়ি একটা single number দেয় আর ভান করে সেটাই সত্য। Google Spanner (Corbett et al., OSDI 2012) উল্টো পথ নেয়: প্রতিটা TT.now() call একটা interval ফেরত দেয় — [earliest, latest] — যার মধ্যে সত্যিকারের সময় থাকবে guaranteed (atomic clock + GPS receiver-এর redundant array আর conservative error-bound দিয়ে)। Commit করার সময় Spanner সেই uncertainty-window শেষ না হওয়া পর্যন্ত ইচ্ছাকৃতভাবে অপেক্ষা করে — একে বলে commit-wait। ফলাফল: দুইটা transaction-এর মধ্যে সত্যিকারের causal সম্পর্ক থাকলে তাদের TrueTime-interval কখনো overlap করে না, তাই timestamp-order-ই বিশ্বাসযোগ্য global order হয়ে ওঠে — কিন্তু দামটা commit-wait-এর latency, আর নির্ভরতা বিশেষ hardware-এর ওপর। পূর্ণ mechanism এই লেসনের সীমার বাইরে — মনে রাখার বিষয় শুধু এই দর্শন-বদল: ঘড়িকে নিখুঁত ভান না করে, ভুলের সীমাটাকেই স্বীকার করে কাজে লাগানো।

Kubernetes lease আর distributed lock। Leader-election lease-র TTL হিসাব সবসময় monotonic clock-ভিত্তিক হওয়া উচিত — না হলে node-এর wall clock একটা বড় NTP-step খেলে lease আগেভাগে expire (বা কখনো expire না) হতে পারে। বাস্তব bug-report-এ ঠিক এই কারণেই split-brain দেখা গেছে যেখানে lease-tracking কোড ভুলে time.time() ব্যবহার করেছিল।

যে ভুলগুলো সবাই করে

“NTP sync করা মানে সব মেশিনের ঘড়ি এখন একই।”

NTP drift bound করে, শূন্য করে না। Sync-এর পরেও দুই machine-এর মধ্যে সাধারণত কয়েক ms (LAN) থেকে কয়েক দশ ms (WAN) ফারাক থেকে যায়। “Sync হয়েছে” মানে |skew| < ε, skew = 0 নয় — আর ε-ই আজকের লেসনের সব anomaly-র উৎস।

“বড় timestamp মানেই পরে ঘটেছে।”

শুধু তখনই সত্য যদি দুই timestamp একই ঘড়ি থেকে আসে। ভিন্ন machine-এর timestamp তুলনা করলে ঘড়ির offset-ও তুলনায় ঢুকে পড়ে — LWW anomaly ঠিক এই ভুল ধারণার ফসল। বড় timestamp মানে শুধু সেই machine-এর নিজের ঘড়িতে বড়; real-time order-এর গ্যারান্টি নয়।

“NTP correction সবসময় ঘড়িকে সামনের দিকেই নিয়ে যায়, তাই elapsed-time হিসাব নিরাপদ।”

Step correction যেকোনো দিকে যেতে পারে — যদি local ঘড়ি সত্যিকারের সময়ের চেয়ে এগিয়ে থাকে, correction ঘড়িকে পেছনে ঠেলে দেয়। তখনই end_time - start_time ঋণাত্মক হয়ে যায়। এই কারণেই elapsed-time কখনো wall clock দিয়ে মাপা উচিত না — monotonic clock-এর সংজ্ঞাই “কখনো পেছায় না”।

“Physical clock-এর সমস্যা শুধু ছোট-বাজেট বা misconfigured সিস্টেমে হয় — ভালো infrastructure-এ ঘটে না।”

Google-এর মতো infrastructure-ও (atomic clock, GPS receiver, redundant hardware) skew শূন্য করেনি — তারা TrueTime বানিয়ে uncertainty-কে explicit করেছে আর algorithm-কে সেই uncertainty মেনে design করতে বাধ্য করেছে (commit-wait)। সমস্যাটা বাজেটের নয়, physics-এর: কোনো দুইটা independent ঘড়ি কখনো নিখুঁত এক থাকে না।

বুঝেছেন কি না দেখুন

1

NTP-র চার timestamp t0, t1, t2, t3 কী কী মুহূর্ত ধরে, আর offset/delay-র formula দুইটা লিখুন।

স্মরণ

t0 client পাঠাল (client ঘড়ি), t1 server পেল (server ঘড়ি), t2 server পাঠাল (server ঘড়ি), t3 client পেল (client ঘড়ি)। offset ≈ ((t1−t0)+(t2−t3))/2, delay ≈ (t3−t0)−(t2−t1)।

2

Experiment-এর দ্বিতীয় অংশে symmetric-delay ধরেও asymmetric (5ms + 15ms) leg দিলে offset ভুল বেরোল (42.5ms, সত্য 50ms) অথচ delay ঠিক থাকল (20ms)। কেন দুইটার ভাগ্য আলাদা?

যুক্তি

delay formula শুধু মোট round-trip সময় বের করে, forward/backward আলাদা করে না — তাই asymmetry-তে অক্ষত থাকে। offset formula-র derivation-এ ধরে নেওয়া হয় forward-leg delay = backward-leg delay, যাতে যোগ করলে delay-অংশ বাতিল হয়ে যায়; সেই ধরে নেওয়াটা ভাঙলে বাতিল হওয়া অংশে error ঢুকে offset-এ থেকে যায়।

3

Build-এ M2 offset -100ms না হয়ে -30ms হলে (একই real-time ৫০ms ব্যবধান-সহ) LWW-র রায় কী হতো, আর কেন?

প্রয়োগ

ts_B = 1050 - 30 = 1020, যা ts_A = 1000-এর চেয়ে বড় — LWW এবার B-কেই জেতাবে, যেটা real-order-এর সাথেও মেলে। মানে skew ৫০ms-এর কম হলে এই নির্দিষ্ট anomaly ঘটে না — বিপদ তখনই আসে যখন skew, লেখাদ্বয়ের real-time ব্যবধানের চেয়ে বড়।

4

Build-এর monotonic-guard অংশে elapsed_wall = -3 কীভাবে জন্মালো — কোন দুইটা মান বিয়োগ হয়ে?

যুক্তি

start_wall = 100 রেকর্ড হওয়ার পরে simulated NTP-step wall clock-কে 97-এ নামিয়ে দেয় (৩ একক পিছনে)। elapsed_wall = wall.time() - start_wall = 97 - 100 = -3। Monotonic ঘড়ি একই ঘটনায় প্রভাবিত হয়নি বলে elapsed_mono সবসময় অ-ঋণাত্মক থেকে গেছে।

5

একটা distributed lock-এর TTL-tracking কোড লিখতে বললে আপনি কোন ঘড়ি ব্যবহার করবেন — acquire-এর audit-log timestamp-এর জন্য, আর TTL-countdown-এর জন্য? দুইটা আলাদা কেন?

ডিজাইন

Audit-log timestamp: wall clock — মানুষ পড়বে, calendar-time দরকার, absolute value-ই উদ্দেশ্য। TTL-countdown: monotonic clock — শুধু elapsed মাপা দরকার, NTP step-এ lock আগেভাগে expire বা কখনো-না-expire হওয়া রোধ করতে হবে। একটা কাজে wall clock ব্যবহার করলে lock-এর correctness নেটওয়ার্ক-sync-এর দয়ায় নির্ভর করত।

6

TrueTime [earliest, latest] interval দেয়, single number নয়। এই ডিজাইন choice কোন misconception-টা সরাসরি এড়ায় যা এই লেসনের অন্য anomaly-গুলোর মূল?

যুক্তি

“ঘড়ি একটা নিখুঁত single instant জানে” — এই ভান TrueTime করে না; বরং uncertainty-কে ডেটা-টাইপের অংশ বানিয়ে দেয়, algorithm-কে সেই uncertainty মেনে (commit-wait) নিতে বাধ্য করে। LWW anomaly ঠিক উল্টো ভুলের ফল — timestamp-কে নিখুঁত single truth ধরে নেওয়া।

এরপর কী

এরপর কী

আজ দেখলাম physical clock — এমনকি NTP-র মতো protocol দিয়ে correct করার পরেও — একটা bounded কিন্তু non-zero skew রেখে যায়, আর সেই skew LWW-এর মতো সহজ নিয়মে নীরবে ভুল write জিতিয়ে দেয়। TrueTime-এর মতো সিস্টেম সেই uncertainty-কে explicit করে সামলায়, কিন্তু বিশেষ hardware আর commit-wait-latency-র দামে।

তাহলে প্রশ্ন দাঁড়ায়: যদি সবচেয়ে যত্নশীল physical-clock-সমাধানও হার্ডওয়্যার নির্ভর আর অসম্পূর্ণ থাকে, তাহলে হয়তো wall-clock সময়টাই ভুল প্রশ্ন। ordering-এর জন্য “কখন ঘটেছে” জানার দরকার নেই — দরকার শুধু “কে কার আগে ঘটেছে জানা”। পরের লেসনে সেই দিকেই মোড়: logical clock — Lamport clock আর vector clock — যারা wall-clock সময় স্পর্শও করে না, শুধু causal সম্পর্ক ট্র্যাক করে। ঘড়ি-নির্ভরতা পুরোপুরি বাদ দিলে কী পাওয়া যায়, কী হারানো যায় — সেটাই সামনের বিষয়।

আরও পড়ুন

  • Network Time Protocol Version 4: Protocol and Algorithms Specification — David L. Mills, J. Martin (ed.), J. Burbank, W. Kasch — RFC 5905, IETF, 2010 · চার-timestamp exchange, offset/delay formula, stratum hierarchy — এই লেসনের NTP-অংশের প্রাথমিক উৎস
  • Internet Time Synchronization: The Network Time Protocol — David L. Mills — IEEE Transactions on Communications, 1991 · NTP-র মূল নকশা-পত্র; slew বনাম step discipline-এর যুক্তি
  • Spanner: Google's Globally-Distributed Database — James C. Corbett et al. — OSDI 2012 · TrueTime — explicit uncertainty interval আর commit-wait দিয়ে single-instant illusion এড়ানোর নকশা
  • Designing Data-Intensive Applications, Chapter 8: The Trouble with Distributed Systems — Martin Kleppmann · Unreliable clocks-এর practitioner-গ্রেড আলোচনা — process pause, NTP step, monotonic বনাম wall clock