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

對於程式設計師和電腦人員來說,不幸的是,不需要像那樣巧妙的現象來讓時間變得怪異;電腦上的時鐘就是沒那麼好。它們會向前跳、向後跳、停頓或加速、取得閏秒、重新調整等等。在分散式系統上,不同的處理器以不同的速度運行,而諸如NTP之類的協定會處理時間校正,但可能會隨時崩潰。
因此,無需離開房間就可以讓電腦時間膨脹並破壞您對世界的理解。即使在單台電腦上,時間也可能以令人沮喪的方式移動。它就是不可靠。
在 Erlang 的上下文中,我們非常關心時間。我們想要低延遲,而且我們幾乎可以在所有操作中指定毫秒級的逾時和延遲:sockets、訊息接收、事件排程等等。我們也想要容錯能力,並能夠編寫可靠的系統。問題是我們如何從如此不可靠的事物中製造出堅固的東西?Erlang 採用了一種有些獨特的方法,並且自 18 版以來,它已經看到了一些非常有趣的演變。
過去的情況
在 18 版之前,Erlang 時間以兩種主要方式之一工作
- 作業系統的時鐘,表示為
{MegaSeconds, Seconds, MicroSeconds}
形式的元組 (os:timestamp()
) - 虛擬機器的時鐘,表示為
{MegaSeconds, Seconds, MicroSeconds}
形式的元組 (erlang:now()
,自動匯入為now()
)
作業系統的時鐘可以遵循任何模式

它可以按照作業系統認為的方式移動。
虛擬機器的時鐘只能向前移動,而且永遠不會傳回相同的值兩次。這是一個名為嚴格單調的屬性

為了讓 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/3
和 erlang:send_after/3
以及 timer
模組相關的計時器,透過調整時鐘頻率使其稍微加快或減慢來抑制突然的變化。
因此,我們不會看到以下任何一條曲線

我們會看到

在 18.0 之前的版本中,如果不需要時間校正,可以透過將 +c
引數傳遞給 Erlang 虛擬機器來關閉時間校正。
目前的情況 (18.0+)
18.0 之前版本的模型相當不錯,但它最終在一些特定方面令人惱火
- 時間校正是傾斜時鐘和不準確時鐘頻率之間的妥協。我們會為了更接近正確的作業系統時間而犧牲一些加速或減慢的頻率。為了避免破壞事件,時鐘只能非常緩慢地校正,因此我們可能會在很長一段時間內同時擁有不準確的時鐘和不準確的時間間隔
- 當人們想要單調且嚴格單調的時間(用於對事件排序)時,會使用
now()
- 當人們想要唯一值(在給定節點的生命週期內)時,會使用
now()
- 將時間設為
{MegaSecs, Secs, MicroSecs}
很惱人,而且是當時較大的整數對虛擬機器來說不切實際,以及轉換為適當的時間單位很麻煩的遺留物。當 Erlang 整數可以是任何大小時,沒有充分的理由使用這種格式。 - 時間的向後跳躍會使 Erlang 時鐘停頓(在每次呼叫之間,它只會以每次微秒的速度進展)
一般來說,問題在於有兩個工具(os:timestamp()
和 now()
)來完成以下所有任務
- 尋找系統的時間
- 測量兩個事件之間經過的時間
- 確定事件的順序(透過使用
now()
標記每個事件) - 建立唯一的值
所有這些都透過將 Erlang 中的時間分解為多個元件來變得更加清晰,從 18.0 開始
- 作業系統系統時間,也稱為 POSIX 時間。
- 作業系統單調時間;有些作業系統提供它,有些則沒有。它在可用時往往相當穩定,並避免時間跳躍。
- Erlang 系統時間。這是虛擬機器對 POSIX 時間的看法。虛擬機器會嘗試將其與 POSIX 對齊,但可能會根據選擇的策略略為移動(這些策略在時間扭曲中描述)。
- Erlang 單調時間。這是 Erlang 對作業系統單調時間的看法(如果可用),或者如果不可用,則是虛擬機器自身單調版本的系統時間。這是用於事件、計時器等的調整時鐘。它的穩定性使其成為計算時間間隔的理想選擇。請注意,此時間是單調的,但不是嚴格單調的,這表示時鐘不能倒退,但可以多次傳回相同的值!
- 時間偏移;由於 Erlang 單調時間是穩定的權威來源,因此將透過具有相對於 Erlang 單調時間的給定偏移來計算 Erlang 系統時間。這樣做的原因是它將允許 Erlang 調整系統時間,而無需修改單調時間頻率。
或更視覺化

如果偏移量是常數 0,那麼虛擬機器的單調時間和系統時間將相同。如果偏移量被正向或負向修改,則可以使 Erlang 系統時間與作業系統系統時間匹配,同時使 Erlang 單調時間保持獨立。實際上,單調時鐘可能是某個很大的負數,並且系統時鐘可以透過偏移量修改以表示正的 POSIX 時間戳記。
有了所有這些新元件,還剩另一個用例:始終遞增的唯一值。now()
函數的高成本是因為它永遠不會傳回相同數字兩次的必要性。如前所述,Erlang 單調時間不是嚴格單調的:例如,如果在兩個不同的核心上同時呼叫它,它可能會傳回相同的數字兩次。相比之下,now()
不會。為了彌補這一點,虛擬機器中新增了一個嚴格單調的數字產生器,以便可以單獨處理時間和唯一整數。
虛擬機器的新元件透過以下函數向使用者公開
erlang:monotonic_time()
和erlang:monotonic_time(Unit)
用於 Erlang 單調時間。它可能會傳回非常低的負數,但它們永遠不會變得更負。erlang:system_time()
和erlang:system_time(Unit)
,用於 Erlang 系統時間(在套用偏移量之後)erlang:timestamp()
以{MegaSecs, Secs, MicroSecs}
格式傳回 Erlang 系統時間,以實現向後相容性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 會讓時鐘加速或減速,直到它們與作業系統提供的時間一致。當時鐘跳動時,這對於保持某種程度的真實時間是可行的,但也意味著隨著時間的推移,跨多個節點的事件和超時會以一個小的百分比加速或減速。你還有一個用於虛擬機的單一開關 +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。
透過遵循這些概念,你的程式碼應該可以安全地在啟用時間校正的多時間扭曲模式中使用,並從其更好的準確性和更低的開銷中受益。
掌握所有這些資訊後,你現在應該能夠在時間中漂移和扭曲了!