Metrics
숫자와 추세로 장애의 규모, 시작 시점, 심각도를 감지한다.
에러율, p95/p99 지연, 처리량, 큐 적체, CPU/메모리 사용률을 볼 때분류: Layer 6 - 운영 심화: 관측성 & 복원력 | 선수지식: CloudWatch Basics
Logs, Metrics, Traces는 서비스가 실패하거나 느려질 때 각각 다른 질문에 답하는 관측성(Observability) 신호다.
짧게 말하면 Metrics는 감지하고, Logs는 설명하고, Traces는 위치를 좁힌다. 브라우저 Network 탭의 워터폴을 본 경험은 도움이 되지만, 여기서는 브라우저가 아니라 여러 서버와 의존성 사이의 요청 흐름을 본다.
CloudWatch Basics에서는 로그 그룹, 지표, 알람, 대시보드의 기본 사용법을 배웠다. 그 다음 문제는 “알람이 울렸다”에서 끝나지 않는다. 운영자는 바로 다음 질문에 답해야 한다.
단일 서버 시대에는 로그 파일을 열어 위에서부터 읽어도 어느 정도 원인을 찾을 수 있었다. 하지만 Queue, Worker, 캐시, 외부 API, 마이크로서비스가 섞이면 하나의 사용자 요청이 여러 프로세스를 지나간다. 이때 로그만 보면 너무 많고, 지표만 보면 너무 요약되어 있고, 추적만 보면 전체 건강 상태가 보이지 않는다.
그래서 세 신호가 나뉘었다. Metrics는 “지금 평소와 다르다”를 빠르게 알려준다. Logs는 “어떤 사건과 에러 문맥이 반복되는가”를 보여준다. Traces는 “한 요청이 어디서 시간을 쓰거나 실패했는가”를 연결한다.
관측성은 대시보드 수를 늘리는 일이 아니다. 서비스 내부를 직접 들여다볼 수 없을 때, 외부로 나온 신호를 조합해서 내부 상태를 추론할 수 있게 만드는 설계다.
flowchart TD UserImpact["사용자 영향: 느림, 실패, 타임아웃"] --> Detect["Metrics: 규모와 추세 감지"] Detect --> Explain["Logs: 반복되는 사건과 에러 문맥 설명"] Explain --> Localize["Traces: 요청 경로에서 병목 위치 국소화"] Localize --> Decision["운영 판단: 롤백, 완화, 확장, 의존성 격리"] Decision --> Verify["Metrics: 조치 후 정상화 확인"] Verify --> Detect
이 순서는 절대 규칙이 아니라 기본 루프다. 예를 들어 특정 Trace ID를 먼저 받은 장애 제보라면 Trace에서 시작할 수 있다. 하지만 최종 판단은 다시 Metrics로 돌아와야 한다. 한 요청의 Trace가 느렸다는 사실만으로 전체 장애라고 말할 수 없기 때문이다.
숫자와 추세로 장애의 규모, 시작 시점, 심각도를 감지한다.
에러율, p95/p99 지연, 처리량, 큐 적체, CPU/메모리 사용률을 볼 때개별 사건의 문맥을 남겨 어떤 에러가 어떤 조건에서 반복되는지 설명한다.
특정 errorType, orderId, correlationId, 사용자 영향 범위를 찾을 때한 요청의 서비스 호출, DB 쿼리, 외부 API 호출을 시간 순서로 묶어 병목 위치를 좁힌다.
API 서버, DB, 캐시, 외부 결제사 중 어느 구간이 느린지 확인할 때이 문서는 도구 이름보다 용어 경계를 먼저 잡아야 읽기 쉽다.
로그는 “어떤 일이 있었다”는 사건 기록이다.
좋은 로그는 사람에게 읽히는 문장만이 아니라 기계가 필터링할 수 있는 필드를 가진다.
예를 들어 payment_failed라는 이벤트 이름, orderId, durationMs, errorType이 함께 있으면 장애 시 같은 종류의 실패를 묶을 수 있다.
구조화 로그(Structured Log)는 로그 한 줄을 JSON 같은 구조로 출력하는 방식이다.
문자열 한 덩어리 대신 필드가 분리되어 있으므로 Log Insights, Elasticsearch, Loki 같은 도구가 level = "ERROR"나 orderId = "..." 조건으로 검색할 수 있다.
단, 비밀번호, 토큰, 주민번호 같은 민감 정보는 구조화하면 더 쉽게 퍼지므로 처음부터 제외해야 한다.
메트릭은 시간 축에 찍히는 숫자다.
request_count, error_rate, p99_latency_ms, queue_depth, cpu_utilization처럼 상태를 숫자로 압축한다.
로그가 사건의 문장이라면, 메트릭은 사건들을 집계한 계기판이다.
Trace는 하나의 요청 전체 경로다. Span은 Trace 안에서 나뉜 하나의 작업 단위다. 컨트롤러 처리, DB 쿼리, Redis 조회, 외부 API 호출이 각각 Span이 될 수 있다.
Trace: GET /orders/9821 요청 1건├── Span: API Gateway (3ms)├── Span: OrderService controller (12ms)├── Span: Redis GET order:9821 (2ms)├── Span: RDS SELECT orders (38ms)└── Span: Payment API status check (420ms)이 예시에서 총 지연이 크다면 Payment API Span을 먼저 의심한다. 반대로 RDS Span이 900ms라면 DB 쿼리나 커넥션 풀을 봐야 한다.
Trace ID는 분산 추적 시스템이 하나의 요청 전체를 묶기 위해 쓰는 식별자다.
W3C TraceContext 표준에서는 traceparent 헤더에 Trace ID와 Span ID를 담아 다음 서비스로 전달한다.
Correlation ID는 여러 로그 줄을 같은 업무 흐름으로 묶기 위해 애플리케이션이 붙이는 식별자다. Trace ID와 같은 값을 써도 되지만 항상 같은 개념은 아니다. 중요한 것은 로그에 Trace ID나 Correlation ID 중 하나가 남아 있어야 Metrics, Logs, Traces 사이를 오갈 수 있다는 점이다.
카디널리티(Cardinality)는 값의 종류 수다.
메트릭 레이블에 붙은 값 조합이 많아질수록 시계열 수가 늘어난다.
status=500은 값 종류가 적지만, userId=12345는 사용자 수만큼 값이 늘어난다.
Sampling은 모든 Trace를 저장하지 않고 일부만 고르는 방식이다. 트래픽이 많으면 Trace를 100% 저장하는 비용이 빠르게 커지기 때문이다. Head-based sampling은 요청 시작 시점에 저장 여부를 결정하고, tail-based sampling은 요청이 끝난 뒤 에러나 고지연 여부를 보고 결정한다.
OpenTelemetry(OTel)는 로그, 메트릭, 트레이스를 수집하고 내보내기 위한 벤더 중립 표준이다. Instrumentation(계측)은 코드와 런타임에 관측 신호를 심는 작업이다. Collector는 애플리케이션과 백엔드 사이에서 신호를 받아 배칭, 필터링, 샘플링, 라우팅을 수행하는 중간 프로세스다.
Application + OTel SDK -> OTLP(OpenTelemetry Protocol) -> OpenTelemetry Collector -> CloudWatch / X-Ray / Prometheus / Grafana / Jaeger / DatadogCollector를 두면 애플리케이션은 “신호를 보낸다”에 집중하고, 어디로 보낼지와 얼마나 줄일지는 Collector 설정으로 바꿀 수 있다.
아래 예시는 세 신호가 왜 함께 필요한지 보여준다.
상황은 온라인 주문 서비스의 POST /checkout API가 느려진 경우다.
대시보드에 다음 변화가 보인다.
| 지표 | 평소 | 현재 | 해석 |
|---|---|---|---|
| Request rate | 800 rpm | 820 rpm | 트래픽 폭증은 아니다 |
| 5xx error rate | 0.1% | 4.8% | 사용자 실패가 급증했다 |
| p95 latency | 420ms | 1.6s | 느린 요청이 넓게 퍼졌다 |
| p99 latency | 900ms | 4.2s | 최악 구간은 사용자 체감 크다 |
| Queue depth | 120 jobs | 130 jobs | Worker 적체는 주원인 아님 |
| DB connection usage | 45% | 48% | DB 풀 포화도 아님 |
여기서 Metrics가 알려주는 것은 원인이 아니라 우선순위다.
트래픽이 평소와 비슷한데 에러율과 tail latency가 오른다면 용량 부족보다 특정 의존성 실패를 의심한다.
p95와 p99의 정확한 의미는 7.2절에서 다시 보지만, 여기서는 “대부분의 요청”과 “가장 느린 꼬리 구간”을 나누어 보는 숫자로 읽으면 된다.
p99 = 4.2s는 100개 중 대략 1개가 4초대 이상으로 밀린다는 뜻이다.
분당 820건이라면 매분 약 8건이 심각하게 느릴 수 있다.
Log Insights나 로그 검색에서 에러 로그를 묶어 본다.
filter level = "ERROR" and event = "checkout_failed"| stats count(*) as cnt by errorType, dependency, bin(5m)| sort cnt desc반복 패턴이 다음처럼 나온다고 하자.
{ "timestamp": "2026-04-07T12:03:22.391Z", "level": "ERROR", "event": "checkout_failed", "orderId": "order-9821", "correlationId": "req-7f2a", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "dependency": "payment-gateway", "errorType": "TimeoutException", "durationMs": 3000}이 로그는 “결제 게이트웨이 timeout이 반복된다”는 원인 후보를 준다. 하지만 아직 결제 게이트웨이가 실제 병목인지, 우리 코드의 retry가 시간을 늘린 것인지, 네트워크 경계에서 막힌 것인지는 알 수 없다. 그래서 Trace로 넘어간다.
느린 Trace 하나를 보면 다음과 같다.
Trace: POST /checkout (4,230ms)├── Span: validate cart (18ms)├── Span: RDS SELECT cart_items (32ms)├── Span: reserve inventory (65ms)├── Span: payment-gateway authorize (3,000ms timeout)├── Span: retry payment-gateway authorize (1,000ms timeout)└── Span: write failure audit log (12ms)Trace는 결제 게이트웨이 호출과 retry가 총 지연의 대부분이라는 위치 정보를 준다. 이제 운영 판단은 더 구체적이다.
Metrics만 봤다면 “checkout이 느리다”까지만 알 수 있다. Logs만 봤다면 “TimeoutException이 많다”까지만 알 수 있다. Traces만 봤다면 “어떤 요청 하나가 결제 게이트웨이에서 느렸다”까지만 알 수 있다.
세 신호를 함께 보면 “트래픽 폭증이 아니라 결제 의존성 timeout이 checkout tail latency와 5xx를 만들고 있다”라고 말할 수 있다. 이 문장이 운영 의사결정의 최소 단위다.
로그는 가장 친숙하지만 가장 쉽게 망가지는 신호다. 무엇이든 로그로 남길 수 있기 때문에, 기준 없이 늘리면 비용과 노이즈가 같이 증가한다.
좋은 운영 로그는 다음 필드를 기본으로 가진다.
| 필드 | 의미 | 예시 |
|---|---|---|
timestamp | 사건 시각 | 2026-04-07T12:03:22.391Z |
level | 심각도 | INFO, WARN, ERROR |
event | 기계가 묶을 수 있는 사건 이름 | checkout_failed, order_created |
service | 로그를 남긴 서비스 | order-api |
correlationId | 업무 흐름이나 요청을 묶는 ID | req-7f2a |
traceId | 분산 추적과 연결되는 ID | 4bf92f3577b34da6a3ce929d0e0e4736 |
errorType | 분류 가능한 실패 유형 | TimeoutException |
durationMs | 사건 처리 시간 | 3000 |
dependency | 실패한 외부 의존성 | payment-gateway, rds, redis |
예시는 다음처럼 한 줄 JSON으로 남기는 것이 좋다.
{ "timestamp": "2026-04-07T12:03:22.391Z", "level": "ERROR", "event": "checkout_failed", "service": "order-api", "orderId": "order-9821", "correlationId": "req-7f2a", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "errorType": "TimeoutException", "dependency": "payment-gateway", "durationMs": 3000}반대로 아래 로그는 사람이 보기에는 익숙하지만 기계 집계가 어렵다.
2026-04-07 12:03:22 ERROR checkout failed for order order-9821 because payment timed out after 3000ms문자열 검색은 할 수 있지만 dependency별 에러율, errorType별 추세, traceId 기반 연결이 모두 불안정해진다.
로그는 사건 설명에 강하다. 사용자별 상세 원인, 외부 API 응답 코드, 어떤 입력이 실패했는지는 로그에 잘 맞는다.
하지만 고빈도 카운터를 로그로만 남기면 비용이 커진다. 헬스체크가 초당 100번 들어오는데 매번 INFO 로그를 남기면 하루 수백만 줄이 된다. 이런 것은 Metric으로 집계하고, 실패나 샘플만 로그로 남기는 편이 낫다.
민감 정보는 “나중에 마스킹”이 아니라 “처음부터 남기지 않기”가 원칙이다. 구조화 로그는 검색하기 쉬운 만큼 유출되면 피해도 커진다.
결제 장애 로그가 1,000줄 있어도 같은 요청에서 나온 로그를 묶을 수 없다면 조사 시간이 길어진다.
correlationId나 traceId가 있으면 한 요청의 로그를 다음처럼 좁힐 수 있다.
filter traceId = "4bf92f3577b34da6a3ce929d0e0e4736"| sort @timestamp asc| fields @timestamp, service, event, level, durationMs, errorType이 쿼리의 목적은 “전체 장애 규모”가 아니다. 한 요청에서 어떤 사건 순서가 있었는지 설명하는 것이다. 규모 판단은 Metrics로 돌아가야 한다.
Metric은 로그를 집계한 숫자처럼 보이지만, 설계 기준이 다르다. Metric은 빠르게 집계되고 비교되어야 하므로 값 종류가 제한적이어야 한다.
서비스 요청을 볼 때는 RED 신호가 기본이다.
인프라 자원을 볼 때는 USE 신호가 유용하다.
RED는 “사용자 요청이 괜찮은가”에 가깝고, USE는 “자원이 한계에 가까운가”에 가깝다. API p99가 오르는데 CPU가 낮다면 코드나 외부 의존성을 의심할 수 있다. API p99와 DB connection usage가 함께 오른다면 DB 풀이나 쿼리 병목을 더 먼저 본다.
평균은 느린 요청이 일부 있을 때 문제를 숨긴다. 100개 요청 중 99개가 100ms이고 1개가 5,000ms라면 평균은 약 149ms다. 대시보드 평균만 보면 괜찮아 보이지만, 그 1개 사용자는 5초를 기다렸다.
p95 = 700ms는 100개 중 95개가 700ms 이하라는 뜻이다.
p99 = 2s는 상위 1% 느린 요청이 2초 근처 이상이라는 뜻이다.
트래픽이 분당 10,000건이면 1%는 분당 100건이다.
작은 비율처럼 보여도 실제 사용자 수로 바꾸면 무시하기 어렵다.
메트릭의 비용과 메모리는 보통 “메트릭 이름 × 레이블 조합 수”로 커진다.
| 설계 | 시계열 수 감각 | 평가 |
|---|---|---|
endpoint 20개 × status 5개 × region 3개 | 300 | 대체로 관리 가능 |
위 조합 × method 4개 | 1,200 | 여전히 의미 있는 분류 |
위 조합 × userId 100,000개 | 120,000,000 | 메트릭으로는 거의 항상 실패 |
userId, orderId, sessionId, traceId는 Metric label로 넣지 않는다.
이 값들은 Logs나 Traces에서 찾는다.
Metric label은 endpoint, status, region, service, method처럼 값 종류가 제한된 분류에 적합하다.
Metric은 “checkout 5xx가 4.8%“라고 말할 수 있다. 하지만 어떤 주문이 실패했는지, 외부 결제사의 어떤 응답 코드가 반복되는지, 특정 Trace에서 어느 Span이 느렸는지는 말하지 못한다. Metric은 감지 신호이기 때문에 원인 설명까지 맡기면 안 된다.
Trace는 분산 시스템에서 특히 중요하다. 하나의 요청이 API 서버, Redis, DB, 외부 API, Worker를 지나면 어느 구간이 느린지 로그만으로 재구성하기 어렵다.
Context propagation은 Trace ID와 Span 정보를 다음 서비스로 전달하는 과정이다.
HTTP에서는 보통 헤더를 통해 전달한다.
W3C TraceContext의 traceparent 헤더는 다음과 같은 모양이다.
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 -- -------------------------------- ---------------- -- version trace id span id flags서비스 A가 서비스 B를 호출할 때 이 헤더를 전달하지 않으면 Trace가 끊긴다. 그 결과 A와 B의 Span이 같은 요청으로 묶이지 않고, “중간에서 사라진 요청”처럼 보인다.
자동 계측(auto instrumentation)은 HTTP 서버, HTTP 클라이언트, DB 드라이버 같은 흔한 라이브러리를 자동으로 Span으로 만든다. 처음 도입할 때는 자동 계측만으로도 “어느 서비스와 DB 호출이 느린가”를 보기 쉽다.
수동 계측(manual instrumentation)은 비즈니스 의미가 있는 구간에 직접 Span을 추가하는 방식이다.
예를 들어 reserve_inventory, authorize_payment, publish_order_created 같은 Span은 자동 계측만으로는 이름이 잘 드러나지 않을 수 있다.
좋은 시작점은 자동 계측으로 큰 흐름을 만든 뒤, 장애 판단에 필요한 핵심 업무 단계만 수동 Span으로 보강하는 것이다. 단, 자동 계측은 앱과 라이브러리가 초기화되는 순서의 영향을 받는다. HTTP 서버나 DB 클라이언트가 먼저 만들어진 뒤 계측을 켜면 일부 Span이 빠져서 “Trace가 있는데 DB 호출이 안 보이는” 상태가 될 수 있다.
초당 10,000 요청을 처리하는 서비스가 있고, 요청 하나가 평균 5개 Span을 만든다고 하자. 100% Trace를 저장하면 초당 50,000 Span이 저장 대상이 된다. 1% head-based sampling을 적용하면 초당 100 Trace, 약 500 Span으로 줄어든다.
문제는 무작위 1%가 꼭 중요한 1%는 아니라는 점이다. 에러율이 0.1%인 서비스에서 head-based 1% sampling을 쓰면 에러 Trace가 충분히 남지 않을 수 있다. 이때 tail-based sampling은 요청 완료 후 에러나 고지연 Trace를 보존할 수 있어 유리하다. 대신 Collector가 완료 전 Span을 잠시 들고 있어야 하므로 메모리와 복잡도가 늘어난다.
Trace는 개별 요청에는 강하지만 전체 추세에는 약하다. 느린 Trace 하나를 보고 전체 장애라고 판단하면 과잉 대응할 수 있다. 반대로 sampling 때문에 느린 Trace가 일부만 남을 수도 있다. Trace에서 원인 후보를 찾았으면 Metric으로 얼마나 넓게 퍼졌는지 다시 확인해야 한다.
관측 신호는 보통 다음 경로로 이동한다.
flowchart LR App["Application"] --> SDK["OTel SDK / Instrumentation"] SDK --> Collector["Collector"] Collector --> Logs["Logs backend"] Collector --> Metrics["Metrics backend"] Collector --> Traces["Traces backend"] Logs --> Query["검색과 분석"] Metrics --> Alert["알람과 대시보드"] Traces --> Timeline["요청 타임라인"]
애플리케이션에 SDK와 계측을 넣으면 신호가 만들어진다. Collector는 신호를 받아 배칭, 재시도, 샘플링, 필터링, 민감 정보 제거, 백엔드 라우팅을 담당한다. 백엔드는 CloudWatch, X-Ray, Prometheus, Grafana, Jaeger, Datadog처럼 조직이 선택한 도구가 될 수 있다.
Collector가 없으면 애플리케이션이 백엔드별 전송, 재시도, 필터링 부담을 직접 가진다. 작은 실험에서는 괜찮지만, 프로덕션에서는 Collector를 두는 쪽이 운영 변경에 더 강하다.
도구 이름을 외우는 것보다 “어떤 제약 때문에 이 선택을 하는가”가 중요하다.
OpenTelemetry는 벤더 중립 계측 표준이다. X-Ray는 AWS에서 Trace를 보고 분석하는 백엔드와 관련 SDK 생태계다.
신규 서비스에서 장기적으로 백엔드 교체 가능성, 멀티클라우드, 표준 계측을 중시한다면 OTel SDK와 Collector를 기본으로 잡는 편이 낫다. 이미 X-Ray SDK가 깊게 들어간 레거시 서비스라면 바로 교체보다 Trace ID 연결, sampling, Collector 전환 경로를 먼저 확인한다.
판단 질문은 다음과 같다.
CloudWatch Metrics는 AWS 리소스와 관리형 서비스 지표를 바로 보기 쉽다. Prometheus는 애플리케이션 커스텀 지표, Kubernetes 워크로드, PromQL 기반 분석에 강하다.
| 조건 | 더 자연스러운 선택 |
|---|---|
| AWS 관리형 리소스 상태를 빠르게 보고 싶다 | CloudWatch Metrics |
| 애플리케이션별 커스텀 지표가 많다 | Prometheus 계열 |
| PromQL과 Grafana 대시보드가 이미 있다 | Prometheus 계열 |
| 운영 팀이 AWS 콘솔 중심으로 움직인다 | CloudWatch Metrics |
| label cardinality를 세밀하게 관리해야 한다 | Prometheus 설계 검토가 중요 |
어느 쪽이든 userId 같은 고카디널리티 값을 Metric label로 넣으면 실패한다.
도구 선택보다 먼저 지표 설계가 맞아야 한다.
Collector는 여러 위치에 둘 수 있다.
| 방식 | 장점 | 비용 또는 경계 |
|---|---|---|
| Sidecar | 서비스별 격리와 설정 분리가 쉽다 | Pod마다 Collector가 떠서 리소스가 든다 |
| DaemonSet | 노드당 하나라 리소스 효율이 좋다 | 서비스별 격리와 설정 분리가 약하다 |
| Gateway | 중앙 정책과 라우팅 관리가 쉽다 | 네트워크 경로와 병목 지점이 생긴다 |
보안 민감 서비스나 서로 다른 sampling 정책이 필요하면 sidecar가 낫다. 일반 워크로드에서 리소스 효율이 중요하면 DaemonSet이 낫다. 여러 클러스터와 백엔드 라우팅을 중앙에서 관리해야 하면 gateway Collector를 추가한다.
Sampling은 “얼마나 줄일까”가 아니라 “무엇을 남길까”의 문제다.
| 조건 | 기준 |
|---|---|
| 트래픽이 낮고 비용 부담이 작다 | 높은 sampling 비율 또는 100% |
| 트래픽이 높고 요청이 균일하다 | head-based sampling |
| 에러와 SLO 위반 Trace를 꼭 남겨야 한다 | tail-based sampling |
| Collector 메모리 여유가 적다 | tail-based 비율과 보관 시간 주의 |
| 규제나 보안상 payload 노출이 위험하다 | attribute 필터링과 redaction 우선 |
예산으로 단순 계산하면 감각을 잡을 수 있다. 월 1억 요청 중 10%를 남기면 1,000만 Trace가 저장 대상이다. 요청당 평균 6개 Span이면 6,000만 Span이다. Trace 비용은 요청 수가 아니라 Span 수, 보존 기간, 조회량과 함께 커질 수 있으므로 Span 수까지 계산해야 한다.
아니다. 필드가 없는 로그, 중복 로그, 민감 정보가 섞인 로그는 관측성이 아니라 비용과 위험을 늘린다. 좋은 로그는 적절한 사건 이름, 식별자, 실패 유형, duration, dependency를 가진다.
사용자별 분석 욕구는 이해할 수 있지만 Metric label로는 위험하다. 사용자가 100,000명이고 endpoint, status, region까지 곱하면 시계열이 폭발한다. 사용자별 원인 조사는 Logs나 Traces에서 하고, Metric은 제한된 분류로 집계한다.
Trace는 경로와 시간을 잘 보여주지만 모든 사건 설명을 담지는 않는다. 비즈니스 실패 이유, 외부 API 응답 코드, 검증 실패 필드는 로그가 더 적합하다. Trace Span attribute에 너무 많은 정보를 넣으면 Trace 저장 비용과 보안 위험이 커진다.
p99는 tail latency를 보이게 하지만 원인은 아니다. p99가 올랐을 때 request rate, error rate, saturation, 배포 시점, dependency 상태를 같이 봐야 한다. 또 트래픽이 매우 낮은 서비스에서는 p99가 샘플 수 부족으로 튈 수 있다.
이 문서는 신호의 의미와 연결 방법을 다룬다. 다음 SRE Practices 문서는 이 신호를 SLI, SLO, error budget, incident response 같은 의사결정 체계로 바꾼다. 즉 Logs/Metrics/Traces는 SRE 판단의 입력이다.
긴 절차를 외우기보다 증상에서 어떤 개념이 깨졌는지 연결하는 연습이 중요하다.
| 증상 | 의미하는 개념 | 왜 문제인가 | 첫 확인 |
|---|---|---|---|
| p99는 급등했는데 평균은 거의 그대로다 | tail latency | 일부 사용자가 매우 느린 경험을 한다 | endpoint별 p95/p99, Trace 샘플 |
| 5xx가 늘었는데 트래픽은 평소와 비슷하다 | 의존성 실패 또는 코드 회귀 | 용량보다 특정 경로 실패일 가능성이 크다 | errorType별 로그, 배포 시점 |
| Prometheus/Collector 메모리가 급증한다 | metric cardinality explosion | label 조합이 폭발해 시계열 저장 비용이 커진다 | label별 시계열 수, userId 여부 |
| Trace는 있는데 관련 로그를 못 찾는다 | trace/correlation 연결 실패 | 신호 사이를 오갈 수 없어 조사 시간이 늘어난다 | 로그에 traceId 필드 존재 여부 |
| 특정 서비스 뒤로 Trace가 끊긴다 | context propagation 실패 | 헤더가 전달되지 않아 요청 경로가 분리된다 | HTTP client header propagation |
| 에러 Trace가 거의 남지 않는다 | sampling 정책 부적합 | 중요한 실패가 무작위 sampling에서 빠질 수 있다 | error trace 보존 규칙 |
| 로그 비용이 갑자기 커진다 | 로그 레벨 또는 고빈도 이벤트 설계 문제 | INFO/DEBUG나 healthcheck 로그가 비용을 만든다 | log group별 ingestion volume |
| 알람은 울리는데 조치 후 정상화가 불명확하다 | Metric 검증 루프 부재 | 완화가 실제 사용자 영향을 줄였는지 알 수 없다 | 조치 전후 SLI 지표 |
이 표는 운영 절차가 아니라 사고의 출발점이다. 증상 하나를 보면 “어떤 신호가 과하거나 부족한가”를 먼저 묻는다.
현재 팀 서비스나 개인 프로젝트의 API 하나를 고른다. 그리고 아래 질문에 답한다.
event, errorType, correlationId 또는 traceId가 남는가?세 질문 중 하나라도 답할 수 없다면 그 지점이 관측성 개선 후보가 된다.
새 메트릭 checkout_requests_total을 만든다고 하자.
label 후보가 endpoint, status, region, paymentProvider, userId라면 조합 수를 손으로 계산한다.
endpoint 8개 × status 5개 × region 3개 × paymentProvider 4개 = 480 series480 series × userId 100,000개 = 48,000,000 series이 계산 하나만으로도 userId는 Metric label이 아니라 Log/Trace 필드여야 한다는 결론이 나온다.
Trace 화면에서 Trace ID 하나를 고른다. 그 ID로 로그 검색이 되는지 확인한다. 검색이 안 된다면 계측이 없는 것이 아니라 연결 필드가 없는 문제일 수 있다.
filter traceId = "4bf92f3577b34da6a3ce929d0e0e4736"| sort @timestamp asc| fields @timestamp, service, event, level, message아래 절차는 실제 on-call에서 출발점으로 쓸 수 있지만, 본문 이해에 필수는 아니다.
Metrics에서 영향 범위를 본다. 에러율, request rate, p95/p99, saturation을 같은 시간축으로 본다.
Logs에서 반복 에러 유형을 묶는다.
errorType, dependency, service, endpoint별 count를 보고 가장 큰 실패 묶음을 찾는다.
Traces에서 대표 느린 요청을 연다. 가장 긴 Span과 실패한 dependency를 확인한다.
운영 판단을 하나의 문장으로 만든다. 예: “트래픽 폭증이 아니라 payment-gateway timeout이 checkout p99와 5xx를 만들고 있다.”
완화 뒤 Metrics로 검증한다. 조치 후 p95/p99, 5xx, dependency error가 내려가는지 본다.
Logs라면 log group별 ingestion volume을 먼저 본다. DEBUG/INFO, healthcheck, 루프 내부 로그, 중복 stack trace가 흔한 원인이다.
Metrics라면 label 조합 수를 계산한다.
userId, orderId, sessionId, traceId가 label에 들어갔는지 확인한다.
즉시 줄일 것은 줄이되 원인을 바꾼다. 로그 레벨만 낮추고 끝내면 다음 배포에서 재발한다. 고빈도 이벤트는 Metric으로, 개별 조사 값은 Log/Trace로 옮긴다.
Collector가 메모리 때문에 죽는다면 queue, batch, sampling, tail decision window를 같이 본다. 단순 재시작은 유실과 반복 장애를 숨길 수 있다.
처음부터 모든 도구를 완벽히 도입하려고 하면 실패하기 쉽다. 작게 시작하는 순서는 다음이 현실적이다.
event, service, correlationId, traceId, errorType, durationMs를 넣는다.이 순서의 핵심은 “신호를 따로 모으는 것”이 아니라 “한 신호에서 다른 신호로 넘어갈 수 있게 만드는 것”이다.
| 개념 A | 개념 B | 차이점 |
|---|---|---|
| Logs | Metrics | Logs는 개별 사건의 문맥, Metrics는 집계된 숫자와 추세 |
| Metrics | Traces | Metrics는 전체 건강 상태, Traces는 개별 요청 경로 |
| Trace ID | Correlation ID | Trace ID는 분산 추적 표준 식별자, Correlation ID는 업무 흐름 연결 식별자 |
| Span | Log event | Span은 시간 구간, Log event는 특정 시점의 사건 |
| Monitoring | Observability | Monitoring은 알려진 증상 감시, Observability는 신호 조합을 통한 추론 |
| Head sampling | Tail sampling | Head는 시작 때 선택, Tail은 완료 후 에러/지연을 보고 선택 |
| Metric label | Log field | label은 값 종류가 적어야 하고, log field는 개별 식별자에 적합 |
| Auto instrumentation | Manual instrumentation | 자동은 기본 호출을 잡고, 수동은 비즈니스 의미를 보강한다 |
traceparent 헤더 표준