返回介绍

不可靠的时钟

发布于 2024-08-24 16:53:17 字数 13251 浏览 0 评论 0 收藏 0

时钟和时间很重要。应用程序以各种方式依赖于时钟来回答以下问题:

  1. 这个请求是否超时了?
  2. 这项服务的第 99 百分位响应时间是多少?
  3. 在过去五分钟内,该服务平均每秒处理多少个查询?
  4. 用户在我们的网站上花了多长时间?
  5. 这篇文章在何时发布?
  6. 在什么时间发送提醒邮件?
  7. 这个缓存条目何时到期?
  8. 日志文件中此错误消息的时间戳是什么?

例 1-4 测量 持续时间 (例如,发送请求与正在接收的响应之间的时间间隔),而 例 5-8 描述 时间点(point in time) (在特定日期,特定时间发生的事件)。

​ 在分布式系统中,时间是一件棘手的事情,因为通信不是即时的:消息通过网络从一台机器传送到另一台机器需要时间。收到消息的时间总是晚于发送的时间,但是由于网络中的可变延迟,我们不知道多少时间。这个事实有时很难确定在涉及多台机器时发生事情的顺序。

​ 而且,网络上的每台机器都有自己的时钟,这是一个实际的硬件设备:通常是石英晶体振荡器。这些设备不是完全准确的,所以每台机器都有自己的时间概念,可能比其他机器稍快或更慢。可以在一定程度上同步时钟:最常用的机制是 网络时间协议(NTP) ,它允许根据一组服务器报告的时间来调整计算机时钟【37】。服务器则从更精确的时间源(如 GPS 接收机)获取时间。

单调钟与时钟

​ 现代计算机至少有两种不同的时钟:时钟和单调钟。尽管它们都衡量时间,但区分这两者很重要,因为它们有不同的目的。

时钟

​ 时钟是您直观地了解时钟的依据:它根据某个日历(也称为 挂钟时间(wall-clock time) )返回当前日期和时间。例如,Linuxv 上的 clock_gettime(CLOCK_REALTIME) 和 Java 中的 System.currentTimeMillis() 返回自 epoch(1970 年 1 月 1 日 午夜 UTC,格里高利历)以来的秒数(或毫秒),根据公历日历,不包括闰秒。有些系统使用其他日期作为参考点。

v. 虽然时钟被称为实时时钟,但它与实时操作系统无关,如第 298 页上的 响应时间保证 中所述。 ↩

​ 时钟通常与 NTP 同步,这意味着来自一台机器的时间戳(理想情况下)意味着与另一台机器上的时间戳相同。但是如下节所述,时钟也具有各种各样的奇特之处。特别是,如果本地时钟在 NTP 服务器之前太远,则它可能会被强制重置,看上去好像跳回了先前的时间点。这些跳跃以及他们经常忽略闰秒的事实,使时钟不能用于测量经过时间【38】。

​ 时钟还具有相当粗略的分辨率,例如,在较早的 Windows 系统上以 10 毫秒为单位前进【39】。在最近的系统中这已经不是一个问题了。

单调钟

​ 单调钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux 上的 clock_gettime(CLOCK_MONOTONIC) ,和 Java 中的 System.nanoTime() 都是单调时钟。这个名字来源于他们保证总是前进的事实(而时钟可以及时跳回)。

​ 你可以在某个时间点检查单调钟的值,做一些事情,且稍后再次检查它。这两个值之间的差异告诉你两次检查之间经过了多长时间。但单调钟的绝对值是毫无意义的:它可能是计算机启动以来的纳秒数,或类似的任意值。特别是比较来自两台不同计算机的单调钟的值是没有意义的,因为它们并不是一回事。

​ 在具有多个 CPU 插槽的服务器上,每个 CPU 可能有一个单独的计时器,但不一定与其他 CPU 同步。操作系统会补偿所有的差异,并尝试向应用线程表现出单调钟的样子,即使这些线程被调度到不同的 CPU 上。当然,明智的做法是不要太把这种单调性保证当回事【40】。

​ 如果 NTP 协议检测到计算机的本地石英钟比 NTP 服务器要更快或更慢,则可以调整单调钟向前走的频率(这称为 偏移(skewing) 时钟)。默认情况下,NTP 允许时钟速率增加或减慢最高至 0.05%,但 NTP 不能使单调时钟向前或向后跳转。单调时钟的分辨率通常相当好:在大多数系统中,它们能在几微秒或更短的时间内测量时间间隔。

​ 在分布式系统中,使用单调钟测量 经过时间(elapsed time) (比如超时)通常很好,因为它不假定不同节点的时钟之间存在任何同步,并且对测量的轻微不准确性不敏感。

时钟同步与准确性

​ 单调钟不需要同步,但是时钟需要根据 NTP 服务器或其他外部时间源来设置才能有用。不幸的是,我们获取时钟的方法并不像你所希望的那样可靠或准确——硬件时钟和 NTP 可能会变幻莫测。举几个例子:

​ 计算机中的石英钟不够精确:它会 漂移(drifts) (运行速度快于或慢于预期)。时钟漂移取决于机器的温度。 Google 假设其服务器时钟漂移为 200 ppm(百万分之一)【41】,相当于每 30 秒与服务器重新同步一次的时钟漂移为 6 毫秒,或者每天重新同步的时钟漂移为 17 秒。即使一切工作正常,此漂移也会限制可以达到的最佳准确度。

  • 如果计算机的时钟与 NTP 服务器的时钟差别太大,可能会拒绝同步,或者本地时钟将被强制重置【37】。任何观察重置前后时间的应用程序都可能会看到时间倒退或突然跳跃。
  • 如果某个节点被 NTP 服务器意外阻塞,可能会在一段时间内忽略错误配置。有证据表明,这在实践中确实发生过。
  • NTP 同步只能和网络延迟一样好,所以当您在拥有可变数据包延迟的拥塞网络上时,NTP 同步的准确性会受到限制。一个实验表明,当通过互联网同步时,35 毫秒的最小误差是可以实现的,尽管偶尔的网络延迟峰值会导致大约一秒的误差。根据配置,较大的网络延迟会导致 NTP 客户端完全放弃。
  • 一些 NTP 服务器错误或配置错误,报告时间已经过去了几个小时【43,44】。 NTP 客户端非常强大,因为他们查询多个服务器并忽略异常值。尽管如此,在互联网上陌生人告诉你的时候,你的系统的正确性还是值得担忧的。
  • 闰秒导致 59 分钟或 61 秒长的分钟,这混淆了未设计闰秒的系统中的时序假设【45】。闰秒已经使许多大型系统崩溃【38,46】的事实说明了,关于时钟的假设是多么容易偷偷溜入系统中。处理闰秒的最佳方法可能是通过在一天中逐渐执行闰秒调整(这被称为 拖尾(smearing) )【47,48】,使 NTP 服务器 撒谎 ,虽然实际的 NTP 服务器表现各异【49】。
  • 在虚拟机中,硬件时钟被虚拟化,这对于需要精确计时的应用程序提出了额外的挑战【50】。当一个 CPU 核心在虚拟机之间共享时,每个虚拟机都会暂停几十毫秒,而另一个虚拟机正在运行。从应用程序的角度来看,这种停顿表现为时钟突然向前跳跃【26】。
  • 如果您在未完全控制的设备上运行软件(例如,移动设备或嵌入式设备),则可能完全不信任该设备的硬件时钟。一些用户故意将其硬件时钟设置为不正确的日期和时间,例如,为了规避游戏中的时间限制,时钟可能会被设置到很远的过去或将来。

如果你足够关心这件事并投入大量资源,就可以达到非常好的时钟精度。例如,针对金融机构的欧洲法规草案 MiFID II 要求所有高频率交易基金在 UTC 时间 100 微秒内同步时钟,以便调试 闪崩 等市场异常现象,并帮助检测市场操纵 【51】。

​ 使用 GPS 接收机,精确时间协议(PTP)【52】以及仔细的部署和监测可以实现这种精确度。然而,这需要很多努力和专业知识,而且有很多东西都会导致时钟同步错误。如果你的 NTP 守护进程配置错误,或者防火墙阻止了 NTP 通信,由漂移引起的时钟误差可能很快就会变大。

依赖同步时钟

​ 时钟的问题在于,虽然它们看起来简单易用,但却具有令人惊讶的缺陷:一天可能不会有精确的 86,400 秒, 时钟 可能会前后跳跃,而一个节点上的时间可能与另一个节点上的时间完全不同。

​ 本章早些时候,我们讨论了网络丢包和任意延迟包的问题。尽管网络在大多数情况下表现良好,但软件的设计必须假定网络偶尔会出现故障,而软件必须正常处理这些故障。时钟也是如此:尽管大多数时间都工作得很好,但需要准备健壮的软件来处理不正确的时钟。

​ 有一部分问题是,不正确的时钟很容易被视而不见。如果一台机器的 CPU 出现故障或者网络配置错误,很可能根本无法工作,所以很快就会被注意和修复。另一方面,如果它的石英时钟有缺陷,或者它的 NTP 客户端配置错误,大部分事情似乎仍然可以正常工作,即使它的时钟逐渐偏离现实。如果某个软件依赖于精确同步的时钟,那么结果更可能是悄无声息且行踪渺茫数据的数据丢失,而不是一次惊天动地的崩溃【53,54】。

​ 因此,如果你使用需要同步时钟的软件,必须仔细监控所有机器之间的时钟偏移。时钟偏离其他时钟太远的节点应当被宣告死亡,并从集群中移除。这样的监控可以确保你在损失发生之前注意到破损的时钟。

有序事件的时间戳

​ 让我们考虑一个特别的情况,一件很有诱惑但也很危险的事情:依赖时钟,在多个节点上对事件进行排序。 例如,如果两个客户端写入分布式数据库,谁先到达? 哪一个更近?

图 8-3 显示了在具有多领导者复制的数据库中对时钟的危险使用(该例子类似于 图 5-9 )。 客户端 A 在节点 1 上写入 x = 1 ;写入被复制到节点 3;客户端 B 在节点 3 上增加 x(我们现在有 x = 2 );最后这两个写入都被复制到节点 2。

图 8-3 客户端 B 的写入比客户端 A 的写入要晚,但是 B 的写入具有较早的时间戳。

​ 在 图 8-3 中,当一个写入被复制到其他节点时,它会根据发生写入的节点上的时钟时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点 1 和节点 3 之间的偏差小于 3ms,这可能比你在实践中预期的更好。

​ 尽管如此, 图 8-3 中的时间戳却无法正确排列事件:写入 x = 1 的时间戳为 42.004 秒,但写入 x = 2 的时间戳为 42.003 秒,即使 x = 2 在稍后出现。当节点 2 接收到这两个事件时,会错误地推断出 x = 1 是最近的值,而丢弃写入 x = 2 。效果上表现为,客户端 B 的增量操作会丢失。

​ 这种冲突解决策略被称为 最后写入为准(LWW) ,它在多领导者复制和无领导者数据库(如 Cassandra 【53】和 Riak 【54】)中被广泛使用(参见 最后写入为准(丢弃并发写入) 一节)。有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变 LWW 的基本问题:

  • 数据库写入可能会神秘地消失:具有滞后时钟的节点无法用快速时钟覆盖之前由节点写入的值,直到节点之间的时钟偏差过去【54,55】。此方案可能导致一定数量的数据被悄悄丢弃,而未向应用报告任何错误。
  • LWW 无法区分 高频顺序写入 (在 图 8-3 中,客户端 B 的增量操作 一定 发生在客户端 A 的写入之后)和 真正并发写入 (写入者意识不到其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止因果关系的冲突(请参阅 检测并发写入 )。
  • 两个节点可以独立生成具有相同时间戳的写入,特别是在时钟仅具有毫秒分辨率的情况下。为了解决这样的冲突,还需要一个额外的 决胜值(tiebreaker) (可以简单地是一个大随机数),但这种方法也可能会导致违背因果关系【53】。

因此,尽管通过保留最 最近 的值并放弃其他值来解决冲突是很诱惑人的,但是要注意, 最近 的定义取决于本地的 时钟 ,这很可能是不正确的。即使用频繁同步的 NTP 时钟,一个数据包也可能在时间戳 100 毫秒(根据发送者的时钟)时发送,并在时间戳 99 毫秒(根据接收者的时钟)处到达——看起来好像数据包在发送之前已经到达,这是不可能的。

​ NTP 同步是否能足够准确,以至于这种不正确的排序不会发生?也许不能,因为 NTP 的同步精度本身受到网络往返时间的限制,除了石英钟漂移这类误差源之外。为了进行正确的排序,你需要一个比测量对象(即网络延迟)要精确得多的时钟。

​ 所谓的逻辑时钟【56,57】是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参见 检测并发写入 )。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的 时钟单调钟 也被称为物理时钟。我们将在 顺序保证 中查看更多订购信息。

时钟读数存在置信区间

​ 您可能能够以微秒或甚至纳秒的分辨率读取机器的时钟。但即使可以得到如此细致的测量结果,这并不意味着这个值对于这样的精度实际上是准确的。实际上,如前所述,即使您每分钟与本地网络上的 NTP 服务器进行同步,很可能也不会像前面提到的那样,在不精确的石英时钟上漂移几毫秒。使用公共互联网上的 NTP 服务器,最好的准确度可能达到几十毫秒,而且当网络拥塞时,误差可能会超过 100 毫秒【57】。

​ 因此,将时钟读数视为一个时间点是没有意义的——它更像是一段时间范围:例如,一个系统可能以 95%的置信度认为当前时间处于本分钟内的第 10.3 秒和 10.5 秒之间,它可能没法比这更精确了【58】。如果我们只知道±100 毫秒的时间,那么时间戳中的微秒数字部分基本上是没有意义的。

​ 不确定性界限可以根据你的时间源来计算。如果您的 GPS 接收器或原子(铯)时钟直接连接到您的计算机上,预期的错误范围由制造商报告。如果从服务器获得时间,则不确定性取决于自上次与服务器同步以来的石英钟漂移的期望值,加上 NTP 服务器的不确定性,再加上到服务器的网络往返时间(只是获取粗略近似值,并假设服务器是可信的)。

​ 不幸的是,大多数系统不公开这种不确定性:例如,当调用 clock_gettime() 时,返回值不会告诉你时间戳的预期错误,所以你不知道其置信区间是 5 毫秒还是 5 年。

​ 一个有趣的例外是 Spanner 中的 Google TrueTime API 【41】,它明确地报告了本地时钟的置信区间。当你询问当前时间时,你会得到两个值:[最早,最晚],这是最早可能的时间戳和最晚可能的时间戳。在不确定性估计的基础上,时钟知道当前的实际时间落在该区间内。间隔的宽度取决于自从本地石英钟最后与更精确的时钟源同步以来已经过了多长时间。

全局快照的同步时钟

​ 在 快照隔离和可重复读取 中,我们讨论了快照隔离,这是数据库中非常有用的功能,需要支持小型快速读写事务和大型长时间运行的只读事务,用于备份或分析)。它允许只读事务看到特定时间点的处于一致状态的数据库,且不会锁定和干扰读写事务。

​ 快照隔离最常见的实现需要单调递增的事务 ID。如果写入比快照晚(即,写入具有比快照更大的事务 ID),则该写入对于快照事务是不可见的。在单节点数据库上,一个简单的计数器就足以生成事务 ID。

​ 但是当数据库分布在许多机器上,也许可能在多个数据中心中时,由于需要协调,(跨所有分区)全局单调递增的事务 ID 可能很难生成。事务 ID 必须反映因果关系:如果事务 B 读取由事务 A 写入的值,则 B 必须具有比 A 更大的事务 ID,否则快照就无法保持一致。在有大量的小规模、高频率的事务情景下,在分布式系统中创建事务 ID 成为一个站不住脚的瓶颈vi

vi. 存在分布式序列号生成器,例如 Twitter 的雪花(Snowflake),其以可扩展的方式(例如,通过将 ID 空间的块分配给不同节点)近似单调地增加唯一 ID。但是,它们通常无法保证与因果关系一致的排序,因为分配的 ID 块的时间范围比数据库读取和写入的时间范围要长。另请参阅 顺序保证 。 ↩

​ 我们可以使用同步时钟的时间戳作为事务 ID 吗?如果我们能够获得足够好的同步性,那么这种方法将具有很合适的属性:更晚的事务会有更大的时间戳。当然,问题在于时钟精度的不确定性。

​ Spanner 以这种方式实现跨数据中心的快照隔离【59,60】。它使用 TrueTime API 报告的时钟置信区间,并基于以下观察结果:如果您有两个置信区间,每个置信区间包含最早和最近可能的时间戳( $A = [A{earliest}, A{latest}]$, $B=[B{earliest}, B{latest}] $),这两个区间不重叠(即:$A{earliest} < A{latest} < B{earliest} < B{latest}$),那么 B 肯定发生在 A 之后——这是毫无疑问的。只有当区间重叠时,我们才不确定 A 和 B 发生的顺序。

​ 为了确保事务时间戳反映因果关系,在提交读写事务之前,Spanner 在提交读写事务时,会故意等待置信区间长度的时间。通过这样,它可以确保任何可能读取数据的事务处于足够晚的时间,因此它们的置信区间不会重叠。为了保持尽可能短的等待时间,Spanner 需要保持尽可能小的时钟不确定性,为此,Google 在每个数据中心都部署了一个 GPS 接收器或原子钟,允许时钟在大约 7 毫秒内同步【41】。

​ 对分布式事务语义使用时钟同步是一个活跃的研究领域【57,61,62】。这些想法很有趣,但是它们还没有在谷歌之外的主流数据库中实现。

暂停进程

​ 让我们考虑在分布式系统中使用危险时钟的另一个例子。假设你有一个数据库,每个分区只有一个领导者。只有领导被允许接受写入。一个节点如何知道它仍然是领导者(它并没有被别人宣告为死亡),并且它可以安全地接受写入?

​ 一种选择是领导者从其他节点获得一个 租约(lease) ,类似一个带超时的锁【63】。任一时刻只有一个节点可以持有租约——因此,当一个节点获得一个租约时,它知道它在某段时间内自己是领导者,直到租约到期。为了保持领导地位,节点必须在周期性地在租约过期前续期。

​ 如果节点发生故障,就会停止续期,所以当租约过期时,另一个节点可以接管。

​ 可以想象,请求处理循环看起来像这样:

while(true){
    request=getIncomingRequest();
    // 确保租约还剩下至少 10 秒
    if (lease.expiryTimeMillis-System.currentTimeMillis()< 10000){
        lease = lease.renew();
    }

    if(lease.isValid()){
        process(request);
    }}
}

​ 这个代码有什么问题?首先,它依赖于同步时钟:租约到期时间由另一台机器设置(例如,当前时间加上 30 秒,计算到期时间),并将其与本地系统时钟进行比较。如果时钟超过几秒不同步,这段代码将开始做奇怪的事情。

​ 其次,即使我们将协议更改为仅使用本地单调时钟,也存在另一个问题:代码假定在执行剩余时间检查 System.currentTimeMillis() 和实际执行请求 process(request) 中间的时间间隔非常短。通常情况下,这段代码运行得非常快,所以 10 秒的缓冲区已经足够确保 租约 在请求处理到一半时不会过期。

​ 但是,如果程序执行中出现了意外的停顿呢?例如,想象一下,线程在 lease.isValid() 行周围停止 15 秒,然后才终止。在这种情况下,在请求被处理的时候,租约可能已经过期,而另一个节点已经接管了领导。然而,没有什么可以告诉这个线程已经暂停了这么长时间了,所以这段代码不会注意到租约已经到期了,直到循环的下一个迭代 ——到那个时候它可能已经做了一些不安全的处理请求。

​ 假设一个线程可能会暂停很长时间,这是疯了吗?不幸的是,这种情况发生的原因有很多种:

  • 许多编程语言运行时(如 Java 虚拟机)都有一个垃圾收集器(GC),偶尔需要停止所有正在运行的线程。这些 停止世界(stop-the-world) GC 暂停有时会持续几分钟【64】!甚至像 HotSpot JVM 的 CMS 这样的所谓的 并行 垃圾收集器也不能完全与应用程序代码并行运行,它需要不时地停止世界【65】。尽管通常可以通过改变分配模式或调整 GC 设置来减少暂停【66】,但是如果我们想要提供健壮的保证,就必须假设最坏的情况发生。
  • 在虚拟化环境中,可以 挂起(suspend) 虚拟机(暂停执行所有进程并将内存内容保存到磁盘)并恢复(恢复内存内容并继续执行)。这个暂停可以在进程执行的任何时候发生,并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移,而不需要重新启动,在这种情况下,暂停的长度取决于进程写入内存的速率【67】。
  • 在最终用户的设备(如笔记本电脑)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。
  • 当操作系统上下文切换到另一个线程时,或者当管理程序切换到另一个虚拟机时(在虚拟机中运行时),当前正在运行的线程可以在代码中的任意点处暂停。在虚拟机的情况下,在其他虚拟机中花费的 CPU 时间被称为 窃取时间(steal time) 。如果机器处于沉重的负载下(即,如果等待运行的线程很长),暂停的线程再次运行可能需要一些时间。
  • 如果应用程序执行同步磁盘访问,则线程可能暂停,等待缓慢的磁盘 I/O 操作完成【68】。在许多语言中,即使代码没有包含文件访问,磁盘访问也可能出乎意料地发生——例如,Java 类加载器在第一次使用时惰性加载类文件,这可能在程序执行过程中随时发生。 I/O 暂停和 GC 暂停甚至可能合谋组合它们的延迟【69】。如果磁盘实际上是一个网络文件系统或网络块设备(如亚马逊的 EBS),I/O 延迟进一步受到网络延迟变化的影响【29】。
  • 如果操作系统配置为允许交换到磁盘(分页),则简单的内存访问可能导致 页面错误(page fault) ,要求将磁盘中的页面装入内存。当这个缓慢的 I/O 操作发生时,线程暂停。如果内存压力很高,则可能需要将不同的页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为 抖动(thrashing) )。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险)。
  • 可以通过发送 SIGSTOP 信号来暂停 Unix 进程,例如通过在 shell 中按下 Ctrl-Z。 这个信号立即阻止进程继续执行更多的 CPU 周期,直到 SIGCONT 恢复为止,此时它将继续运行。 即使你的环境通常不使用 SIGSTOP,也可能由运维工程师意外发送。

所有这些事件都可以随时 抢占(preempt) 正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时机做任何假设,因为随时可能发生上下文切换,或者出现并行运行。

​ 当在一台机器上编写多线程代码时,我们有相当好的工具来实现线程安全:互斥量,信号量,原子计数器,无锁数据结构,阻塞队列等等。不幸的是,这些工具并不能直接转化为分布式系统操作,因为分布式系统没有共享内存,只有通过不可靠网络发送的消息。

​ 分布式系统中的节点,必须假定其执行可能在任意时刻暂停相当长的时间,即使是在一个函数的中间。在暂停期间,世界的其它部分在继续运转,甚至可能因为该节点没有响应,而宣告暂停节点的死亡。最终暂停的节点可能会继续运行,在再次检查自己的时钟之前,甚至可能不会意识到自己进入了睡眠。

响应时间保证

​ 在许多编程语言和操作系统中,线程和进程可能暂停一段无限制的时间,正如讨论的那样。如果你足够努力,导致暂停的原因是 可以 消除的。

​ 某些软件的运行环境要求很高,不能在特定时间内响应可能会导致严重的损失:飞机主控计算机,火箭,机器人,汽车和其他物体的计算机必须对其传感器输入做出快速而可预测的响应。在这些系统中,软件必须有一个特定的 截止时间(deadline) ,如果截止时间不满足,可能会导致整个系统的故障。这就是所谓的 硬实时(hard real-time) 系统。

实时是真的吗?

在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与 Web 上实时术语的模糊使用相反,它描述了服务器将数据推送到客户端以及流处理,而没有严格的响应时间限制(见 第 11 章 )。

例如,如果车载传感器检测到当前正在经历碰撞,你肯定不希望安全气囊释放系统因为 GC 暂停而延迟弹出。

​ 在系统中提供 实时保证 需要各级软件栈的支持:一个实时操作系统(RTOS),允许在指定的时间间隔内保证 CPU 时间的分配。库函数必须记录最坏情况下的执行时间;动态内存分配可能受到限制或完全不允许(实时垃圾收集器存在,但是应用程序仍然必须确保它不会给 GC 太多的负担);必须进行大量的测试和测量,以确保达到保证。

​ 所有这些都需要大量额外的工作,严重限制了可以使用的编程语言,库和工具的范围(因为大多数语言和工具不提供实时保证)。由于这些原因,开发实时系统非常昂贵,并且它们通常用于安全关键的嵌入式设备。而且, 实时高性能 不一样——事实上,实时系统可能具有较低的吞吐量,因为他们必须优先考虑及时响应高于一切(另请参见 延迟和资源利用 )。

​ 对于大多数服务器端数据处理系统来说,实时保证是不经济或不合适的。因此,这些系统必须承受在非实时环境中运行的暂停和时钟不稳定性。

限制垃圾收集的影响

​ 过程暂停的负面影响可以在不诉诸昂贵的实时调度保证的情况下得到缓解。语言运行时在计划垃圾回收时具有一定的灵活性,因为它们可以跟踪对象分配的速度和随着时间的推移剩余的空闲内存。

​ 一个新兴的想法是将 GC 暂停视为一个节点的短暂计划中断,并让其他节点处理来自客户端的请求,同时一个节点正在收集其垃圾。如果运行时可以警告应用程序一个节点很快需要 GC 暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行 GC。这个技巧隐藏了来自客户端的 GC 暂停,并降低了响应时间的高百分比【70,71】。一些对延迟敏感的金融交易系统【72】使用这种方法。

​ 这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象要快速收集),并定期在积累大量长寿对象(因此需要完整 GC)之前重新启动进程【65,73】。一次可以重新启动一个节点,在计划重新启动之前,流量可以从节点移开,就像 滚动升级 一样。

​ 这些措施不能完全阻止垃圾回收暂停,但可以有效地减少它们对应用的影响。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文