后记:时光流逝
关于时间
时间是一个非常非常棘手的东西。在现实世界中,我们至少可以确定一件事:时间向前流动,并且通常以恒定的速度流动。如果我们开始研究高级物理学(任何涉及相对论的物理学,所以不是 *那么* 高级),那么时间就会开始漂移和移动。飞机上的时钟比地面的时钟走得慢,靠近黑洞的人的衰老速度不同于绕月球运行的人。
不幸的是,对于程序员和计算机人员来说,即使不需要像这样的奇特现象来使时间变得奇怪;计算机上的时钟并不那么出色。它们会向前跳跃、向后跳跃、停顿或加速,获得 闰秒,重新调整,等等。在分布式系统中,不同的处理器以不同的速度运行,诸如 NTP 之类的协议将对时间进行校正,但可能随时崩溃。
因此,没有必要离开房间让计算机时间膨胀并破坏您对世界的理解。即使在单台计算机上,时间也有可能以令人沮丧的方式移动。它只是不太可靠。
在 Erlang 的上下文中,我们非常关心时间。我们希望低延迟,并且可以在几乎所有操作中以毫秒为单位指定超时和延迟:套接字、消息接收、事件调度等等。我们还希望容错,并且能够编写可靠的系统。问题是如何用这些不可靠的东西构建坚实的东西?Erlang 采取了某种独特的做法,并且自 18 版发布以来,它经历了一些非常有趣的演变。
过去的情况
在 18.0 版之前,Erlang 时间主要以两种方式工作
- 操作系统的时钟,表示为
{MegaSeconds, Seconds, MicroSeconds}
格式的元组 (os:timestamp()
) - 虚拟机的时钟,表示为
{MegaSeconds, Seconds, MicroSeconds}
格式的元组 (erlang:now()
,自动导入为now()
)
操作系统的时钟可以遵循任何模式
它可以按照操作系统想要的方式移动它。
VM 的时钟只能向前移动,并且永远不会返回相同的值两次。这称为 *严格单调* 属性
为了让 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/3
和 erlang:send_after/3
以及 timer
模块相关的计时器,通过调整时钟频率以稍微更快或更慢的速度运行来抑制突然的变化。
因此,我们不会看到以下曲线中的任何一条
我们将看到
18.0 版之前的版本中的时间校正,如果不需要,可以通过将 +c
参数传递给 Erlang VM 来关闭。
目前的情况 (18.0+)
18.0 版之前的版本中看到的模型相当不错,但它最终在一些特定方面变得令人讨厌
- 时间校正是在扭曲的时钟和不准确的时钟频率之间的一种折衷方案。我们将牺牲一些加速或减速的频率,以更接近正确的操作系统时间。为了避免破坏事件,时钟只能非常缓慢地校正,因此我们可能在很长一段时间内同时拥有不准确的时钟 *和* 不准确的间隔
- 人们在想要单调 *且* 严格单调时间时使用
now()
(用于对事件进行排序) - 人们在想要唯一值时使用
now()
(对于给定节点的生存期) - 将时间表示为
{MegaSecs, Secs, MicroSecs}
很烦人,这是过去时代遗留下来的产物,当时更大的整数对于 VM 来说难以表示,转换为适当的时间单位很痛苦。当 Erlang 整数可以是任何大小的时候,没有充分的理由使用这种格式。 - 时间倒退会使 Erlang 时钟停顿(它只会每次调用之间增加微秒)
总的来说,问题在于有两个工具 (os:timestamp()
和 now()
) 来完成以下所有任务
- 查找系统的当前时间
- 测量两个事件之间经过的时间
- 确定事件的顺序(通过用
now()
标记每个事件) - 创建唯一值
所有这些都通过从 18.0 开始将 Erlang 中的时间分解为多个组件变得更加清晰
- 操作系统系统时间,也称为 POSIX 时间。
- 操作系统单调时间;一些操作系统提供它,而另一些操作系统则不提供。它在可用时往往相当稳定,并避免时间跳跃。
- Erlang 系统时间。这是 VM 对 POSIX 时间的理解。VM 将尝试将其与 POSIX 对齐,但它可能会根据所选策略(这些策略在 时间扭曲 中描述)略微移动。
- Erlang 单调时间。这是 Erlang 对操作系统单调时间的理解,如果可用,或者 VM 自己的系统时间的单调版本,如果不可用。这是用于事件、计时器等的调整后的时钟。它的稳定性使其非常适合计算时间间隔。请注意,此时间是 *单调的*,但不是 *严格单调的*,这意味着时钟不能倒退,但它可以多次返回相同的值!
- 时间偏移;由于 Erlang 单调时间是稳定的权威来源,因此 Erlang 系统时间将通过与 Erlang 单调时间相关的给定偏移量来计算。这样做是为了使 Erlang 能够调整系统时间而不修改单调时间频率。
或者更直观地说
如果偏移量始终为 0,则 VM 的单调时间和系统时间将相同。如果偏移量正向或负向修改,则可以使 Erlang 系统时间与操作系统系统时间匹配,而 Erlang 单调时间保持独立。实际上,单调时钟可能是一个很大的负数,系统时钟可以根据偏移量进行修改以表示正 POSIX 时间戳。
有了所有这些新组件,另一个用例仍然存在:*始终* 递增的唯一值。now()
函数的高成本是由于它永远不会返回相同数字两次的必要性。如前所述,Erlang 单调时间不是 *严格* 单调的:如果它在例如两个不同的内核上同时被调用,它可能会返回相同的数字两次。相比之下,now()
不会。为了弥补这一点,VM 中添加了一个严格单调的数字生成器,以便可以分别处理时间和唯一整数。
VM 的新组件通过以下函数公开给用户
erlang:monotonic_time()
和erlang:monotonic_time(Unit)
用于 Erlang 单调时间。它可能会返回非常小的负数,但它们永远不会变得更负。erlang:system_time()
和erlang:system_time(Unit)
用于 Erlang 系统时间(应用偏移量后)erlang:timestamp()
返回 Erlang 系统时间,格式为{MegaSecs, Secs, MicroSecs}
,以确保向后兼容性erlang:time_offset()
和erlang:time_offset(Unit)
用于确定 Erlang 单调时钟和 Erlang 系统时钟之间的差异erlang:unique_integer()
和erlang:unique_integer(Options)
返回唯一值。Options 列表可以包含positive
(强制数字大于 0)和monotonic
(因此它们始终增长)中的一个或两个。Options 默认为[]
,这意味着虽然整数是唯一的,但它们可能是正数或负数,并且可能比之前给出的整数大或小。erlang:system_info(os_system_time_source)
,它提供对操作系统系统时间的容差、间隔和值的访问。erlang:system_info(os_monotonic_time_source)
:如果操作系统有单调时钟,则可以从这里获取它的容差、间隔和值。
以上所有函数中的 Unit 选项可以是 seconds
、milli_seconds
、micro_seconds
、nano_seconds
或 native
。默认情况下,返回的时间戳类型为 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}
。
那么时间是如何调整的呢?准备好迎接时间扭曲吧!
时间扭曲
旧式的 Erlang 东西只会让时钟更快或更慢地漂移,直到它们与操作系统提供的时钟匹配。这对于在时钟跳跃时保持某种程度的实时性是可行的,但也意味着随着时间的推移,事件和超时将在多个节点上以较小的百分比更快或更慢地发生。您还可以使用 VM 的一个开关 +c
,完全禁用时间校正。
Erlang 18.0 引入了对执行方式的区分,使其更加强大和复杂。在 18.0 之前的版本中,只有时间漂移,这意味着时钟会加速或减速,18.0 引入了时间校正和一个名为“时间扭曲”的东西。
基本上,时间扭曲 使用 +C
配置,是关于选择偏移量(因此是Erlang 系统时间)如何跳跃以与操作系统保持一致。时间扭曲是时间跳跃。然后是时间校正,使用 +c
配置,它描述了Erlang 单调时间在操作系统单调时钟跳跃时的行为。
时间校正只有两种策略,但时间扭曲有三种。问题是选择的时间扭曲策略会影响时间校正的影响,因此最终会产生惊人的6种可能的行为。为了弄清楚这一点,以下表格可能有所帮助
+C no_time_warp |
|
||||
+C multi_time_warp |
|
||||
+C single_time_warp |
这是一种特殊的混合模式,在您知道 Erlang 在操作系统时钟同步之前启动的嵌入式硬件上使用。它分为两个阶段
|
哇。简而言之,最好的做法是确保您的代码能够处理时间扭曲,并进入多时间扭曲模式。如果您的代码不安全,请坚持使用无时间扭曲。
如何在时间扭曲中生存
- 要查找系统时间:
erlang:system_time/0-1
- 要测量时间差异:两次调用
erlang:monotonic_time/0-1
并减去它们 - 要在节点上定义事件之间的绝对顺序:
erlang:unique_integer([monotonic])
- 测量时间并确保定义了绝对顺序:
{erlang:monotonic_time(), erlang:unique_integer([monotonic])}
- 创建唯一名称:
erlang:unique_integer([positive])
。如果希望该值在集群中唯一,请将其与节点名称配对,或者尝试使用 UUIDv1。
遵循这些概念,您的代码应该可以在启用时间校正的多时间扭曲模式下正常使用,并受益于其更高的精度和更低的开销。
有了所有这些信息,您现在应该能够穿越时间漂移和扭曲了!