MSA 장애 분석을 위한 Observability 구현

이크로서비스 아키텍처(MSA)로의 전환은 서비스의 확장성과 배포 유연성을 높였지만, 동시에 디버깅과 장애 분석의 난이도를 기하급수적으로 증가시켰습니다. 모놀리식 환경에서는 단일 로그 파일이나 스택 트레이스만으로 원인을 파악할 수 있었으나, 수십 개의 서비스가 상호작용하는 분산 환경에서는 단순한 상태 확인(Health Check)만으로는 부족합니다. 특정 요청이 어느 서비스 구간에서 지연되었는지, 데이터베이스 쿼리 문제인지 혹은 네트워크 병목인지 명확히 식별하기 어렵기 때문입니다. 본 글에서는 단순 모니터링을 넘어, 시스템의 내부 상태를 외부 출력을 통해 유추할 수 있는 Observability(관측 가능성)의 핵심 원칙과 구현 전략을 분석합니다.

1. Monitoring과 Observability의 공학적 차이

많은 엔지니어링 조직에서 모니터링과 Observability를 혼용하지만, 두 개념은 문제 해결의 접근 방식에서 근본적인 차이를 가집니다. 모니터링은 "알려진 미지(Known Unknowns)"를 다룹니다. 즉, CPU 사용률이 80%를 넘거나, 디스크 용량이 부족한 상황처럼 우리가 사전에 예측하고 정의한 실패 조건을 대시보드로 확인하는 행위입니다.

반면, Observability는 "알려지지 않은 미지(Unknown Unknowns)"를 해결하기 위한 시스템 속성입니다. 사전에 정의하지 않은 이상 징후가 발생했을 때, 시스템이 내뱉는 데이터를 근거로 "왜(Why)" 그런 현상이 발생했는지 역추적할 수 있는 능력을 의미합니다. 이는 시스템이 얼마나 관측 가능한 상태로 설계되었느냐에 따라 결정됩니다.

Control Theory Perspective: 제어 이론에서 Observability는 시스템의 외부 출력(Output)만으로 내부 상태(Internal State)를 얼마나 잘 추정할 수 있는지를 나타내는 수학적 척도입니다. IT 인프라에서도 이 정의는 동일하게 적용됩니다.

2. 3대 기둥(Three Pillars)과 상관관계 분석

Observability를 달성하기 위해서는 메트릭(Metrics), 로그(Logs), 트레이스(Traces)라는 세 가지 데이터 유형이 유기적으로 연결되어야 합니다. 각각의 데이터는 독립적으로 존재할 때보다 상호 연관될 때 비로소 가치를 가집니다.

유형 특성 주요 용도 비용(Storage/Process)
Metrics 집계 가능한 수치 데이터 트렌드 분석, 경보(Alerting) 낮음 (일정함)
Logs 이산적인 이벤트 기록 상세 에러 내용 확인 높음 (데이터 양에 비례)
Traces 요청의 전파 경로 (Span) 서비스 간 지연 구간 식별 중간 (샘플링 의존)

High Cardinality 문제

메트릭 수집 시 가장 주의해야 할 점은 High Cardinality(높은 카디널리티) 문제입니다. 예를 들어, HTTP 상태 코드나 서버 호스트네임은 적절한 레이블(Label)이지만, user_idemail과 같이 무제한으로 증가할 수 있는 값을 메트릭의 레이블로 사용하면 시계열 데이터베이스(TSDB)의 인덱스 크기가 폭발적으로 증가하여 쿼리 성능을 저하시킵니다.

Anti-Pattern: Prometheus와 같은 TSDB에 path="/api/v1/user/12345"와 같이 동적 경로를 그대로 레이블로 저장하지 마십시오. 이는 path="/api/v1/user/{id}"로 정규화(Normalize)해야 합니다.

3. OpenTelemetry를 이용한 분산 추적 구현

과거에는 각 벤더(Vendor)마다 별도의 에이전트와 SDK를 사용했으나, 현재는 CNCF의 OpenTelemetry(OTel)가 사실상의 업계 표준으로 자리 잡았습니다. OTel은 벤더 중립적인 수집기(Collector)와 API를 제공하여 데이터 수집과 백엔드 저장소(Jaeger, Datadog, Prometheus 등)를 분리합니다.

분산 추적의 핵심은 Context Propagation(문맥 전파)입니다. 서비스 A가 서비스 B를 호출할 때, Trace IDSpan ID를 HTTP 헤더(W3C Trace Context 등)에 주입(Inject)하여 전달하고, 서비스 B는 이를 추출(Extract)하여 자신의 스팬(Span)을 연결해야 합니다.


// Go 언어에서의 Context Propagation 예시 (OpenTelemetry)

import (
    "context"
    "net/http"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/propagation"
)

func makeRequest(ctx context.Context, url string) (*http.Response, error) {
    client := &http.Client{}
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }

    // Global Propagator를 사용하여 현재 Context의 Trace 정보를 
    // HTTP 헤더에 주입(Inject)합니다.
    // 이는 수신 측에서 동일한 Trace ID를 유지하게 해줍니다.
    otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))

    return client.Do(req)
}

위 코드에서 Inject 메서드는 현재 활성화된 스팬의 컨텍스트 정보를 HTTP 헤더에 추가합니다. 이를 통해 다운스트림 서비스는 해당 요청이 어디서 시작되었는지 추적할 수 있으며, 전체 트랜잭션의 워터폴(Waterfall) 차트를 완성할 수 있습니다.

4. 샘플링 전략과 트레이드오프

모든 요청을 100% 추적하고 저장하는 것은 스토리지 비용과 네트워크 오버헤드 측면에서 비효율적입니다. 따라서 적절한 샘플링(Sampling) 전략을 수립해야 합니다.

  • Head-based Sampling: 트레이스 시작 시점에 수집 여부를 결정합니다. (예: 전체 트래픽의 5%만 수집). 구현이 간단하고 오버헤드가 적지만, 간헐적으로 발생하는 에러나 긴 지연 시간을 놓칠 수 있습니다.
  • Tail-based Sampling: 트레이스가 완료된 후, 전체 데이터를 분석하여 특정 조건(에러 발생, 지연 시간 2초 이상)을 만족하는 경우에만 저장합니다. "흥미로운" 데이터를 보존할 확률이 높지만, 모든 데이터를 수집기에 임시 버퍼링해야 하므로 메모리와 연산 리소스 비용이 높습니다.
Best Practice: 대규모 트래픽 환경에서는 기본적으로 Head-based Sampling을 적용하되, 치명적인 오류(Critical Error) 경로에 대해서는 별도의 강제 수집 로직을 추가하거나, 리소스가 허용하는 범위 내에서 Tail-based Sampling을 혼합하여 운영하는 것이 좋습니다.

결론 및 제언

Observability는 도구의 도입만으로 완성되지 않습니다. 개발 단계에서부터 의미 있는 로그를 구조화(Structured Logging)하여 남기고, 적절한 카디널리티의 메트릭을 정의하며, 분산 추적을 위한 컨텍스트 전파를 고려하는 엔지니어링 문화가 뒷받침되어야 합니다. 초기 구축 비용이 발생하지만, 장애 탐지 시간(MTTD)과 복구 시간(MTTR)을 획기적으로 단축시킴으로써 전체 시스템의 안정성과 운영 효율성을 보장하는 필수적인 투자입니다. OpenTelemetry 표준을 준수하여 벤더 종속성을 피하고, 서비스의 중요도에 따라 유연한 샘플링 전략을 적용하십시오.

OlderNewest

Post a Comment