这个模式是分布式系统的模式
Clock-Bound等
在读取和写入值之前,等待在时间上覆盖集群节点之间的不确定性,以便值可以在集群节点之间正确排序。
2022年8月17日
问题
考虑一个键-值存储,其中值使用时间戳存储,以指定每个版本。任何处理客户机请求的集群节点都能够使用请求处理节点上的当前时间戳读取最新版本。
在下面的例子中,值“Before Dawn”在时间2被更新为值“After Dawn”,根据Green的时钟。Alice和Bob都试图读取'title'的最新值。当Alice的请求由集群节点Amber处理时,Bob的请求由集群节点Blue处理。琥珀的时钟慢在1;这意味着当Alice读取最新的值时,它会传递“Before Dawn”的值。蓝色的时钟是2点;当Bob读取最新值时,它返回值为"After Dawn"

这违反了一种被称为外部一致性.如果爱丽丝和鲍勃现在打电话,爱丽丝会感到困惑;Bob将告知最新的值是“After Dawn”,而她的集群节点将显示“Before Dawn”。
如果Green的时钟比较快,并且写操作发生在“未来”,那么与Amber的时钟相比也是一样的。
如果使用系统的时间戳作为存储值的版本,这将是一个问题,因为挂钟不是单调的.来自两个不同服务器的时钟值不能也不应该比较。当混合动力时钟用作版本在版本化的价值,它允许在单个服务器上以及在不同的服务器上排序值有原因地相关的。然而,混合时钟(或任何Lamport时钟基于时钟)只能给予部分订单。这意味着不能对任何没有因果关系且由跨不同节点的两个不同客户机存储的值进行排序。这在使用时间戳跨集群节点读取值时产生了一个问题。如果读请求起源于具有滞后时钟的集群节点,那么它可能无法读取给定值的最新版本。
解决方案
集群节点等待,直到集群中每个节点上的时钟值在读取或写入时保证高于分配给该值的时间戳。
如果时钟之间的差异非常小,那么写请求可以等待,而不会增加大量的开销。例如,假设跨集群节点的最大时钟偏移为10ms。(这意味着,在任何给定的时间点,集群中最慢的时钟都落后t - 10ms。)为了保证每个其他集群节点的时钟设置都超过了t,处理任何写操作的集群节点必须等待t + 10ms才能存储该值。
考虑一个键值存储版本化的价值每次更新都作为新值添加,使用时间戳作为版本。在上面提到的Alice和Bob示例中,存储title@2的写操作将等待,直到集群中的所有时钟都位于2。这确保Alice总是看到标题的最新值,即使Alice的集群节点上的时钟落后。
考虑一个稍微不同的场景。菲利普正在将标题更新为《黎明之后》。格林的钟是2点。但格林知道,可能有一台服务器的时钟延迟了1个单元。因此,它必须在写操作中等待1个单元的时间。

当Philip更新标题时,Bob的读请求由服务器Blue处理。蓝色的时钟是2,所以它试图在时间戳2读取标题。此时Green还没有提供该值。这意味着Bob得到了时间戳小于2的最高值,也就是'Before Dawn'

Alice的读请求由服务器Amber处理。Amber的时钟是1,所以它试图在时间戳1读取标题。Alice得到的值是'Before Dawn'

一旦Philip的写请求完成——在max_diff的等待结束之后——如果Bob现在发送一个新的读请求,服务器Blue将尝试根据它的时钟(已经提前到3)读取最新的值;返回值为After Dawn

如果Alice初始化一个新的读请求,服务器Blue将尝试根据它的时钟读取最新的值——现在是2。因此,它也会返回值"After Dawn"

尝试实现此解决方案时的主要问题是,使用当前可用的日期/时间硬件和操作系统api,不可能获得集群节点之间的确切时间差。这就是挑战的本质,谷歌有自己的专门的日期时间API称为真正的时间.类似亚马逊时间同步服务还有一个图书馆叫做ClockBound.然而,这些api非常特定于谷歌和Amazon,因此无法真正扩展到这些组织的范围之外
通常键值存储使用混合动力时钟来实现版本化的价值.虽然不可能得到时钟之间的确切差异,但可以根据历史观察选择一个合理的默认值。跨数据中心的服务器上的最大时钟漂移的观测值通常是200到500ms。
键值存储在存储值之前等待配置的最大偏移量。
类KVStore……
int maxOffset = 200;NavigableMapkv = new ConcurrentSkipListMap<>();public void put(String key, String value) {HybridTimestamp writeTimestamp = clock.now();waitTillSlowestClockCatchesUp (writeTimestamp);千伏。put(new HybridClockKey(key, writeTimestamp), value);} private void waitTillSlowestClockCatchesUp(HybridTimestamp writeTimestamp) {var waitUntilTimestamp = writeTimestamp。添加(maxOffset, 0);sleepUntil (waitUntilTimestamp);} private void sleepUntil(HybridTimestamp waitUntil) {HybridTimestamp now = clock.now();while (clock.now().before(waitUntil)) {var waitTime = (waitUntil. getwallclocktime () - now.getWallClockTime()); Uninterruptibles.sleepUninterruptibly(waitTime, TimeUnit.MILLISECONDS); now = clock.now(); } } public String get(String key, HybridTimestamp readTimestamp) { return kv.get(new HybridClockKey(key, readTimestamp)); }
读重启
对于等待每个写请求来说,200ms的时间间隔太高了。这就是为什么数据库喜欢CockroachDB或YugabyteDB取而代之的是在读请求中实现检查。
在处理读请求时,集群节点检查在readTimestamp和readTimestamp +最大时钟漂移的时间间隔内是否有可用的版本。如果该版本可用——假设读取器的时钟可能滞后——那么将要求它用该版本重新启动读请求。
类KVStore……
public void put(String key, String value) {HybridTimestamp writeTimestamp = clock.now();千伏。put(new HybridClockKey(key, writeTimestamp), value);} public String get(String key, HybridTimestamp readTimestamp) {checksifversioninunsuretyinterval (key, readTimestamp);返回千伏。floorEntry(新HybridClockKey(关键,readTimestamp)) .getValue ();} private void checksifversioninunsuretyinterval (String key, HybridTimestamp readTimestamp) {HybridTimestamp unsuretylimit = readTimestamp.}添加(maxOffset, 0);HybridClockKey versionedKey = kv。floorKey(新HybridClockKey(关键,uncertaintyLimit));if (versionedKey == null){返回; } HybridTimestamp maxVersionBelowUncertainty = versionedKey.getVersion(); if (maxVersionBelowUncertainty.after(readTimestamp)) { throw new ReadRestartException(readTimestamp, maxOffset, maxVersionBelowUncertainty); } ; }
类客户…
String read(String key) {int attemptNo = 1;int maxAttempts = 5;while(attemptNo < maxAttempts) {try {HybridTimestamp now = clock.now();kvStore返回。(关键,现在);} catch (ReadRestartException e) {logger.info(" Got read restart error " + e + "尝试失败。“+ attemptNo);Uninterruptibles.sleepUninterruptibly (e.getMaxOffset (), TimeUnit.MILLISECONDS);attemptNo + +;}}抛出新的ReadTimeoutException("Unable to read after " + attemptNo + " attempts.");}
在上面的Alice和Bob例子中,如果有一个版本的“title”在时间戳2可用,而Alice发送了一个读请求,读时间戳1,则会抛出一个ReadRestartException,要求Alice在时间戳2重新启动读请求。

只有在不确定区间内写入了一个版本时,才会重新启动读。写请求不需要等待。
重要的是要记住,最大时钟漂移的配置值是一个假设,它不能保证。在某些情况下,坏服务器的时钟漂移可能超过假定值。在这种情况下,问题就来了将继续存在。
使用时钟绑定api
像谷歌和Amazon这样的云提供商使用原子钟和GPS实现时钟机制,以确保时钟在集群节点间的漂移保持在几毫秒以下。正如我们刚才所讨论的,谷歌是真正的时间.AWS已经时间同步服务而且ClockBound.
要确保正确实现这些等待,集群节点有两个关键要求。
- 跨集群节点的时钟漂移保持在最小值。谷歌的True-Time在大多数情况下保持在1ms以下(在最坏的情况下是7ms)
- 可能的时钟漂移在日期时间API中总是可用的,这确保程序员不需要猜测值。
集群节点上的时钟机制计算日期-时间值的错误边界。考虑到本地系统时钟返回的时间戳中可能存在错误,API将此错误显式显示。它将给出时钟值的下限和上限。保证实时值在此区间内。
公共类ClockBound{公共最终长最早;公终长最新;public ClockBound(长最早,长最晚){这个。最早=最早;这一点。最新=最新;} public Boolean before(长时间戳){返回时间戳<最早的;}公共布尔值后(长时间戳){返回时间戳>最新;}
如本章所述AWS的博客错误在每个集群节点上计算为lockerrorbound。实时值总是在本地时钟时间和+- clockkerrorbound之间的某个位置。
每当请求日期-时间值时,就返回错误边界。
public ClockBound now(){返回现在;}
时钟绑定API保证了两个属性
- 时钟边界应该在集群节点之间重叠
- 对于两个时间值t1和t2,如果t1小于t2,则clock_bound(t1)。early小于clock_bound(t2)。所有集群节点的最新版本
假设我们有三个集群节点:绿色、蓝色和橙色。每个节点可能有不同的错误界限。假设绿色的误差是1,蓝色是2,橙色是3。在time=4时,跨集群节点的时钟将看起来像这样:

在此场景中,需要遵循两条规则来实现提交-等待。
- 对于任何写操作,都应该选择时钟边界的最新值作为时间戳。这将确保它总是高于分配给前一个写操作的任何时间戳(考虑下面的第二条规则)。
- 在存储该值之前,系统必须等到写时间戳小于时钟边界的最早值。
这是因为在所有集群节点上,保证最早的值低于时钟边界的最新值。这个写操作将被任何在未来用时钟绑定的最新值读取的人访问。另外,保证在将来发生任何其他写操作之前对该值进行排序。
类KVStore……
public void put(String键,String值){ClockBound now = boundedClock.now();long writeTimestamp = now.latest;addPending (writeTimestamp);waitUntilTimeInPast (writeTimestamp);千伏。put(new VersionedKey(key, writeTimestamp), value);removePending (writeTimestamp);} private void waitUntilTimeInPast(long writeTimestamp) {ClockBound now = boundedClock.now();而(现在。{uninterruptible . sleepuninterruptibly(现在。——写入时间戳,时间单元。毫秒); now = boundedClock.now(); } } private void removePending(long writeTimestamp) { pendingWriteTimestamps.remove(writeTimestamp); try { lock.lock(); cond.signalAll(); } finally { lock.unlock(); } } private void addPending(long writeTimestamp) { pendingWriteTimestamps.add(writeTimestamp); }
如果我们回到上面的Alice和Bob示例,当Philip在服务器Green上写入“title”的值——“After Dawn”——时,Green上的put操作会等待,直到所选的写入时间戳低于时钟边界的最早值。这保证了每个其他集群节点对于时钟绑定的最新值都具有更高的时间戳。为了说明这一点,考虑一下这个场景。绿色的误差范围是+ 1
.因此,使用从时间4开始的put操作,当它存储值时,Green将获取时钟边界的最新值,即5。然后等待,直到时钟绑定的最早值大于5。实际上,Green在将值实际存储到键值存储区之前等待不确定区间。

当该值在键值存储区中可用时,保证时钟绑定的最新值在每个集群节点上都大于5。这意味着由Blue处理的Bob请求和由Amber处理的Alice请求都保证获得标题的最新值。


如果格林的时间范围“更大”,我们也会得到同样的结果。误差范围越大,等待时间越长。如果Green的错误界限是最大的,它将继续等待,直到在键值存储区中提供可用的值。Amber和Blue都不能获得该值,直到它们的最新时间值超过7。当Alice在最晚时间7获得title的最新值时,每个其他集群节点将保证在其最新时间值处获得它。

Read-Wait
当读取该值时,客户机将始终从其集群节点绑定的时钟中选择最大值。
接收请求的集群节点需要确保在特定的请求时间戳返回响应后,在该时间戳或较低的时间戳处没有写入值。
如果请求中的时间戳高于服务器上的时间戳,集群节点将等待时钟赶上,然后返回响应。
然后,它将检查在较低的时间戳处是否有尚未存储的挂起的写请求。如果有,则读请求将暂停,直到请求完成。
然后,服务器将读取请求时间戳处的值并返回该值。这确保一旦响应在特定的时间戳返回,就不会在较低的时间戳上写入任何值。这个保证被称为快照隔离
类KVStore……
final Lock Lock = new ReentrantLock();QueuependingWriteTimestamps = new ArrayDeque<>();cond = lock.newCondition();public可选 read(long readTimestamp) {waitUntilTimeInPast(readTimestamp);waitForPendingWrites (readTimestamp);可选 max = kv.keySet().stream().max(Comparator.naturalOrder());if(max.isPresent()){返回可选的(kv.get(max.get()));}返回Optional.empty ();} private void waitForPendingWrites(long readTimestamp) {try {lock.lock();而(pendingWriteTimestamps.stream()。anyMatch(ts -> ts <= readTimestamp)) { cond.awaitUninterruptibly(); } } finally { lock.unlock(); } }
考虑最后一个场景:服务器Amber处理Alice的读请求,错误界限为3。它将读取标题的最晚时间设置为7。同时,Philip的写请求由Green处理(错误界为+-1),它取5来存储值。Alice的读请求一直等待到Green的最早时间超过7点,而挂起的写请求。然后它返回时间戳小于7的最新值。

例子
谷歌的视时API为我们提供了一个时钟边界。扳手使用它来实现提交-等待
时间同步服务确保最小的时钟漂移。可以使用ClockBoundAPI来实现等待,以便跨集群对事件进行排序。
CockroachDB实现读取重启。它还有一个实验性选项,可以基于配置的最大时钟漂移值使用提交-等待。
YugabyteDB根据配置的最大时钟漂移值实现读重新启动。
本页是:
分布式系统的模式

模式
重大修改
2022年8月17日:Publisheddate