HybridTime introduces a hybrid between physical and logical clocks that can be used to implement globally consistent database snapshots. HybridTime is one implementation of the general concept of Hybrid Logical Clocks that combine the logical clock semantics of Lamport and Vector clocks with a physical representation of time such as Spanner’s TrueTime.
HybridTime follows similar update semantics as Lamport and Vector Clocks, where each node in the system updates their internal clock in response to events received. It differs in that HybridTime values are not purely logical — they include a physical time component that allows values to be associated with a physical point-in-time.
TrueTime’s key innovation is that timestamps assigned by the system serve a dual purpose: they are used to achieve external consistency, but they also have a physical meaning. This is in contrast to Lamport Clocks and Vector Clocks, in which timestamps are purely logical. HybridTime provides similar similar semantics to TrueTime by providing meaningful physical timestamps with bounded errors, but also leverages logical clocks to aid with consistency and correctness calculations.
Assumptions
HybridTime makes a few assumptions about the environment where it runs.
First, it assumes that nodes in the system have a reasonably accurate physical clock. This can be represented as a function \( PC_i(e) \) that returns a timestamp for process \( i \) for event \( e \).
Second, it assumes that the physical clocks on each system are periodically synchronized using a reference clock system. This can be represented by the function \( PC_{ref}(e) \) which returns the timestamp recorded by the reference process for an event \( e \). In addition, we assume that the reference synchronization system returns an error bound denoted by \( E_i(e) \) which returns the numeric error \( \epsilon \) of the process \( i \) at the time event \( e \) occurred.
We can formally codify these assumptions:
$$ \begin{aligned} \text{Assumption 1:} && \forall i,e : | PC_{ref}(e) - PC_i(e) | \leq E_i(e) && \text{(1)} \end{aligned} $$
Meaning that the physical timestamp returned by each server’s physical clock is within \( E_i(e) \) of the reference time. This assumption is plausible since most NTP deployments do provide a maximum error with regard to a master daemon, the time reference.
$$ \begin{aligned} \text{Assumption 2:} && \forall i,e : e \to f \Rightarrow PC_i(e) \leq PC_i(f) && \text{(2)} \end{aligned} $$
Meaning that the timestamp returned by a physical clock is always monotonically increasing. That is, when a server’s physical clock is queried for the current time, it never outputs a value that is less than a previous result.
These assumptions may seem like a large hurdle to overcome, but they actually are already implemented by the NTP system. In certain cases, NTP may skip time forward or backward, but such cases are extreme and easily detected by monitoring the NTP daemon status. Servers may choose to decommission themselves or fail-stop upon detection of such an event.
The HybridTime Clock Algorithm
A timestamp in the HybridTime protocol is represented as a pair \( ( \text{physical}, \text{logical} ) \), where \( \text{physical} \) represents the current physical time and \( \text{logical} \) is a logical sequence counter.
With this representation in hand, the full HybridTime algorithm can be represented with the following pseudo-code, reproduced from tech report HybridTime - Accessible Global Consistency with High Clock Uncertainty:
type Timestamp of {int physical, int logical}
int last physical = 0
int next logical = 0
PhysicalClock pc
function NOW : Timestamp
Timestamp now;
int cur physical = pc.now().physical;
if cur physical ≥ last physical then
now.physical = cur physical;
now.logical = 0
last physical = cur physical;
next logical = 1;
else
now.physical = last physical;
now.logical = next logical;
next logical++;
end if
return now;
end function
function UPDATE(Timestamp in) : void
int Timestamp now = Now();
if now.physical > in.physical then
return;
end if
last physical = in.physical;
next logical = in.logical + 1;
end function
The first function, NOW
, is used to obtain timestamps from the
HybridTime clock. As we noted, this timestamp consists of two components,
the current physical time as measured by the local clock, and a logical
time defined by a sequence number. The physical component is defined as
the larger of the value of the physical clock or the last known physical
value. The logical timestamp is the next value in the sequence or 0. This
determination is made depending on if the last time a measurement was
taken, we selected the logical or physical timestamps as a record of time.
The second function, UPDATE
, increases the local representation of time
in response to receiving an incoming event with a timestamp. For any
incoming timestamp if the incoming value’s physical component is lower
that the one obtained through NOW()
then no action is required. On the
other hand, if the incoming value’s physical component is equal to the one
obtained through NOW()
then we set next logical to be the maximum of the
current logical value and the incoming logical value, plus one, as in
Lamport Clocks. Finally, if the physical component of the incoming value
is higher than the one obtained through Now()
we set last physical
to
it and next logical
to be the incoming timestamp’s logical component
plus one.
If you are familiar with the Lamport Clock algorithm, you will notice many similarities between the two algorithms. In fact, this algorithm actually implements a Lamport Clock, with the additional advantage that generated timestamps have physical meaning and are accurate representations of physical time within a bounded error term.
When reflecting on the simple concept of time, made much more difficult when measured across distributed systems, it’s amazing how far a simple concept like Lamport Clocks can be extended to solving real-world problems. HybridTime, like Vector Clocks, extend the ideas given by Lamport to provide consistent snapshots of data across multiple servers and are widely used techniques in modern databases.