CockroachDB博客翻译:Living without atomic clocks(下)

翻译CockroachDB的这篇博客 Living without atomic clocks: Where CockroachDB and Spanner diverge(不依赖原子钟:CockroachDB和Spanner的分野。原文地址 https://www.cockroachlabs.com/blog/living-without-atomic-clocks/)。
因为原文较长,所以分成了两篇发布。这是下篇。
TrueTime是如何提供线性一致性的?
好的,回到Spanner和TrueTime。重要的是要记住,TrueTime不能保证时钟完全同步。相反,TrueTime给出了集群中节点之间时钟偏移的上限。同步硬件(译者注:指原子钟)有助于最小化上限。在Spanner的案例中,谷歌提到了7ms的上限。这很严格了;相比之下,使用NTP进行时钟同步可能会在100ms到250ms之间。
那么,在时钟之间仍然存在不准确的情况下,Spanner是如何使用TrueTime来提供线性化的呢?事实上,它非常简单。它在等待。在允许节点报告事务已提交之前,它必须等待7毫秒。因为系统中的所有时钟都在7ms以内,所以等待7ms意味着没有后续事务可以在更早的时间戳提交,即使更早的事务是在时钟最快7ms的节点上提交的。相当聪明。(译者注:这就是为什么Spanner的性能不高的原因,因为任何一个事务提交,至少也需要7ms的时间用来等待)
细心的读者会注意到,整个“等待不确定性”的想法并不是基于原子钟的存在。可以很好地等待任何系统中的最大时钟偏移并实现线性化。当然,在每次写入时都必须消耗NTP偏移是不切实际的(译者注:这里的意思是,你每提交一个事务等待7ms,感觉还是可以接受的;但你每提交一个事务等待200ms,这就完全没法接受了,所以NTP没法替代原子钟),尽管最近在这一领域的研究可能有助于将其降低到一毫秒以下。
有趣的事实是:早期的CockroachDB有一个隐藏的、可线性化的开关,基本上可以实现上述功能,所以从理论上讲,如果你确实有一些原子钟(或者通常有一个可接受的最大时钟偏移),你会得到类似Spanner的能力。考虑到它的测试不足,我们已经删除了它,但随着云提供商倾向于公开类似TrueTime的API,也许恢复它是有意义的。
此外芯片级原子钟(chip-scaleatomic clock,也称CPT原子钟)逐渐走向实用;一个放在服务器主板上的芯片级原子钟将来会把石英晶体振荡器(译者注:也就是目前计算机的本地时钟)打得落花流水。(译者注:这块我也了解了一下,芯片级原子钟是近几年出现的,结合了集成电路制造的技术工艺方法,以相干布居数囚禁(CPT)原理为基础,研制出来的一种器件级别的微型化原子频率基准产品。相比于传统的原子钟(比如铯原子钟),芯片级原子钟体积小、耗电少,但精度有所下降。)
Linearizability真的重要吗?
更有力的保证是一件好事,但有些比较重要,有些没那么重要。有先后关系事务提交时间戳颠倒的可能性在实践中可能就是一个没那么重要问题。可能发生的情况是,以历史时间戳检查数据库可能会产生矛盾的情况,即事务T1(的结果)在事务T2已提交后还不可见,即使事务T1已知早于T2, 因为它们有顺序关系。然而,只有当(a)在事务期间读取或写入的key之间没有重叠,以及(b)客户端之间存在可能影响DBMS活动的外部低延迟通信通道时,才会发生这种情况。(译者注:再拿起我们前面分析过的扣款例子,A的账户扣款后,就会触发给B发一个短信,可以对外表现出来的确是B先收到了短信,A才扣了款。但这种情况会有什么影响吗?可以认为基本是没有的,因为这并不违反事务最高的隔离级别Serializability,只是看起来不太符合我们的客观印象而已。)
对于顺序颠倒可能有问题的情况,CockratchDB使用交易过程中遇到的最大时间戳作为“顺序令牌(causality token)”。它在顺序关系链中从一个参与者传递到下一个参与者,并作为连续事务的最小时间戳,以确保每个事务都有一个有序的提交时间戳。当然,这种机制并不能正确地排列独立的顺序链,尽管想象一个有问题的用例需要创造力。(译者注:这里其实描述的挺玄乎,其实本质上很简单,就是记录一个事务的提交时间戳,并且后面一个事务的提交时间戳不能小于前面的提交时间戳,从而强制保证了所有事务提交的单调递增。但在CockroachDB中,这肯定不是一个全局生效的机制,因为那就跟TrueTime一样了嘛。这里说的挺模棱两可的,我猜测CockroachDB可能是设计了一种特殊的语法,比如跟某个定制化的hint,用来对这种情况做兜底处理,后面有机会研究一下。)
但TrueTime还有一个比给事务排序更重要的用途。当启动从多个节点读取数据的事务时,必须选择一个时间戳,该时间戳保证至少与所有节点的最高提交时间一样大。如果不是这样,那么新事务可能无法读取已经提交的数据——这是对一致性的不可接受的破坏。有了TrueTime,解决方案就很简单;只需选择当前的TrueTime。由于每个已经提交的事务必须至少在7ms前提交,因此当前节点取到的时间就一定大于或等于最近提交的事务。哇,这既简单又高效。那么CockroachDB是怎么做的呢?
(译者注:再简单阐述一下文章这一节的论述逻辑,上篇已经讨论了Linearizability>Serializability,这一节首先讨论了Linearizability在实践中并不重要,所以CockroachDB没有特别着重的去保证Linearizability;但Serializability,也就是分布式事务的概念,还是要保证的,所以下一节就会论述Cockroach是怎么在不依赖Percolator的单点Timestamp Oracle,也不依赖TrueTime的基础上实现分布式事务的。而这个论证的重点就在于,分布式事务的提交版本号必须比已经开始的读更大,否则就会出现脏读的情况了,Cockroach怎么避免这一点呢?)
Cockroach如何选择交易时间戳?
先简短的回答一下?是一个不那么容易也不那么有效的方法。更具体的答案是,CockroachDB在事务进行时会为事务找到一个合适的时间戳,如果需要,有时会在稍后的时间戳重新启动。
如前所述,我们为事务选择的时间戳必须大于或等于我们打算从中读取的所有节点的最大提交时间戳。如果我们提前知道要读取的节点,我们可以向所有的这些节点发送一个并行请求,以获取最大时间戳,并使用这个最新的时间戳。但这有点笨拙,因为CockratchDB是为了支持读/写集不确定的会话SQL而设计的,所以我们无法提前知道节点。这也是低效的,因为我们甚至必须等待最慢的节点响应才能开始执行。除此之外:读者可能对Calvin和SLOG感兴趣,这是一个围绕预先声明读/写集(尽管放弃了会话式SQL)开发的一系列研究,因此能够避免这类问题。(译者注:Calvin和SLOG都属于确定性数据库,deterministic database。对于需要处理的事务,确定性数据库会在全局确定好事务的顺序,并按照这个顺序执行。Calvin一系列的确定性分布式事务模型算是Percolator之外的另一流派,学术界研究较多,工业界不怎么主流)
CockroachDB所做的实际上与Spanner所做的惊人地相似,尽管时钟同步要求要宽松得多。简单地说:
虽然Spanner总是在写入后等待,但CockratchDB有时会重试读取。
当CockroachDB启动事务时,它会根据当前节点的wall time(译者注:一个专有名词,可以理解成CockroachDB定义的一个节点上的特殊时间)选择一个临时提交时间戳。它还通过添加集群的最大时钟偏移量\[commit timestamp, commit timestamp + maximum clock offset]来建立所选wall time的上限。这个时间间隔代表了不确定性的窗口。(译者注:也就是说,CockroachDB的提交时间戳是一个时间窗口)
当事务从各个节点读取数据时,只要不遇到在此间隔内写入的key,它就可以顺利进行。如果事务在低于其临时提交时间戳的时间戳处遇到一个value,它会在读取期间简单地观察该值,并在写入期间覆盖较高时间戳处的值。只有当观察到一个值在这个不确定性区间内时,CockroachDB特定的机制才会启动。这里的核心问题是,考虑到时钟偏移,我们不能确定遇到的值是否在交易开始前提交。在这种情况下,我们只需执行不确定性重新启动(uncertainty restart),将临时提交时间戳刚好高于遇到的时间戳,就可以做到这一点。至关重要的是,不确定性区间的上限在重新启动时不会改变,因此不确定性窗口会缩小。从许多节点读取不断更新的数据的事务可能会被迫多次重新启动,但时间永远不会超过不确定性间隔,每个节点也不会超过一次。
如上所述,Spanner和CockroachDB之间的对比是Spanner总是将写入延迟一小段时间,而CockroachDB有时会延迟读取。延迟多长时间?这主要取决于几乎同时读取和写入同一行的频率。大多数时候,当这种情况发生时,只需重试一次读取,因此假设的2ms读取变为4ms。如果运气不好,可能需要多次重试读取。根据时钟的同步方式,可以进行的重试次数有上限。对于NTP,这可能是250毫秒,因此即使是最倒霉的事务也不必因时钟相关原因重试超过250毫秒。
由于CockroachDB依赖于时钟同步,节点会定期比较它们之间的时钟偏移。如果任何节点超过了配置的最大偏移量,则会自行终止。如果你想知道当违反最大时钟偏移量时会发生什么,我们在这里已经考虑过了。
总结
如果你已经走到了这一步,谢谢你一直坚持下去。如果你是新手,这是一件棘手的事情。甚至我们偶尔也需要好好琢磨一下这一切是如何结合在一起的,而我们创造了这该死的东西。(译者注:作者的自我调侃,现在面对分布式场景下的时钟问题,注定是性能和一致性两方面的艰难取舍。而分布式事务的研究,归根结底,就是要在保证一致性的基础上,尽可能的达到更高的性能。)