분산 추적의 핵심 개념
분산 추적은 클라이언트 요청이 여러 서비스(예: 프론트엔드, 백엔드, 데이터베이스)를 통해 흐르는 과정을 기록하고 분석하는 기술입니다. 이는 복잡한 마이크로서비스 아키텍처에서 성능 장애를 진단하거나 요청 경로를 이해하는 데 필수적인 도구입니다.
Quickwit는 구조화되지 않은 데이터를 인덱싱하고 검색하는 클라우드 네이티브 엔진으로, 트레이스 데이터 저장소로 매우 적합합니다. 특히 OpenTelemetry OTLP gRPC 및 HTTP(Protobuf만 지원), 그리고 Jaeger gRPC API(SpanReader만 지원)를 내장하고 있어, 외부 시스템과의 호환성이 뛰어납니다. 이를 통해 직접 트레이스 데이터를 저장하고, Jaeger UI에서 검색할 수 있습니다.
Quickwit와 Jaeger 통합 설정
Quickwit는 Jaeger UI와 호환되는 gRPC 스토리지 플러그인을 제공합니다. `grpc-plugin` 타입의 저장소로 설정하면, 이름이 `otel-traces-v0_*` 패턴에 맞는 인덱스에 저장된 트레이스 정보를 조회할 수 있습니다.
트레이스 데이터 전송 방식
Quickwit 서버 실행
OTLP 수신 기능을 활성화하여 실행합니다:
QW_ENABLE_OPENTELEMETRY_OTLP_EXPORTER=true \
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:7281 \
./quickwit run
이 설정은 자체 트레이스 데이터를 자동으로 보낼 수 있도록 합니다.
Jaeger UI 시작
Docker를 이용해 Jaeger 인스턴스를 구동하며, 스토리지로 Quickwit를 지정해야 합니다.
macOS / Windows
docker run --rm --name jaeger-qw \
-e SPAN_STORAGE_TYPE=grpc-plugin \
-e GRPC_STORAGE_SERVER=host.docker.internal:7281 \
-p 16686:16686 \
jaegertracing/jaeger-query:latest
Linux
docker run --rm --name jaeger-qw --network=host \
-e SPAN_STORAGE_TYPE=grpc-plugin \
-e GRPC_STORAGE_SERVER=127.0.0.1:7281 \
-p 16686:16686 \
jaegertracing/jaeger-query:latest
Jaeger UI에서 트레이스 확인
Quickwit가 자체 트레이스를 인덱싱하므로, 약 5초 후에는 웹 인터페이스에서 결과를 확인할 수 있습니다. 다음은 내부 동작 흐름입니다:
find_traces: UI 검색 요청 처리find_trace_ids: 고유 트레이스 ID 추출root_search: 전체 인덱스에 대한 병렬 검색 시작leaf_search: 각 파티션에서의 검색 수행leaf_search_single_split: 단일 파트에서의 검색 실행warmup: 실제 검색을 위한 데이터 사전 로딩tantivy_search: Tantivy 엔진을 이용한 빠른 검색 수행
OpenTelemetry Collector 연동
기존 수집기를 사용 중이라면, `config.yaml` 파일에 새로운 OTLP gRPC 내보내기 설정을 추가하세요.
macOS / Windows
exporters:
otlp/quickwit:
endpoint: host.docker.internal:7281
tls:
insecure: true
# 기본 인덱스: otel-traces-v0_7
# 커스텀 인덱스 지정 가능 (주석 해제)
# headers:
# qw-otel-traces-index: otel-traces-v0_8
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/quickwit]
Linux
exporters:
otlp/quickwit:
endpoint: 127.0.0.1:7281
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/quickwit]
설정 테스트
- Quickwit 서버 시작:
./quickwit run - Collector 실행 (macOS/Windows):
docker run -v ${PWD}/otel-collector-config.yaml:/etc/otelcol/config.yaml -p 4317:4317 -p 4318:4318 -p 7281:7281 otel/opentelemetry-collector - Collector에 트레이스 전송:
curl -XPOST "http://localhost:4318/v1/traces" -H "Content-Type: application/json" \ --data-binary @- << EOF { "resource_spans": [ { "resource": { "attributes": [ { "key": "service.name", "value": { "string_value": "test-with-curl" } } ] }, "scope_spans": [ { "scope": { "name": "manual-test" }, "spans": [ { "time_unix_nano": "1678974011000000000", "observed_time_unix_nano": "1678974011000000000", "start_time_unix_nano": "1678974011000000000", "end_time_unix_nano": "1678974021000000000", "trace_id": "3c191d03fa8be0653c191d03fa8be065", "span_id": "3c191d03fa8be065", "kind": 2, "events": [], "status": { "code": 1 } } ] } ] } ] } EOF
Quickwit 로그에서 다음과 같은 메시지를 확인할 수 있습니다:
2023-03-16T13:44:09.369Z INFO quickwit_indexing::actors::indexer: new-split split_id="01GVNAKT5TQW0T2QGA245XCMTJ" partition_id=6444214793425557444
이는 새 트레이스가 수신되었고, 새로운 파티션 생성이 완료되었음을 의미합니다. 검색 전에 인덱스 발행을 기다려야 합니다.
Python Flask 애플리케이션에 트레이싱 적용
Flask 기반 애플리케이션에 OpenTelemetry를 통합하여 트레이스를 생성하고, 그 결과를 Quickwit로 전송하는 예제입니다.
사전 준비
- Python 3 설치
- Docker 설치
Quickwit 실행
./quickwit run
Jaeger UI 실행
OS별 네트워킹 설정을 반영하여 실행합니다.
macOS / Windows
docker run --rm --name jaeger-qw \
-e SPAN_STORAGE_TYPE=grpc-plugin \
-e GRPC_STORAGE_SERVER=host.docker.internal:7281 \
-p 16686:16686 \
jaegertracing/jaeger-query:latest
Linux
docker run --rm --name jaeger-qw --network=host \
-e SPAN_STORAGE_TYPE=grpc-plugin \
-e GRPC_STORAGE_SERVER=127.0.0.1:7281 \
-p 16686:16686 \
jaegertracing/jaeger-query:latest
간단한 Flask 앱 구현
HTTP 요청 `/process-ip`에 대해 외부 IP 조회 → 파싱 → 출력 작업을 수행합니다.
import random
import time
import requests
from flask import Flask
app = Flask(__name__)
@app.route("/process-ip")
def process_ip():
body = fetch()
ip = parse(body)
display(ip)
return ip
def fetch():
resp = requests.get('https://httpbin.org/ip')
body = resp.json()
return body
def parse(body):
secs = random.randint(1, 100) / 1000
time.sleep(secs)
return body["origin"]
def display(ip):
secs = random.randint(1, 100) / 1000
time.sleep(secs)
message = f"Your IP address is `{ip}`."
print(message)
if __name__ == "__main__":
app.run(port=5000)
자동 인스트루먼트
OpenTelemetry의 opentelemetry-bootstrap를 사용하면 코드 수정 없이도 자동으로 추적이 가능합니다.
opentelemetry-bootstrap -a install
실행 명령어:
OTEL_METRICS_EXPORTER=none \
OTEL_TRACES_EXPORTER=console \
OTEL_SERVICE_NAME=my_app \
python my_app.py
수동 인스트루먼트
각 단계별로 세부적인 메트릭을 포함하도록 개선합니다.
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@app.route("/process-ip")
@tracer.start_as_current_span("process_ip")
def process_ip():
body = fetch()
ip = parse(body)
display(ip)
return ip
@tracer.start_as_current_span("fetch")
def fetch():
resp = requests.get('https://httpbin.org/ip')
body = resp.json()
current_span = trace.get_current_span()
current_span.set_attribute("status_code", resp.status_code)
current_span.set_attribute("content_type", resp.headers["Content-Type"])
current_span.set_attribute("content_length", resp.headers["Content-Length"])
return body
@tracer.start_as_current_span("parse")
def parse(body):
secs = random.randint(1, 100) / 1000
time.sleep(secs)
return body["origin"]
@tracer.start_as_current_span("display")
def display(ip):
secs = random.randint(1, 100) / 1000
time.sleep(secs)
message = f"Your IP address is `{ip}`."
print(message)
current_span = trace.get_current_span()
current_span.add_event(message)
실행:
OTEL_METRICS_EXPORTER=none \
OTEL_TRACES_EXPORTER=console \
OTEL_SERVICE_NAME=my_app \
opentelemetry-instrument python my_instrumented_app.py
Quickwit로 데이터 전송
OTLP 내보내기를 활성화하여 데이터를 전달합니다:
OTEL_METRICS_EXPORTER=none \
OTEL_SERVICE_NAME=my_app \
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:7281 \
opentelemetry-instrument python my_instrumented_app.py
약 30초 후, 인덱싱이 완료되면 http://localhost:16686에서 결과를 확인할 수 있습니다.
OpenTelemetry Collector를 거쳐 전송하기
기존 수집기를 사용한다면, 애플리케이션은 아래처럼 실행합니다:
OTEL_METRICS_EXPORTER=none \
OTEL_SERVICE_NAME=my_app \
opentelemetry-instrument python instrumented_app.py
데이터는 수집기 → Quickwit 순서로 전달됩니다.
결론
본 문서에서는 Python 기반 애플리케이션에 OpenTelemetry를 적용하고, 그 트레이스 데이터를 Quickwit에 저장하는 전반적인 프로세스를 설명했습니다. 또한, 이를 기반으로 한 분산 추적 시스템을 어떻게 구성할 수 있는지 보여주었습니다.
모든 샘플 코드는 공식 튜토리얼 저장소에서 확인할 수 있습니다:
OpenTelemetry OTLP 서비스
Quickwit는 기본적으로 OTLP(gRPC) 수신 엔드포인트를 제공하며, 컬렉터 또는 애플리케이션에서 직접 데이터를 전송할 수 있습니다. 이 엔드포인트는 기본적으로 활성화되어 있으며, 수신된 트레이스는 otel-trace-v0_7 인덱스에 자동 저장됩니다. 해당 인덱스가 없을 경우 자동 생성됩니다.
필요 시, 다음 두 가지 방법으로 비활성화 가능:
- 환경 변수:
QW_ENABLE_OTLP_ENDPOINT=false - 구성 파일:
indexer.enable_otlp_endpoint: false
사용자 정의 인덱스 지정
gRPC 요청 헤더 qw-otel-traces-index에 원하는 인덱스명을 설정하면, 해당 인덱스에 데이터를 저장할 수 있습니다.
트레이스 및 스팬 데이터 모델
OpenTelemetry 규격을 기반으로, 다음과 같은 필드 구조로 인덱싱됩니다:
version: 0.7
index_id: otel-trace-v0_7
doc_mapping:
mode: strict
field_mappings:
- name: trace_id
type: bytes
input_format: hex
output_format: hex
fast: true
- name: service_name
type: text
tokenizer: raw
fast: true
- name: span_name
type: text
tokenizer: raw
fast: true
- name: span_start_timestamp_nanos
type: datetime
input_formats: [unix_timestamp]
output_format: unix_timestamp_nanos
fast: true
fast_precision: milliseconds
- name: span_duration_millis
type: u64
indexed: false
fast: true
- name: span_attributes
type: json
tokenizer: raw
fast: true
- name: span_status
type: json
indexed: true
- name: events
type: array<json>
tokenizer: raw
fast: true
- name: links
type: array<json>
tokenizer: raw
timestamp_field: span_start_timestamp_nanos