Measuring time in the context of computer systems
Physical Time
Two types of clock
- Physical clock: number of seconds elapsed
- Logical clock: count events, e.g. messages sent
Time is hard! So many different ways of measuring time
- Greenwich Mean Time (GMT): the normal human time format, based on Earth rotation
- International Atomic Time (TAI): some multiple of Caesium-133 resonant frequency
- Compromise, UTC is TAI with corrections to account for Earth rotation
- Unix Time: number of seconds since the epoch (Jan 1, 1970) not counting leap seconds
- ISO8601: year, month, day, hour, minute, second, and timezone offset relative to UTC
We periodically adjust our local clocks with a server that has a more accurate time source using Network Time Protocol (NTP) or Precision Time Protocol (PTP)
Logical Time
Computers typically have a service like NTP which synchronizes computer clocks with well known time sources on the internet. Because of this, two consecutive readings of the system time on a given server can have time going backwards. As there is no upper bound on clock drift across servers, it is impossible to trust timestamps on two different servers as a way to infer causality!
We use logical clocks to work based off of the number of events that have occurred rather than actual time passed.
Lamport Clocks
Provides a partial order on events
Logic
- On initialization, set
t := 0
for each node - On any event on local node,
fn tick() -> t += 1
- On sending message ,
fn send(m) -> tick(); actually_send(t, m)
- On receiving
fn receive(t', m) -> t = max(t, t') + 1; do_something(m)
Properties
- If then
- However, does not imply
- Possible that for
This means that two identical Lamport timestamps might not correspond to the same unique event. However if we include the node for the node where event occurred, then uniquely identifies event .
We attempt to define a total causal order
However even now, given timestamps , we can’t tell whether or
To separate causality from concurrent events, we need vector clocks!
Vector Clocks
Provides a causal order on events
Instead of having a single counter t
for all nodes, we keep a vector timestamp of an event for each node so we have where is the number of events observed by node
Each node has a current vector timestamp , on an event on node , increment vector element
Logic
- On initialization , set
t := [0] * n
- On any event on node ,
fn tick() -> t[i] += 1
- Each time a process experiences an internal event, it increments its own clock in the vector by one
- On sending message from node ,
fn send(m) -> tick(); actually_send(t, m)
- On receiving
fn receive(t', m) -> t = tick(); zip(t, t').map(max); do_something(m)
Thus, a vector timestamp of an event actually represents all of its causal dependencies:
E.g. represents first two events from , first two events from , and no events from
Ordering
- (T and T’ are same if each element has the same value)
- (T happened at the same time or earlier than T’ if each element in T is less than or equal to its value in T’)
- (T happened earlier than T’ if each element in T is less than its value in T’, at least one element in T differs from T’)
- (T is incomparable to T’)
Properties (based on Order theory)
You can tell that versions are in conflict when neither vector clock “descends” from the other. In order for vector clock B to be considered a descendant of vector clock A, each marker in vector clock A must have a corresponding marker in B that has a revision number greater than or equal to the marker in vector clock A. Markers not contained in a vector clock can be considered to have revision number zero.
Vector Clock Example
Hybrid Logical Clocks
Physical and logical clocks both have non-ideal properties.
- Logical clocks don’t actually store any sort of date-time when events happen. Clients usually have a notion of time through actual wall time
- BUT wall time isn’t perfect either as clock drift is non-trivial and users can manually turn time backwards on their local machines
Note that this is not a substitute for Vector Clocks as they only provide partial order instead of causal order
Can we combine them to achieve better properties?
Hybrid Logical Clocks (HLCs) achieve
- partial ordering
- constant space
- bounded different from physical time
We can store a tuple containing:
pt
: physical time (wall time)l
: logical time (holds maximumpt
so far)c
: capturing causality whenl
is equal
This tuple can be used directly as a replacement for a physical clock timestamp (and in fact works as a superposition on top of the NTP protocol without any interference)
Pseudocode
- Initial state
l := 0
c := 0
- Send / local event
l' := l
l := max(l', pt)
updatel
topt
if applicable- if
pt
is the same (l == l'
):c += 1
increment causality as logical time is the same
- if
pt
is updated:c := 0
resetc
- timestamp message with
(l, c)
- Receive of message
m
l' := l
l := max(l', m.l, pt)
- if all logical clocks are the same
l == l' == m.l
:c := max(c, m.c) + 1
set to max causality known
- if our logic clocks are the same but message logical clock is behind
l == l'
:c += 1
(ignore as message clock is behind)
- if our logic clock was behind the message logical clock and just got updated
l == m.l
c := m.c + 1
- otherwise
pt
was just updatedc := 0
resetc
- timestamp message with
(l, c)