LabHub
배우기 러닝패스 코스

분산 트레이싱이 끊기는 자리 · 스팬이 안 보인다 · 이론

스팬이 안 보인다는 신고를 받았을 때

LabHub 에서 이어서 보기

한 줄 요약

스팬이 없는 이유는 넷인데 증상은 하나라서, 짐작으로 고치지 말고 요청 기록과 덤프를 대조해 원인마다 다른 흔적으로 갈라야 한다.

왜 이게 필요했나

신고는 언제나 같은 문장으로 온다. "이 주문 번호로 트레이스를 찾았는데 안 나와요." 그 문장 하나로 시작해 이틀을 쓴 적이 있다. 처음 이틀은 표본 추출 설정을 의심했다. 비율을 올려 두고 하루를 기다렸는데 신고가 그대로 들어왔다. 그다음에는 익스포터를 의심해 배치 크기를 키웠고, 그것도 아니었다. 사흘째에 로그와 덤프를 요청 식별자로 맞춰 보고 나서야, 없어진 것이 전부 한 경로의 요청이라는 것을 알았다.

되짚어 보면 첫날에 할 수 있는 일이었다. 우리가 하지 않은 것은 두 가지다. 첫째, 몇 건이 없는지를 숫자로 만들지 않았다. "안 보인다" 는 말만 들고 움직였기 때문에 고친 뒤에도 나아졌는지 알 수 없었고, 그래서 같은 의심을 두 번 했다. 둘째, 없는 것들의 공통점을 보지 않았다. 전부 없는 것과 한 경로만 없는 것은 원인이 아예 다른데, 그 구분을 하지 않은 채로 전역 설정을 만졌다.

어떻게 동작하나

진단은 네 걸음이다. 대조해서 목록을 만들고, 공통점으로 좁히고, 후보를 배제하고, 남은 것을 확인한다.

첫걸음은 없는 것의 목록이다. 애플리케이션 로그에는 요청이 한 줄씩 남아 있고, 스팬 덤프의 서버 스팬에는 같은 요청 식별자가 속성으로 붙어 있다. 두 집합의 차를 구하면 "로그에는 있는데 덤프에는 없는 요청" 이 나온다. 이 숫자가 있어야 뒤의 모든 이야기가 진짜가 된다.

둘째 걸음은 좁히기다. 없는 요청을 경로별로 세고 시간대별로 센다. 한 경로에 몰려 있으면 그 경로의 처리 코드를 보면 되고, 시간대에 몰려 있으면 그 시각에 무슨 일이 있었는지 보면 되고, 고르게 흩어져 있으면 전역 설정을 봐야 한다. 이 표 한 장이 조사 범위를 열 배 줄인다.

셋째 걸음이 배제다. 후보는 넷이고, 넷이 덤프에 남기는 흔적은 서로 다르다.

| 후보 원인 | 루트 스팬 | 자식 스팬 | 없는 요청의 분포 |
| --- | --- | --- | --- |
| 표본에서 빠졌다 | 없다 | 없다 | 흩어져 있다 |
| 스팬을 끝내지 않았다 | 없다 | 남아 있고 부모를 가리키는데 그 부모가 덤프에 없다 | 흩어져 있다(대개 한 경로) |
| 프로세스가 먼저 끝났다 | 없다 | 없다 | 로그 끝에서부터 잇달아 |
| 부모 문맥이 끊겼다 | 있다 | 남아 있지만 다른 트레이스의 루트가 되어 있다 | 없는 요청이 없다 |

표본 추출과 이른 종료는 둘 다 아무 흔적도 남기지 않아서 덤프만 보면 구별되지 않는다. 가르는 것은 분포다. 표본 추출은 전 구간에 흩어져 버리고, 프로세스가 죽으면 그 시각 이후가 통째로 없다. 끝내지 않은 스팬은 정반대로 아주 뚜렷한 흔적을 남긴다 — 자식은 내보내졌는데 그 자식이 가리키는 부모가 덤프 어디에도 없다. 부모 문맥이 끊긴 경우는 아예 다른 증상이다. 없는 요청은 한 건도 없는데, 요청 식별자가 붙은 스팬이 하나도 없는 트레이스가 따로 떠 있다. 화면에서는 "트레이스에 스팬이 하나뿐" 으로 보인다.

넷째 걸음은 같은 판정을 스크립트로 굳히는 일이다. 사람이 매번 표를 그리면 다음 신고 때 또 이틀이 든다. 규칙을 순서까지 정해 코드로 적어 두면 다음 사람은 명령 한 줄로 같은 답을 얻는다. 규칙에 순서를 두는 이유는 흔적이 겹칠 수 있기 때문이다 — 끝내지 않은 스팬은 "요청 식별자가 없는 트레이스" 도 함께 만들어 내므로, 부모 문맥 판정보다 먼저 봐야 한다.

표본 추출이 정확히 무엇을 버리고 자식이 왜 부모의 결정을 따르는지는 [표본 추출 개념 문서](https://opentelemetry.io/docs/concepts/sampling/) 에 정리되어 있고, 프로세스가 끝나기 전에 내보내는 일이 왜 명시적인 호출인지는 [Trace SDK 명세](https://opentelemetry.io/docs/specs/otel/trace/sdk/) 의 ForceFlushShutdown 절에 있다. 부모 문맥이 들어오고 나가는 규격은 [W3C Trace Context](https://www.w3.org/TR/trace-context/) 가 원본이다.

여기서 다루지 않는 것을 분명히 해 둔다. 결함이 든 SDK 배선을 찾아 고치는 일은 SDK 수명주기 모듈의 몫이다. 이 모듈이 가르치는 것은 그 앞이다 — 여러 원인 가운데 어느 것인지를 자료로 가려내는 절차이고, 산출물은 고친 코드가 아니라 분류표와 진단 스크립트다.

이 환경이 판정할 수 없는 것도 적어 둔다. 실습 파드에는 OpenTelemetry Collector 도 추적 백엔드도 없다. 그래서 백엔드 화면에서 그 트레이스가 어떻게 그려지는지, 컬렉터가 중간에서 무엇을 버리는지는 여기서 확인할 수 없다. 우리가 보는 것은 SDK 가 내보낸 스팬을 그대로 적은 JSONL 덤프와 서비스가 남긴 로그 두 가지뿐이고, 판정은 전부 그 둘의 대조로 한다.

현장에서 만나는 모습

가장 자주 보는 것은 세 번째다. 배치 작업이나 짧은 명령형 프로그램에서 마지막 요청 몇 건이 늘 없어지는데, 신고는 "가끔 빠진다" 로 들어온다. 로그와 대조해 보면 없어진 것이 언제나 끝 쪽이라는 게 바로 보인다. 흩어져 있지 않다는 사실 하나가 표본 추출 후보를 곧바로 지워 준다.

두 번째로 자주 보는 것은 네 번째다. 큐 작업자나 콜백 안에서 문맥을 넘기지 않으면 그 안의 스팬이 새 트레이스의 루트가 되어 버린다. 없어진 것이 없으니 대조표에는 아무것도 안 잡히는데, 사람들은 "트레이스가 반쪽" 이라고 신고한다. 이때 세어야 하는 것은 없는 요청이 아니라 요청 식별자가 없는 트레이스다.

다음 실습에서 할 것

신고가 들어온 사건 하나의 증거 두 장(요청 로그와 스팬 덤프)을 놓고 시작한다. 먼저 대조해 없는 요청을 세고, 경로별·시간대별로 나눠 어디에 몰려 있는지 본다. 그다음 네 원인을 직접 재현해 각각이 덤프에 남기는 흔적을 확인하고, 그 차이를 분류표로 정리한다. 정리한 규칙을 진단 스크립트로 옮겨 사건에 돌려 보고, 마지막으로 원인이 다른 두 번째 사건에 같은 스크립트를 돌려 다른 답이 나오는지 확인한 뒤 조사 기록을 남긴다.