経過時間がマイナスで記録された
一言でいうと
「今何時か」と「どれだけ経ったか」は、別々の時計に尋ねる必要があります。1つで両方をしようとすると、時計を合わせる日に、経過時間がマイナスになります。
なぜ必要なのか
経過時間を測るコードは、たいてい次のような形をしています。開始時に現在時刻を書いておき、終了時に現在時刻からそれを引きます。このコードは、ほとんどいつも正しい値を返します。間違えるのは、その間に誰かが時計を直したときだけです。
そして、時計は直されます。ずれが大きくなったマシンを見つけると、運用者が強制的に同期し、デーモンが大きなずれに出会うと、時刻を一度に飛ばします。仮想マシンがスナップショットから復元されたり、スリープから復帰したりするときにも、同じことが起きます。その瞬間にまたがっていた処理は、経過時間がマイナスになったり、逆に数分ずつ膨らんだりします。
マイナスそのものは、目立つので、まだましです。本当の問題は、その値を使うコードです。ロックの期限を「今 + 30秒」としておいたのに、時計が5分進むと、期限がすでに過ぎたことになって、他人のロックが解除されます。逆に、5分戻ると、30秒のロックが5分半解除されません。どちらの場合もロックが守ろうとしたものが壊れますが、どちらもログに「時計」という単語を残しません。
どう動くのか
オペレーティングシステムは、この2つを、最初から別の時計に分けています。clock_gettime(2)の説明が、その違いを正確に書いています。
CLOCK_REALTIME 설정 가능한 시계. 1970-01-01 UTC 부터의 초를 센다.
관리자가 시각을 바꾸면 **불연속적으로 점프** 하고,
NTP 의 주파수 조정에도 영향을 받는다.
→ "지금 몇 시인가" 에 답한다.
CLOCK_MONOTONIC 설정할 수 없는 시계. 리눅스에서는 부팅 이후의 초다.
시각을 바꿔도 **점프하지 않는다**(주파수 조정에는 영향을 받는다).
연속한 두 번의 호출이 뒤로 가는 일은 없다.
시스템이 잠들어 있던 시간은 세지 않는다.
→ "얼마나 지났는가" 에 답한다.
CLOCK_BOOTTIME CLOCK_MONOTONIC 과 같은데 절전 시간까지 포함한다.
このコードブロックの韓国語の説明は、順に、CLOCK_REALTIMEは設定可能な時計で1970-01-01 UTCからの秒を数え、管理者が時刻を変えると不連続にジャンプし、NTPの周波数調整の影響も受け、「今何時か」に答える、CLOCK_MONOTONICは設定できない時計でLinuxでは起動後の秒であり、時刻を変えてもジャンプせず(周波数調整の影響は受ける)、連続する2回の呼び出しが後ろへ戻ることはなく、システムが眠っていた時間は数えず、「どれだけ経ったか」に答える、CLOCK_BOOTTIMEはCLOCK_MONOTONICと同じだがスリープ時間まで含む、という意味です。
最後の行が、さりげなく重要です。ノートPCやスリープを使うマシンで経過時間を測ると、CLOCK_MONOTONICは眠っていた時間を除いて数えるので、壁時計では3時間経ったのに、経過は10分と出ることがあります。何を測りたいかによって、選ぶ必要があります。
言語も、同じ区別をそのまま公開しています。Pythonのtimeモジュールは、time.time()とtime.monotonic()を分けており、Goのtimeパッケージは、time.Now()が返す値の中に、単調時計の値も一緒に持たせ、2つのTimeを引くときに、そちらを使います。選ぶ必要がある状況を、言語が代わりに処理してくれているわけです。
そのため、コードのルールは、次のように整理されます。画面に表示したり保存したりする時刻は壁時計で、2つの時点の間隔を計算する値は単調時計で測ります。記録を残すときは、両方を書いておくとよいです。壁時計は人が読み、単調時計は計算が読みます。
タイムアウトをかけるときにも、同じ原則が適用されます。データベースのロックのタイムアウト、HTTPリクエストのタイムアウト、リトライの間の待機は、すべて「どれだけ経ったか」なので、単調時計の上で動く必要があります。逆に、証明書の有効期間やトークンの期限切れは、「今何時か」を他者と合わせる問題なので、壁時計を使うしかありません。そちらの話が、次の番です。
現場での姿
分散ロックで、この問題が最も高くつく形で現れます。ロックサービスが「このロックは30秒後に期限切れ」と、壁時計の時刻で答え、ロックを取った側も、壁時計でその時刻を確認します。2つのマシンの時計が違うと、片方はまだ有効だと信じ、もう片方はすでに期限切れだと信じる区間が生まれます。その区間の間、2つのプロセスが同じリソースを同時に触ります。そのため、ロックのプロトコルは、たいてい絶対時刻の代わりに残り時間をやり取りします。
観測指標でも、同じことが起きます。レスポンス時間を壁時計の引き算で測るサービスは、時計を合わせる日に、ヒストグラムにマイナスや途方もなく大きな値が入ります。大半の指標ライブラリは、マイナスを捨てるか、一番端のバケットにまとめて入れますが、どちらにしても、その日のp99は信頼できない値になります。
最後に、コードレビューで見分けられるサインが1つあります。経過時間を入れた変数に、マイナスが入りうることをまったく考慮していないコードです。if elapsed > timeoutのような比較は、マイナスでは黙って通過します。時計が戻った日、そのコードは、タイムアウトのないコードになります。
次のクイズで確認すること
2つの時計がそれぞれどんな質問に答えるか、単調時計が何の影響を受け、何の影響を受けないか、ロックとタイムアウトに、どちらを使うべきかを確認します。