LabHub
배우기 러닝패스 코스

로그가 미래에서 왔다 — 시계가 만든 다섯 사건 · 경과 시간이 음수로 찍혔다 · 이론

경과 시간이 음수로 찍혔다

LabHub 에서 이어서 보기

한 줄 요약

"지금 몇 시인가" 와 "얼마나 지났는가" 는 서로 다른 시계에 물어야 한다. 하나로
둘 다 하려고 하면, 시계를 맞추는 날 경과 시간이 음수가 된다.

왜 이게 필요했나

경과 시간을 재는 코드는 대개 이렇게 생겼다. 시작할 때 현재 시각을 적어 두고, 끝날 때
현재 시각에서 그것을 뺀다. 이 코드는 거의 언제나 맞는 값을 준다. 틀리는 순간은
그 사이에 누군가 시계를 고쳤을 때 뿐이다.

그리고 시계는 고쳐진다. 오차가 커진 기계를 발견하면 운영자가 강제로 동기화하고,
데몬이 큰 오차를 만나면 시각을 한 번에 건너뛰게 만든다. 가상 머신이 스냅샷에서
복원되거나 절전에서 깨어날 때도 같은 일이 일어난다. 그 순간에 걸쳐 있던 작업은
경과 시간이 음수가 되거나, 반대로 몇 분씩 부풀어 오른다.

음수 자체는 눈에 띄니 그나마 낫다. 진짜 문제는 그 값을 쓰는 코드다. 잠금 기한을
"지금 + 30초" 로 잡아 두었는데 시계가 5분 앞으로 뛰면 기한이 이미 지난 것이 되어
남의 잠금이 풀린다. 반대로 5분 뒤로 뛰면 30초짜리 잠금이 5분 반 동안 안 풀린다.
두 경우 모두 잠금이 지키려던 것이 깨지는데, 어느 쪽도 로그에 "시계" 라는 낱말을
남기지 않는다.

어떻게 동작하나

운영체제는 이 둘을 처음부터 다른 시계로 나눠 놓았다.
[clock_gettime(2)](https://man7.org/linux/man-pages/man2/clock_gettime.2.html)의
설명이 그 차이를 정확히 적고 있다.

CLOCK_REALTIME    설정 가능한 시계. 1970-01-01 UTC 부터의 초를 센다.                  관리자가 시각을 바꾸면 **불연속적으로 점프** 하고,                  NTP 의 주파수 조정에도 영향을 받는다.                  → "지금 몇 시인가" 에 답한다.CLOCK_MONOTONIC   설정할 수 없는 시계. 리눅스에서는 부팅 이후의 초다.                  시각을 바꿔도 **점프하지 않는다**(주파수 조정에는 영향을 받는다).                  연속한 두 번의 호출이 뒤로 가는 일은 없다.                  시스템이 잠들어 있던 시간은 세지 않는다.                  → "얼마나 지났는가" 에 답한다.CLOCK_BOOTTIME    CLOCK_MONOTONIC 과 같은데 절전 시간까지 포함한다.

마지막 줄이 은근히 중요하다. 노트북이나 절전을 쓰는 기계에서 경과 시간을 재면
CLOCK_MONOTONIC 은 잠든 시간을 빼고 세므로, 벽시계로 세 시간이 지났는데 경과는
10분으로 나올 수 있다. 무엇을 재고 싶은지에 따라 골라야 한다.

언어들도 같은 구분을 그대로 노출한다. 파이썬의
[time 모듈](https://docs.python.org/3/library/time.html)은 time.time()
time.monotonic() 을 나눠 두었고, Go 의 [time 패키지](https://pkg.go.dev/time)는
아예 time.Now() 가 돌려주는 값 안에 단조 시계 값을 함께 담아 두고 두 Time
뺄 때 그쪽을 쓴다. 골라야 하는 상황을 언어가 대신 처리해 준 셈이다.

그래서 코드의 규칙은 이렇게 정리된다. **화면에 보여 주거나 저장할 시각은 벽시계로,
두 시점의 간격을 계산할 값은 단조 시계로 잰다.** 기록을 남길 때는 둘 다 적어 두면
좋다. 벽시계는 사람이 읽고, 단조 시계는 계산이 읽는다.

시간 제한을 걸 때도 같은 원칙이 적용된다. 데이터베이스의 잠금 시간 제한, HTTP
요청의 시간 제한, 재시도 사이의 대기는 전부 「얼마나 지났는가」이므로 단조 시계
위에서 돌아야 한다. 반대로 인증서 유효 구간이나 토큰 만료는 「지금 몇 시인가」를
남과 맞춰야 하는 문제라 벽시계를 쓸 수밖에 없다. 그쪽 이야기가 다음 차례다.

현장에서 만나는 모습

분산 잠금에서 이 문제가 가장 비싸게 나타난다. 잠금 서비스가 "이 잠금은 30초 뒤에
만료" 라고 벽시계 시각으로 답하고, 잠금을 잡은 쪽도 벽시계로 그 시각을 확인한다.
두 기계의 시계가 다르면 한쪽은 아직 유효하다고 믿고 다른 쪽은 이미 만료됐다고 믿는
구간이 생긴다. 그 구간 동안 두 프로세스가 같은 자원을 동시에 만진다. 그래서 잠금
프로토콜들은 대개 절대 시각 대신 남은 시간 을 주고받는다.

관측 지표에서도 같은 일이 벌어진다. 응답 시간을 벽시계 뺄셈으로 재는 서비스는
시계를 맞추는 날 히스토그램에 음수나 터무니없이 큰 값이 들어간다. 대부분의 지표
라이브러리는 음수를 버리거나 맨 끝 버킷에 몰아넣는데, 어느 쪽이든 그날의 p99 는
믿을 수 없는 값이 된다.

마지막으로, 코드 검토에서 알아볼 수 있는 신호가 하나 있다. 경과 시간을 담은 변수에
음수가 들어올 수 있다는 것을 아예 고려하지 않은 코드다. if elapsed > timeout
같은 비교는 음수에서 조용히 통과한다. 시계가 뒤로 간 날, 그 코드는 시간 제한이 없는
코드가 된다.

다음 퀴즈에서 확인할 것

두 시계가 각각 어떤 질문에 답하는지, 단조 시계가 무엇에는 영향을 받고 무엇에는 받지
않는지, 잠금과 시간 제한에 어느 쪽을 써야 하는지 확인한다.