Fuwari Banner
지니제스트Tech Archive
Docker7분 소요

도커 컨테이너 내부 시계 동기화 실패와 시스템 시간 왜곡 해결

•
ggeniezst
•조회 1

도커 컨테이너 환경에서 발생하는 시계 동기화 문제로 인한 애플리케이션 장애와 데이터 불일치를 해결하는 심층 가이드입니다. OS 커널, 도커 런타임, NTP 설정 등 복합적인 원인을 분석하고 실무적인 해결책을 제시합니다.

Sponsored

프로덕션 환경에서 도커 컨테이너를 운영하다 보면 예상치 못한 문제에 직면할 때가 많습니다. 그중에서도 시스템 시간과 관련된 문제는 겉으로 드러나지 않아 디버깅이 까다롭고, 데이터 무결성이나 애플리케이션 로직에 치명적인 영향을 줄 수 있습니다. 최근 저희 팀은 특정 도커 컨테이너에서 실행되는 마이크로서비스 간의 통신에서 타임스탬프 불일치 오류가 빈번하게 발생하여 서비스 장애로 이어지는 상황을 겪었습니다.

문제의 발단은 분산 트랜잭션 처리 과정에서 발생했습니다. 여러 서비스가 메시지 큐를 통해 이벤트를 주고받는데, 특정 컨테이너에서 발행된 이벤트의 타임스탬프가 다른 컨테이너에서 수신된 시점보다 미래로 기록되는 현상이 관찰되었습니다. 이는 곧바로 메시지 재처리 로직의 오작동, 데이터베이스에 잘못된 시퀀스 삽입, 그리고 궁극적으로는 사용자 요청 처리 지연 및 실패로 이어졌습니다. 초기에는 애플리케이션 코드의 버그나 네트워크 지연 문제로 의심했지만, 심층 분석 결과 컨테이너 내부의 시스템 시계가 호스트 OS의 시간과 동기화되지 않아 발생하는 문제임을 확인했습니다. 특히 systemd-timesyncd 서비스가 컨테이너 내부에서 제대로 작동하지 않거나, NTP 서버에 접근하지 못하는 경우에 이런 현상이 두드러지게 나타났습니다.


도커 컨테이너 시간 불일치 원인 분석: 호스트와 컨테이너 시계의 독립성

도커 컨테이너는 기본적으로 호스트 OS의 커널을 공유하지만, 사용자 공간(userspace)은 격리된 환경에서 실행됩니다. 리눅스 커널은 시스템 시계를 관리하며, 컨테이너는 이 커널의 시계에 접근합니다. 하지만 systemd-timesyncd나 ntpd와 같은 시간 동기화 데몬은 컨테이너 내부에서 독립적으로 실행되거나, 아예 실행되지 않을 수 있습니다. 특히 systemd-timesyncd는 systemd가 컨테이너 내부의 init 시스템으로 사용되지 않는 경우, 즉 대부분의 경량 컨테이너 이미지(예: Alpine, Debian slim)에서는 기본적으로 비활성화되어 있습니다.

이러한 환경에서 컨테이너가 장시간 실행되거나, 호스트 OS가 시간 동기화를 수행하는 동안 컨테이너가 일시 정지되었다가 재개될 경우, 컨테이너 내부의 시계는 호스트 OS의 시계와 미묘하게 틀어질 수 있습니다. 또한, 도커 컨테이너가 CAP_SYS_TIME 권한 없이 실행될 경우, 컨테이너 내부에서 시계 변경을 시도하더라도 커널 수준에서 거부되어 동기화가 불가능해집니다. 이는 보안상의 이유로 컨테이너가 호스트 시스템의 시간을 임의로 조작하는 것을 방지하기 위함입니다. 이러한 시간 불일치는 특히 마이크로서비스 아키텍처나 분산 시스템에서 이벤트 순서, 캐시 유효성, 로그 타임스탬프 등 다양한 영역에서 심각한 문제를 야기합니다.


도커 컨테이너 내부 NTP 동기화 문제 재현 및 로그 분석

저희가 겪었던 문제는 다음과 같은 시나리오에서 재현되었습니다. Ubuntu 20.04 호스트에서 실행되는 Debian 기반 도커 컨테이너에서 date 명령어를 통해 시간을 확인했을 때, 호스트 시간과 수 초에서 수십 초의 차이가 발생했습니다.

BASH
# 호스트 OS에서 현재 시간 확인
date

# 도커 컨테이너 내부에서 현재 시간 확인
docker exec <container_id> date

이때, 컨테이너 내부에서 NTP 동기화 상태를 확인하기 위해 timedatectl status 명령을 실행하면 다음과 유사한 출력을 볼 수 있었습니다.

BASH
# 컨테이너 내부에서 timedatectl 상태 확인
docker exec my-app-container timedatectl status
OUTPUT
               Local time: Mon 2023-10-26 10:30:05 UTC
           Universal time: Mon 2023-10-26 10:30:05 UTC
                 RTC time: Mon 2023-10-26 10:30:05
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: no  # <-- 여기가 핵심! 'no'로 표시됨
              NTP service: inactive
          RTC in local TZ: no

위 출력에서 System clock synchronized: no와 NTP service: inactive는 컨테이너 내부에서 시간 동기화 서비스가 제대로 작동하지 않거나, 아예 활성화되지 않았음을 명확히 보여줍니다. 이는 systemd-timesyncd가 컨테이너 내부에서 init 프로세스로 실행되지 않거나, NTP 서버에 대한 네트워크 접근이 차단되었을 가능성을 시사합니다.


해결 방안 1: 호스트 NTP 설정 공유 및 컨테이너 권한 부여

가장 간단하고 일반적인 해결책은 도커 컨테이너가 호스트 OS의 NTP 설정을 활용하고, 시간 동기화를 위한 충분한 권한을 갖도록 하는 것입니다. 이를 위해 CAP_SYS_TIME 권한을 컨테이너에 부여하고, 호스트의 systemd-timesyncd 설정 파일을 컨테이너 내부에 마운트하여 활용할 수 있습니다.

단계별 조치 방법

  1. CAP_SYS_TIME 권한 부여: 컨테이너가 시스템 시계를 변경할 수 있도록 CAP_SYS_TIME 권한을 --cap-add 옵션을 사용하여 부여합니다.

    BASH
    # 도커 컨테이너 실행 시 CAP_SYS_TIME 권한 추가 (docker run 예시)
    docker run --cap-add=SYS_TIME -d --name my-app-container my-app-image:latest
    DOCKERFILE
    # Dockerfile 예시 (권한 추가는 docker run 시점에 이루어지는 것이 일반적)
    # 직접 Dockerfile에서 CAP_SYS_TIME을 설정하는 표준적인 방법은 없으며,
    # 런타임 시 docker run 또는 docker-compose에서 설정해야 합니다.
    # 하지만, 컨테이너 내부에서 NTP 클라이언트를 설치해야 한다면 Dockerfile에 포함할 수 있습니다.
    FROM debian:stable-slim
    
    # NTP 클라이언트 설치 (예: ntpdate 또는 chrony)
    RUN apt-get update && apt-get install -y ntpdate && rm -rf /var/lib/apt/lists/*
    
    # 컨테이너 시작 시 NTP 동기화 명령 실행 (entrypoint.sh 등)
    COPY entrypoint.sh /usr/local/bin/entrypoint.sh
    RUN chmod +x /usr/local/bin/entrypoint.sh
    ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
    
    # entrypoint.sh 예시
    # #!/bin/bash
    # ntpdate -s time.nist.gov || true # NTP 서버와 동기화 시도, 실패해도 컨테이너 시작
    # exec "$@"
  2. 호스트의 NTP 설정 파일 마운트 (선택 사항): 만약 컨테이너 내부에서 systemd-timesyncd나 ntpd를 직접 실행하고 싶다면, 호스트의 /etc/systemd/timesyncd.conf 또는 /etc/ntp.conf 파일을 컨테이너에 마운트하여 동일한 NTP 서버 설정을 공유할 수 있습니다. 그러나 이 방법은 컨테이너가 systemd를 init 시스템으로 사용하지 않는 한 실용적이지 않습니다. 대부분의 경우 ntpdate 같은 단일 명령어로 동기화하거나, chrony와 같은 경량 NTP 클라이언트를 설치하는 것이 더 효율적입니다.

    BASH
    # ntpdate를 이용한 수동 동기화 (컨테이너 내에서 실행)
    docker exec my-app-container ntpdate -s time.nist.gov

    주의: ntpdate는 한 번의 동기화만 수행하므로, 지속적인 동기화를 위해서는 chrony나 ntpd와 같은 데몬이 필요합니다.

적용 후 검증 (Verification)

컨테이너 재시작 후 timedatectl status를 다시 확인하여 System clock synchronized: yes로 변경되었는지 확인합니다.

BASH
# 컨테이너 내부에서 timedatectl 상태 확인
docker exec my-app-container timedatectl status
OUTPUT
               Local time: Mon 2023-10-26 10:30:05 UTC
           Universal time: Mon 2023-10-26 10:30:05 UTC
                 RTC time: Mon 2023-10-26 10:30:05
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes  # <-- 'yes'로 변경됨
              NTP service: active
          RTC in local TZ: no

또한, 호스트와 컨테이너 내부에서 date 명령어를 여러 번 실행하여 시간 차이가 거의 없는지 확인합니다.


해결 방안 2: chrony를 이용한 경량 NTP 동기화

chrony는 ntpd보다 가볍고 빠르며, 간헐적인 네트워크 연결이나 가상 환경에 더 적합한 NTP 클라이언트/서버입니다. 컨테이너 내부에서 chrony를 실행하여 지속적인 시간 동기화를 유지하는 것이 더 견고한 해결책이 될 수 있습니다.

단계별 조치 방법

  1. Dockerfile에 chrony 설치: 컨테이너 이미지 빌드 시 chrony 패키지를 설치합니다.

    DOCKERFILE
    # Dockerfile 예시
    FROM debian:stable-slim
    
    RUN apt-get update && apt-get install -y chrony && rm -rf /var/lib/apt/lists/*
    
    # chrony 설정 파일 복사 (선택 사항, 기본 설정 사용 가능)
    # COPY chrony.conf /etc/chrony/chrony.conf
    
    # chrony 데몬 시작을 위한 entrypoint 스크립트
    COPY entrypoint.sh /usr/local/bin/entrypoint.sh
    RUN chmod +x /usr/local/bin/entrypoint.sh
    ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
  2. chrony 서비스 시작 스크립트 작성: 컨테이너가 시작될 때 chrony 데몬이 백그라운드에서 실행되도록 entrypoint.sh 스크립트를 작성합니다. 이때 CAP_SYS_TIME 권한은 여전히 필요합니다.

    BASH
    # /usr/local/bin/entrypoint.sh 예시
    #!/bin/bash
    
    # chrony 데몬을 백그라운드에서 시작
    # -d: 디버그 모드 (로그 확인용)
    # -P: PID 파일 경로 지정 (컨테이너 환경에서 /var/run/chrony.pid가 없을 수 있음)
    # -x: 시스템 시계 조정 권한이 없을 때 강제 종료하지 않음 (CAP_SYS_TIME이 없으면 작동 안 함)
    # -r: RTC 시간 사용 안 함
    # -s: 시계 동기화 후 종료 (초기 동기화에 유용)
    # -q: 시계 동기화 후 종료 (강제 동기화)
    # 데몬으로 실행할 경우 -d, -q, -s 옵션은 사용하지 않습니다.
    # chronyd -d -P /var/run/chrony/chronyd.pid & # 백그라운드 실행 예시
    
    # chrony 데몬 시작 (systemd 없이 직접 실행)
    chronyd -u chrony &
    
    # 애플리케이션의 원래 CMD 실행
    exec "$@"

    이 entrypoint.sh는 chronyd를 백그라운드에서 실행하고, 컨테이너의 원래 CMD(애플리케이션)를 실행하도록 합니다.

  3. 컨테이너 실행 시 CAP_SYS_TIME 권한 부여: docker run 명령에 --cap-add=SYS_TIME 옵션을 추가합니다.

    BASH
    # 도커 컨테이너 실행 시 CAP_SYS_TIME 권한 추가
    docker run --cap-add=SYS_TIME -d --name my-app-container my-app-image:latest

적용 후 검증 (Verification)

컨테이너 내부에서 chronyc tracking 명령을 사용하여 동기화 상태를 확인합니다.

BASH
# 컨테이너 내부에서 chrony 동기화 상태 확인
docker exec my-app-container chronyc tracking
OUTPUT
Reference ID    : 81C9264E (time.nist.gov)
Stratum         : 2
Ref time (UTC)  : Mon Oct 26 10:45:30 2023
System time     : 0.000000000 seconds slow of NTP time
Last offset     : +0.000000000 seconds
RMS offset      : 0.000000000 seconds
Frequency       : 0.000 ppm slow
Residual freq   : 0.000 ppm
Skew            : 0.000 ppm
Root delay      : 0.0000 seconds
Root dispersion : 0.0000 seconds
Update interval : 2.0 seconds
Leap status     : Normal

System time과 Last offset이 0에 가깝고 Reference ID가 정상적으로 표시되면 동기화가 성공적으로 이루어지고 있음을 의미합니다.


사이드 이펙트 방지 및 FAQ

Q. CAP_SYS_TIME 권한 부여가 보안에 미치는 영향은 무엇인가요?

A. CAP_SYS_TIME 권한은 컨테이너가 호스트 시스템의 시계를 변경할 수 있도록 허용합니다. 이는 컨테이너가 악의적인 목적으로 시간을 조작하여 시스템 로그를 왜곡하거나, 인증 시스템을 우회하는 등의 공격에 사용될 수 있는 잠재적 위험이 있습니다. 따라서 이 권한은 반드시 필요한 경우에만 최소한으로 부여하고, 컨테이너 내부에서 실행되는 애플리케이션의 신뢰성을 확보해야 합니다. 가능하다면 컨테이너 자체에서 시간 동기화를 수행하기보다는, 호스트 OS가 정확한 시간을 유지하고 컨테이너는 단순히 호스트의 시간을 읽는 방식을 선호하는 것이 좋습니다.

Q. 컨테이너 내부에서 NTP 서버에 접근할 수 없는 경우는 어떻게 해결하나요?

A. 컨테이너 내부에서 NTP 서버에 접근할 수 없는 경우는 주로 네트워크 방화벽 규칙이나 프록시 설정 문제 때문입니다.

  1. 방화벽 확인: 호스트 OS의 방화벽(예: ufw, firewalld)이 UDP 포트 123(NTP)을 허용하는지 확인해야 합니다.
  2. 도커 네트워크 설정: 도커 네트워크 드라이버 설정(예: bridge 모드)에서 외부 NTP 서버로의 아웃바운드 연결이 허용되는지 확인합니다.
  3. 프록시 설정: 기업 환경에서 프록시 서버를 통해 외부 네트워크에 접근해야 한다면, 컨테이너 환경 변수에 HTTP_PROXY, HTTPS_PROXY, NO_PROXY 등을 설정해야 합니다. NTP는 일반적으로 HTTP 프록시를 사용하지 않지만, 경우에 따라 네트워크 경로에 영향을 줄 수 있습니다.
  4. 내부 NTP 서버 사용: 가능하다면 내부 네트워크에 자체 NTP 서버를 구축하고, 컨테이너가 이 내부 NTP 서버와 동기화하도록 설정하는 것이 가장 안전하고 효율적인 방법입니다.

Q. systemd-timesyncd와 chrony 또는 ntpd 중 어떤 것을 사용해야 하나요?

A.

  • systemd-timesyncd: systemd 기반의 경량 NTP 클라이언트로, 대부분의 리눅스 배포판에 기본으로 포함되어 있습니다. 간단한 설정으로 충분하며 리소스 소모가 적습니다. 하지만 컨테이너 내부에서 systemd를 init 시스템으로 사용하지 않는 경우(대부분의 경량 컨테이너)에는 적합하지 않습니다.
  • chrony: ntpd보다 더 빠르고 정확하며, 간헐적인 네트워크 연결이나 가상 환경에 최적화되어 있습니다. 리소스 소모가 적고, 시계 점프(clock jump)를 더 잘 처리합니다. 컨테이너 환경에서 독립적인 NTP 클라이언트가 필요할 때 chrony가 좋은 선택입니다.
  • ntpd: 전통적인 NTP 데몬으로 기능이 풍부하지만, chrony에 비해 리소스 소모가 많고 시작 시간이 길 수 있습니다. 컨테이너 환경에서는 chrony가 더 권장됩니다.

일반적으로 컨테이너 환경에서는 chrony를 설치하고 CAP_SYS_TIME 권한을 부여하여 사용하는 것이 가장 유연하고 강력한 해결책입니다.

Sponsored