LabHub
배우기 러닝패스 코스

Designing a Log Pipeline

create and copytruncate

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

create로그를 잃지 않지만 프로세스의 협조가 필요하고, copytruncate협조가 필요 없지만 로그를 조금 잃는다. 둘 다 공짜가 아니다.

Concept map: 로그를 잃지 않지만 프로세스의 협조가 필요하고 · 협조가 필요 없지만 로그를 조금 잃는다. · 파일을 열어 둔 채로 · create — 이름을 바꾸고 새로 만든다

왜 이게 필요했나

로그 파일이 커지면 디스크가 찬다. 그런데 프로세스가 파일을 열어 둔 채로 쓰고 있어서 그냥 지울 수 없다. 리눅스에서 rm 은 디렉터리 항목만 지우고, 열려 있는 파일의 블록은 마지막 핸들이 닫힐 때까지 반환되지 않는다. 그래서 df 는 꽉 찼다고 하고 du 는 파일이 없다고 한다.

로테이션은 이 문제를 두 가지 방식으로 푼다.

create — 이름을 바꾸고 새로 만든다

app.log → app.log.1 로 rename
새 app.log 생성
프로세스에 SIGHUP 을 보내 다시 열게 한다

rename 은 inode 를 그대로 두므로 프로세스는 여전히 옛 파일에 쓴다. 그래서 반드시 다시 열게 해야 한다. 그걸 안 하면 새 app.log 는 영원히 0바이트고 실제 로그는 app.log.1 로 계속 흘러간다. 로그를 잃지는 않지만 아무도 못 찾는다.

copytruncate — 복사하고 잘라 낸다

app.log → app.log.1 로 복사
app.log 를 0바이트로 truncate

파일이 그대로이므로 프로세스는 아무것도 몰라도 된다. 대신 복사와 자르기 사이에 쓴 로그는 사라진다. 초당 수천 줄이 쌓이는 로그라면 그 틈이 크다.

어떻게 동작하나

/var/log/app/*.log {
    daily
    rotate 14              # 14개까지 보관
    size 100M              # 또는 100MB 넘으면
    compress
    delaycompress          # 직전 것은 압축하지 않는다(아직 쓰고 있을 수 있다)
    missingok
    notifempty
    create 0640 app app
    postrotate
        kill -HUP $(cat /run/app.pid) 2>/dev/null || true
    endscript
}

delaycompress 가 중요하다. create 방식에서 프로세스가 아직 옛 파일에 쓰고 있을 수 있는데, 그걸 바로 압축하면 쓰기가 깨진다.

흔한 착각

컨테이너에서 logrotate 를 돌리는 것. 컨테이너는 대개 로그를 stdout 으로 낸다. 그러면 컨테이너 런타임(containerd)이 파일로 받아 적고 런타임이 로테이션한다. kubelet 의 containerLogMaxSize(기본 10Mi), containerLogMaxFiles(기본 5)가 그 설정이다. 컨테이너 안에서 logrotate 를 돌릴 이유가 없다.

로테이션이 수집기를 깨뜨리는 것. Fluent Bit 같은 수집기는 inode 를 기준으로 파일을 추적한다. create 로 rename 되면 수집기가 옛 inode 를 계속 읽다가 새 파일을 놓칠 수 있다. Rotate_Wait 설정이 그 틈을 메운다.

컨테이너에서는 로테이션을 직접 하지 않는다

지금까지의 이야기는 파일에 쓰는 프로세스의 것입니다. 컨테이너에서는 표준 출력에 쓰고 로테이션은 런타임에 맡깁니다.

// /etc/docker/daemon.json 또는 containerd 설정
{"log-driver": "json-file",
 "log-opts": {"max-size": "10m", "max-file": "3"}}

이 설정이 없으면 로그 파일이 무한히 자라 노드의 디스크를 채웁니다. 그러면 그 노드의 모든 파드가 죽습니다. 쿠버네티스에서 노드가 DiskPressure 로 파드를 쫓아내는 사고의 흔한 원인입니다.

kubelet 쪽에도 같은 설정이 있습니다(containerLogMaxSize, containerLogMaxFiles). 컨테이너 런타임 설정과 kubelet 설정 중 어느 쪽이 적용되는지는 런타임에 따라 다르므로, 노드에서 실제 파일 크기를 확인 하는 것이 확실합니다.

du -sh /var/log/pods/* | sort -h | tail -5

로그가 사라지는 세 자리

로테이션과 별개로, 로그는 여러 곳에서 조용히 없어집니다.

버퍼링. 프로세스가 죽을 때 버퍼에 있던 것은 사라집니다. 파이썬은 PYTHONUNBUFFERED=1, 셸 스크립트는 stdbuf -oL 로 줄 단위로 바꿉니다.

수집기 큐 넘침. Fluent Bit·Vector 는 메모리 버퍼가 차면 새 로그를 버립니다 (기본 정책). 디스크 버퍼로 바꾸면 유실은 줄지만 노드 디스크를 씁니다.

# Fluent Bit — 넘칠 때 디스크로
[SERVICE]
    storage.path              /var/log/flb-storage/
    storage.max_chunks_up     128
[INPUT]
    storage.type              filesystem

컨테이너 삭제. 파드가 지워지면 /var/log/pods 아래도 사라집니다. 그래서 죽은 파드의 로그를 나중에 보려면 수집기가 그 전에 가져갔어야 합니다. 수집기가 30초마다 훑는데 파드가 그 사이에 지워지면 마지막 로그가 없습니다. terminationGrace PeriodSeconds 를 조금 늘리면 그 창이 줄어듭니다.

무엇을 로그로 남길 것인가

로테이션 설정을 아무리 잘해도 양이 많으면 비용이 됩니다. 세 가지를 지키면 크게 줄어듭니다.

실무에서 진짜 중요한 것

보관 기간은 디스크가 아니라 규정과 조사 필요로 정한다. "디스크가 남으니 90일" 이 아니라 "장애 조사에 보통 2주가 필요하고 감사 요건이 1년" 이라면 뜨거운 저장소 2주 + 차가운 저장소 1년으로 나눈다.

그리고 압축은 거의 항상 이득이다. 텍스트 로그는 gzip 으로 10~20배 줄어든다. CPU 를 조금 쓰고 디스크를 크게 아낀다.