LabHub
배우기 러닝패스 코스

관측성 · 로그 수준과 표본 추출 · 이론

로그를 다 남기면 아무것도 못 찾는다

LabHub 에서 이어서 보기

한 줄 요약

로그는 줄 단위로 값이 매겨지지 않는다. 요청 단위로 남느냐 사라지느냐가 조사 가능성을 정하고, 수준과 표본률은 그 위에 얹는 비용 손잡이다.

왜 이게 필요했나

[SRE Book 의 문제 해결 장](https://sre.google/sre-book/effective-troubleshooting/)이 말하는 조사의 첫 단계는 증상을 남긴 요청을 찾아내는 일인데, 장애 조사 중에 이런 일이 벌어진다. 실패한 요청의 request_id 는 알아냈다. 로그 저장소에 그 아이디를 넣었더니 두 줄이 나온다 — "request started" 와 "request failed". 그 사이에서 무슨 일이 있었는지는 없다. 지난달 비용 회의 뒤에 DEBUG 를 껐기 때문이다.

반대 상황도 똑같이 나쁘다. DEBUG 를 다시 켠 팀은 한 달 뒤 저장소 요금이 네 배가 됐고, 검색이 느려져 조사에 쓸 수 없게 됐다. 그다음 손쉬운 선택은 "10% 만 남기자" 였다. 그런데 표본을 줄 단위로 뽑았더니, 같은 요청의 절반만 남아 어느 요청도 끝까지 읽을 수 없게 됐다. 비용은 줄었고 조사 가능성은 0 이 됐다.

세 번 다 같은 실수를 하고 있다. 로그의 단위를 줄로 보는 것이다. 사람이 조사할 때 읽는 단위는 줄이 아니라 하나의 요청이 남긴 줄의 묶음이다.

어떻게 동작하나

그래서 표본은 요청 단위로 뽑는다. request_id 를 해시해 N 으로 나눈 나머지가 0 인 요청만 남기면, 한 요청의 줄은 통째로 남거나 통째로 사라진다. 이것을 일관된 머리 표본(consistent head sampling)이라고 부른다. 해시는 [파이썬 표준 라이브러리의 hashlib](https://docs.python.org/3/library/hashlib.html) 만으로 충분하다. 해시가 하는 일은 하나다 — 같은 아이디에 대해 언제 어디서 계산해도 같은 답을 주는 것. 그래서 여러 서비스가 같은 규칙을 쓰면 한 요청의 흐름이 서비스 경계를 넘어서도 이어진다.

여기에 규칙 하나를 더 얹는다. 오류로 끝난 요청은 표본률과 무관하게 전부 남긴다. 조사할 가치가 있는 요청은 대개 실패한 요청인데, 그런 요청은 전체의 몇 퍼센트뿐이라 보존해도 비용이 거의 늘지 않는다. 표본률 10% 와 오류 보존을 함께 쓰면 줄 수는 9.6% 에서 14.3% 로 조금 늘어나는 대신, 실패한 요청의 보존율이 0% 에서 100% 로 올라간다. 비용 대비 효과가 이보다 큰 손잡이는 드물다.

남은 두 손잡이는 반복을 다루는 것이다. 재시도 고리에 걸린 코드는 같은 줄을 초당 수백 번 찍는다. 연속된 같은 줄을 하나로 접고 repeated=<k> 를 붙이면 정보를 잃지 않고 부피만 줄인다. 그래도 남으면 초당 상한을 건다 — 다만 상한은 DEBUG 에만 걸어야 한다. 상한에 걸려 ERROR 가 사라지면 비용은 줄고 조사는 불가능해진다.

| 손잡이 | 줄이는 것 | 잃는 것 |
| --- | --- | --- |
| 수준 내리기 | 부피의 대부분 | 실패한 요청의 흐름 전체 |
| 요청 단위 표본 | 부피에 비례 | 표본에 안 걸린 요청 전부 |
| 오류 보존 | 늘어남(조금) | 없음 |
| 반복 접기 | 재시도 고리의 부피 | 없음(횟수는 남긴다) |
| 초당 상한 | 폭주 구간 | 상한을 넘은 줄 |

정책 문서에는 이 손잡이들과 함께 보존 기간을 수준별로 적는다. DEBUG 를 90일 보관할 이유는 거의 없고, ERROR 를 3일만 보관하면 분기 회고에서 아무것도 못 찾는다. 수준을 하나의 보존 기간으로 묶으면 둘 중 하나가 된다 — DEBUG 에 맞춰 ERROR 를 잃거나, ERROR 에 맞춰 DEBUG 값을 몇 배로 내거나.

이 모든 손잡이가 동작하려면 전제가 하나 있다. 줄에 request_id 가 있어야 한다. [OpenTelemetry 의 로그 데이터 모델](https://opentelemetry.io/docs/specs/otel/logs/)이 trace_id·span_id 를 로그 레코드의 1급 필드로 둔 이유가 이것이다. 아이디가 없는 로그는 요청 단위로 묶을 수도, 요청 단위로 표본을 뽑을 수도 없어서 결국 줄 단위로 자르는 수밖에 없다. 비용을 줄이는 일의 절반은 계측 단계에서 이미 결정돼 있다.

현장에서 만나는 모습

어느 팀은 표본률을 1% 로 낮추고 "비용을 99% 줄였다" 고 보고했다. 석 달 뒤 결제 실패 조사에서 관련 요청이 로그에 하나도 없었다. 오류 보존 규칙이 없었기 때문이다. 규칙을 넣자 비용은 1.3% 로 올랐고 실패 요청은 전부 남았다. 1.3% 와 1% 의 차이로 살 수 있는 것이 '조사 가능' 이었다.

다른 팀에서는 초당 상한을 수준 구분 없이 걸었다. 평소에는 아무 문제가 없었는데, 진짜 장애가 나서 초당 수천 줄이 쏟아진 그 순간 상한이 걸려 ERROR 줄이 잘려 나갔다. 가장 필요한 시각의 로그만 없었다. 상한은 평소가 아니라 최악의 순간에 동작한다는 것을 잊은 설계였다.

세 번째 사례는 접기에 관한 것이다. 재시도 고리에 빠진 배치 작업이 하루에 4억 줄을 찍었고, 그 줄들은 글자 하나 다르지 않았다. 연속 접기 하나를 넣자 그 4억 줄이 2만 줄로 줄었고 repeated= 숫자 덕분에 "몇 번 재시도했나" 라는 질문에는 여전히 답할 수 있었다. 버리는 것과 요약하는 것은 다른 일이다.

다음 실습에서 할 것

파드에 미리 들어 있는 고정된 로그 파일로 수준별 비용 표를 만들고, DEBUG 를 껐을 때 사고 난 요청의 아홉 줄 중 몇 줄이 남는지 직접 센다. 그다음 request_id 해시로 요청 단위 표본을 뽑는 필터를 만들고, 오류로 끝난 요청을 보존하는 규칙을 더해 표본률과 조사 가능성의 맞바꿈을 표로 만든다. 반복을 접고 초당 상한을 거는 필터를 하나 더 만든 뒤, 두 필터를 이어 붙인 파이프라인으로 88% 를 줄이면서도 그 아홉 줄이 전부 남는 것을 확인하고, 마지막에 수준·표본률·보존기간·예상 비용을 담은 정책 파일을 쓴다.