Content #
混合逻辑时钟(Hybrid Logical Clock,HLC),同样是多时间源、多点授时,但时钟采用了物理时钟与逻辑时钟混合的方式。
TrueTime 依赖 Google 强大的工程能力和特殊硬件,不具有普适性。相反,HLC 作为一种纯软的实现方式,更加灵活,所以在 CockroachDB、YugabyteDB 和很多分布式存储系统得到了广泛使用。
HLC 不只是字面上的意思, TiDB 的 TSO 也混合了物理时钟与逻辑时钟,但两者截然不同。HLC 代表了一种计时机制,它的首次提出是在论文“Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases”中,CockroachDB 和 YugabyteDB 的设计灵感都来自于这篇论文。下面,我们结合图片介绍一下这个机制。
假如我们有 ABCD 四个节点,方框是节点上发生的事件,方框内的三个数字依次是节点的本地物理时间(简称本地时间,Pt)、HLC 的高位(简称 L 值)和 HLC 的低位(简称 C 值)。
A 节点的本地时间初始值为 10,其他节点的本地时间初始值都是 0。四个节点的第一个事件都是在节点刚启动的一刻发生的。首先看 A1,它的 HLC 应该是 (10,0),其中高位直接取本地时间,低位从 0 开始。同理,其他事件的 HLC 都是 (0,0)。
然后我们再看一下,随着时间的推移,接下来的事件如何计时。
事件 D2 发生时,首先取上一个事件 D1 的 L 值和本地时间比较。L 值等于 0,本地时间已经递增变为 1,取最大值,那么用本地时间作为 D2 的 L 值。高位变更了,低位要归零,所以 D2 的 HLC 就是 (1,0)。
如果你看懂了 D2 的计时逻辑就会发现,D1 其实是一样的,只不过 D1 没有上一个事件的 L 值,只能用 0 代替,是一种特殊情况。
如果节点间有调用关系,计时逻辑会更复杂一点。我们看事件 B2,要先判断 B2 的 L 值,就有三个备选:
- 本节点上前一个事件 B1 的 L 值
- 当前本地时间
- 调用事件 A1 的 L 值,A1 的 HLC 是随着函数调用传给 B 节点的
这三个值分别是 0、1 和 10。按照规则取最大值,所以 B2 的 L 值是 10,也就是 A1 的 L 值,而 C 值就在 A1 的 C 值上加 1,最终 B2 的 HLC 就是 (10,1)。
B3 事件发生时,发现当前本地时间比 B2 的 L 值还要小,所以沿用了 B2 的 L 值,而 C 值是在 B2 的 C 值上加一,最终 B3 的 HLC 就是 (10,2)。
论文中用伪码表述了完整的计时逻辑。
Initially l:j := 0; c:j := 0
Send or local event
l’:j := l:j;
l:j := max(l’:j; pt:j);
If (l:j=l’:j) then c:j := c:j + 1
Else c:j := 0;
Timestamp with l:j; c:j
Receive event of message m
l’:j := l:j;
l:j := max(l’:j; l:m; pt:j);
If (l:j=l’:j=l:m) then c:j := max(c:j; c:m)+1
Elseif (l:j=l’:j) then c:j := c:j + 1
Elseif (l:j=l:m) then c:j := c:m + 1
Else c:j := 0
Timestamp with l:j; c:j
其中,对于节点 J,l.j 表示 L 值,c.j 表示 C 值,pt.j 表示本地物理时间。
在 HLC 机制下,每个节点会使用本地时钟作为参照,但不受到时钟回拨的影响,可以保证单调递增。本质上,HLC 还是 Lamport 逻辑时钟的变体,所以对于不同节点上没有调用关系的两个事件,是无法精确判断先后关系的。比如,上面例子中的 C2 和 D2 有同样的 HLC,但从上帝视角看,C2 是早于 D2 发生的,因为两个节点的本地时钟有差异,就没有体现这种先后关系。HLC 是一种松耦合的设计,所以不会去校正节点的本地时钟,本地时钟是否准确,还要靠 NTP 或类似的协议来保证。