LabHub
배우기 러닝패스 코스

관측성 · 추적으로 느린 구간 찾기 · 이론

가장 오래 걸린 스팬이 범인이 아니다

LabHub 에서 이어서 보기

한 줄 요약

트레이스에서 제일 긴 스팬을 고치는 것은 대개 헛수고다. 응답 시간을 줄이는 것은 자기 시간이 크면서 임계 경로 위에 있는 스팬뿐이다.

왜 이게 필요했나

주문 API 의 p50 이 290밀리초였다. 트레이스를 열자 화면 맨 위에 158밀리초짜리 막대가 보였다 — 재고 조회였다. 두 사람이 사흘을 들여 그 조회에 캐시를 붙였고, 배포한 다음 날 대시보드를 보니 p50 은 289밀리초였다. 1밀리초.

원인은 간단했다. 재고 조회는 가격 계산과 동시에 시작한다. 재고 조회가 158밀리초에 끝날 때 가격 계산은 아직 돌고 있었고, 게이트웨이는 둘 다 끝나기를 기다린다. 늦게 끝나는 쪽이 응답 시간을 정한다. 재고 조회를 0으로 만들어도 게이트웨이는 여전히 가격 계산을 기다린다.

이 착각은 화면 탓이 크다. 폭포수(waterfall) 화면은 스팬을 길이순으로 보여 주고, 사람 눈은 제일 긴 막대에 먼저 간다. 그런데 막대의 길이는 '그 구간에 시간이 얼마나 걸렸나' 이지 '그 구간을 줄이면 전체가 줄어드나' 가 아니다. 두 질문은 다르고, 두 번째 질문에 답하려면 계산이 필요하다.

어떻게 동작하나

답을 주는 것은 두 가지 계산이다.

자기 시간(self time) 은 그 스팬이 자식에게 넘기지 않고 직접 쓴 시간이다. 총 시간에서 자식들이 차지한 시간을 뺀다. 여기서 한 번 미끄러지는데, 자식 시간을 그냥 더해서 빼면 안 된다. 병렬로 도는 자식이 있으면 합이 부모의 총 시간을 넘어 자기 시간이 음수가 된다. 자식 구간의 합집합 길이를 빼야 한다.

임계 경로(critical path) 는 루트의 끝에서 거꾸로 훑어 만든다. 지금 보고 있는 시각보다 늦게 끝나는 자식은 건너뛰고, 가장 늦게 끝나는 자식으로 내려간다. 그 자식이 시작한 시각까지 올라온 다음, 그보다 앞서 끝난 형제로 옮겨 간다. 이렇게 하면 루트의 시작부터 끝까지가 빈틈없이 덮이고, 각 조각의 주인이 정해진다. 조각의 합은 정확히 루트의 지속 시간과 같다 — 이 등식이 계산이 맞았는지 알려 주는 검산이다.

두 계산을 합치면 순위가 뒤집힌다. 위의 사고에서 재고 조회는 총 시간 1위, 자기 시간 1위였지만 임계 경로 기여는 0 이었다. 반대로 가격 규칙 평가는 총 시간 3위였지만 임계 경로 기여가 142밀리초로 압도적이었다. 고쳐야 할 것은 3위였다.

여기에 하나가 더 붙는다. N+1 이다. 같은 이름의 형제 스팬이 수십 개 줄지어 있으면 하나하나는 3밀리초짜리라 순위에 들지 않는다. 그런데 열여섯 개를 더하면 45밀리초가 되고, 한 번의 질의로 묶으면 그중 42밀리초가 사라진다. 개별 스팬이 아니라 그룹을 봐야 보인다.

OpenTelemetry 의 [트레이스 문서](https://opentelemetry.io/docs/concepts/signals/traces/)는 스팬을 작업의 한 단위로, 트레이스를 요청이 지나간 경로로 정의한다. 스팬에는 시작 시각과 지속 시간, 부모 스팬의 식별자가 들어 있고, 임계 경로 계산에 필요한 것은 그 셋뿐이다. 도구가 없어도 표준 라이브러리로 계산할 수 있다는 뜻이다.

현장에서 만나는 모습

네 가지가 반복된다.

첫째, p50 과 p99 의 임계 경로가 다르다. 평소에는 가격 계산이 마지막에 끝나지만, 재고 데이터베이스가 멎는 순간 재고 조회가 마지막이 된다. p50 만 보고 고치면 p99 는 그대로다. 그래서 두 트레이스를 각각 뽑아 임계 경로를 견주는 습관이 필요하다.

둘째, 자료가 지저분하다. 수집기가 조각을 잃으면 부모가 파일에 없는 고아 스팬이 남고, 루트가 통째로 빠진 트레이스도 생긴다. 이런 트레이스를 그냥 섞어 계산하면 합계가 조용히 틀어진다. 계산에 앞서 '루트가 하나이고 고아가 없는 트레이스' 만 골라내는 단계가 반드시 필요하다.

셋째, 계산 대상을 몇 건으로 잡을지가 결론을 바꾼다. 지연은 [네 가지 골든 시그널](https://sre.google/sre-book/monitoring-distributed-systems/) 중 하나이고, 그 지표를 개선하려면 어느 트레이스를 대표로 삼을지부터 정해야 한다. 트레이스 한 편만 보고 임계 경로를 정하면 그 한 건의 우연을 병목으로 오해하기 쉽다. 반대로 수천 건을 평균 내면 병렬 구조가 서로 상쇄돼 아무 데도 두드러지지 않는다. 현실적인 타협은 같은 진입점(같은 루트 스팬 이름)끼리 묶어 놓고 지속 시간 분포의 대표 지점 몇 곳을 뽑아 각각 계산한 다음, 경로에 드는 스팬 이름이 겹치는지 보는 것이다. 이름이 겹치면 구조적인 병목이고, 갈리면 조건에 따라 달라지는 병목이라 고치는 방법도 달라진다.

넷째, 예상 절감을 적어 두지 않은 개선은 검증되지 않는다. "이걸 고치면 빨라질 것 같다" 는 배포 뒤에 확인할 방법이 없다. 고치기 전에 "루트에서 142밀리초가 줄어든다" 를 숫자로 적어 두면, 배포 뒤에 맞았는지 틀렸는지가 남는다. 틀렸다면 모델이 틀린 것이고, 그건 다음 판단을 고쳐 준다.

다음 실습에서 할 것

파드에 들어 있는 3,454개 스팬(150 트레이스)을 표준 라이브러리만으로 분석한다. 부모-자식을 다시 이어 깊이를 붙이고, 겹치는 자식을 합집합으로 처리해 자기 시간을 구하고, 임계 경로를 계산해 조각의 합이 루트 지속 시간과 같은지 검산한다. 그다음 '가장 오래 걸린 스팬' 목록과 '자기 시간 상위' 목록과 '임계 경로 기여 상위' 목록이 서로 다르다는 것을 표로 확인하고, N+1 을 찾아 묶었을 때의 절감을 계산한다. 마지막에는 p50 과 p99 트레이스의 임계 경로를 견주고, 무엇을 고칠지 예상 절감 시간과 함께 파일로 제출한다.