后记:时光流逝

关于时间

时间是一个非常非常棘手的东西。在现实世界中,我们至少可以确定一件事:时间向前流动,并且通常以恒定的速度流动。如果我们开始研究高级物理学(任何涉及相对论的物理学,所以不是 *那么* 高级),那么时间就会开始漂移和移动。飞机上的时钟比地面的时钟走得慢,靠近黑洞的人的衰老速度不同于绕月球运行的人。

a pirate with a hook scared of time and clocks

不幸的是,对于程序员和计算机人员来说,即使不需要像这样的奇特现象来使时间变得奇怪;计算机上的时钟并不那么出色。它们会向前跳跃、向后跳跃、停顿或加速,获得 闰秒,重新调整,等等。在分布式系统中,不同的处理器以不同的速度运行,诸如 NTP 之类的协议将对时间进行校正,但可能随时崩溃。

因此,没有必要离开房间让计算机时间膨胀并破坏您对世界的理解。即使在单台计算机上,时间也有可能以令人沮丧的方式移动。它只是不太可靠。

在 Erlang 的上下文中,我们非常关心时间。我们希望低延迟,并且可以在几乎所有操作中以毫秒为单位指定超时和延迟:套接字、消息接收、事件调度等等。我们还希望容错,并且能够编写可靠的系统。问题是如何用这些不可靠的东西构建坚实的东西?Erlang 采取了某种独特的做法,并且自 18 版发布以来,它经历了一些非常有趣的演变。

过去的情况

在 18.0 版之前,Erlang 时间主要以两种方式工作

  1. 操作系统的时钟,表示为 {MegaSeconds, Seconds, MicroSeconds} 格式的元组 (os:timestamp())
  2. 虚拟机的时钟,表示为 {MegaSeconds, Seconds, MicroSeconds} 格式的元组 (erlang:now(),自动导入为 now())

操作系统的时钟可以遵循任何模式

A curve with the x-axis being real time and the y-axis being OS time; the curve goes up and down randomly although generally trending up

它可以按照操作系统想要的方式移动它。

VM 的时钟只能向前移动,并且永远不会返回相同的值两次。这称为 *严格单调* 属性

A curve with the x-axis being real time and the y-axis being VM time; the curve goes up steadily though at irregular rates

为了让 now() 尊重这些属性,它需要所有 Erlang 进程的协调访问。每当它在短时间间隔内或时间倒流时连续调用两次时,VM 将增加微秒以确保不会返回相同的值两次。这种协调机制(获取锁等等)在繁忙的系统中可能成为瓶颈。

注意: 单调性主要分为两种类型:严格和非严格。

严格单调计数器或时钟保证始终返回递增值(或始终返回递减值)。序列 1, 2, 3, 4, 5 是严格单调的。

常规(非严格)单调计数器只需要返回非递减值(或非递增值)。序列 1, 2, 2, 2, 3, 4 是单调的,但不是严格单调的。

现在,拥有永远不倒退的时间是一个有用的属性,但在许多情况下,这还不够。其中一个常见的情况是人们在自己的笔记本电脑上对 Erlang 进行编程时遇到的情况。您坐在电脑前,定期运行 Erlang 任务。这运行良好,从未让您失望。但有一天,您听到外面冰激凌车的铃声,然后在跑到户外买东西吃之前,把电脑关机。15 分钟后,您回来,唤醒笔记本电脑,程序中的一切就开始爆炸。发生了什么事?

答案取决于如何计算时间。如果以周期为单位计算(“我在 CPU 上看到了 N 条指令飞过,那是 12 秒!”),那么您可能没事。如果通过查看墙上的时钟并说“哇,现在是 6:15,上次是 4:20!已经过去了 1 小时 55 分钟!”来计算时间,那么对于那些预计每隔几秒钟运行一次的任务来说,睡觉会造成很大的伤害。

另一方面,如果您使用周期并保持它们稳定,那么您实际上永远不会看到程序中的时钟与底层操作系统同步。这意味着我们要么可以获得准确的 now() 值,要么获得准确的间隔,但不能同时获得两者。

出于这个原因,Erlang VM 引入了 *时间校正*。时间校正使 VM 能够针对与 now()receive 中的 after 位、erlang:start_timer/3erlang:send_after/3 以及 timer 模块相关的计时器,通过调整时钟频率以稍微更快或更慢的速度运行来抑制突然的变化。

因此,我们不会看到以下曲线中的任何一条

Two curves with the x-axis being real time and the y-axis being uncorrected VM time; the first curve (labelled 'frequency-based') goes up, plateaus, then goes up again. The second curve (labelled 'clock-based') goes up, plateaus, jumps up directly, then resumes going up

我们将看到

Three curves with the x-axis being real time and the y-axis being VM time; the first curve (labelled 'frequency-based') goes up, plateaus, then goes up again. The second curve (labelled 'clock-based') goes up, plateaus, jumps up directly, then resumes going up. The third curve, labelled 'corrected time' closes the gap between both other curves after the plateau

18.0 版之前的版本中的时间校正,如果不需要,可以通过将 +c 参数传递给 Erlang VM 来关闭。

目前的情况 (18.0+)

18.0 版之前的版本中看到的模型相当不错,但它最终在一些特定方面变得令人讨厌

总的来说,问题在于有两个工具 (os:timestamp()now()) 来完成以下所有任务

所有这些都通过从 18.0 开始将 Erlang 中的时间分解为多个组件变得更加清晰

或者更直观地说

comparison of timelines for real time, os monotonic time, Erlang monotonic time, Erlang system time, and OS system time, with their respective synchronization points and offsets.

如果偏移量始终为 0,则 VM 的单调时间和系统时间将相同。如果偏移量正向或负向修改,则可以使 Erlang 系统时间与操作系统系统时间匹配,而 Erlang 单调时间保持独立。实际上,单调时钟可能是一个很大的负数,系统时钟可以根据偏移量进行修改以表示正 POSIX 时间戳。

有了所有这些新组件,另一个用例仍然存在:*始终* 递增的唯一值。now() 函数的高成本是由于它永远不会返回相同数字两次的必要性。如前所述,Erlang 单调时间不是 *严格* 单调的:如果它在例如两个不同的内核上同时被调用,它可能会返回相同的数字两次。相比之下,now() 不会。为了弥补这一点,VM 中添加了一个严格单调的数字生成器,以便可以分别处理时间和唯一整数。

VM 的新组件通过以下函数公开给用户

以上所有函数中的 Unit 选项可以是 secondsmilli_secondsmicro_secondsnano_secondsnative。默认情况下,返回的时间戳类型为 native 格式。该单位在运行时确定,可以使用一个函数在它们之间进行转换

1> erlang:convert_time_unit(1, seconds, native).
1000000000

这意味着我的 Linux VPS 的单位为纳秒。实际分辨率可能低于此(可能只有毫秒是准确的),但无论如何,它以纳秒为单位原生工作。

工具库中的最后一个工具是一种新型监控器,可用于检测时间偏移跳跃。它可以调用为 erlang:monitor(time_offset, clock_service)。它返回一个引用,当时间漂移时,收到的消息将是 {'CHANGE', MonitorRef, time_offset, clock_service, NewTimeOffset}

那么时间是如何调整的呢?准备好迎接时间扭曲吧!

时间扭曲

A Dali-style melting clock

旧式的 Erlang 东西只会让时钟更快或更慢地漂移,直到它们与操作系统提供的时钟匹配。这对于在时钟跳跃时保持某种程度的实时性是可行的,但也意味着随着时间的推移,事件和超时将在多个节点上以较小的百分比更快或更慢地发生。您还可以使用 VM 的一个开关 +c,完全禁用时间校正。

Erlang 18.0 引入了对执行方式的区分,使其更加强大和复杂。在 18.0 之前的版本中,只有时间漂移,这意味着时钟会加速或减速,18.0 引入了时间校正和一个名为“时间扭曲”的东西。

基本上,时间扭曲 使用 +C 配置,是关于选择偏移量(因此是Erlang 系统时间)如何跳跃以与操作系统保持一致。时间扭曲是时间跳跃。然后是时间校正,使用 +c 配置,它描述了Erlang 单调时间在操作系统单调时钟跳跃时的行为。

时间校正只有两种策略,但时间扭曲有三种。问题是选择的时间扭曲策略会影响时间校正的影响,因此最终会产生惊人的6种可能的行为。为了弄清楚这一点,以下表格可能有所帮助

+C no_time_warp
+c true 与 18.0 之前的版本完全一样。时间不会扭曲(不会跳跃),但时钟频率会进行调整以进行补偿。这是默认值,为了向后兼容。
+c false 如果操作系统系统时间向后跳跃,Erlang 单调时钟将一直停滞,直到操作系统系统时间向前跳跃,这可能需要一段时间。
+C multi_time_warp
+c true Erlang 系统时间通过偏移量进行前后调整,以匹配操作系统系统时间。单调时钟可以保持尽可能稳定和准确。
+c false Erlang 系统时间通过偏移量进行前后调整,但由于没有时间校正,单调时钟可能会短暂暂停(不会长时间冻结)。
+C single_time_warp

这是一种特殊的混合模式,在您知道 Erlang 在操作系统时钟同步之前启动的嵌入式硬件上使用。它分为两个阶段

    1. (使用 +c true) 系统启动时,单调时钟保持尽可能稳定,但不会进行系统时间调整
    2. (使用 +c false) 与 no_time_warp 相同
  1. 用户调用 erlang:system_flag(time_offset, finalize),Erlang 系统时间扭曲一次以匹配操作系统系统时间,然后时钟变得等效于 no_time_warp 下的时钟。

哇。简而言之,最好的做法是确保您的代码能够处理时间扭曲,并进入多时间扭曲模式。如果您的代码不安全,请坚持使用无时间扭曲。

如何在时间扭曲中生存

遵循这些概念,您的代码应该可以在启用时间校正的多时间扭曲模式下正常使用,并受益于其更高的精度和更低的开销。

有了所有这些信息,您现在应该能够穿越时间漂移和扭曲了!