後記:時光流逝

關於時間的時間

時間是非常非常棘手的東西。在真實的日常世界中,我們至少有一個確定性:時間向前移動,而且通常以恆定的速率移動。如果我們開始研究花俏的物理學(任何涉及相對論的東西,所以不是那麼花俏),那麼時間就會開始漂移和移動。飛機上的時鐘比地面上的時鐘慢,而接近黑洞的人的年齡與繞月球運行的人的年齡不同。

a pirate with a hook scared of time and clocks

對於程式設計師和電腦人員來說,不幸的是,不需要像那樣巧妙的現象來讓時間變得怪異;電腦上的時鐘就是沒那麼好。它們會向前跳、向後跳、停頓或加速、取得閏秒、重新調整等等。在分散式系統上,不同的處理器以不同的速度運行,而諸如NTP之類的協定會處理時間校正,但可能會隨時崩潰。

因此,無需離開房間就可以讓電腦時間膨脹並破壞您對世界的理解。即使在單台電腦上,時間也可能以令人沮喪的方式移動。它就是不可靠。

在 Erlang 的上下文中,我們非常關心時間。我們想要低延遲,而且我們幾乎可以在所有操作中指定毫秒級的逾時和延遲:sockets、訊息接收、事件排程等等。我們也想要容錯能力,並能夠編寫可靠的系統。問題是我們如何從如此不可靠的事物中製造出堅固的東西?Erlang 採用了一種有些獨特的方法,並且自 18 版以來,它已經看到了一些非常有趣的演變。

過去的情況

在 18 版之前,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

它可以按照作業系統認為的方式移動。

虛擬機器的時鐘只能向前移動,而且永遠不會傳回相同的值兩次。這是一個名為嚴格單調的屬性

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 程式的協調存取。每當它在短時間內連續呼叫兩次或時間倒退時,虛擬機器都會增加微秒以確保不會傳回相同的值兩次。這種協調機制(取得鎖定等等)可能會在繁忙的系統中充當瓶頸。

注意:單調性有兩種主要類型:嚴格和非嚴格。

保證嚴格單調計數器或時鐘始終傳回遞增的值(或始終遞減的值)。序列 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 虛擬機器引入了時間校正。時間校正使得虛擬機器針對與 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 虛擬機器來關閉時間校正。

目前的情況 (18.0+)

18.0 之前版本的模型相當不錯,但它最終在一些特定方面令人惱火

一般來說,問題在於有兩個工具(os:timestamp()now())來完成以下所有任務

所有這些都透過將 Erlang 中的時間分解為多個元件來變得更加清晰,從 18.0 開始

或更視覺化

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,那麼虛擬機器的單調時間和系統時間將相同。如果偏移量被正向或負向修改,則可以使 Erlang 系統時間與作業系統系統時間匹配,同時使 Erlang 單調時間保持獨立。實際上,單調時鐘可能是某個很大的負數,並且系統時鐘可以透過偏移量修改以表示正的 POSIX 時間戳記。

有了所有這些新元件,還剩另一個用例:始終遞增的唯一值。now() 函數的高成本是因為它永遠不會傳回相同數字兩次的必要性。如前所述,Erlang 單調時間不是嚴格單調的:例如,如果在兩個不同的核心上同時呼叫它,它可能會傳回相同的數字兩次。相比之下,now() 不會。為了彌補這一點,虛擬機器中新增了一個嚴格單調的數字產生器,以便可以單獨處理時間和唯一整數。

虛擬機器的新元件透過以下函數向使用者公開

上述所有函數中的 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 會讓時鐘加速或減速,直到它們與作業系統提供的時間一致。當時鐘跳動時,這對於保持某種程度的真實時間是可行的,但也意味著隨著時間的推移,跨多個節點的事件和超時會以一個小的百分比加速或減速。你還有一個用於虛擬機的單一開關 +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 下的時鐘等效。

呼~簡而言之,最佳的行動方案是確保你的程式碼可以處理時間扭曲,並進入多時間扭曲模式。如果你的程式碼不安全,請堅持使用無時間扭曲模式。

如何在時間扭曲中生存

透過遵循這些概念,你的程式碼應該可以安全地在啟用時間校正的多時間扭曲模式中使用,並從其更好的準確性和更低的開銷中受益。

掌握所有這些資訊後,你現在應該能夠在時間中漂移和扭曲了!