Three Hosts Were Logging by Three Different Clocks
한국어 원문으로 표시합니다.
목표
세 호스트가 서로 다른 시계로 찍은 로그에서, 오프셋이 없거나 뜻이 다른 표기를 가려내고, 기준 사건으로 호스트별 지역 오프셋과 시계 오차를 갈라 추정하고, 모든 줄을 믿을 수 있는 UTC 로 다시 써서 뒤집힌 인과 순서를 바로 세웁니다.
왜 중요한가
장애 조사에서 제일 먼저 정하는 것은 "무엇이 먼저 일어났는가" 입니다. 그 순서의 근거가 로그의 시각인데, 그 시각은 사실이 아니라 주장입니다. 오프셋이 없으면 어느 지역의 시계인지 밖에서 알 수 없고, -00:00 은 Z 와 뜻이 다르고, 오프셋이 정확해도 호스트의 시계가 몇 초 앞서 있으면 원인이 결과보다 뒤에 기록됩니다. 시각을 믿을 수 있게 만드는 일은 분석의 준비가 아니라 분석 자체입니다.
단계
/root/clock/gen_clock.py를 만들어 실행해/root/clock/raw/아래 app-seoul.log · db-frankfurt.log · edge-newyork.log 를 만드세요./root/clock/stamps.json— 파일마다 시각 표기가 어떻게 다른지, 지금 당장 UTC 로 바꿀 수 있는 줄이 몇 줄인지 적으세요./root/clock/dst_probe.json— 서머타임이 있는 지역의 지역시각이 사라지거나 두 번 오는 것을 zoneinfo 로 실측하세요./root/clock/anchors.json— 세 파일 모두에 자국을 남긴 점검 신호를 찾으세요./root/clock/skew.json— 기준 사건으로 호스트별 지역 오프셋과 시계 오차를 갈라 추정하세요./root/clock/fixed.ndjson— 모든 줄을 보정한 UTC 시각으로 다시 쓰고 시간순으로 세우세요./root/clock/causality.json— 보정 전에 원인이 결과보다 뒤에 찍혀 있던 요청을 세고, 보정 뒤와 견주세요./root/clock/clock_report.md— 무엇을 어떤 근거로 보정했는지 보고서로 남기세요.
참고
- 보정한
ts는 모두2026-03-08T06:05:00.200Z모양으로 적습니다 — 날짜와 시각 사이에 대문자T, 밀리초 세 자리, 끝에 대문자Z. - 줄의 시각을 읽는 규칙은 하나로 통일합니다. 오프셋이 붙어 있으면 그대로 읽고, 없으면 일단 UTC 로 읽습니다. 그 가정이 맞는지는 기준 사건이 판정합니다.
- 파이썬의
strptime은%z로-05:00과-00:00을 모두 읽습니다.zoneinfo.ZoneInfo와datetime의fold로 서머타임 구간을 다룹니다. - 지역 오프셋은 15분의 배수입니다. 기준과의 차이를 가장 가까운 15분으로 접으면 지역 오프셋이 나오고, 남는 초가 그 호스트의 시계 오차입니다.
- 흔한 실수:
-00:00을Z와 같은 뜻으로 읽기, 오프셋 없는 줄을 UTC 로 둔 채 정렬하기, 남는 몇 초를 잡음으로 보고 버리기, 서머타임 전환일에 오프셋을 한 가지로 고정하기. - 이 실습의 산출물은 전부
/root/clock/아래에 모읍니다. 세션이 끝나면 사라지므로 중요한 것은 화면에 남겨 두세요.
세 호스트의 로그를 재현하기
/root/clock/gen_clock.py 를 만들어 실행해 /root/clock/raw/ 아래 app-seoul.log(65줄) · db-frankfurt.log(41줄) · edge-newyork.log(61줄)을 만드세요.
세 파일은 서로 다른 호스트가 자기 시계로 쓴 것이라 시각 표기가 다릅니다. 하나는 오프셋이 아예 없고, 하나는 오프셋을 -00:00 으로 달고, 하나는 지역 오프셋을 제대로 답니다. 먼저 /root/clock/raw 를 만들고 그 안에 세 파일을 씁니다.
어느 줄이 지금 당장 UTC 로 바뀌는지 세기
/root/clock/stamps.json 에 호스트 이름(app-seoul · db-frankfurt · edge-newyork)마다 lines · offset_style · utc_resolvable · distinct_offsets 를 적으세요. offset_style 은 오프셋이 하나도 없으면 none, 모든 오프셋이 -00:00 이면 -00:00, 그 밖이면 explicit 입니다. utc_resolvable 은 그 줄만 보고 UTC 로 바꿀 수 있는 줄 수입니다.
원본은 /root/clock/raw/app-seoul.log · db-frankfurt.log · edge-newyork.log 세 개입니다. 오프셋이 붙어 있는 줄만 그 줄 하나로 UTC 가 정해집니다. distinct_offsets 는 그 파일에 실제로 나타난 오프셋 문자열을 중복 없이 모은 것이고, 한 파일에서 값이 두 가지 나온다면 그 창이 무엇을 가로질렀는지 생각해 보세요.
사라지는 시각과 두 번 오는 시각을 실측하기
/root/clock/dst_probe.json 에 아래 여섯 개를 이 순서 그대로 배열로 적으세요. 각 항목은 zone · local · count · utc 네 키를 갖고, count 는 그 지역시각에 대응하는 UTC 순간의 개수, utc 는 그 순간들을 2026-03-08T08:30:00.000Z 모양으로 오름차순으로 담은 배열입니다. (1) America/New_York 2026-03-08 02:30:00 (2) America/New_York 2026-03-08 04:30:00 (3) America/New_York 2026-11-01 01:30:00 (4) Asia/Seoul 2026-03-08 02:30:00 (5) Europe/Berlin 2026-03-29 02:30:00 (6) Europe/Berlin 2026-10-25 02:30:00
zoneinfo.ZoneInfo 로 지역을 붙이고 datetime 의 fold 를 0과 1로 바꿔 두 후보를 만듭니다. 각 후보를 UTC 로 바꿨다가 다시 그 지역으로 되돌렸을 때 원래 지역시각과 같아지는 것만 진짜입니다. 되돌아오는 것이 없으면 그 지역시각은 존재하지 않고, 서로 다른 둘이 되돌아오면 두 번 오는 시각입니다.
세 파일에 함께 찍힌 기준 사건 찾기
/root/clock/anchors.json 에 reference_host 를 db-frankfurt 로 적고, anchors 에는 세 파일 모두에 나타난 probe= 값마다 corr · app-seoul · db-frankfurt · edge-newyork 네 키를 담아 corr 오름차순으로 적으세요. 세 호스트 값은 그 파일에 적힌 시각 문자열 그대로입니다.
원본은 /root/clock/raw/app-seoul.log · /root/clock/raw/db-frankfurt.log · /root/clock/raw/edge-newyork.log 세 개입니다. 운영 스케줄러가 뿌린 점검 신호는 probe=SYNC-xxxx 로 찍혀 있습니다. 한 호스트에만 있는 신호는 자로 쓸 수 없으니 세 파일의 교집합만 남깁니다. 시각 문자열은 오프셋까지 포함해 원문 그대로 넣어야 나중에 되짚을 수 있습니다. 기준은 오프셋을 달아 UTC 를 알려 주는 호스트 가운데 시계를 믿을 수 있는 쪽으로 고릅니다.
지역 오프셋과 시계 오차를 갈라 추정하기
/root/clock/skew.json 에 reference_host 와 hosts 를 적으세요. hosts 는 호스트마다 zone_offset_minutes · skew_seconds · anchors_used 를 갖습니다. 기준 사건마다 그 줄이 말하는 시각(오프셋이 있으면 그대로, 없으면 UTC 로 읽은 값)에서 기준 호스트의 시각을 뺀 차이를 구하고, 그 차이를 가장 가까운 15분으로 접은 것이 zone_offset_minutes, 남는 초가 skew_seconds 입니다.
재료는 4단계에서 만든 /root/clock/anchors.json 입니다. 차이 하나에 두 가지가 섞여 들어옵니다 — 시계판이 UTC 에서 얼마나 옮겨져 있는가(지역 오프셋)와 그 시계가 얼마나 틀렸는가(시계 오차). IANA 지역 오프셋은 15분의 배수라서 이 둘을 가를 수 있습니다. 기준 사건이 여럿이면 값이 흔들릴 수 있으니 중앙값처럼 한 값으로 모으고, 몇 개를 썼는지 anchors_used 에 남기세요.
모든 줄을 믿을 수 있는 시각으로 다시 쓰기
/root/clock/fixed.ndjson 에 세 파일의 모든 줄을 ts · host · raw_ts · corr · msg 로 한 줄에 하나씩 쓰고 ts 오름차순으로 세우세요. ts 는 그 줄이 말하는 시각에서 zone_offset_minutes 와 skew_seconds 를 빼 얻은 UTC 이고 2026-03-08T06:05:00.200Z 모양입니다. raw_ts 는 원문 시각 문자열, corr 은 req= 나 probe= 값(없으면 null), msg 는 시각 뒤에 남은 본문입니다.
재료는 /root/clock/raw/ 아래 세 파일과 5단계에서 만든 /root/clock/skew.json 입니다. 보정은 뺄셈 두 번입니다 — 시계판이 옮겨진 만큼 빼고, 시계가 틀린 만큼 뺍니다. 원본 문자열을 덮어쓰지 말고 raw_ts 로 함께 남기세요. 나중에 기준을 바꾸면 처음부터 다시 계산해야 하는데, 원문이 없으면 그럴 수 없습니다.
원인이 결과보다 뒤에 찍힌 요청 세기
/root/clock/causality.json 에 pairs · inverted_before · inverted_after · examples 를 적으세요. pairs 는 edge-newyork 과 db-frankfurt 둘 다에 나타난 req=RQ- 값의 개수입니다. inverted_before 는 보정 전(그 줄이 말하는 시각 그대로) 앞단의 시각이 데이터베이스의 시각보다 늦은 건수, inverted_after 는 보정한 시각으로 다시 센 건수입니다. examples 는 보정 전에 역전된 값 가운데 오름차순으로 앞의 세 개입니다.
보정한 시각은 6단계에서 만든 /root/clock/fixed.ndjson 에 이미 있고, 보정 전 시각은 같은 파일의 raw_ts 로 되살릴 수 있습니다. 앞단이 요청을 받은 것이 원인이고 데이터베이스가 그 요청을 처리한 것이 결과이므로, 앞단의 시각이 더 늦다면 기록만 보고는 결과가 원인보다 먼저 일어난 셈이 됩니다. 두 호스트 모두 오프셋을 제대로 달고 있다는 점이 이 단계의 핵심입니다.
무엇을 어떤 근거로 고쳤는지 남기기
/root/clock/clock_report.md 에 ## 시계가 어떻게 어긋나 있었나 ## 무엇을 기준으로 삼았나 ## 보정한 뒤 무엇이 달라졌나 ## 다음에 받을 때의 요구사항 네 절로 적으세요. 보정한 전체 레코드 수 · app-seoul 의 zone_offset_minutes · 보정 전 역전 건수를 숫자로 포함해야 합니다.
재료는 /root/clock/skew.json · /root/clock/fixed.ndjson · /root/clock/causality.json 입니다. 보정값은 측정값이 아니라 여러분이 기준 사건으로 세운 가정이므로, 그 가정이 무엇에 기대고 있는지 함께 적어야 다음 사람이 되짚을 수 있습니다. 마지막 절에는 고객에게 무엇을 요구하면 이 추정이 통째로 사라지는지 적습니다.