홈으로 돌아가기

Go에서의 관측성: QueryTracer, metrics 및 OTel

이 기사는 Go에서의 관측성 도구를 비교합니다: QueryTracer, metrics 및 OpenTelemetry. 애플리케이션 차이점과 일반적인 오류를 설명합니다. 다양한 시나리오에 대한 코드 예제 및 스택 선택 권장 사항을 제공합니다.

QueryTracer vs OpenTelemetry: Go에서 관측성 도구 선택 방법
Advertisement 728x90

# Go에서의 관측성: QueryTracer, Metrics, OpenTelemetry 중 선택하기

Go 애플리케이션에 PostgreSQL을 통합할 때 개발자들은 종종 관측성 도구를 선택해야 하는 딜레마에 직면합니다. Metrics, QueryTracer, OpenTelemetry는 각각 다른 문제를 해결하지만, 쉽게 혼동되기 쉽습니다. 실제 사례(데코레이터와 트랜잭션 관련)를 바탕으로 각 도구를 언제 사용해야 하는지, 흔한 함정을 어떻게 피할지 분해해 보겠습니다.

관측성의 세 단계: Metrics, Tracing, Context

Metrics는 관측성의 기본 단계입니다. "무슨 일이 일어나고 있는가"라는 질문에 답합니다. 예를 들어, 배포 후 오류가 급증하거나 p99 엔드포인트 응답 시간이 50ms에서 800ms로 뛰는 경우입니다. Metrics 수집은 자원 비용이 적고 빠른 이상 징후 탐지를 가능하게 합니다. 그러나 Metrics는 맥락이 부족합니다: 쿼리가 느려졌다는 사실만 보여줄 뿐 이유는 설명하지 못합니다. 그 이유를 파악하려면 Tracing이 필요합니다.

QueryTracer는 pgx 드라이버가 제공하는 인터페이스로, 드라이버 수준에서 작업을 가로채는 역할을 합니다. 각 트랜잭션 단계(BEGIN, 쿼리 자체, COMMIT)에 대해 별도의 이벤트를 생성합니다. Metrics 수집 도구가 아니라 스팬 생성, SQL 로깅, Metrics 수집 등의 로직을 삽입할 수 있는 훅입니다. 중요한 점: QueryTracer 자체는 데이터를 전송하지 않습니다. 단지 데이터를 생성하는 메커니즘을 제공할 뿐입니다.

Google AdInline article slot

OpenTelemetry(OTel)는 분산 추적을 처리합니다. HTTP 요청 핸들러 수준에서 스팬이 생성되고 context.Context를 통해 스택 아래로 전달됩니다: 서비스, 리포지토리, 드라이버. 각 계층이 자식 스팬을 추가해 실행 트리를 형성합니다. 시각화 시스템(예: Jaeger)에서 전체 그림을 볼 수 있습니다: 어떤 특정 DB 쿼리가 느렸는지, 어떤 맥락에서 발생했는지. OTel은 지속적으로 실행되지만 오버헤드를 줄이기 위해 샘플링을 사용합니다: 예를 들어 요청의 10%만 추적합니다. 샘플링된 요청의 경우 전체 스팬 트리가 기록됩니다.

데코레이터 예시: 문제가 숨겨진 곳

일반적인 프로젝트에서 리포지토리 위에 Metrics 수집을 위한 데코레이터를 사용합니다. 구현 예시:

func WithDBMetrics(operation string, fn func() (T any, error)) (T, error) {
    start := time.Now()
    result, err := fn()
    seconds := time.Since(start).Seconds()

    metrics.ObserveDatabaseQueryDuration(operation, seconds)
    if err != nil {
        metrics.IncDatabaseErrors(operation)
        return result, CheckDBError(operation, err)
    }

    return result, err
}

단일 SQL 쿼리를 실행하는 메서드에서는 데코레이터가 정확하게 작동합니다: Metrics가 쿼리 실행 시간과 일치합니다. 그러나 트랜잭션이 포함된 메서드에서는 상황이 달라집니다. 다음 예를 보세요:

Google AdInline article slot
func (r *BookingRepo) CreateBooking(ctx context.Context, b *models.Booking) (int64, error) {
    const operation = "create_booking"

    return WithDBMetrics(operation, func() (int64, error) {
        if err := r.validateBooking(b); err != nil {
            return 0, err
        }

        tx, err := r.db.BeginTxx(ctx, nil)
        if err != nil {
            return 0, err
        }
        defer func() { _ = tx.Rollback() }()

        var id int64
        err = tx.QueryRowContext(ctx, createBookingAtomicQuery, ...).Scan(&id)

        if err != nil {
            if errors.Is(err, sql.ErrNoRows) {
                return 0, models.ErrSlotOccupied
            }
            return 0, err
        }

        if err = tx.Commit(); err != nil {
            return 0, err
        }

        return id, nil
    })
}

데코레이터는 검증, 트랜잭션 시작, 쿼리 실행, 커밋을 포함한 전체 블록의 시간을 측정합니다. create_booking = 45ms Metrics는 어느 단계가 40ms를 차지하는지 밝히지 못합니다. 반면 QueryTracer는 트랜잭션을 세 개의 별도 이벤트로 분해합니다:

  • BEGIN = 0.1ms
  • INSERT = 40ms
  • COMMIT = 2ms

이러한 세밀함은 성능 병목 현상을 진단하는 데 핵심입니다.

QueryTracer 구현: 인터페이스와 함정

QueryTracer는 pgx의 인터페이스들로 구성되며, 각 작업 유형에 맞는 인터페이스가 있습니다. 예를 들어 Batch 작업에는 BatchTracer가 사용됩니다:

Google AdInline article slot
type BatchTracer interface {
    TraceBatchStart(ctx context.Context, conn *Conn, data TraceBatchStartData) context.Context
    TraceBatchQuery(ctx context.Context, conn *Conn, data TraceBatchQueryData)
    TraceBatchEnd(ctx context.Context, conn *Conn, data TraceBatchEndData)
}

구현을 위해 이러한 인터페이스를 만족하는 구조체를 만들어야 합니다:

type MyTracer struct{}

func (t *MyTracer) TraceQueryStart(ctx context.Context, conn *Conn, data TraceQueryStartData) context.Context {
    // Query 시작: 스팬 생성 또는 로깅
    return ctx
}

func (t *MyTracer) TraceQueryEnd(ctx context.Context, conn *Conn, data TraceQueryEndData) {
    // Query 종료: 시간 기록, Metrics 전송
}

// Config에 연결
config.ConnConfig.Tracer = &MyTracer{}

주요 함정:

  • BatchTracer를 구현하지 않으면 Batch 작업이 무시됩니다. 오류는 발생하지 않지만 데이터가 손실됩니다.
  • OTel 통합 시 QueryTracer 메서드 안에서 스팬을 수동으로 생성하고 context를 올바르게 전달해야 합니다.
  • QueryTracer는 Metrics를 대체하지 않습니다: 집계 로직이 추가로 필요합니다.

스택 비교: sqlx, pgx, 커스텀 솔루션

도구 선택은 현재 스택과 세부 요구사항에 따라 다릅니다. 세 가지 옵션:

  • sqlx + otelsql

- 설정: 한 줄 코드.

- 장점: 드라이버 변경 불필요, 기본 추적 지원.

- 단점: Batch나 CopyFrom 미지원, 커스텀 로직 불가.

- 권장: 복잡한 DB 작업이 없는 간단한 프로젝트에 적합.

  • pgx + otelpgx

- 설정: 한 줄 코드.

- 장점: Batch와 CopyFrom 완전 지원, 자동 스팬 생성.

- 단점: sqlx에서 pgx로 전환 필요.

- 권장: 신규 프로젝트나 마이그레이션에 최적.

  • pgx + MyTracer

- 설정: 인터페이스 수동 구현.

- 장점: 완전 제어(커스텀 Metrics, 고급 로깅).

- 단점: 높은 복잡도, 유지보수 필요.

- 권장: otelpgx로 커버되지 않는 특정 요구사항에만.

중요: 모든 경우에 context.Context가 핸들러부터 드라이버까지 모든 앱 계층을 통해 전달되어야 합니다. 그렇지 않으면 스팬이 루트 수준이 되어 원본 사용자 요청과의 연결이 끊어집니다.

주요 요점

  • Metrics는 문제를 보여주지만 원인을 밝히지 못합니다. 모니터링에 사용하고 깊이 있는 진단에는 사용하지 마세요.
  • QueryTracer는 추적 훅이며 Metrics용이 아닙니다. 트랜잭션을 단계별로 분해해 상세 분석을 가능하게 합니다.
  • OpenTelemetry는 적절한 context 전파가 필요합니다. context.Context가 모든 앱 계층을 흐르도록 하세요.
  • 데코레이터는 간단한 Metrics에는 좋지만 트랜잭션에는 부적합합니다. 트랜잭션 내 진단에는 QueryTracer나 OTel을 사용하세요.
  • 스택 선택: 복잡한 작업이 없고 sqlx를 사용 중이라면 otelsql 유지. 완전한 가시성을 위해 pgx + otelpgx로 전환하세요.

— Editorial Team

Advertisement 728x90

다음 읽기