사전 분산 추적
개념

분산 추적

gabury1고친 사람 github-actions[bot]

분산 추적은 요청 하나가 여러 서비스를 거쳐 가는 길을 처음부터 끝까지 따라가는 방식입니다. 요청이 시스템에 처음 들어올 때 번호를 하나 붙입니다. 서비스가 다음 서비스를 부를 때 그 번호를 같이 넘깁니다. 나중에 번호로 기록을 모으면 요청 하나가 어디를 거쳤고 어디서 오래 걸렸는지가 한 줄기로 드러납니다.

쉽고 빠른 이해

분산 추적은 요청 한 건의 이동 경로를 통째로 남겨 두는 일입니다. 주문 한 건이 주문 서비스와 재고 서비스와 결제 서비스를 차례로 들렀다면, 그 세 곳에서 생긴 기록이 한 벌로 묶입니다.

이게 없으면 서비스마다 따로 쌓인 기록을 사람이 손으로 맞춰 봐야 합니다. 같은 시간에 요청 수천 건이 섞여 흐르니 어느 줄이 내가 쫓는 요청의 것인지 가려내기가 어렵습니다.

어떻게 도는가:

  1. 요청이 시스템에 처음 들어올 때 그 요청만의 번호를 붙입니다
  2. 서비스가 다음 서비스를 부를 때 번호를 요청에 실어 넘깁니다
  3. 서비스마다 「내가 맡은 일이 언제 시작해 얼마나 걸렸나」를 번호와 함께 남깁니다
  4. 남은 기록을 한곳에 모아 둡니다. 번호로 찾아 한 화면에 펼쳐 봅니다

대가도 있습니다. 코드에 기록을 남기는 손질을 넣어야 합니다. 모인 기록을 받아 둘 서버도 따로 필요합니다. 요청을 모두 남기면 양이 감당 못 하게 불어나서 대개 일부만 골라 남깁니다.

서비스가 하나뿐이면 로그만으로 충분합니다. 여러 개로 쪼개지고 호출이 서로 얽히기 시작할 때부터 쓸모가 생깁니다.

상세

택배 상자를 따라다니는 것은 송장 번호 한 장뿐입니다. 상자가 지나는 집하장과 터미널과 배송 지점은 저마다 자기 장부에 「몇 시에 받았고 몇 시에 넘겼다」를 찍어 둡니다. 상자가 어디서 하루를 통째로 묵었는지는 나중에 송장 번호로 그 흩어진 장부를 한곳에 불러 모아야 보입니다.

분산 추적은 요청에 똑같은 일을 합니다. 요청마다 번호를 하나 붙여 둡니다. 그 요청을 만진 모든 서비스가 같은 번호로 기록을 남기게 합니다.

서비스를 여러 개로 쪼개 놓은 마이크로서비스 구조에서 주문 하나가 유독 오래 걸렸다고 해 봅시다. 분산 추적이 있으면 그 시간이 재고 조회에서 갔는지 결제 호출에서 갔는지가 기록 한 벌 안에서 갈립니다.

로그만으로는 못 잇는 까닭

서비스가 하나뿐이던 시절에는 로그로 충분했습니다. 요청 하나가 프로세스 하나 안에서 끝나니, 그 프로세스의 기록만 시간 순으로 읽으면 무슨 일이 있었는지 다 나옵니다.

서비스를 쪼개면 기록도 함께 쪼개집니다. 주문 서비스의 기록에는 주문 서비스가 한 일만 있습니다. 재고 서비스가 왜 답을 늦게 줬는지는 그쪽 기록에 있습니다. 한 요청의 이야기가 서버 대수만큼 흩어집니다.

흩어진 기록을 시각만 보고 맞추기는 어렵습니다. 같은 순간에 다른 요청 수천 건이 같은 서비스를 지나가므로 시각이 비슷한 줄이 수없이 많습니다. 서버마다 시계가 조금씩 어긋나 있어서 순서가 뒤집혀 보이기도 합니다.

그래서 요청 자신에게 이름표를 붙이는 쪽으로 갑니다. 시각으로 맞추는 대신 번호로 맞추는 것입니다.

요청에 붙는 번호 셋

붙이는 번호는 셋입니다. 트레이스 ID는 요청 전체에 하나만 붙는 번호로, 한 요청에서 생긴 기록은 전부 이 번호를 똑같이 나눠 가집니다. 스팬 ID는 기록 조각 하나하나에 따로 붙는 번호입니다. 부모 스팬 ID는 그 일을 시킨 바로 윗 기록의 번호라서, 누가 누구를 불렀는지를 잇습니다.

이 셋만 있으면 흩어진 조각을 모아 요청의 모양을 다시 세울 수 있습니다. 트레이스 ID로 한 요청의 조각을 다 긁어모읍니다. 그다음 부모 번호를 따라 부른 순서대로 이어 붙이면 됩니다.

번호를 뒤 서비스로 넘기는 일을 컨텍스트 전파라고 부릅니다. HTTP(HyperText Transfer Protocol)로 부를 때는 번호를 요청 헤더에 싣습니다. 받는 쪽은 넘어온 트레이스 ID를 그대로 씁니다. 넘어온 스팬 ID는 자기 기록의 부모로 적습니다.

아래는 주문 요청 하나가 서비스 셋을 지나는 동안 번호가 어떻게 따라가는지 그린 것입니다. 맨 앞의 게이트웨이는 바깥에서 들어온 요청을 받아 안쪽 서비스로 넘기는 서버입니다.

sequenceDiagram
    participant 게이트웨이
    participant 주문
    participant 재고
    participant 결제
    게이트웨이->>주문: 요청 + 트레이스 ID + 스팬 ID
    Note over 주문: 같은 트레이스 ID로<br/>자기 기록을 연다
    주문->>재고: 요청 + 트레이스 ID + 주문의 스팬 ID
    재고-->>주문: 응답
    주문->>결제: 요청 + 트레이스 ID + 주문의 스팬 ID
    Note over 결제: 부모는 주문의 기록이다
    결제-->>주문: 응답
    주문-->>게이트웨이: 응답

번호를 붙이고 넘기는 일은 보통 라이브러리가 대신합니다. 이 라이브러리가 서비스마다 다르면 헤더 이름이 갈려서 번호가 안 읽힙니다.

그래서 헤더 모양을 미리 정해 둔 표준이 있습니다. 널리 쓰이는 것이 Trace Context입니다. 도구가 서로 달라도 이 모양을 지키면 번호가 그대로 이어집니다.

조각 하나가 스팬, 한 벌이 트레이스

기록 조각 하나를 스팬이라고 부릅니다. 스팬은 「한 가지 일을 한 번 한 것」의 기록이라서, 요청 하나를 처리한 것이나 쿼리 하나를 보낸 것이 각각 스팬 하나가 됩니다.

스팬에는 무슨 일이었는지를 말하는 이름과 시작 시각과 걸린 시간이 담깁니다. 앞 절의 번호 셋도 함께 붙습니다. 그 밖에 필요한 값을 이름표처럼 덧붙일 수 있습니다.

같은 트레이스 ID를 가진 스팬을 전부 모은 한 벌을 트레이스라고 부릅니다. 분산 추적은 이 한 벌을 만들어 두고 나중에 꺼내 보는 방식입니다. 트레이스는 그 결과로 남는 기록입니다.

부모 번호를 따라 이어 붙이면 한 벌이 나무 모양이 됩니다. 아래는 주문 한 건의 트레이스를 그린 것입니다.

flowchart TD
    subgraph T["트레이스 ID t-1"]
        A["게이트웨이가 주문 요청을 받음<br/>스팬 s-1 · 부모 없음"]
        B["주문 서비스가 주문을 처리<br/>스팬 s-2 · 부모 s-1"]
        C["재고 서비스가 재고를 확인<br/>스팬 s-3 · 부모 s-2"]
        D["결제 서비스에 결제를 요청<br/>스팬 s-4 · 부모 s-2"]
    end
    A --> B
    B --> C
    B --> D

화살표는 부모에서 자식으로 갑니다. 주문 아래에서 재고와 결제 둘로 갈라진 것은 주문 서비스가 두 곳을 불렀기 때문입니다.

한 벌을 시간 축에 눕혀 보면 어디서 시간이 갔는지가 바로 보입니다. 아래는 설명을 위해 지어낸 값으로, 주문 한 건이 결제 호출에 걸려 있던 경우입니다.

스팬 시작 걸린 시간
게이트웨이가 주문 요청을 받음 0.00초 0.48초
주문 서비스가 주문을 처리 0.01초 0.47초
재고 서비스가 재고를 확인 0.01초 0.06초
결제 서비스에 결제를 요청 0.08초 0.39초

전체 0.48초 가운데 0.39초를 결제 요청이 차지했습니다. 재고 쪽은 0.06초에 끝났으니 손댈 곳이 아닙니다.

추적 도구는 이 표를 막대가 겹친 그림으로 보여 줍니다. 위에서 아래로 갈수록 막대가 들여쓰기되어 폭포처럼 보입니다. 이 그림을 흔히 폭포 그림이라고 부릅니다.

굴리려면 갖춰야 하는 다섯 단계

분산 추적은 코드 한 줄로 켜지는 기능이 아니라 다섯 단계를 이어 붙인 흐름입니다. 하나라도 빠지면 기록이 안 남거나 남아도 못 찾습니다.

flowchart TD
    A["계측 · 코드가 스팬을 열고 닫는다"] --> B["전파 · 번호를 다음 서비스로 넘긴다"]
    B --> C["수집 · 흩어진 스팬을 한곳으로 보낸다"]
    C --> D["저장 · 트레이스 ID로 찾게 쌓아 둔다"]
    D --> E["조회 · 한 요청을 펼쳐 본다"]

첫 단계인 계측은 「여기서 일이 시작됐고 여기서 끝났다」를 코드가 알리게 만드는 손질입니다. 웹 프레임워크나 데이터베이스 드라이버는 이 손질이 라이브러리에 미리 들어 있는 경우가 많습니다. 설정만 켜면 스팬이 저절로 열리고 닫힙니다. 직접 짠 함수의 안쪽을 보고 싶으면 그 부분은 손으로 넣어야 합니다.

가운데 단계인 수집은 서비스가 뱉은 스팬을 한곳으로 모으는 일입니다. 서비스가 저장소로 직접 보내게 하면 서비스마다 주소와 재시도를 따로 챙겨야 합니다. 보통은 수집기라는 중간 서버를 두고 그쪽으로만 보냅니다. 수집기는 받은 스팬을 다듬어 저장소로 넘깁니다.

마지막 두 단계는 따로 띄우는 서버가 맡습니다. 수집기가 넘기는 그 저장소와 조회 화면을 합쳐 추적 백엔드라고 부릅니다. 애플리케이션 서버와는 별개의 물건이라 운영할 사람과 저장 공간이 따로 듭니다.

아래는 스팬이 어디로 모이는지 그린 것입니다. 서비스가 셋이든 서른이든 보내는 곳은 수집기 하나입니다.

flowchart TD
    subgraph S["서비스"]
        O["주문"]
        I["재고"]
        P["결제"]
    end
    O --> K["수집기"]
    I --> K
    P --> K
    K --> BE["추적 백엔드 · 저장과 조회"]

서비스가 백엔드로 직접 보냈다면 백엔드 주소를 바꿀 때 서비스를 전부 고쳐야 합니다. 수집기를 끼우면 고칠 곳이 수집기 하나로 줄어듭니다.

남길 요청을 고르는 두 방법

요청을 하나도 빠짐없이 남기면 저장할 양이 요청 수에 비례해 불어납니다. 그래서 요청 가운데 일부만 골라 남깁니다. 이 고르기를 샘플링이라고 합니다.

고르는 때는 크게 둘입니다. 요청이 처음 들어올 때 동전을 던지듯 정하는 방식을 헤드 샘플링이라고 부릅니다. 가볍고 판단이 빨리 끝납니다. 대신 나중에 터지거나 오래 걸린 요청이 하필 안 뽑혔으면 그 기록이 없습니다.

요청이 다 끝난 뒤 스팬을 모아 보고 정하는 방식은 테일 샘플링입니다. 이쪽은 그 약점을 덮어서, 실패한 요청과 오래 걸린 요청을 골라 남길 수 있습니다. 대신 판단할 때까지 스팬을 어딘가에 들고 있어야 해서 수집기 쪽이 무거워집니다.

어느 쪽이든 그 결정은 뒤따르는 서비스에도 그대로 전해져야 합니다. 앞 서비스는 남기고 뒤 서비스는 버리면 트레이스가 중간에서 잘려 반쪽만 남기 때문입니다. 그래서 「이 요청은 남기기로 했다」는 표시도 번호와 함께 실어 넘깁니다.

기록이 끊기는 곳과 치르는 비용

추적은 가장 약한 고리에서 끊깁니다. 번호를 넘기지 않는 서비스가 하나 끼면 그 뒤의 스팬은 새 트레이스로 시작해 버립니다. 앞뒤가 남남이 됩니다. 옛 시스템이나 남이 만든 구간이 사이에 있을 때 자주 생깁니다.

flowchart TD
    subgraph T1["트레이스 t-1"]
        A["게이트웨이"] --> B["주문 서비스"]
        B --> X["번호를 안 넘기는 서비스"]
    end
    subgraph T2["트레이스 t-2 · 새로 시작"]
        C["결제 서비스"]
    end
    X -- "번호가 안 실림" --> C

한 벌이어야 할 요청에 트레이스 ID가 둘 생겼습니다. 조회 화면에서 두 벌을 따로 펴 보게 되고, 둘이 같은 요청이라는 것을 알려 줄 실마리가 없습니다.

메시지 큐를 거치는 일도 끊기기 쉽습니다. 보내는 쪽과 받는 쪽은 시간을 두고 떨어져 있습니다. 메시지 안에 번호를 실어 주지 않으면 이어 붙일 실마리가 없습니다.

비용도 치릅니다. 요청마다 스팬을 열고 닫는 만큼 일이 조금씩 늘어납니다. 남긴 기록을 나르고 쌓는 데도 돈이 계속 듭니다.

스팬에 사용자 ID나 요청 내용을 담다 보면 남기면 안 될 값이 같이 실려 나갈 수도 있습니다. 무엇을 담을지 미리 정해 두어야 합니다.

치를 비용은 요청이 경계를 몇 번 넘느냐에 달려 있습니다. 서비스가 하나뿐이고 요청이 한 프로세스 안에서 끝나는 시스템이라면 로그와 프로파일링으로도 오래 걸리는 곳을 찾을 수 있습니다. 서비스가 열 개를 넘어가고 호출이 서로 얽히기 시작하면 그때부터는 추적 없이 원인을 짚기가 어려워집니다.

관련 항목

분산 추적이 남기는 기록의 구성 요소

트레이스 · 스팬 · 루트 스팬 · 트레이스 ID · 스팬 ID · 부모 스팬 · 스팬 속성 · 스팬 이벤트

요청 번호를 서비스 사이로 넘기는 수단

컨텍스트 전파 · Trace Context · traceparent · 배기지 · B3 전파 · 상관 ID

분산 추적과 나란히 쓰이는 관측 신호

로그 · 메트릭 · 텔레메트리 · 프로파일링 · 로그 집계

분산 추적을 굴리는 처리 단계

계측 · 자동 계측 · 샘플링 · 헤드 샘플링 · 테일 샘플링 · 수집기 · 익스포터

분산 추적을 구현한 도구와 명세

OpenTelemetry · Jaeger · Zipkin · OpenTracing · OpenCensus · Dapper · APM

분산 추적이 속하는 상위 분류

관측성 · 모니터링 · 대시보드 · 사이트 신뢰성 공학

분산 추적으로 찾아내는 장애와 증상

지연 · 병목 · 연쇄 장애 · 타임아웃 · 부분 실패 · N+1 문제 · 꼬리 지연

분산 추적이 전제하는 시스템 구조

마이크로서비스 · 분산 시스템 · API 게이트웨이 · 메시지 큐 · 서비스 메시 · 원격 프로시저 호출

분산 추적이 재는 값을 받아 쓰는 지표

서비스 수준 지표 · 서비스 수준 목표 · 백분위 · 응답 시간

분산 추적과 이름이 헷갈리는 이웃

스택 트레이스 · 실행 추적 · 감사 로그 · strace

다른 이름: distributed tracing · 분산 트레이싱