본문 바로가기
삵
Operating System

chrony로 서버 시계 틀어짐 잡기

나나크나로·2026년 10월 10일·조회 0

장애 티켓을 열어보면 원인이 애플리케이션이 아니라 시계인 경우가 종종 있다. 두 서버의 로그를 합쳐 시간순으로 보려는데 순서가 뒤엉켜 있거나, 멀쩡한 인증서인데 TLS 핸드셰이크가 'certificate is not yet valid'로 떨어지거나, 방금 발급한 토큰이 만료됐다고 거부당하는 식이다. 이런 증상의 밑바닥에는 호스트 시계가 실제 시각에서 벗어나 있는 clock drift가 깔려 있는 경우가 많다. 특히 VM과 컨테이너 호스트는 하이퍼바이저 스케줄링, 절전 복귀, 라이브 마이그레이션 때문에 베어메탈보다 시계가 잘 틀어진다.

결론부터 말하자면, Linux 서버의 시각 동기화는 chrony로 맞추고 chronyc로 상태를 들여다보면 된다. 드리프트는 chronyc tracking의 System time 오프셋으로 진단하고, 이 값을 주기적으로 읽어 임계치를 넘으면 경고하도록 상시 감시를 건다. 아래에서 개념, 설치, 진단, 감시 순서로 살펴본다.

1. clock drift와 NTP, chrony가 무엇인가

clock drift는 하드웨어 발진기의 주파수 오차 때문에 시스템 시계가 실제 시각보다 조금씩 빨라지거나 느려지는 현상이다. 보정하지 않으면 하루에 수 초에서 수십 초까지 벌어질 수 있고, 이 오차가 쌓이면 로그 타임스탬프, 인증서 유효기간 검사, 토큰 만료 판정이 전부 어긋난다.

NTP(Network Time Protocol)는 네트워크 상위 시각 서버와 통신해 로컬 시계를 그 기준에 맞추는 프로토콜이다. chrony는 그 NTP 구현 중 하나로, chronyd 데몬과 chronyc 조회 도구로 구성된다. 간헐적으로 연결되거나 시계가 자주 흔들리는 환경에서 빠르게 수렴하도록 설계돼 있어, 가상화 환경과 노트북, 간헐 접속 서버에 특히 잘 맞는다.

chrony가 시계를 맞추는 방식은 두 가지다. 오차가 작으면 시계가 가는 속도를 미세하게 조절해 서서히 따라잡는 slew, 오차가 크면 시각을 한 번에 건너뛰는 step이다. slew는 시간이 거꾸로 흐르지 않아 로그 순서가 깨지지 않고, step은 부팅 직후처럼 크게 벌어진 시계를 빨리 맞출 때 쓴다.

2. 설치와 기본 설정

RHEL 8 이후 계열(RHEL, Rocky, AlmaLinux)은 설치 시 chrony가 기본 NTP 구현으로 들어간다. Ubuntu는 사정이 다르다. 16.04부터 24.04 LTS까지는 기본 시각 동기화가 systemd-timesyncd이고 chrony는 별도 설치다. chrony가 기본이 된 것은 Ubuntu 25.10부터이며, 25.04 이하에서 업그레이드한 시스템은 timesyncd가 그대로 활성일 수 있다.

RHEL 계열:

sudo dnf install -y chrony
sudo systemctl enable --now chronyd

Ubuntu, Debian:

sudo apt update && sudo apt install -y chrony
sudo systemctl enable --now chrony

서비스 이름이 배포판마다 다르다. RHEL 계열은 chronyd, Ubuntu, Debian은 chrony다. 여기서 한 번 걸린다. Ubuntu에서 timesyncd와 chrony를 둘 다 켜두면 두 데몬이 같은 시계를 건드려 충돌한다. chrony를 설치하면 패키지가 timesyncd를 비활성화하지만, 직접 확인하는 편이 확실하다.

systemctl is-active systemd-timesyncd
inactive
timedatectl show-timesync --property=NTPSynchronized 2>/dev/null

설정 파일 경로도 다르다. RHEL 계열은 /etc/chrony.conf, Ubuntu, Debian은 /etc/chrony/chrony.conf다. 상위 시각 서버는 pool 또는 server 지시어로 지정한다.

# /etc/chrony.conf 핵심 지시어
pool 2.pool.ntp.org iburst
makestep 1.0 3
rtcsync
driftfile /var/lib/chrony/drift

iburst는 데몬 시작 직후 짧은 간격으로 여러 번 질의해 빠르게 첫 동기화에 이르게 한다. makestep 1.0 3은 처음 세 번의 시각 갱신까지는 오차가 1초를 넘으면 slew 대신 step으로 한 번에 맞춘다는 뜻이다. 부팅 시 크게 틀어진 시계를 즉시 교정하되, 이후 운영 중에는 로그 순서가 깨지지 않도록 slew로만 보정한다. 설정을 바꿨으면 재시작한다.

sudo systemctl restart chronyd

사내망에서 외부 NTP가 막혀 있으면 pool 대신 사내 NTP 서버를 server 10.0.0.10 iburst 형태로 지정한다. 방화벽에서 NTP는 UDP 123 포트를 쓴다. 이 포트가 아웃바운드로 열려 있어야 동기화가 된다.

3. 동기화 상태 진단하기

가장 먼저 보는 명령은 chronyc tracking이다. 현재 시계가 기준에서 얼마나 벗어나 있는지, 어느 서버를 따라가는지 한 화면에 보여준다.

chronyc tracking
Reference ID    : 8F2A1B03 (time.example.net)
Stratum         : 3
Ref time (UTC)  : Fri Oct 10 02:14:51 2026
System time     : 0.000094211 seconds slow of NTP time
Last offset     : -0.000012043 seconds
RMS offset      : 0.000210517 seconds
Frequency       : 12.374 ppm slow
Residual freq   : -0.001 ppm
Skew            : 0.145 ppm
Root delay      : 0.003218 seconds
Root dispersion : 0.000412 seconds
Update interval : 64.3 seconds
Leap status     : Normal

읽는 순서는 이렇다. System time은 지금 이 시계가 NTP 기준에서 벗어난 오프셋이다. 위 예시는 약 94마이크로초로, 잘 맞은 상태다. Last offset은 마지막 갱신에서 측정된 로컬 오프셋이고, RMS offset은 오프셋의 장기 평균이라 시계가 얼마나 안정적인지 보여준다. Frequency는 chronyd가 보정하지 않았다면 시계가 틀어졌을 비율로, 단위는 ppm(백만분율)이다. Leap status가 Normal이면 정상이고, 'Not synchronised'면 아직 동기화되지 않았다는 뜻이다.

어느 서버와 어떻게 통신하는지는 chronyc sources -v로 본다. -v는 각 열의 의미를 머리말로 설명해 준다.

chronyc sources -v
MS Name/IP address         Stratum Poll Reach LastRx Last sample
===============================================================================
^* time.example.net              2   6   377    23   +118us[ +143us] +/-  4120us
^- time2.example.net             2   6   377    27  -2954us[-2954us] +/-  8731us

맨 앞 기호가 중요하다. ^*는 현재 동기화 기준으로 선택된 서버, ^-는 후보지만 선택되지 않은 서버다. Reach는 최근 질의 성공 여부를 8진수로 나타내며 377이면 최근 8번이 전부 성공했다는 뜻이다. 이 값이 0이거나 계속 낮으면 NTP 서버에 도달하지 못하는 것이고, 방화벽의 UDP 123이나 서버 주소를 의심한다.

막상 돌려보면 ^?만 뜨고 ^*가 안 잡히는 경우가 있다. 부팅 직후라면 아직 수렴 전이니 잠깐 기다리면 된다. 스크립트에서 동기화를 확정하고 다음 단계로 넘어가야 할 때는 chronyc waitsync로 블로킹한다.

# 최대 60회 시도, 오프셋 0.1초 이내로 수렴할 때까지 대기
chronyc waitsync 60 0.1

시계가 크게 틀어져 있어 지금 당장 한 번에 맞춰야 하면 makestep으로 강제 step을 건다. 다만 시각이 건너뛰므로 실행 중 애플리케이션의 타이머나 로그 순서에 영향이 갈 수 있다. 운영 중 서비스가 올라와 있을 때는 신중하게 쓴다.

sudo chronyc makestep

4. 드리프트를 상시 감시하기

한 번 맞췄다고 끝이 아니다. NTP 서버 도달이 끊기거나 VM이 멈췄다 깨어나면 다시 벌어진다. System time 오프셋을 주기적으로 읽어 임계치를 넘으면 경고하도록 감시를 건다.

스크립트로 파싱할 때는 사람이 읽는 chronyc tracking 대신 -c 옵션의 CSV 출력을 쓴다. 공식 문서에 따르면 -c 출력에서 초 단위 값은 다른 단위로 변환되지 않고 초 그대로 나온다. 다섯 번째 필드가 System time 오프셋이다.

chronyc -c tracking
8F2A1B03,time.example.net,3,1760062491.0,-9.4211e-05,-1.2043e-05,2.10517e-04,12.374,-0.001,0.145,0.003218,0.000412,64.3,Normal

chronyc -c tracking | cut -d, -f5
-9.4211e-05

여기서 함정이 하나 있다. 오프셋은 -9.4211e-05처럼 과학적 표기에 음수 부호가 붙어 나온다. 절댓값을 뽑겠다고 tr -d '-'로 부호만 지우면 지수의 음수 부호까지 함께 사라져 9.4211e05, 즉 942110초라는 터무니없는 값으로 둔갑한다. 그러면 정상 동기화 상태에서도 매번 임계치를 넘겨 오탐한다. 절댓값은 반드시 숫자로 해석한 뒤 구해야 한다. awk는 과학적 표기를 부동소수점으로 제대로 파싱하므로 awk 안에서 처리한다.

#!/usr/bin/env bash
# /usr/local/bin/check-clock-drift.sh
# System time 오프셋 절댓값이 임계치(초)를 넘으면 경고
set -euo pipefail

THRESHOLD=0.5   # 허용 오차(초)

# Leap status가 Normal이 아니면 미동기화로 간주
leap=$(chronyc -c tracking | cut -d, -f14)
if [ "$leap" != "Normal" ]; then
  echo "CRITICAL: chrony not synchronised (leap=$leap)"
  exit 2
fi

offset=$(chronyc -c tracking | cut -d, -f5)

# awk가 과학적 표기를 숫자로 파싱해 절댓값 비교. tr로 부호 지우지 말 것.
awk -v off="$offset" -v th="$THRESHOLD" 'BEGIN{
  a = off + 0;          # "-9.4211e-05" -> -0.000094211
  if (a < 0) a = -a;    # 절댓값
  if (a > th) {
    printf "WARNING: clock offset %.6fs exceeds %.3fs\n", a, th;
    exit 1;
  }
  printf "OK: clock offset %.6fs within %.3fs\n", a, th;
  exit 0;
}'

실행하면 정상일 때와 틀어졌을 때 출력이 이렇게 갈린다.

sudo install -m 0755 check-clock-drift.sh /usr/local/bin/check-clock-drift.sh
/usr/local/bin/check-clock-drift.sh
OK: clock offset 0.000094s within 0.500s

exit 코드를 1(WARNING), 2(CRITICAL)로 나눴으므로 Nagios, Icinga 계열 모니터링의 플러그인으로 바로 꽂을 수 있다. 크론이나 systemd 타이머로 돌려도 된다. 타이머로 5분마다 점검하려면 유닛 두 개를 만든다.

# /etc/systemd/system/clock-drift.service
[Unit]
Description=Check clock drift via chrony

[Service]
Type=oneshot
ExecStart=/usr/local/bin/check-clock-drift.sh
# /etc/systemd/system/clock-drift.timer
[Unit]
Description=Run clock drift check every 5 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now clock-drift.timer
systemctl list-timers clock-drift.timer

결과는 journalctl -u clock-drift.service로 확인한다. WARNING이 찍히면 chronyc sources -v로 Reach부터 보고, UDP 123 아웃바운드와 NTP 서버 주소를 점검한다.

5. 컨테이너와 가상화 환경 주의점

컨테이너는 호스트 커널의 시계를 그대로 공유한다. 리눅스에서 시스템 시계는 네임스페이스로 격리되지 않으므로, 컨테이너 안에서 chrony를 또 돌리면 안 된다. 컨테이너가 권한이 충분하면 호스트 시계를 건드리려다 충돌하고, 권한이 없으면 시계를 못 바꿔 무의미하다. 시각 동기화는 호스트에서 한 번만 chrony로 맞추고, 컨테이너는 그 시계를 읽기만 한다.

VM은 상황이 조금 다르다. 많은 하이퍼바이저가 게스트에이전트를 통해 호스트 시각을 게스트로 밀어 넣는다. 이 경로와 게스트 안의 chrony가 동시에 시계를 조정하면 서로 당겨 진동할 수 있다. VMware, KVM 같은 환경에서 게스트 안에서 chrony로 NTP를 쓰기로 했다면, 하이퍼바이저 쪽 게스트 시각 동기화 기능은 끄고 한쪽만 담당하게 하는 편이 안정적이다.

절전이나 스냅샷에서 복귀한 직후처럼 시계가 크게 벌어질 수 있는 시점에는 chrony가 slew로 천천히 따라잡느라 한동안 오프셋이 남는다. 이때 앞의 감시 스크립트가 일시적으로 WARNING을 낼 수 있는데, 복귀 직후 몇 분은 수렴 구간으로 보고 지속적으로 임계치를 넘는지를 기준으로 판단한다.

자주 묻는 질문

시계가 틀어지면 왜 TLS와 토큰이 깨지나?

TLS 인증서는 notBefore, notAfter로 유효기간이 박혀 있고, JWT 같은 토큰은 iat, exp로 발급과 만료 시각을 검사한다. 검증하는 쪽 시계가 과거로 틀어져 있으면 멀쩡한 인증서가 'not yet valid'로, 미래로 틀어져 있으면 방금 발급한 토큰이 'expired'로 거부된다. 양쪽 호스트 시계가 같은 NTP 기준에 맞춰져 있어야 이 판정이 신뢰된다.

chrony와 systemd-timesyncd 중 무엇을 쓰나?

서버에는 chrony를 권장한다. timesyncd는 SNTP 클라이언트라 단순 조회만 하고, chrony는 여러 소스를 통계로 선택하고 드리프트를 보정하며 chronyc로 상태를 상세히 조회할 수 있다. RHEL 계열은 chrony가 기본이고, Ubuntu는 24.04 LTS까지 timesyncd가 기본이므로 운영 서버라면 chrony를 따로 설치해 쓴다. 두 데몬을 동시에 켜지는 않는다.

chronyc tracking에서 어떤 값을 봐야 드리프트를 알 수 있나?

System time 필드가 현재 시계와 NTP 기준 사이의 오프셋이다. 이 값이 밀리초 이하면 잘 맞은 것이고, 수백 밀리초 이상으로 벌어지면 드리프트가 쌓이고 있다는 신호다. Leap status가 Normal인지, sources에서 Reach가 377인지도 함께 본다.

감시 스크립트에서 오프셋 절댓값을 tr로 뽑으면 왜 안 되나?

chronyc -c tracking의 오프셋은 -9.4211e-05처럼 과학적 표기로 나온다. tr -d '-'로 부호를 지우면 지수의 음수 부호(e-05)까지 지워져 9.4211e05, 즉 94만 초로 둔갑한다. 정상 상태에서도 항상 임계치를 넘겨 오탐한다. 절댓값은 awk로 숫자 파싱한 뒤 a<0이면 -a로 구한다.

컨테이너 안에서 chrony를 돌려도 되나?

돌리지 않는다. 리눅스 시스템 시계는 네임스페이스로 격리되지 않아 컨테이너가 호스트 커널의 시계를 그대로 공유한다. 시각 동기화는 호스트에서 chrony로 한 번만 맞추고, 컨테이너는 그 시계를 읽기만 하면 된다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.