로그로 원인 찾기 · 보존과 회전을 숫자로 정한다 · 이론
그 날짜 로그는 이미 지워져 있었다
한 줄 요약
보존은 감이 아니라 계산이다. 며칠치가 실제로 남아 있는지는 파일 이름이 아니라 내용으로 재야 하고, 며칠치를 남겨야 하는지는 압축률을 실측해 디스크 예산과 견주어 정해야 한다.
왜 이게 필요했나
조사 첫날 가장 자주 만나는 벽은 어려운 질문이 아니라 빈 디렉터리다. "3주 전 그 시각 로그 좀 보내 주세요" 라고 했는데 남아 있는 것이 닷새치뿐인 상황. 이때 필요한 것은 한탄이 아니라 세 개의 숫자다 — 며칠이 모자랐는가, 얼마를 더 남겨야 하는가, 그 값이 디스크 예산에 맞는가.
그리고 이 세 숫자는 전부 지금 있는 파일에서 잴 수 있다. 하루치 원본 크기와 압축본 크기를 재면 압축률이 나오고, 압축률과 하루치가 있으면 N일치의 바이트가 나온다. 예산을 알면 N의 상한이 나온다. 여기에 추측은 한 군데도 없다.
어떻게 동작하나
회전은 이름 규칙이 두 가지다. [logrotate(8)](https://www.man7.org/linux/man-pages/man8/logrotate.8.html)의 기본은 번호를 붙이는 방식이고, 이때는 숫자가 클수록 오래된 것이다(app.log.1 보다 app.log.2 가 과거다). dateext 를 켜면 대신 날짜가 붙는데 기본 형식은 -%Y%m%d 이고, 이때는 이름이 작을수록 오래된 것이다. 한 서버에서 두 규칙이 섞여 있는 일이 드물지 않다 — olddir 로 옮겨 둔 옛 파일만 날짜 이름인 경우다. 그래서 "파일을 시간순으로 세우는 일" 이 조사의 실제 첫 단계가 된다.
rotate 는 현재 파일을 뺀 개수다. 매뉴얼은 rotate count 를 "지워지기 전까지 회전되는 횟수" 로 정의하고, count 가 0이면 회전 없이 바로 지운다고 적는다. 즉 30일치를 남기고 싶으면 daily + rotate 29 다. 여기서 하나씩 어긋나는 실수가 흔하다.
delaycompress 는 압축을 한 주기 미룬다. 매뉴얼은 이 옵션의 목적을 분명히 적는다 — 로그 파일을 닫으라고 말할 수 없는 프로그램이 이전 파일에 한동안 더 쓸 수 있기 때문이다. 그래서 최근 두 날은 압축되지 않은 채로 있고, 용량 계산에서도 그 두 날은 원본 크기로 잡아야 한다.
copytruncate 는 줄을 잃는다. 같은 매뉴얼이 명시한다 — 복사한 뒤 원본을 0으로 비우는데, 그 둘 사이에 아주 짧은 틈이 있어 그 사이에 쓰인 로그는 사라질 수 있다. 재시작하거나 시그널을 받아 파일을 다시 열 수 없는 프로그램을 위한 최후의 수단이지, 기본값으로 쓸 것이 아니다.
systemd 를 쓴다면 예산은 또 다른 곳에 있다. [journald.conf(5)](https://www.man7.org/linux/man-pages/man5/journald.conf.5.html)의 SystemMaxUse= 는 기본값이 파일시스템의 10퍼센트이되 4G 로 잘린다. SystemMaxFiles= 는 기본 100이고, MaxRetentionSec= 는 기본이 0 — 즉 시간 기준 삭제는 꺼져 있다. 용량으로만 밀려 나가므로, 트래픽이 늘면 보존 기간이 조용히 짧아진다.
현장에서 만나는 모습
보관을 늘려도 안 남는 것이 있다. journald 의 RateLimitIntervalSec= 와 RateLimitBurst= 는 한 서비스가 정해진 구간 안에 정해진 수보다 많이 찍으면 그 구간의 나머지를 버린다. 기본값은 30초에 10000건이고 서비스별로 적용되며, 버린 개수를 알리는 메시지가 남는다. 매뉴얼은 여기에 한 가지를 더 적는다 — 실효 한도는 journal 에 남은 여유 디스크 공간에 따라 곱해진다(밑이 2인 로그로 계산한 배수). 즉 디스크가 차 있을수록 더 일찍 버린다. 장애가 나서 로그가 폭주하는 바로 그 순간에 가장 많이 버려지는 구조다.
그래서 사고 구간의 로그가 "없다" 는 두 가지 뜻이다. 회전으로 지워졌거나, 애초에 기록되지 않았거나. 둘은 대책이 완전히 다르다. 앞엣것은 보존을 늘리면 되고, 뒤엣것은 등급을 낮추거나 한도를 올리거나 그 서비스의 로그를 따로 빼야 한다.
압축률은 데이터마다 다르다. 비슷한 줄이 반복되는 접근 로그는 10분의 1 아래로 줄지만, 스택트레이스나 JSON 본문이 섞이면 훨씬 덜 줄어든다. 그래서 "보통 10배" 같은 어림을 쓰지 말고 그 고객의 파일로 재라. 재는 데 1분이 걸리고, 어림으로 잡았다가 디스크가 차면 로그가 아니라 서비스가 멈춘다.
용량 계산에서 가장 자주 빠뜨리는 것이 압축되지 않은 날들이다. delaycompress 를 쓰면 현재 파일과 바로 전날 파일 두 개가 원본 크기로 디스크에 있다. 압축본 크기로만 곱해 놓으면 그 둘만큼이 예산에서 빠지는데, 하필 압축률이 좋을수록 이 오차가 상대적으로 커진다. 하루치 원본이 200MB 이고 압축본이 30MB 라면 두 날의 차이만 340MB 다 — 30일치 예산의 3분의 1이 될 수도 있는 값이다.
그리고 예산은 로그만 쓰는 것이 아니다. 같은 파일시스템에 코어 덤프, 감사 로그, 컨테이너 런타임의 로그가 함께 쌓인다. journald 의 SystemKeepFree= 가 기본 15퍼센트를 남겨 두려 하는 이유도 그것이고, 매뉴얼은 두 한도 가운데 작은 쪽이 적용된다고 적는다. 그래서 회전 정책을 정할 때는 "로그에 얼마를 줄 수 있는가" 를 먼저 합의하고, 그 숫자 안에서 일수를 계산해야 한다. 순서를 뒤집으면 필요한 일수를 먼저 정해 놓고 디스크가 차기를 기다리게 된다.
다음 실습에서 할 것
회전본이 두 가지 이름 규칙으로 섞여 있는 보관 상태를 만들고, 그것을 오래된 것부터 세운다. 그다음 파일마다 실제로 담긴 구간을 내용으로 재서 사고 구간이 범위 밖임을 숫자로 밝힌다. 압축률을 실측해 30일치 바이트와 예산 안에서 가능한 최대 일수를 계산하고, 그 값으로 logrotate 설정을 적는다. 마지막에는 속도 제한 설정에서 버려질 줄 수를 세어, 보존만 늘려서는 못 막는 손실이 있다는 것을 보인다.