追記: 時は流れる

時間について考える時間

時間は非常に厄介なものです。物理的な日常生活では、少なくとも一つの確実性があります。時間は常に前進し、一般的には一定の速度で進みます。しかし、高度な物理学(相対性理論が関係する分野。それほど高度ではないかもしれませんが)に目を向けると、時間は漂流し、変化し始めます。飛行機の時計は地上の時計よりも遅く進み、ブラックホールの近くにいる人は月の軌道を周回している人とは異なる速度で歳をとります。

a pirate with a hook scared of time and clocks

プログラマーやコンピューター関係者にとって残念なことに、時間がおかしくなるには、そのような素晴らしい現象は必要ありません。コンピューターの時計はそれほど正確ではないのです。時計は進んだり、遅れたり、停止したり、加速したり、うるう秒が挿入されたり、再調整されたりします。分散システムでは、異なるプロセッサが異なる速度で動作し、NTPなどのプロトコルが時刻の修正を行いますが、いつでもクラッシュする可能性があります。

したがって、コンピューターの時間が遅れて世界の理解を損なうには、部屋を出る必要はありません。単一のコンピューター上でも、時間がイライラするような動きをする可能性があります。コンピューターの時間は、ただ信頼性が低いだけなのです。

Erlang のコンテキストでは、時間について非常に気を使います。低レイテンシを望んでおり、ソケット、メッセージ受信、イベントスケジューリングなど、ほぼすべての操作でタイムアウトと遅延をミリ秒単位で指定できます。また、フォールトトレランスと信頼性の高いシステムを構築できることも望んでいます。問題は、そのような信頼性の低いものからどのようにして堅牢なシステムを作ることができるかということです。Erlang はやや独自のアプローチを採用しており、リリース 18 以降、非常に興味深い進化を遂げています。

以前の状況

リリース 18 以前は、Erlang の時間は主に次の 2 つの方法で機能していました。

  1. {メガ秒, 秒, マイクロ秒} の形式のタプルで表されるオペレーティングシステムのクロック (os:timestamp())
  2. {メガ秒, 秒, マイクロ秒} の形式のタプルで表される仮想マシンのクロック (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

OS がどのように動かしたいかによって、どのようにでも動くことができます。

VM のクロックは前進することしかできず、同じ値を 2 回返すことはありません。これは*厳密な単調性*と呼ばれる性質です。

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 はマイクロ秒を増分して、同じ値が 2 回返されないようにします。この調整メカニズム(ロックの取得など)は、ビジー状態のシステムではボトルネックとなる可能性があります。

**注:** 単調性には、主に厳密な単調性と非厳密な単調性の 2 種類があります。

厳密な単調性を持つカウンターまたはクロックは、常に増加する値(または常に減少する値)を返すことが保証されています。シーケンス `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()receiveafter ビット、erlang:start_timer/3 および erlang: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 より前の時間補正は、不要な場合は、Erlang VM に `+c` 引数を渡すことで無効にすることができます。

現状 (18.0 以降)

18.0 より前のバージョンのモデルは非常に優れていましたが、最終的にはいくつかの特定の方法で煩わしいことが判明しました。

一般的に、問題は、次のすべてのタスクを実行するために 2 つのツール (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 システム時間は OS システム時間と一致するように変更できますが、Erlang 単調時間は独立したままになります。実際には、単調クロックが非常に大きな負の数になり、システムクロックがオフセットによって変更されて正の POSIX タイムスタンプを表す可能性があります。

これらの新しいコンポーネントにより、もう 1 つのユースケースが残ります。*常に*増加する一意の値です。 `now()` 関数の高コストは、同じ数値を 2 回返さないというこの必要性によるものでした。前述のように、Erlang 単調時間は*厳密に*単調ではありません。たとえば、2 つの異なるコアで同時に呼び出された場合、同じ数値を 2 回返す可能性があります。それに比べて、`now()` はそうではありませんでした。これを補うために、厳密に単調な数値ジェネレーターが VM に追加され、時間と一意の整数を個別に処理できるようになりました。

VM の新しいコンポーネントは、次の関数を使用してユーザーに公開されます。

上記のすべての関数の 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}` になります。

では、時間はどのように調整されるのでしょうか? タイムワープの準備をしましょう!

タイムワープ

A Dali-style melting clock

以前のErlangでは、OSが提供する時間に合わせて、クロックを速くしたり遅くしたりしていました。 これは、クロックがジャンプしたときにリアルタイム性を維持するには問題ありませんでしたが、時間の経過とともに、複数のノードでイベントとタイムアウトがわずかな割合で速くなったり遅くなったりすることを意味していました。 また、VMには `+c` という単一のスイッチがあり、時間の修正を完全に無効にしていました。

Erlang 18.0では、動作の区別が導入され、はるかに強力で複雑になりました。 18.0より前のバージョンでは時間のドリフト(クロックの加速または減速)しかありませんでしたが、18.0では時間修正と*タイムワープ*と呼ばれるものが導入されました。

基本的に、`+C` で設定される *タイムワープ* は、OSと同期するために *オフセット*(したがって *Erlangシステム時間*)をどのようにジャンプさせるかを選択することです。 タイムワープは時間のジャンプです。 そして、`+c` で設定される *時間修正* は、OSの単調増加クロックがジャンプしたときの *Erlang単調増加時間* の動作です。

時間修正の戦略は2つだけですが、タイムワープには3つあります。 問題は、選択されたタイムワープ戦略が時間修正に影響を与えるため、最終的に6種類の動作が発生することです。 これを理解するために、次の表が役立つ場合があります。

+C no_time_warp
+c true 18.0より前とまったく同じように動作します。 時間はワープせず(ジャンプせず)、クロック周波数を調整して補正します。 これは下位互換性のためにデフォルトです。
+c false OSシステム時間が逆方向にジャンプした場合、Erlang単調増加クロックはOSシステム時間が順方向にジャンプするまで停止します。これには時間がかかる場合があります。
+C multi_time_warp
+c true Erlangシステム時間は、オフセットを介して前後に調整され、OSシステム時間と一致します。 単調増加クロックは可能な限り安定して正確に保つことができます。
+c false Erlangシステム時間はオフセットを介して前後に調整されますが、時間修正がないため、単調増加クロックは(長時間フリーズすることなく)一時的に停止する場合があります。
+C single_time_warp

これは、ErlangがOSクロックが同期される前に起動することがわかっている場合に、組み込みハードウェアで使用される特別なハイブリッドモードです。 2つのフェーズで動作します。

    1. (`+c true` を使用)システムの起動時、単調増加クロックは可能な限り安定に保たれますが、システム時間の調整は行われません。
    2. (`+c false` を使用)`no_time_warp` と同じです。
  1. ユーザーが `erlang:system_flag(time_offset, finalize)` を呼び出すと、Erlangシステム時間はOSシステム時間に合わせて1回ワープし、その後、クロックは `no_time_warp` の場合と同じになります。

ふう。 要するに、最善の策は、コードがタイムワーピングに対応できるようにし、マルチタイムワープモードにすることです。 コードが安全でない場合は、タイムワープなしにしましょう。

タイムワープを乗り切る方法

これらの概念に従うことで、コードは時間修正を有効にしたマルチタイムワープモードで使用できるようになり、その優れた精度と低いオーバーヘッドの恩恵を受けることができます。

これらすべての情報を手にしたことで、あなたは時間の中をドリフトし、ワープすることができるようになりました!