createとcopytruncate
한국어 원문으로 표시합니다.
한 줄 요약
create 는 로그를 잃지 않지만 프로세스의 협조가 필요하고, copytruncate 는 협조가 필요 없지만 로그를 조금 잃는다. 둘 다 공짜가 아니다.
왜 이게 필요했나
로그 파일이 커지면 디스크가 찬다. 그런데 프로세스가 파일을 열어 둔 채로 쓰고 있어서 그냥 지울 수 없다. 리눅스에서 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 를 조금 늘리면 그 창이 줄어듭니다.
무엇을 로그로 남길 것인가
로테이션 설정을 아무리 잘해도 양이 많으면 비용이 됩니다. 세 가지를 지키면 크게 줄어듭니다.
- 정상 경로는 요약만. 요청마다 한 줄이면 충분합니다. 디버그 로그를 운영에서 켜 두는 것이 로그량 폭발의 첫 번째 원인입니다.
- 반복되는 것은 집계로. 같은 오류가 초당 1,000번 나면 1,000줄 대신 "이 오류 1,000회" 한 줄이 낫습니다.
- 구조화한다. JSON 으로 쓰면 나중에 필드로 걸러 낼 수 있어, 보존 정책을 세밀하게 나눌 수 있습니다.
실무에서 진짜 중요한 것
보관 기간은 디스크가 아니라 규정과 조사 필요로 정한다. "디스크가 남으니 90일" 이 아니라 "장애 조사에 보통 2주가 필요하고 감사 요건이 1년" 이라면 뜨거운 저장소 2주 + 차가운 저장소 1년으로 나눈다.
그리고 압축은 거의 항상 이득이다. 텍스트 로그는 gzip 으로 10~20배 줄어든다. CPU 를 조금 쓰고 디스크를 크게 아낀다.