Observability/Observability

EP07 [시리즈 1: Observability의 개념과 방향성] #7 Observability의 핵심: 상관관계의 중요성과 실제 구현 방법

ygtoken 2025. 3. 19. 13:22
728x90

이번 글에서는 프로메테우스와 그라파나를 활용한 Observability 구성 시리즈의 일곱 번째 포스트로, Observability의 진정한 핵심인 '상관관계(Correlation)'의 중요성과 실제 구현 방법에 대해 알아보겠습니다.


📌 상관관계의 중요성

지금까지 살펴본 Observability의 세 가지 핵심 요소(메트릭, 로그, 트레이스)는 각각 중요한 정보를 제공하지만, 이들을 서로 연결하지 못한다면 복잡한 시스템에서 발생하는 문제를 효과적으로 분석하기 어렵습니다. 상관관계는 이러한 데이터 간의 연결고리를 만들어 총체적인 시스템 이해를 가능하게 합니다.

✅ 상관관계란 무엇인가?

Observability 맥락에서의 상관관계는 서로 다른 데이터 소스(메트릭, 로그, 트레이스)의 정보를 연결하여 단일 이벤트나 문제의 전체적인 그림을 구성하는 능력을 의미합니다.

▶️ 상관관계의 예시:

  • 특정 시간대에 CPU 사용률 급증(메트릭)과 관련된 오류 로그(로그) 연결
  • 지연 시간이 높은 API 요청(메트릭)과 해당 요청의 전체 경로(트레이스) 연결
  • 데이터베이스 쿼리 오류(로그)와 해당 쿼리를 발생시킨 사용자 요청(트레이스) 연결

데이터 간의 상관관계

 

✅ 왜 상관관계가 중요한가?

복잡한 분산 시스템에서 문제를 효과적으로 진단하고 해결하기 위해서는 각 데이터 소스를 독립적으로 분석하는 것보다 상관관계를 통해 통합된 관점에서 분석하는 것이 필수적입니다.

▶️ 상관관계의 주요 이점:

  • 근본 원인 분석 가속화: 증상(메트릭)에서 원인(로그, 트레이스)으로 빠르게 이동
  • 컨텍스트 유지: 단편적인 정보가 아닌 전체적인 맥락에서 문제 이해
  • 패턴 인식 강화: 시간, 서비스, 사용자 등 다양한 차원에서 패턴 발견
  • 선제적 문제 해결: 초기 징후를 종합적으로 분석하여 심각한 장애 예방
  • 협업 향상: 개발, 운영, 보안 등 다양한 팀이 동일한 데이터 기반으로 소통
# 상관관계 없이 각 도구에서 개별적으로 문제를 추적하는 경우:
1. 메트릭 대시보드에서 API 서버의 메모리 사용량 증가 감지
2. 로그 시스템에서 "메모리 부족" 오류 메시지 검색
3. 분산 추적 시스템에서 최근 요청들을 수동으로 확인
4. 세 가지 데이터 간의 관계를 수동으로 분석하여 패턴 찾기

# 상관관계가 구현된 경우:
1. 메트릭 대시보드에서 API 서버의 메모리 사용량 증가 감지
2. 연결된 로그와 트레이스 데이터로 직접 이동 (클릭 한 번)
3. 메모리 사용량 증가와 연관된 특정 요청 패턴 즉시 확인
4. 근본 원인(특정 API 엔드포인트의 메모리 누수)을 빠르게 식별

📌 상관관계를 위한 키 식별자

효과적인 상관관계를 구현하기 위해서는 여러 데이터 소스를 연결할 수 있는 공통 식별자가 필요합니다. 이러한 식별자는 다양한 시스템 간에 일관되게 전파되어야 합니다.

✅ 주요 상관관계 식별자

▶️ 트레이스 ID (Trace ID):

  • 정의: 단일 요청의 전체 경로를 식별하는 고유 ID
  • 사용: 분산 시스템의 여러 서비스에 걸친 요청 추적
  • 전파 방식: HTTP 헤더, 메시지 속성, 컨텍스트 객체 등
# HTTP 요청 헤더에 포함된 트레이스 ID 예시
GET /api/users/123 HTTP/1.1
Host: example.com
X-Trace-ID: 4bf92f3577b34da6a3ce929d0e0e4736
X-Span-ID: 00f067aa0ba902b7

▶️ 요청 ID (Request ID):

  • 정의: 단일 요청을 식별하는 고유 ID (트레이스 ID와 유사하나 더 단순한 형태)
  • 사용: API 게이트웨이나 웹 서버에서 생성하여 백엔드 서비스로 전달
  • 전파 방식: HTTP 헤더, 로그 필드, 메트릭 레이블
// Java 서블릿 필터에서 요청 ID 생성 예시
@Component
public class RequestIdFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                   FilterChain filterChain) throws ServletException, IOException {
        String requestId = request.getHeader("X-Request-ID");
        
        // 요청 ID가 없으면 생성
        if (requestId == null || requestId.isEmpty()) {
            requestId = UUID.randomUUID().toString();
        }
        
        // 응답 헤더에 요청 ID 추가
        response.setHeader("X-Request-ID", requestId);
        
        // MDC에 요청 ID 추가 (로깅용)
        MDC.put("requestId", requestId);
        
        try {
            filterChain.doFilter(request, response);
        } finally {
            // MDC 정리
            MDC.remove("requestId");
        }
    }
}

▶️ 세션 ID (Session ID):

  • 정의: 사용자 세션을 식별하는 고유 ID
  • 사용: 사용자 활동 추적, 세션 기반 문제 분석
  • 전파 방식: 쿠키, 인증 토큰, 로그 컨텍스트

▶️ 사용자 ID (User ID):

  • 정의: 특정 사용자를 식별하는 ID
  • 사용: 사용자별 경험 모니터링, 사용자 관련 문제 분석
  • 전파 방식: 인증 토큰, 로그 필드, 메트릭 레이블

▶️ 배포 또는 버전 ID:

  • 정의: 애플리케이션 배포나 버전을 식별하는 ID
  • 사용: 특정 배포나 버전과 관련된 문제 분석
  • 전파 방식: 환경 변수, 로그 필드, 메트릭 레이블

상관관계를 위한 키 식별자 정리

✅ W3C Trace Context 표준

분산 시스템에서 상관관계 정보를 일관되게 전파하기 위해 W3C에서는 Trace Context 표준을 제정했습니다. 이 표준은 벤더 중립적인 방식으로 트레이싱 정보를 전달하는 방법을 정의합니다.

▶️ W3C Trace Context의 주요 구성 요소:

  • traceparent: 트레이스 ID, 스팬 ID, 샘플링 플래그 등의 주요 추적 정보를 포함
  • tracestate: 벤더별 추가 정보를 포함할 수 있는 확장 필드
# W3C Trace Context 헤더 예시
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7
  • traceparent 형식: 버전-트레이스ID-부모스팬ID-트레이스플래그
    • 버전: 00 (현재 버전)
    • 트레이스 ID: 32자리 16진수 (128비트)
    • 부모 스팬 ID: 16자리 16진수 (64비트)
    • 트레이스 플래그: 8비트 플래그 (01: 샘플링됨, 00: 샘플링되지 않음)

📌 상관관계 구현 방법

이제 실제로 상관관계를 구현하는 방법에 대해 살펴보겠습니다. 주요 단계와 구현 패턴을 알아보겠습니다.

✅ 상관관계 ID 생성 및 전파

▶️ 상관관계 ID 생성:

  • 시기: 외부 요청이 시스템에 처음 도착할 때 (예: API 게이트웨이, 로드 밸런서)
  • 방법: UUID, ULID 등의 고유 식별자 생성
  • 주의 사항: 충분한 고유성 보장, 가독성과 디버깅 편의성 고려
// Go 언어에서 트레이스 ID 생성 예시
import (
    "github.com/google/uuid"
    "net/http"
)

func TracingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 기존 트레이스 ID 확인
        traceID := r.Header.Get("X-Trace-ID")
        if traceID == "" {
            // 없으면 새로 생성
            traceID = uuid.New().String()
        }
        
        // 요청 및 응답 헤더에 트레이스 ID 설정
        r.Header.Set("X-Trace-ID", traceID)
        w.Header().Set("X-Trace-ID", traceID)
        
        // 컨텍스트에 트레이스 ID 추가
        ctx := context.WithValue(r.Context(), "traceID", traceID)
        
        // 다음 핸들러로 전달
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

▶️ 서비스 간 전파:

  • 동기 통신: HTTP 헤더, gRPC 메타데이터
  • 비동기 통신: 메시지 속성, 이벤트 메타데이터
  • 배치 작업: 작업 컨텍스트, 메타데이터 파일
// Spring Boot RestTemplate에서 트레이스 ID 전파 예시
@Component
public class TracingRestTemplateInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, 
                                      ClientHttpRequestExecution execution) throws IOException {
        // 현재 스레드의 MDC에서 트레이스 ID 가져오기
        String traceId = MDC.get("traceId");
        
        // 요청 헤더에 트레이스 ID 추가
        request.getHeaders().add("X-Trace-ID", traceId);
        request.getHeaders().add("traceparent", "00-" + traceId + "-" + generateSpanId() + "-01");
        
        return execution.execute(request, body);
    }
    
    private String generateSpanId() {
        // 16자리 16진수 스팬 ID 생성
        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }
}

✅ 로깅 시스템에 상관관계 정보 포함

로그 메시지에 상관관계 ID를 포함하면 동일한 요청과 관련된 모든 로그를 쉽게 검색하고 분석할 수 있습니다.

▶️ 구조화된 로깅:

  • JSON 형식의 로그에 상관관계 ID를 표준 필드로 포함
  • MDC(Mapped Diagnostic Context)를 사용하여 로그 패턴에 자동으로 포함
<!-- Logback 구성 예시 (logback.xml) -->
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <!-- MDC 값을 로그에 포함 -->
        <includeMdc>true</includeMdc>
        <!-- 추가 필드 -->
        <customFields>{"application":"user-service","environment":"production"}</customFields>
    </encoder>
</appender>

<root level="INFO">
    <appender-ref ref="JSON" />
</root>
// Java 로깅 예시
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
    
    public User findUser(String userId) {
        // 상관관계 ID는 이미 필터나 인터셉터에서 MDC에 설정되어 있다고 가정
        String traceId = MDC.get("traceId");
        String requestId = MDC.get("requestId");
        
        logger.info("사용자 조회 시작: userId={}", userId);
        // 로그 출력 예시:
        // {"timestamp":"2023-03-15T14:22:33.456Z","level":"INFO","logger":"UserService",
        //  "message":"사용자 조회 시작: userId=user123","traceId":"abc-123","requestId":"req-456",
        //  "application":"user-service","environment":"production"}
        
        try {
            return userRepository.findById(userId);
        } catch (Exception e) {
            // 오류 로그에도 동일한 상관관계 ID가 포함됨
            logger.error("사용자 조회 중 오류 발생: userId={}", userId, e);
            throw e;
        }
    }
}

✅ 메트릭에 상관관계 레이블 추가

메트릭에 상관관계 정보를 레이블로 추가하면 특정 요청, 사용자, 배포 등과 관련된 메트릭을 필터링하고 분석할 수 있습니다.

▶️ 상관관계 레이블 추가:

  • 주의 사항: 카디널리티가 높은 레이블(트레이스 ID, 요청 ID 등)은 제한적으로 사용
  • 적절한 레이블: 서비스, 엔드포인트, 환경, 배포 버전 등
// Micrometer + Spring Boot에서 메트릭에 레이블 추가 예시
@Component
public class WebMvcMetricsFilter extends OncePerRequestFilter {
    private final MeterRegistry meterRegistry;
    
    public WebMvcMetricsFilter(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                   FilterChain filterChain) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        
        try {
            filterChain.doFilter(request, response);
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            
            // 메트릭에 레이블 추가 (상관관계 정보 포함)
            // 요청 ID와 같은 고카디널리티 레이블은 피함
            Timer.builder("http.server.requests")
                .tag("method", request.getMethod())
                .tag("uri", getUriPattern(request))
                .tag("status", String.valueOf(response.getStatus()))
                .tag("service", "user-service")  // 서비스 식별
                .tag("version", getApplicationVersion())  // 배포 버전
                .register(meterRegistry)
                .record(duration, TimeUnit.MILLISECONDS);
        }
    }
    
    private String getUriPattern(HttpServletRequest request) {
        // URI 템플릿 패턴 반환 (예: /users/{id})
        // ...
    }
    
    private String getApplicationVersion() {
        // 애플리케이션 버전 정보 반환
        // ...
    }
}

✅ 트레이스와 로그, 메트릭 연결

분산 추적 시스템에서 트레이스와 로그, 메트릭을 연결하면 완전한 상관관계를 구현할 수 있습니다.

▶️ 트레이스와 로그 연결:

  • 로그에 트레이스 ID와 스팬 ID 포함
  • 트레이스 스팬에 관련 로그 이벤트 연결
// OpenTelemetry Java SDK에서 트레이스와 로그 연결 예시
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;

// 트레이서 및 로거 설정
Tracer tracer = openTelemetry.getTracer("my-service");
Logger logger = LoggerFactory.getLogger(MyService.class);

// 요청 처리
@Override
public void processRequest(HttpServletRequest request) {
    // 트레이스 컨텍스트 추출
    Context extractedContext = textMapPropagator.extract(Context.current(), request, getter);
    Span span = tracer.spanBuilder("processRequest")
            .setParent(extractedContext)
            .startSpan();
    
    try (Scope scope = span.makeCurrent()) {
        // 현재 스팬 정보를 MDC에 추가하여 로그에 포함되도록 함
        MDC.put("traceId", span.getSpanContext().getTraceId());
        MDC.put("spanId", span.getSpanContext().getSpanId());
        
        // 로그와 스팬 이벤트를 모두 기록
        logger.info("요청 처리 시작: {}", request.getRequestURI());
        span.addEvent("요청 처리 시작", Attributes.of(AttributeKey.stringKey("uri"), request.getRequestURI()));
        
        // 비즈니스 로직 처리
        processBusinessLogic(request);
        
        // 처리 완료 로그
        logger.info("요청 처리 완료");
        span.setStatus(StatusCode.OK);
    } catch (Exception e) {
        // 오류 발생 시 로그와 스팬에 모두 기록
        logger.error("요청 처리 중 오류 발생", e);
        span.recordException(e);
        span.setStatus(StatusCode.ERROR, e.getMessage());
        throw e;
    } finally {
        // MDC 정리
        MDC.remove("traceId");
        MDC.remove("spanId");
        span.end();
    }
}

▶️ 트레이스와 메트릭 연결:

  • 트레이스 ID로 메트릭 필터링 (제한적으로 사용)
  • 메트릭 데이터에서 관련 트레이스로 이동할 수 있는 링크 제공
// Prometheus 클라이언트 라이브러리와 OpenTelemetry 통합 예시 (Go)
import (
    "github.com/prometheus/client_golang/prometheus"
    "go.opentelemetry.io/otel/trace"
)

// 요청 처리 시간 히스토그램 메트릭 정의
var requestDuration = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "HTTP 요청 처리 시간 히스토그램",
        Buckets: []float64{0.1, 0.3, 0.5, 1, 3, 5},
    },
    []string{"method", "endpoint", "status", "service", "version"},
)

func HandleRequest(w http.ResponseWriter, r *http.Request) {
    startTime := time.Now()
    
    // 트레이스 컨텍스트 추출
    ctx := r.Context()
    span := trace.SpanFromContext(ctx)
    
    // 요청 처리
    // ...
    
    // 메트릭 기록 (상관관계 레이블 포함)
    duration := time.Since(startTime).Seconds()
    requestDuration.WithLabelValues(
        r.Method,
        r.URL.Path,
        strconv.Itoa(statusCode),
        "user-service",
        "v1.0.0",
    ).Observe(duration)
    
    // 트레이스 이벤트에 메트릭 정보 추가
    span.AddEvent("metric_recorded", trace.WithAttributes(
        attribute.String("metric_name", "http_request_duration_seconds"),
        attribute.Float64("duration", duration),
    ))
}

📌 상관관계 시각화 및 분석

상관관계 정보를 수집한 후에는 이를 효과적으로 시각화하고 분석하는 도구와 방법이 필요합니다.

✅ 통합 대시보드

그라파나와 같은 도구를 사용하여 메트릭, 로그, 트레이스 데이터를 통합한 대시보드를 구성할 수 있습니다.

▶️ 그라파나 통합 대시보드 구성:

  • 데이터 소스 통합: Prometheus(메트릭), Loki(로그), Tempo(트레이스) 연결
  • 데이터 소스 간 연결: 상관관계 필드를 사용한 데이터 소스 간 이동
  • 상황별 대시보드: 서비스, 엔드포인트, 사용자별 대시보드 구성
# 그라파나 대시보드 예시 (JSON)
{
  "panels": [
    {
      "title": "HTTP 요청 속도",
      "datasource": "Prometheus",
      "targets": [
        {
          "expr": "sum(rate(http_requests_total{service=\"user-service\"}[5m])) by (endpoint)",
          "legendFormat": "{{endpoint}}"
        }
      ],
      "links": [
        {
          "title": "관련 로그 보기",
          "url": "/explore?left=%7B\"datasource\":\"Loki\",\"queries\":%5B%7B\"expr\":\"{service=\\\"user-service\\\"}\",%7D%5D%7D",
          "targetBlank": true
        }
      ]
    },
    {
      "title": "오류 로그",
      "datasource": "Loki",
      "targets": [
        {
          "expr": "{service=\"user-service\"} |= \"ERROR\"",
          "refId": "A"
        }
      ],
      "options": {
        "showTime": true,
        "showLabels": false,
        "showCommonLabels": true,
        "wrapLogMessage": true,
        "prettifyLogMessage": true,
        "enableLogDetails": true,
        "dedupStrategy": "none"
      },
      "transformations": [
        {
          "id": "organize",
          "options": {
            "excludeByName": {
              "host": true,
              "id": true
            },
            "renameByName": {}
          }
        }
      ]
    }
  ]
}

통합 대시보드 예시

✅ 시각화 전략

▶️ 시간 기반 상관관계:

  • 동일한 시간대의 이벤트 함께 표시: 메트릭 스파이크, 오류 로그, 느린 트레이스를 단일 타임라인에 표시
  • 시간 동기화: 모든 데이터 소스의 시간을 정확히 동기화하여 정확한 상관관계 분석
  • 범위 선택 연동: 한 패널에서 시간 범위를 선택하면 다른 패널도 자동으로 해당 범위로 필터링

▶️ 컨텍스트 전환 링크:

  • 메트릭에서 로그로: 특정 메트릭 데이터 포인트에서 관련 로그로 이동할 수 있는 링크 제공
  • 로그에서 트레이스로: 로그 항목에서 관련 트레이스로 이동할 수 있는 링크 제공
  • 트레이스에서 메트릭으로: 트레이스 세부 정보에서 관련 메트릭 대시보드로 이동할 수 있는 링크 제공

▶️ 종합 검색 및 필터링:

  • 통합 검색: 트레이스 ID, 요청 ID, 사용자 ID 등으로 모든 데이터 소스를 한 번에 검색
  • 공통 필터: 서비스, 환경, 버전 등 공통 필터를 적용하여 모든 패널에 일관되게 반영
  • 드릴다운 필터링: 상위 수준 뷰에서 클릭하여 더 상세한 정보로 드릴다운

📌 상관관계 구현의 모범 사례 및 주의사항

✅ 모범 사례

▶️ 일관된 식별자 사용:

  • 표준화된 식별자: W3C Trace Context와 같은 업계 표준을 적극 활용
  • 일관된 형식: 모든 서비스와 시스템에서 동일한 형식의 식별자 사용
  • 선택적 세분화: 필요한 수준에 따라 다양한 식별자(트레이스 ID, 요청 ID, 사용자 ID 등) 활용

▶️ 효율적인 구현:

  • 자동화된 전파: 상관관계 ID의 자동 생성 및 전파를 위한 미들웨어, 인터셉터, 라이브러리 활용
  • 프레임워크 활용: OpenTelemetry, Spring Cloud Sleuth 등 기존 프레임워크 활용
  • 샘플링 전략: 고부하 시스템에서는 지능적인 샘플링으로 오버헤드 관리

▶️ 문서화 및 교육:

  • 개발자 가이드: 상관관계 ID 사용 및 전파에 대한 명확한 가이드라인 제공
  • 표준화된 접근법: 모든 팀이 일관된 방식으로 상관관계를 구현하도록 표준화
  • 실제 사례 공유: 상관관계를 통해 해결된 실제 문제 사례를 공유하여 가치 입증

✅ 주의사항

▶️ 성능 영향:

  • 로깅 오버헤드: 과도한 로깅과 상관관계 정보 추가로 인한 성능 저하 주의
  • 메트릭 카디널리티: 고유 식별자를 메트릭 레이블로 사용할 때 카디널리티 폭발 위험
  • 트레이싱 오버헤드: 100% 샘플링 대신 지능적인 샘플링으로 오버헤드 관리

▶️ 복잡성 관리:

  • 단계적 구현: 한 번에 모든 상관관계를 구현하기보다 단계적으로 접근
  • 필수 식별자 집중: 모든 식별자가 아닌 가장 중요한 식별자에 집중
  • 통합 도구 활용: 복잡성을 줄이기 위해 통합된 도구와 프레임워크 활용

▶️ 데이터 보존 정책:

  • 일관된 보존 기간: 상관관계 분석을 위해 모든 데이터 소스의 보존 기간 조정
  • 선택적 보존: 중요 이벤트와 관련된 데이터는 더 오래 보존
  • 규정 준수: 데이터 보존 정책 수립 시 개인정보 보호 규정 고려

📌 결론

Observability의 세 가지 핵심 요소(메트릭, 로그, 트레이스)는 각각 중요한 정보를 제공하지만, 이들 간의 상관관계를 구현할 때 진정한 가치를 발휘합니다. 상관관계를 통해 복잡한 분산 시스템에서 발생하는 문제의 근본 원인을 빠르게 파악하고, 효과적으로 해결할 수 있습니다.

효과적인 상관관계 구현을 위해서는:

  • 표준화된 식별자 사용
  • 자동화된 전파 메커니즘 구축
  • 통합된 시각화 및 분석 도구 활용
  • 성능과 복잡성의 균형 유지

이러한 접근법을 통해 Observability의 완전한 잠재력을 실현하고, 더 안정적이고 효율적인 시스템을 운영할 수 있습니다. 상관관계는 단순한 기술적 구현을 넘어, 시스템 문제 해결에 대한 사고방식의 변화를 의미합니다. 개별 데이터 포인트가 아닌, 시스템 전체의 맥락에서 문제를 바라보고 해결하는 능력이야말로 현대적인 Observability의 핵심입니다.

728x90