Quickwit를 활용한 분산 추적 시스템 구성: 오픈테레메트리와 함께하는 실시간 모니터링

분산 추적의 핵심 개념

분산 추적은 클라이언트 요청이 여러 서비스(예: 프론트엔드, 백엔드, 데이터베이스)를 통해 흐르는 과정을 기록하고 분석하는 기술입니다. 이는 복잡한 마이크로서비스 아키텍처에서 성능 장애를 진단하거나 요청 경로를 이해하는 데 필수적인 도구입니다.

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]

설정 테스트

  1. Quickwit 서버 시작: ./quickwit run
  2. 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
  3. 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

태그: Quickwit OpenTelemetry Jaeger Distributed Tracing OTLP

7월 11일 05:29에 게시됨