LabHub
배우기 러닝패스 코스

로그로 원인 찾기 · 어긋난 시계를 맞추기 · 이론

오프셋 없는 시각은 시각이 아니다

LabHub 에서 이어서 보기

한 줄 요약

로그에 적힌 시각은 주장이지 사실이 아니다. 오프셋이 없으면 어느 지역의 시계인지 알 수 없고, 오프셋이 정확해도 그 호스트의 시계 자체가 몇 초 어긋나 있으면 원인이 결과보다 뒤에 선다.

왜 이게 필요했나

형식을 아무리 잘 정규화해도 시각이 거짓이면 그 위에 쌓은 결론은 전부 거짓이다. 장애 조사에서 제일 먼저 하는 일이 "무엇이 먼저 일어났는가" 인데, 그 순서를 정하는 근거가 바로 이 숫자들이다.

현장에서 받는 묶음에는 세 가지가 함께 온다. 첫째, 오프셋이 아예 없는 지역시각. 애플리케이션 로거의 기본값이 2026-03-08 15:04:57.200 인 경우가 아직도 흔하다. [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html)는 4.4절에서 이것을 못 박았다 — 오프셋 없는 지역시각은 해석이 지구의 약 23/24 에서 실패하므로 인터넷에서는 상호운용성 문제로 받아들일 수 없다고 적는다. 그 줄이 서울에서 찍혔는지 프랑크푸르트에서 찍혔는지는 줄 안에 없다.

둘째, 뜻이 미묘하게 다른 오프셋. 같은 문서 4.3절은 -00:00 을 따로 규정한다. UTC 시각은 아는데 그 기록을 남긴 곳의 지역 오프셋을 모를 때 쓰는 표기이고, Z+00:00의미가 다르다 — 뒤의 둘은 UTC 가 그 시각의 기준점이라는 뜻이다. 숫자로는 둘 다 0이라 계산 결과는 같지만, 읽는 사람에게 주는 정보가 다르다.

셋째, 시계 자체의 오차. NTP 에 물리지 않은 호스트는 하루에도 몇 초씩 흐른다. 오프셋을 정확히 달고 있어도 시계가 7초 앞서 있으면, 그 호스트가 찍은 모든 시각이 7초 앞선다. 응답이 1초 만에 끝나는 요청이라면 원인이 결과보다 6초 뒤에 기록된다. 로그만 보면 데이터베이스가 요청을 받기도 전에 답한 것처럼 보인다.

어떻게 동작하나

믿을 수 없는 시각을 믿을 수 있게 만드는 방법은 기준을 하나 정하고 거기에 맞춰 재는 것이다.

기준 사건을 찾는다. 여러 호스트에 동시에 자국을 남기는 것이면 무엇이든 된다 — 배포 표식, 설정 재적재, 운영 스케줄러가 뿌리는 점검 신호. 같은 표식이 세 파일에 모두 있으면, 그 세 줄의 시각 차이가 곧 세 시계의 차이다. 이 방법의 정밀도는 그 신호가 세 호스트에 도달하는 시간의 흩어짐을 넘지 못한다 — 그 사실을 함께 적어 두어야 추정값이 과장되지 않는다.

차이를 두 몫으로 가른다. 기준과의 차이 안에는 지역 오프셋과 시계 오차가 섞여 있다. IANA 지역 오프셋은 15분의 배수이므로, 차이를 가장 가까운 15분으로 접으면 지역 오프셋이 나오고 남는 초가 시계 오차다. 8시간 59분 57초는 "9시간 오프셋 + 3초 뒤진 시계" 로 읽는다. 이 3초를 반올림해 버리면 안 된다 — 인과 순서를 뒤집는 것이 바로 그 3초다.

지역 이름은 오프셋이 아니다. 지역은 [IANA 시간대 데이터베이스](https://www.iana.org/time-zones)가 관리하는 규칙 묶음이고, 같은 지역의 오프셋도 계절마다 바뀐다. 파이썬은 [zoneinfo](https://docs.python.org/3/library/zoneinfo.html)로 이 규칙을 그대로 읽는다. 서머타임이 있는 지역에서 오프셋 없는 지역시각은 두 번 위험해진다 — 봄에는 존재하지 않는 시각이 생기고(America/New_York 의 2026-03-08 02:30), 가을에는 두 번 오는 시각이 생긴다. [datetime](https://docs.python.org/3/library/datetime.html)의 fold 가 뒤쪽을 가리키는 손잡이다.

마지막에 한 가지 표기로 굳힌다. RFC 3339 5.1절이 적는 성질 그대로, 오프셋 표기와 소수 자릿수가 같으면 문자열 정렬이 곧 시간순 정렬이 된다. syslog 를 다룬다면 [RFC 5424](https://www.rfc-editor.org/rfc/rfc5424.html) 6.2.3절이 더 조인다 — TZ 는 반드시 대문자여야 하고, 윤초는 쓸 수 없고, 초의 소수는 여섯 자리를 넘을 수 없다.

현장에서 만나는 모습

원본 시계와 관측 시계를 둘 다 남긴다. [OpenTelemetry 로그 데이터 모델](https://opentelemetry.io/docs/specs/otel/logs/data-model/)이 Timestamp 와 ObservedTimestamp 를 나눠 둔 이유가 바로 이것이다. 앞의 것은 사건이 일어난 원본 시계의 시각이고 없을 수도 있다. 뒤의 것은 수집기가 그 사건을 본 시각이다. 둘을 함께 들고 있으면 원본 시계가 의심스러울 때 대조할 자가 생긴다. 하나만 받는 시스템에 넘길 때는 Timestamp 가 있으면 그것을, 없으면 ObservedTimestamp 를 쓰라고 명세가 권한다.

보정값은 데이터가 아니라 가정이다. "이 호스트는 +09:00 이고 3초 뒤쳐져 있다" 는 여러분이 기준 사건으로 추정한 값이다. 원본 줄을 덮어쓰지 말고, 보정한 시각과 원본 문자열을 나란히 남겨라. 나중에 기준을 바꾸면 처음부터 다시 계산할 수 있어야 한다.

진짜 해법은 고객 쪽에 있다. 추정은 이번 묶음을 살리는 응급처치다. 다음 묶음부터는 모든 호스트에 NTP 를 물리고, 로거가 오프셋을 반드시 달게 하고, 가능하면 UTC 로 찍어 달라고 요구하는 것이 진짜 해법이다. 그 요구를 할 근거가 바로 여러분이 잰 숫자다.

다음 실습에서 할 것

세 호스트의 로그를 재현해 표기를 조사하고, 서머타임 지역의 지역시각이 어떻게 사라지고 어떻게 두 번 오는지 직접 시험한다. 그다음 세 파일에 함께 찍힌 점검 신호로 호스트별 지역 오프셋과 시계 오차를 갈라 추정하고, 모든 줄을 보정해 하나의 타임라인으로 세운다. 마지막에는 보정 전에 원인이 결과보다 뒤에 찍혀 있던 요청이 몇 건이었는지 세고, 무엇을 어떤 근거로 고쳤는지 보고서로 남긴다.