You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

177 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 05 | 全局时钟物理时钟和逻辑时钟你Pick谁
你好我是王磊你也可以叫我Ivan。
今天,我想和你聊聊时间的话题。
“时光一去永不回,往事只能回味”,这种咏叹时光飞逝的歌曲,你一定听过很多。但是,在计算机的世界里,时间真的是一去不回吗?还真不一定。
还记得我在[第2讲](https://time.geekbang.org/column/article/272104)提到的TrueTime吗作为全局时钟的一种实现形式它是Google通过 GPS和原子钟两种方式混合提供的授时机制误差可以控制在7毫秒以内。正是在这7毫秒内时光是可能倒流的。
为什么我们这么关注时间呢是穿越剧看多了吗其实这是因为分布式数据库的很多设计都和时间有关更确切地说是和全局时钟有关。比如我们在第2讲提到的线性一致性它的基础就是全局时钟还有后面会讲到的多版本并发控制MVCC、快照、乐观协议与悲观协议都和时间有关。
## 常见授时方案
那既然有这么多分布式数据库,授时机制是不是也很多,很复杂呢?其实,要区分授时机制也很简单,抓住三个要素就可以了。
1. 时间源:单个还是多个
2. 使用的时钟类型:物理时钟还是混合逻辑时钟
3. 授时点:一个还是多个
根据排列组合一共产生了8种可能性其中NTPNetwork Time Protocol误差大也不能保证单调递增所以就没有单独使用NTP的产品还有一些方案在实践中则是不适用的N/A。因此常见的方案主要只有4类我画了张表格总结了一下。
![](https://static001.geekbang.org/resource/image/85/f4/85d161f3cbf5a162b78ayydf318cbdf4.jpg)
### 1\. TrueTime
Spanner采用的方案是TrueTime。它的时间源是GPS和原子钟所以属于多时间源和物理时钟同时它也采用了多点授时机制就是说集群内有多个时间服务器都可以提供授时服务。
就像这一讲开头说的TrueTime是会出现时光倒流的。例如A、B两个进程先后调用TrueTime服务各自拿到一个时间区间如果在其中随机选择则可能出现B的时间早于A的时间。不只是TrueTime任何物理时钟都会存在时钟偏移甚至回拨。
单个物理时钟会产生误差而多点授时又会带来整体性的误差那TrueTime为什么还要这么设计呢
因为它也有两个显著的优势:首先是高可靠高性能,多时间源和多授时点实现了完全的去中心化设计,不存在单点;其次是支持全球化部署,客户端与时间服务器的距离也是可控的,不会因为两者通讯延迟过长导致时钟失效。
### 2\. HLC
CockroachDB和YugabyteDB也是以高性能高可靠和全球化部署为目标不过Truetime是Google的独门绝技它依赖于特定硬件设备的思路不适用于开源软件。所以它们使用了混合逻辑时钟Hybrid Logical ClockHLC同样是多时间源、多点授时但时钟采用了物理时钟与逻辑时钟混合的方式。HLC在实现机制上也是蛮复杂的而且和TrueTime同样有整体性的时间误差。
对于这个共性问题Spanner和CockroachDB都会通过一些容错设计来消除时间误差我会在第12讲中具体介绍相关内容。
### 3\. TSO
其他的分布式数据库大多选择了单时间源、单点授时的方式承担这个功能的组件在NewSQL风格架构中往往被称为TSOTimestamp Oracle而在PGXC风格架构中被称为全局事务管理器Golobal Transcation ManagerGTM。这就是说一个单点递增的时间戳和全局事务号基本是等效的。这种授时机制的最大优点就是实现简便如果能够保证时钟单调递增还可以简化事务冲突时的设计。但缺点也很明显集群不能大范围部署同时性能也有上限。TiDB、OceanBase、GoldenDB和TBase等选择了这个方向。
### 4\. STP
最后还有一些小众的方案比如巨杉的STP(SequoiaDB Time Protoco)。它采用了单时间源、多点授时的方式优缺点介于HLC和TSO之间。
到这里我已经介绍了4种方案在技术路线上大致的区别。其中TrueTime是基于物理设备的外部授时方案所以Spanner直接使用就可以了自身不需要做专门的设计。而对于其他3种方案如果我们想要深入理解那么还得结合具体的产品来看。
## 中心化授时TSOTiDB
首先我们从最简单的TSO开始。
最早提出TSO的大概是Google的论文“ [Large-scale Incremental Processing Using Distributed Transactions and Notifications](https://www.cs.princeton.edu/courses/archive/fall10/cos597B/papers/percolator-osdi10.pdf)”。这篇论文主要是介绍分布式存储系统Percolator的实现机制其中提到通过一台Oracle为集群提供集中授时服务称为Timestamp Oracle。所以后来的很多分布式系统也用它的缩写来命名自己的单点授时机制比如TiDB和Yahoo的Omid。
考虑到TiDB的使用更广泛些这里主要介绍TiDB的实现方式。
TiDB的全局时钟是一个数值它由两部分构成其中高位是物理时间也就是操作系统的毫秒时间低位是逻辑时间是一个18位的数值。那么从存储空间看1毫秒最多可以产生262,144个时间戳2^18这已经是一个很大的数字了一般来说足够使用了。
单点授时首先要解决的肯定是单点故障问题。TiDB中提供授时服务的节点被称为Placement Driver简称PD。多个PD节点构成一个Raft组这样通过共识算法可以保证在主节点宕机后马上选出新主在短时间内恢复授时服务。
那问题来了如何保证新主产生的时间戳一定大于旧主呢那就必须将旧主的时间戳存储起来存储也必须是高可靠的所以TiDB使用了etcd。但是每产生一个时间戳都要保存吗显然不行那样时间戳的产生速度直接与磁盘I/O能力相关会存在瓶颈的。
如何解决性能问题呢TiDB采用预申请时间窗口的方式我画了张图来表示这个过程。
![](https://static001.geekbang.org/resource/image/3c/d6/3c9703cafb44f53596b673d9293e12d6.jpg)
当前PD主节点的系统时间是103毫秒PD向etcd申请了一个“可分配的时间窗口”。要知道时间窗口的跨度是可以通过参数指定的系统的默认配置是3毫秒示例采用了默认配置所以这个窗口的起点是PD当前时间103时间窗口的终点就在106毫秒处。。写入etcd成功后PD将得到一个从103到106的“可分配时间窗口”在这个时间窗口内PD可以使用系统的物理时间作为高位拼接自己在内存中累加的逻辑时间对外分配时间戳。
上述设计意味着所有PD已分配时间戳的高位也就是物理时间永远小于etcd存储的最大值。那么如果PD主节点宕机新主就可以读取etcd中存储的最大值在此基础上申请新的“可分配时间窗口”这样新主分配的时间戳肯定会大于旧主了。
此外为了降低通讯开销每个客户端一次可以申请多个时间戳时间戳数量作为参数由客户端传给PD。但要注意的是一旦在客户端缓存多个客户端之间时钟就不再是严格单调递增的这也是追求性能需要付出的代价。
## 分布式授时HLCCockroachDB
前面已经说过TrueTime依赖Google强大的工程能力和特殊硬件不具有普适性。相反HLC作为一种纯软的实现方式更加灵活所以在CockroachDB、YugabyteDB和很多分布式存储系统得到了广泛使用。
HLC不只是字面上的意思 TiDB的TSO也混合了物理时钟与逻辑时钟但两者截然不同。HLC代表了一种计时机制它的首次提出是在论文“[Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases](https://cse.buffalo.edu/~demirbas/publications/hlc.pdf)”中CockroachDB和YugabyteDB的设计灵感都来自于这篇论文。下面我们结合图片介绍一下这个机制。
![](https://static001.geekbang.org/resource/image/1c/6b/1c40af51f993yyc296635ef27de7e26b.jpg)
假如我们有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)。
![](https://static001.geekbang.org/resource/image/7b/52/7b34a1c812284bc7049f9ece2323bd52.jpg)
如果你看懂了D2的计时逻辑就会发现D1其实是一样的只不过D1没有上一个事件的L值只能用0代替是一种特殊情况。
如果节点间有调用关系计时逻辑会更复杂一点。我们看事件B2要先判断B2的L值就有三个备选
1. 本节点上前一个事件B1的L值
2. 当前本地时间
3. 调用事件A1的L值A1的HLC是随着函数调用传给B节点的
这三个值分别是0、1和10。按照规则取最大值所以B2的L值是10也就是A1的L值而C值就在A1的C值上加1最终B2的HLC就是(10,1)。
![](https://static001.geekbang.org/resource/image/e6/a4/e6de74cb1b9d2a92cb3bcb711120c3a4.jpg)
B3事件发生时发现当前本地时间比B2的L值还要小所以沿用了B2的L值而C值是在B2的C值上加一最终B3的HLC就是(10,2)。
![](https://static001.geekbang.org/resource/image/d0/55/d0473eb6c84a264d2be5104a62d77655.jpg)
论文中用伪码表述了完整的计时逻辑,我把它们复制在下面,你可以仔细研究。
```
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
```
其中对于节点Jl.j表示L值c.j表示C值pt.j表示本地物理时间。
在HLC机制下每个节点会使用本地时钟作为参照但不受到时钟回拨的影响可以保证单调递增。本质上HLC还是Lamport逻辑时钟的变体所以对于不同节点上没有调用关系的两个事件是无法精确判断先后关系的。比如上面例子中的C2和D2有同样的HLC但从上帝视角看C2是早于D2发生的因为两个节点的本地时钟有差异就没有体现这种先后关系。HLC是一种松耦合的设计所以不会去校正节点的本地时钟本地时钟是否准确还要靠NTP或类似的协议来保证。
## 多层级中心化授时STP巨杉
巨杉采用了单时间源、多点授时机制它有自己的全局时间协议称为STPSerial Time Protocol是内部逻辑时间同步的协议并不依赖于NTP协议。
下面是STP体系下各角色节点的关系。
![](https://static001.geekbang.org/resource/image/c4/ca/c463c3aa47d625a964422yy3df7d2cca.jpg)
STP是独立于分布式数据库的授时方案该体系下的各角色节点与巨杉的其他角色节点共用机器但没有必然的联系。
STP下的所有角色统称为STP Node具体分为两类
1. **STP Server。**多个STP Server构成STP Server组组内根据协议进行选主主节点被称为Primary对外提供服务。
2. **STP Client。**按照固定的时间间隔从Primary Server 同步时间。
巨杉数据库的其他角色节点如编目节点CATALOG、协调节点COORD和数据节点DATA都从本地的STP Node节点获得时间。
STP与 TSO一样都是单时间源但通过增加更多的授时点避免了单点性能瓶颈而负副作用是多点授时就会造成全局性的时间误差因此和HLC一样需要做针对性设计。
## 小结
好了,今天的内容就到这里了,我们一起回顾下这节课的重点。
1. 分布式数据库有多种授时机制,它们的区别主要看三个维度。一,是单时间源还是多时间源;二,时间源采用的是物理时钟还是混合逻辑时钟;三,授时点是一个还是多个。
2. TrueTime是多时间源、多授时点方案虽然仍存在时间误差的问题但实现了高可靠高性能能够支持Spanner做到全球化部署是一种非常强悍的设计方案。TrueTime是GPS加原子钟的整合方案可以看作为一种物理时钟它完全独立于Spanner的授时服务不需要Spanner做专门的设计。
3. HLC同样是多时间源、多授时点由于是纯软方案所以具有更好的通用性。CockroachDB和YugabyteDB都采用了这种方案也都具备全球化部署能力。HLC的设计基础是Lamport逻辑时钟对NTP的时间偏移有一定的依赖。
4. TSO是典型的单时间源、单点授时方案实现简便所以成为多数分布式数据库的选择。如果TSO能够做到单调递增会简化读写冲突时候的处理过程但缺点是集群部署范围受到极大的限制。
5. 还有一些小众的方案比如巨杉的STP也试图在寻求新的平衡点。
有关时间的话题我们就聊到这了。时间误差是普遍存在的,只不过长期在单体应用系统下开发,思维惯性让我们忽略了它,但随着分布式架构的普及,我相信更多的架构设计中都要考虑这个因素。我建议你收藏今天的内容,因为即使抛开分布式数据库不谈,这些设计依然是值得借鉴的。
![](https://static001.geekbang.org/resource/image/1a/b1/1a6ccbf7fa3801216468c311363a9fb1.jpg)
## 思考题
最后,今天留给你的思考题还是关于时间的。在后续课程没有展开之前,我们不妨先来开放式地讨论一下,你觉得时间对于分布式数据库的影响是什么?或者你也可以谈谈在其他分布式系统中曾经遇到的关于时间的问题。
欢迎你在评论区留言和我一起讨论,我会在答疑篇回复这个问题。如果你身边的朋友也对全局时钟或者分布式架构下如何同步时间这个话题感兴趣,你也可以把今天这一讲分享给他,我们一起讨论。
## 学习资料
Daniel Peng and Frank Dabek: [_Large-scale Incremental Processing Using Distributed Transactions and Notifications_](https://www.cs.princeton.edu/courses/archive/fall10/cos597B/papers/percolator-osdi10.pdf)
Sandeep S. Kulkarni et al.: [_Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases_](https://cse.buffalo.edu/~demirbas/publications/hlc.pdf)