为看事故的三分钟,把一整天的日志读了个遍
한국어 원문으로 표시합니다.
목표
회전까지 끝난 여섯 시간치 로그에서 사고 구간 3분만 꺼냅니다. 겹치지 않는 파일은 열지 않고, 남은 파일에서는 이진 탐색으로 바이트 구간을 찍어 그 사이만 읽고, 압축본은 앞에서부터 훑되 일찍 멈춥니다.
왜 중요한가
"3분만 보여 달라" 는 요청에 하루치를 통째로 읽는 것이 로그 작업에서 가장 흔한 사고입니다. 그런데 시각이 RFC 3339 표기로 굳어 있고 파일이 시간순이면 문자열 비교가 곧 시간 비교라, 파일을 읽지 않고 이진 탐색으로 구간의 시작 자리를 찍을 수 있습니다. 어려운 것은 탐색이 아니라 경계입니다. 구간을 반열림으로 잡아야 이어지는 구간이 겹치지 않고, 같은 시각의 줄이 여럿일 때 그 시각의 첫 줄을 찾아야 앞의 몇 줄이 조용히 빠지지 않습니다. 그리고 빠르다는 말만으로는 아무것도 증명되지 않으므로, 통째로 훑는 느린 방법과 한 번은 대조해야 합니다.
단계
/root/slice/gen_slice.py를 만들어 실행해/root/slice/var/log/아래 app.log · app.log.1 · app.log.2.gz 를 만드세요./root/slice/index.json— 회전본을 오래된 것부터 세우고 파일마다 담긴 구간을 적으세요./root/slice/plan.json— 사고 구간과 겹치는 파일만 고르고 나머지는 이유와 함께 건너뛰세요./root/slice/offsets.json— 이진 탐색으로 구간의 시작 바이트와 끝 바이트를 찍으세요./root/slice/window_a.log— 찍어 둔 바이트 구간만 읽어 사고 구간을 뽑으세요./root/slice/proof.json— 느린 방법과 대조해 결과가 같음을 증명하세요./root/slice/window_b.log와/root/slice/gz_scan.json— 압축본 안의 두 번째 구간을 일찍 멈추며 뽑으세요./root/slice/slice_report.md— 방법과 근거를 보고서로 남기세요.
참고
- 사고 구간은
2026-04-12T03:58:30.000Z부터2026-04-12T04:01:30.000Z까지이고, 두 번째 구간은2026-04-12T00:20:00.000Z부터2026-04-12T00:23:00.000Z까지입니다. 둘 다 시작은 포함하고 끝은 포함하지 않는 반열림 구간입니다. - 줄의 앞 24자가 시각입니다. 표기가 통일되어 있으므로
line[:24] >= key같은 문자열 비교가 그대로 시간 비교입니다. - 이진 탐색에서
seek한 자리는 거의 늘 줄 한가운데입니다. 한 줄을 읽어 버려 줄 경계에 선 다음 판정하세요. - 흔한 실수: 같은 시각의 줄 가운데 첫 줄이 아닌 것을 찾기, 끝 시각의 줄까지 넣기, 회전 경계 너머의 파일을 빼먹기, 압축본에 이진 탐색을 걸기.
- 현장의 파일은 수 GB 이지만 이 실습은 같은 구조를 20MB 로 줄여 재현합니다. 산출물은 전부
/root/slice/아래에 모이고, 세션이 끝나면 사라집니다.
회전까지 끝난 로그 묶음 재현하기
/root/slice/gen_slice.py 를 만들어 실행해 /root/slice/var/log/ 아래 app.log(57600줄) · app.log.1(57600줄) · app.log.2.gz(풀면 57600줄)를 만드세요.
세 파일은 같은 프로그램이 쓴 같은 형식이고, 회전으로 쪼개진 것뿐입니다. 초당 여덟 줄로 두 시간치씩 담고, 30초마다 같은 밀리초에 세 줄이 겹치게 하세요. 압축본은 파이썬 gzip.GzipFile 에 mtime=0 을 주어 쓰면 다시 만들어도 같은 파일이 나옵니다.
회전본을 시간순으로 세우고 담긴 구간 재기
/root/slice/index.json 에 files(오래된 것부터 담은 배열, 항목마다 file · compressed · bytes · first_ts · last_ts)와 rule(그렇게 세운 규칙 한 문장, 20자 이상)을 적으세요. 시각은 줄의 앞 24자를 그대로 씁니다.
번호가 붙은 회전본은 숫자가 클수록 과거이고, 확장자가 없는 것이 지금 쓰는 파일입니다. logrotate 의 dateext 를 쓰면 이름이 날짜라 방향이 반대가 되니 규칙을 적을 때 함께 짚어 두세요. 비압축 파일의 마지막 줄은 파일 끝에서 몇 KB 만 읽어도 꺼낼 수 있습니다 — 통째로 읽지 마세요. 재료는 /root/slice/var/log/ 아래 세 파일입니다.
겹치지 않는 파일은 아예 열지 않기
/root/slice/plan.json 에 window(start 는 2026-04-12T03:58:30.000Z, end 는 2026-04-12T04:01:30.000Z)와 open(구간과 겹치는 파일을 오래된 것부터 담은 배열), skip(나머지 파일을 file · reason 으로 담은 배열)을 적으세요.
2단계에서 잰 first_ts 와 last_ts 만 있으면 파일을 열지 않고도 겹치는지 알 수 있습니다. 구간이 반열림이므로 겹침 판정도 반열림입니다 — 파일의 시작이 구간의 끝보다 앞서고, 파일의 끝이 구간의 시작보다 앞서지 않으면 겹칩니다. 재료는 /root/slice/index.json 입니다. reason 은 10자 이상으로 적으세요.
이진 탐색으로 구간의 시작과 끝을 바이트로 찍기
/root/slice/offsets.json 에 offsets(3단계에서 고른 비압축 파일마다 file · start_offset · end_offset 을 담은 배열, 오래된 것부터)를 적으세요. start_offset 은 시각이 구간 시작 이상인 첫 줄의 시작 바이트이고, end_offset 은 구간 끝 이상인 첫 줄의 시작 바이트입니다(없으면 파일 크기).
파일 크기를 반으로 접어 그 바이트로 seek 하면 거의 늘 줄 한가운데입니다. 한 줄을 읽어 버려 줄 경계에 선 뒤 그 줄의 앞 24자로 판정하세요. 같은 시각의 줄이 셋 있으므로 '조건을 만족하는 아무 줄' 이 아니라 '조건을 처음 만족하는 줄' 을 찾아야 합니다 — 조건을 만족하면 범위의 오른쪽 끝을 그 줄의 시작으로 당기세요. 재료는 /root/slice/plan.json 입니다.
찍어 둔 바이트 구간만 읽어 사고 구간 뽑기
/root/slice/window_a.log 에 사고 구간 [2026-04-12T03:58:30.000Z, 2026-04-12T04:01:30.000Z) 의 줄을 시간순으로, 원문 그대로 쓰세요. 파일은 오래된 것부터 이어 붙입니다.
4단계에서 찍은 두 오프셋 사이를 그대로 읽어 내면 됩니다. end_offset 이 구간 밖 첫 줄의 시작이라 그 앞까지가 정확히 반열림 구간입니다. 줄을 다시 파싱하거나 다듬지 마세요 — 원문 바이트를 그대로 옮겨야 다음 단계의 해시 대조가 성립합니다. 재료는 /root/slice/offsets.json 입니다.
느린 방법과 대조해 결과를 증명하기
/root/slice/proof.json 에 window · fast_lines · slow_lines · sha256_fast · sha256_slow · match(불리언) · bytes_read_fast · bytes_read_slow 를 적으세요. bytes_read_fast 는 4단계의 두 오프셋 사이 바이트의 합이고, bytes_read_slow 는 압축본까지 풀어 세 파일을 통째로 읽을 때의 바이트입니다.
느린 방법은 세 파일을 전부(압축본은 풀어서) 훑으며 구간의 줄을 모으는 것입니다. 시간은 재지 마세요 — 기계마다 흔들립니다. 대신 줄 수와 sha256 해시를 견줍니다. 해시는 뽑힌 줄을 이어 붙인 바이트에 대해 계산하고, 두 방법의 값이 같아야 match 가 참입니다. 재료는 /root/slice/window_a.log 와 /root/slice/offsets.json 입니다.
압축본에서는 일찍 멈추는 것이 답이다
두 번째 질문은 [2026-04-12T00:20:00.000Z, 2026-04-12T00:23:00.000Z) 입니다. /root/slice/window_b.log 에 그 구간의 줄을 원문 그대로 쓰고, /root/slice/gz_scan.json 에 window · file · total_lines · lines_read · lines_kept 를 적으세요. lines_read 는 멈출 때까지 실제로 푼 줄 수입니다.
이 구간은 압축본 안에 있습니다. gzip 스트림은 앞의 내용에 기대어 풀리므로 임의 지점으로 건너뛸 수 없고, 이진 탐색을 걸면 같은 자리를 몇 번이고 다시 푸는 꼴이 됩니다. 앞에서부터 한 번만 훑되 구간의 끝을 지나는 줄을 만나면 그 자리에서 빠져나오세요. total_lines 는 얼마나 아꼈는지 적기 위한 값이라 한 번은 끝까지 세어야 합니다. 재료는 /root/slice/var/log/app.log.2.gz 입니다.
방법과 근거를 문서로 남기기
/root/slice/slice_report.md 에 ## 무엇을 물었나 ## 왜 통째로 읽지 않아도 되었나 ## 압축본은 무엇이 달랐나 ## 결과를 어떻게 증명했나 네 절로 적으세요. 사고 구간의 줄 수 · 빠른 방법이 읽은 바이트 · 압축본에서 푼 줄 수를 숫자로 포함해야 합니다.
이 문서를 읽을 사람은 다음에 같은 질문을 받는 사람입니다. 그 사람이 알아야 하는 것은 명령이 아니라 전제입니다 — 왜 이진 탐색이 성립했는지, 어떤 파일을 왜 열지 않았는지, 압축본에서 무엇이 달랐는지, 결과를 무엇과 대조했는지. 숫자는 지어내지 말고 /root/slice/proof.json 과 /root/slice/gz_scan.json 에서 꺼내 쓰세요.