追記: 時は流れる
時間について考える時間
時間は非常に厄介なものです。物理的な日常生活では、少なくとも一つの確実性があります。時間は常に前進し、一般的には一定の速度で進みます。しかし、高度な物理学(相対性理論が関係する分野。それほど高度ではないかもしれませんが)に目を向けると、時間は漂流し、変化し始めます。飛行機の時計は地上の時計よりも遅く進み、ブラックホールの近くにいる人は月の軌道を周回している人とは異なる速度で歳をとります。
プログラマーやコンピューター関係者にとって残念なことに、時間がおかしくなるには、そのような素晴らしい現象は必要ありません。コンピューターの時計はそれほど正確ではないのです。時計は進んだり、遅れたり、停止したり、加速したり、うるう秒が挿入されたり、再調整されたりします。分散システムでは、異なるプロセッサが異なる速度で動作し、NTPなどのプロトコルが時刻の修正を行いますが、いつでもクラッシュする可能性があります。
したがって、コンピューターの時間が遅れて世界の理解を損なうには、部屋を出る必要はありません。単一のコンピューター上でも、時間がイライラするような動きをする可能性があります。コンピューターの時間は、ただ信頼性が低いだけなのです。
Erlang のコンテキストでは、時間について非常に気を使います。低レイテンシを望んでおり、ソケット、メッセージ受信、イベントスケジューリングなど、ほぼすべての操作でタイムアウトと遅延をミリ秒単位で指定できます。また、フォールトトレランスと信頼性の高いシステムを構築できることも望んでいます。問題は、そのような信頼性の低いものからどのようにして堅牢なシステムを作ることができるかということです。Erlang はやや独自のアプローチを採用しており、リリース 18 以降、非常に興味深い進化を遂げています。
以前の状況
リリース 18 以前は、Erlang の時間は主に次の 2 つの方法で機能していました。
{メガ秒, 秒, マイクロ秒}の形式のタプルで表されるオペレーティングシステムのクロック (os:timestamp()){メガ秒, 秒, マイクロ秒}の形式のタプルで表される仮想マシンのクロック (erlang:now()、now()として自動インポート)
オペレーティングシステムのクロックは、どのようなパターンでもたどることができます。
OS がどのように動かしたいかによって、どのようにでも動くことができます。
VM のクロックは前進することしかできず、同じ値を 2 回返すことはありません。これは*厳密な単調性*と呼ばれる性質です。
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()、receive の after ビット、erlang:start_timer/3 および erlang:send_after/3、そして `timer` モジュールに関連するタイマーについて、クロック周波数をわずかに速くまたは遅く調整することで、急激な変化を抑制します。
そのため、これらの曲線のいずれかを見る代わりに
次のような曲線が見られます。
バージョン 18.0 より前の時間補正は、不要な場合は、Erlang VM に `+c` 引数を渡すことで無効にすることができます。
現状 (18.0 以降)
18.0 より前のバージョンのモデルは非常に優れていましたが、最終的にはいくつかの特定の方法で煩わしいことが判明しました。
- 時間補正は、歪んだクロックと不正確なクロック周波数の間の妥協案でした。適切な OS 時間に近づくために、多少の加速または減速された周波数と引き換えにすることになります。イベントの破損を避けるために、クロックは非常にゆっくりとしか修正できないため、非常に長い期間、不正確なクロックと不正確な間隔の両方を持つ可能性があります。
- 単調*かつ*厳密に単調な時間が必要な場合 (イベントの順序付けに役立つ)、人々は `now()` を使用しました。
- 人々は、一意の値 (特定のノードの存続期間中) が必要な場合に `now()` を使用しました。
- 時間を `{メガ秒, 秒, マイクロ秒}` とするのは煩わしく、VM がより大きな整数を表現するのが実用的でなかった時代の名残であり、適切な時間単位への変換は面倒です。Erlang の整数は任意のサイズにできるため、この形式を使用する正当な理由はありません。
- 時間が逆戻りすると、Erlang クロックが停止します (各呼び出しの間に、マイクロ秒単位でしか進みません)。
一般的に、問題は、次のすべてのタスクを実行するために 2 つのツール (os:timestamp() と `now()`) があったことです。
- システム時刻を確認する
- 2 つのイベント間の経過時間を測定する
- イベントの順序を決定する (各イベントに `now()` でタグを付ける)
- 一意の値を作成する
これらはすべて、18.0 以降、Erlang の時間を複数のコンポーネントに分割することで明確になります。
- OS システム時間。POSIX 時間とも呼ばれます。
- OS 単調時間。一部のオペレーティングシステムはそれを提供し、一部は提供しません。利用可能な場合はかなり安定しており、時間の飛躍を回避します。
- Erlang システム時間。VM による POSIX 時間の見解です。VM は POSIX と一致させようとしますが、選択された戦略によっては多少変動する可能性があります (戦略については、タイムワープ で説明しています)。
- Erlang 単調時間。これは、OS 単調時間が利用可能な場合は Erlang による OS 単調時間の見解であり、利用できない場合は VM 自身のシステム時間の単調バージョンです。これは、イベント、タイマーなどに使用される調整済みクロックです。その安定性により、時間間隔のカウントに最適です。この時間は*単調*ですが、*厳密に単調*ではないことに注意してください。つまり、時計は逆戻りできませんが、同じ値を何度も返す可能性があります!
- 時間オフセット。Erlang 単調時間は権限の安定したソースであるため、Erlang システム時間は Erlang 単調時間に対する特定のオフセットを持つことで計算されます。これは、Erlang が単調時間の周波数を変更することなくシステム時間を調整できるようにするためです。
または、より視覚的に
オフセットが定数 0 の場合、VM の単調時間とシステム時間は同じになります。オフセットが正または負に変更された場合、Erlang システム時間は OS システム時間と一致するように変更できますが、Erlang 単調時間は独立したままになります。実際には、単調クロックが非常に大きな負の数になり、システムクロックがオフセットによって変更されて正の POSIX タイムスタンプを表す可能性があります。
これらの新しいコンポーネントにより、もう 1 つのユースケースが残ります。*常に*増加する一意の値です。 `now()` 関数の高コストは、同じ数値を 2 回返さないというこの必要性によるものでした。前述のように、Erlang 単調時間は*厳密に*単調ではありません。たとえば、2 つの異なるコアで同時に呼び出された場合、同じ数値を 2 回返す可能性があります。それに比べて、`now()` はそうではありませんでした。これを補うために、厳密に単調な数値ジェネレーターが VM に追加され、時間と一意の整数を個別に処理できるようになりました。
VM の新しいコンポーネントは、次の関数を使用してユーザーに公開されます。
- Erlang 単調時間については、
erlang:monotonic_time()とerlang:monotonic_time(Unit)。非常に低い負の数を返す場合がありますが、それ以上負になることはありません。 - オフセットが適用された後の Erlang システム時間については、
erlang:system_time()とerlang:system_time(Unit)。 erlang:timestamp()は、下位互換性のために `{メガ秒, 秒, マイクロ秒}` 形式で Erlang システム時間を返します。- Erlang 単調クロックと Erlang システムクロックの差を調べるには、
erlang:time_offset()とerlang:time_offset(Unit)。 - 一意の値を返す
erlang:unique_integer()とerlang:unique_integer(Options)。 Options リストには、`positive` (0 より大きい数値を強制する) と `monotonic` (常に大きくなる) のいずれかまたは両方を含めることができます。 Options のデフォルトは `[]` です。これは、整数は一意ですが、正または負になる可能性があり、指定された以前の整数よりも大きくなるか小さくなる可能性があることを意味します。 - OS システム時間の許容範囲、間隔、および値にアクセスできる
erlang:system_info(os_system_time_source)。 erlang:system_info(os_monotonic_time_source): OS に単調クロックがある場合、その許容範囲、間隔、および値をここで取得できます。
上記のすべての関数の 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では、OSが提供する時間に合わせて、クロックを速くしたり遅くしたりしていました。 これは、クロックがジャンプしたときにリアルタイム性を維持するには問題ありませんでしたが、時間の経過とともに、複数のノードでイベントとタイムアウトがわずかな割合で速くなったり遅くなったりすることを意味していました。 また、VMには `+c` という単一のスイッチがあり、時間の修正を完全に無効にしていました。
Erlang 18.0では、動作の区別が導入され、はるかに強力で複雑になりました。 18.0より前のバージョンでは時間のドリフト(クロックの加速または減速)しかありませんでしたが、18.0では時間修正と*タイムワープ*と呼ばれるものが導入されました。
基本的に、`+C` で設定される *タイムワープ* は、OSと同期するために *オフセット*(したがって *Erlangシステム時間*)をどのようにジャンプさせるかを選択することです。 タイムワープは時間のジャンプです。 そして、`+c` で設定される *時間修正* は、OSの単調増加クロックがジャンプしたときの *Erlang単調増加時間* の動作です。
時間修正の戦略は2つだけですが、タイムワープには3つあります。 問題は、選択されたタイムワープ戦略が時間修正に影響を与えるため、最終的に6種類の動作が発生することです。 これを理解するために、次の表が役立つ場合があります。
+C no_time_warp |
|
||||
+C multi_time_warp |
|
||||
+C single_time_warp |
これは、ErlangがOSクロックが同期される前に起動することがわかっている場合に、組み込みハードウェアで使用される特別なハイブリッドモードです。 2つのフェーズで動作します。
|
ふう。 要するに、最善の策は、コードがタイムワーピングに対応できるようにし、マルチタイムワープモードにすることです。 コードが安全でない場合は、タイムワープなしにしましょう。
タイムワープを乗り切る方法
- システム時間を調べるには:`erlang:system_time/0-1`
- 時間差を測定するには:`erlang:monotonic_time/0-1` を2回呼び出して、それらを減算します。
- ノード上のイベント間の絶対的な順序を定義するには:`erlang:unique_integer([monotonic])`
- 時間を測定*し*、絶対的な順序が定義されていることを確認するには:`{erlang:monotonic_time(), erlang:unique_integer([monotonic])}`
- 一意の名前を作成するには:`erlang:unique_integer([positive])`。 クラスタ内で値を一意にする場合は、ノード名と組み合わせるか、UUIDv1を使用してみてください。
これらの概念に従うことで、コードは時間修正を有効にしたマルチタイムワープモードで使用できるようになり、その優れた精度と低いオーバーヘッドの恩恵を受けることができます。
これらすべての情報を手にしたことで、あなたは時間の中をドリフトし、ワープすることができるようになりました!