Kubernetes 환경에서 Prometheus와 Loki를 구축하면 시스템 상태와 로그를 상당히 편리하게 확인할 수 있다.
하지만 한 가지 문제가 남는다.
예를 들어 사용자가 다음 API를 호출했다고 생각해보자.
POST /api/orders
요청이 다음과 같은 구조로 처리된다.
사용자
|
v
Nginx / Ingress
|
v
FastAPI
|
+------> Redis
|
+------> PostgreSQL
그런데 사용자가 500 오류를 받았다.
Prometheus에서는:
HTTP 500 증가
를 확인할 수 있고,
Loki에서는:
Database connection timeout
이라는 로그를 찾을 수 있다.
하지만 다음 질문에는 바로 답하기 어렵다.
이 요청이 정확히 어떤 Pod를 거쳤는가?
Redis 요청에는 몇 ms가 걸렸는가?
PostgreSQL 쿼리에 몇 ms가 걸렸는가?
전체 요청 시간 중 어디에서 가장 많은 시간이 발생했는가?
Nginx → FastAPI → Redis → PostgreSQL의 호출 관계는 어떻게 되는가?
이 문제를 해결하는 기술이 **Distributed Tracing(분산 추적)**이다.
이번 실습에서는 다음 구조를 구축한다.
Client
|
v
Ingress/Nginx
|
v
FastAPI
/ \
/ \
v v
Redis PostgreSQL
\ /
\ /
\ /
v v
OpenTelemetry
|
v
OpenTelemetry
Collector
|
v
Tempo
|
v
Grafana
Distributed Tracing은 하나의 요청이 여러 시스템을 거쳐 처리되는 과정을 추적하는 기술이다.
예를 들어 사용자가 다음 API를 호출한다고 하자.
GET /api/users/100
실제 내부에서는 다음과 같은 작업이 수행될 수 있다.
HTTP Request
|
v
FastAPI
|
+---- Redis GET
|
+---- PostgreSQL SELECT
|
v
Response
이때 전체 요청을 하나의 Trace라고 한다.
Trace 내부에는 여러 개의 Span이 존재한다.
Trace
|
+-- Span: HTTP Request
|
+-- Span: Redis GET
|
+-- Span: PostgreSQL SELECT
|
+-- Span: Response
즉:
Trace = 하나의 요청 전체
Span = 요청을 처리하는 각각의 작업
이라고 이해하면 쉽다.
분산 추적의 기본 개념은 반드시 이해해야 한다.
예를 들어 요청 하나가 다음과 같이 처리되었다고 하자.
Trace ID
abc123
그리고 내부 작업이:
FastAPI HTTP
Redis GET
PostgreSQL SELECT
로 구성된다.
그러면:
Trace: abc123
|
+-- Span A: FastAPI HTTP
|
+-- Span B: Redis GET
|
+-- Span C: PostgreSQL SELECT
형태가 된다.
Span에는 다음과 같은 정보가 들어갈 수 있다.
Span ID
Trace ID
Service Name
Operation
Start Time
End Time
Duration
Status
Attributes
예:
Trace ID: abc123
Span: PostgreSQL SELECT
Duration: 83ms
Status: OK
이 정보를 이용하면 어느 구간에서 시간이 오래 걸렸는지 확인할 수 있다.
OpenTelemetry는 애플리케이션의 Metrics, Logs, Traces와 같은 Telemetry 데이터를 수집하고 전달하기 위한 오픈 표준 및 프로젝트다.
이번 실습에서는 특히 Trace에 집중한다.
Application
|
v
OpenTelemetry SDK
|
v
OpenTelemetry Collector
|
v
Tempo
OpenTelemetry의 장점은 특정 백엔드에 종속되지 않는다는 것이다.
예를 들어:
OpenTelemetry
|
+---- Tempo
+---- Jaeger
+---- 다른 Trace Backend
와 같이 구성할 수 있다.
따라서 애플리케이션에 OpenTelemetry를 적용해 놓으면 나중에 Trace 저장 시스템을 변경하기도 상대적으로 편하다.
Tempo는 Grafana 생태계의 Distributed Tracing Backend다.
기존에 구축한 시스템과 비교하면 이해하기 쉽다.
| 역할 | 기술 |
|---|---|
| Metrics | Prometheus |
| Logs | Loki |
| Traces | Tempo |
| Visualization | Grafana |
| Alerts | Alertmanager |
| Telemetry Collection | OpenTelemetry Collector / Alloy |
전체 구조는 다음과 같다.
Observability
|
+--------------+--------------+
| | |
v v v
Metrics Logs Traces
| | |
Prometheus Loki Tempo
| | |
+--------------+--------------+
|
v
Grafana
이제 Kubernetes 환경에서 세 가지 데이터를 함께 볼 수 있게 된다.
예를 들어 Loki에서 다음 로그를 발견했다고 하자.
Database query timeout
로그만 보면 PostgreSQL 문제가 의심된다.
하지만 실제 요청을 Trace로 보면:
HTTP Request 500ms
|
+-- Redis GET 10ms
|
+-- PostgreSQL 450ms
|
+-- Response 40ms
PostgreSQL에서 대부분의 시간이 소요되고 있다는 사실을 바로 확인할 수 있다.
반대로:
HTTP Request 500ms
|
+-- Redis GET 470ms
|
+-- PostgreSQL 20ms
라면 Redis가 병목이다.
따라서 Distributed Tracing은 성능 병목 구간을 찾는 데 매우 유용하다.
이번 실습에서는 다음 환경을 기준으로 한다.
Kubernetes Cluster
namespace: tax-prod
Internet
|
v
Ingress/Nginx
|
v
FastAPI
/ \
/ \
v v
Redis PostgreSQL
Observability
FastAPI
|
v
OpenTelemetry SDK
|
v
OTel Collector
|
v
Tempo
|
v
Grafana
그리고 기존 로그 시스템은:
FastAPI/Nginx
|
v
Grafana Alloy
|
v
Loki
Metrics는:
FastAPI/Node/Pod
|
v
Prometheus
|
v
Grafana
로 구성한다.
먼저 Grafana Helm Repository를 등록한다.
helm repo add grafana https://grafana.github.io/helm-charts
Repository를 업데이트한다.
helm repo update
Tracing용 Namespace를 만든다.
kubectl create namespace tracing
환경에 맞는 Tempo values 파일을 작성한다.
vi tempo-values.yaml
학습 환경에서는 단일 인스턴스 형태로 시작할 수 있다.
예:
deploymentMode: SingleBinary
tempo:
reportingEnabled: false
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 1Gi
persistence:
enabled: true
size: 20Gi
설치:
helm upgrade --install tempo grafana/tempo \
-n tracing \
-f tempo-values.yaml
상태 확인:
kubectl get pods -n tracing
kubectl get svc -n tracing
정상적으로 설치되었다면 Tempo Pod가 Running 상태인지 확인한다.
Trace 데이터도 계속 증가한다.
예를 들어:
1초에 100 requests
|
v
Trace 데이터 지속 증가
따라서 운영 환경에서는 Storage를 고려해야 한다.
개념적으로:
OpenTelemetry
|
v
Tempo
|
v
Object Storage
|
+-- S3
+-- MinIO
+-- Cloud Storage
개발 환경에서는 Persistent Volume으로 시작할 수 있지만 운영 환경에서는 Object Storage 기반 설계를 검토하는 것이 좋다.
OpenTelemetry Collector는 Telemetry 데이터를 중간에서 수집하고 전달하는 역할을 한다.
구조는 다음과 같다.
FastAPI
|
| OTLP
v
OpenTelemetry Collector
|
| OTLP
v
Tempo
Collector를 사용하는 이유는 애플리케이션과 Backend를 직접 연결하지 않아도 되기 때문이다.
예를 들어:
Application
|
v
Collector
|
+---- Tempo
+---- 다른 Trace Backend
형태로 확장할 수 있다.
Collector는 다음과 같은 기능을 수행할 수 있다.
Receive
Process
Filter
Batch
Export
Collector도 Kubernetes 환경에서 Helm으로 구성할 수 있다.
OpenTelemetry Helm Repository를 등록한다.
helm repo add open-telemetry \
https://open-telemetry.github.io/opentelemetry-helm-charts
업데이트:
helm repo update
Collector용 Namespace:
kubectl create namespace observability
Collector values 파일:
vi otel-values.yaml
예제:
mode: deployment
image:
repository: otel/opentelemetry-collector-k8s
config:
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
exporters:
otlp:
endpoint: tempo.tracing.svc.cluster.local:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers:
- otlp
processors:
- batch
exporters:
- otlp
설치:
helm upgrade --install otel-collector \
open-telemetry/opentelemetry-collector \
-n observability \
-f otel-values.yaml
확인:
kubectl get pods -n observability
OpenTelemetry에서 중요한 개념이 **OTLP(OpenTelemetry Protocol)**다.
Application에서 Collector로:
FastAPI
|
| OTLP
v
Collector
Collector에서 Tempo로:
Collector
|
| OTLP
v
Tempo
형태로 데이터를 전달한다.
OTLP는 gRPC와 HTTP 방식으로 사용할 수 있다.
대표적인 포트는:
4317 = OTLP gRPC
4318 = OTLP HTTP
따라서 Kubernetes Service를 구성할 때 이 포트를 확인해야 한다.
이제 실제 애플리케이션에 OpenTelemetry를 적용한다.
Python 패키지를 설치한다.
pip install \
opentelemetry-api \
opentelemetry-sdk \
opentelemetry-exporter-otlp \
opentelemetry-instrumentation-fastapi
FastAPI 자동 계측을 사용할 수 있다.
예:
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
이렇게 하면 FastAPI 요청에 대한 Trace를 자동으로 생성할 수 있다.
Collector로 Trace를 보내도록 설정한다.
예:
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
resource = Resource.create({
"service.name": "fastapi-backend"
})
provider = TracerProvider(
resource=resource
)
exporter = OTLPSpanExporter(
endpoint="otel-collector.observability.svc.cluster.local:4317",
insecure=True
)
provider.add_span_processor(
BatchSpanProcessor(exporter)
)
trace.set_tracer_provider(provider)
그리고 FastAPI instrumentation:
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
FastAPIInstrumentor.instrument_app(app)
구조는 다음과 같다.
FastAPI
|
v
TracerProvider
|
v
OTLP Exporter
|
v
Collector
|
v
Tempo
운영 환경에서는 Collector 주소를 코드에 직접 넣는 것보다 환경변수를 사용하는 것이 좋다.
예:
env:
- name: OTEL_SERVICE_NAME
value: "fastapi-backend"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector.observability.svc.cluster.local:4317"
- name: OTEL_EXPORTER_OTLP_INSECURE
value: "true"
그러면 환경별로 쉽게 변경할 수 있다.
개발:
otel-collector-dev
운영:
otel-collector-prod
와 같이 분리할 수 있다.
FastAPI에서 Redis를 사용하는 경우 Redis 작업도 Trace에 포함시키면 좋다.
OpenTelemetry에서는 Redis와 같은 라이브러리에 대한 instrumentation을 사용할 수 있다.
개념적으로:
FastAPI Span
|
+-- Redis GET Span
|
+-- Redis SET Span
이렇게 된다.
예:
Trace ID: abc123
FastAPI GET /users
|
+-- Redis GET user:100
|
+-- PostgreSQL SELECT
이제 Redis에서 지연이 발생하는지 쉽게 확인할 수 있다.
Database도 마찬가지다.
PostgreSQL을 사용하는 Python 애플리케이션에 Database instrumentation을 적용하면 SQL 관련 Span을 확인할 수 있다.
구조:
FastAPI
|
+-- Redis
|
+-- PostgreSQL
|
+-- SELECT
+-- INSERT
+-- UPDATE
예를 들어 PostgreSQL SELECT가:
Duration = 850ms
라고 나타난다면 Database query가 병목일 가능성이 높다.
Database Trace에는 SQL 관련 정보가 포함될 수 있다.
따라서 운영 환경에서는 개인정보나 비밀번호 등의 민감한 데이터가 Trace에 들어가지 않도록 주의해야 한다.
좋지 않은 예:
SELECT * FROM users
WHERE password='123456'
Trace에 민감한 정보가 그대로 남으면 안 된다.
따라서 다음을 고려한다.
민감 데이터 제거
SQL parameter masking
Trace retention 설정
접근 권한 제한
자동 instrumentation만으로 부족한 경우 직접 Span을 만들 수 있다.
예:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
그리고:
with tracer.start_as_current_span("calculate-tax"):
result = calculate_tax()
이렇게 하면 다음과 같은 Span이 생성된다.
FastAPI Request
|
+-- calculate-tax
|
+-- DB Query
복잡한 비즈니스 로직을 추적할 때 매우 유용하다.
Distributed Tracing에서 가장 중요한 정보 중 하나가 Trace ID다.
예:
Trace ID:
4bf92f3577b34da6a3ce929d0e0e4736
하나의 요청에 동일한 Trace ID가 연결된다.
Nginx
|
| Trace ID = ABC
v
FastAPI
|
| Trace ID = ABC
+------> Redis
|
| Trace ID = ABC
+------> PostgreSQL
따라서 로그에도 Trace ID를 기록하면 강력한 장애 분석이 가능해진다.
앞에서 구축한 Loki와 결합하면 더욱 강력해진다.
예:
Trace ID
ABC123
FastAPI 로그:
ERROR Database timeout
trace_id=ABC123
Grafana에서 해당 Trace를 클릭하면:
Trace
|
+-- FastAPI
|
+-- PostgreSQL
그리고 관련 로그를 바로 확인할 수 있다.
개념적으로:
Trace ID
|
+--------+--------+
| |
v v
Tempo Loki
| |
+--------+--------+
|
v
Grafana
이것이 **Logs와 Traces의 상관관계(Correlation)**다.
이제 Prometheus까지 연결한다.
Prometheus
|
| Metrics
v
Grafana
|
+------ Loki
|
+------ Tempo
예를 들어 Dashboard에서:
HTTP Error Rate
|
v
500 증가
|
v
해당 시간대 Trace 확인
|
v
PostgreSQL Span 확인
|
v
Query 1.2초
|
v
관련 Loki 로그 확인
|
v
Database timeout
이런 분석이 가능해진다.
Grafana에서:
Connections
↓
Data Sources
↓
Add data source
↓
Tempo
를 선택한다.
Tempo Service 주소를 입력한다.
예:
http://tempo.tracing.svc.cluster.local:3200
환경에 따라 Service 이름과 포트는 실제 설치 결과를 확인해야 한다.
kubectl get svc -n tracing
정상 연결을 확인한다.
Grafana:
Explore
↓
Tempo
를 선택한다.
Trace ID를 이용해 검색할 수 있다.
예:
Trace ID
4bf92f3577b34da6a3ce929d0e0e4736
결과는 다음과 같은 구조로 나타날 수 있다.
Trace
|
+-- GET /api/users
|
+-- Redis GET
|
+-- PostgreSQL SELECT
각 Span을 클릭하면:
Duration
Attributes
Status
Service
Operation
등을 확인할 수 있다.
Distributed Tracing에서 가장 중요한 화면 중 하나가 Waterfall이다.
예:
0ms 1000ms
FastAPI |-----------------------------|
|
Redis |----|
|
PostgreSQL | |---------------|
|
Response |------------------------------|
이 화면을 보면 어느 작업이 오래 걸렸는지 바로 알 수 있다.
예를 들어:
FastAPI 1000ms
Redis 20ms
PostgreSQL 850ms
라면 PostgreSQL이 병목이다.
반면:
FastAPI 1000ms
Redis 900ms
PostgreSQL 30ms
라면 Redis가 병목이다.
이제 실제 장애를 만들어보자.
API:
GET /api/users/100
를 호출한다.
정상 상태:
FastAPI 100ms
Redis 10ms
PostgreSQL 30ms
그런데 PostgreSQL에 의도적으로 느린 Query를 발생시킨다.
결과:
FastAPI 1200ms
Redis 10ms
PostgreSQL 1100ms
Grafana Tempo에서 다음처럼 확인할 수 있다.
GET /api/users/100
|
+-- Redis GET 10ms
|
+-- PostgreSQL SELECT 1100ms
이것만으로도 Database가 병목이라는 것을 확인할 수 있다.
Tempo에서 느린 PostgreSQL Span을 찾았다.
Trace ID:
ABC123
이제 Loki에서 Trace ID를 검색한다.
{app="backend"} |= "ABC123"
결과:
Database connection timeout
그리고 PostgreSQL 로그:
{app="postgres"} |= "timeout"
결과:
connection pool exhausted
최종적으로:
HTTP 500
|
v
FastAPI
|
v
PostgreSQL Span 1100ms
|
v
Loki
|
v
connection pool exhausted
라는 장애 분석이 가능해진다.
이번에는 Redis를 느리게 만들어본다.
FastAPI
|
v
Redis
|
X
느린 응답
Trace:
FastAPI 1000ms
Redis 950ms
PostgreSQL 20ms
Waterfall에서 Redis Span이 길게 나타난다.
이런 방식으로 성능 병목을 빠르게 찾을 수 있다.
PostgreSQL Pod를 삭제한다.
kubectl delete pod <postgres-pod> -n tax-prod
FastAPI에서 DB API를 호출한다.
Trace에서는:
FastAPI
|
+-- PostgreSQL
|
+-- ERROR
+-- Timeout
가 나타날 수 있다.
동시에 Loki에서는:
Database connection refused
를 확인할 수 있다.
Kubernetes에서는:
kubectl get pods -n tax-prod
로 PostgreSQL Pod 상태를 확인한다.
이렇게 세 가지 정보를 연결한다.
Tempo
↓
어디에서 실패했는가?
Loki
↓
무슨 오류가 발생했는가?
Kubernetes
↓
Pod/서비스 상태는 어떠한가?
Distributed Tracing에서 매우 중요한 개념이 Trace Context Propagation이다.
요청이:
Client
↓
Nginx
↓
FastAPI
↓
Redis
↓
PostgreSQL
로 이동할 때 Trace Context가 전달되어야 한다.
대표적으로 W3C Trace Context가 사용된다.
HTTP Header 예:
traceparent:
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
이 정보를 기반으로 하나의 요청을 하나의 Trace로 연결할 수 있다.
운영 환경에서 다음과 같은 로그가 있다고 하자.
ERROR Database timeout
이것만으로는 어떤 요청인지 알기 어렵다.
하지만:
ERROR Database timeout
trace_id=ABC123
라면 바로 Tempo에서 찾을 수 있다.
따라서 이상적인 구조는:
Application Log
|
+-- timestamp
+-- level
+-- service
+-- trace_id
+-- span_id
+-- message
이다.
이제 지금까지 구축한 모든 시스템을 연결해보자.
Kubernetes
|
+----------------+----------------+
| | |
v v v
Metrics Logs Traces
| | |
Prometheus Loki Tempo
| | |
+----------------+----------------+
|
v
Grafana
|
+-------------+-------------+
| | |
v v v
Dashboard Logs Traces
Alertmanager까지 연결하면:
Prometheus
|
v
Alertmanager
|
+-- Email
+-- Slack
+-- Webhook
구조가 된다.
운영 중 다음 알림이 발생했다고 가정하자.
CRITICAL
FastAPI HTTP 500 rate > 5%
운영자는 다음 순서로 확인한다.
Prometheus에서:
HTTP 500 증가
확인.
Loki에서:
{app="backend"} |= "ERROR"
검색.
결과:
Database timeout
Tempo에서 해당 요청 Trace를 확인.
FastAPI
|
+-- Redis 10ms
|
+-- PostgreSQL 2.8s
kubectl get pods -n tax-prod
PostgreSQL 상태 확인.
PostgreSQL 로그에서:
connection pool exhausted
확인.
이제 단순히 "FastAPI 500 오류"가 아니라:
PostgreSQL Connection Pool 부족
이라는 근본 원인까지 좁혀갈 수 있다.
세 가지 기술의 역할을 명확히 구분하면 이해하기 쉽다.
| 데이터 | 질문 |
| Metrics | 문제가 발생하고 있는가? |
| Logs | 무슨 오류가 발생했는가? |
| Traces | 요청이 어디에서 느려지거나 실패했는가? |
예:
Metrics
HTTP 500 증가
↓
Logs
Database timeout
↓
Trace
PostgreSQL 2.8초
↓
Kubernetes
PostgreSQL Pod 상태 확인
↓
Root Cause
Connection Pool 문제
이것이 현대적인 Observability의 기본적인 장애 분석 방법이다.
Distributed Tracing을 운영 환경에 적용할 때는 몇 가지를 반드시 고려해야 한다.
트래픽이 매우 많다면 모든 Trace를 저장하면 데이터 양이 증가한다.
따라서 Sampling을 사용할 수 있다.
예:
전체 요청 = 100%
Trace 저장 = 10%
또는:
정상 요청 = 일부 Sampling
오류 요청 = 높은 우선순위
와 같은 전략을 고려할 수 있다.
Sampling은 Trace 데이터의 양을 조절하는 방법이다.
예를 들어 초당:
10,000 requests
가 발생한다면 모든 Trace를 저장하는 것은 부담이 될 수 있다.
따라서:
10,000 requests
|
v
Sampling
|
v
1,000 traces
처럼 줄일 수 있다.
다만 장애 분석을 위해 오류 Trace는 최대한 보존하는 전략을 고려할 수 있다.
Trace도 로그와 마찬가지로 보존 기간이 필요하다.
예:
Hot Trace
7일
Archive
30일
Delete
90일
실제 보존 기간은 시스템 규모와 장애 분석 요구사항에 따라 결정한다.
OpenTelemetry Collector도 Kubernetes Pod이므로 Resource 제한을 설정해야 한다.
예:
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
Trace가 증가하면 Collector 자체의 CPU와 Memory 사용량도 증가할 수 있다.
따라서 Prometheus에서 Collector를 모니터링하는 것이 좋다.
Collector CPU
Collector Memory
Dropped Spans
Export Errors
Queue Size
등을 확인해야 한다.
다음 상황을 생각해보자.
FastAPI
|
v
Collector
X
|
v
Tempo
Collector가 장애가 발생하면 Trace가 유실될 수 있다.
따라서:
Application
|
v
Collector
|
v
Tempo
각 구간을 모니터링해야 한다.
특히 운영 환경에서는:
Collector replicas
Queue
Retry
Batch
Resource
등을 고려해야 한다.
앞서 Kubernetes 애플리케이션에서 개발과 운영 환경을 분리했듯이 Observability 환경도 분리할 수 있다.
예:
Namespace
|
+-- tax-dev
|
+-- tax-prod
|
+-- observability
|
+-- tracing
|
+-- logging
또는:
dev
└── Trace
prod
└── Trace
환경을 구분할 수 있다.
Trace에 다음 정보를 넣는 것도 좋다.
deployment.environment=production
그러면 Grafana에서 개발/운영 데이터를 구분할 수 있다.
지금까지 작성한 Prometheus, Loki, Tempo를 모두 연결하면 다음과 같은 구조가 된다.
Internet
|
v
Ingress / Nginx
|
v
FastAPI
/ | \
/ | \
v v v
Redis PostgreSQL Other Service
Kubernetes Observability
|
+----------------+----------------+
| | |
v v v
Metrics Logs Traces
| | |
Prometheus Loki Tempo
| | |
+----------------+----------------+
|
v
Grafana
|
v
Unified Observability
|
+----------+----------+
| | |
v v v
Dashboard Logs Trace
|
v
Root Cause
여기에:
Prometheus
|
v
Alertmanager
|
+-- Email
+-- Slack
+-- Webhook
까지 추가하면 운영 환경에서 필요한 기본적인 Observability 구성이 완성된다.
전체 내용을 가장 간단하게 정리하면 다음과 같다.
Prometheus
|
+-- CPU
+-- Memory
+-- Request
+-- Error Rate
+-- Latency
Loki
|
+-- Application Log
+-- Nginx Log
+-- Kubernetes Log
+-- Error Log
Tempo
|
+-- Request Trace
+-- Service Span
+-- Redis Span
+-- Database Span
Grafana
|
+-- Metrics
+-- Logs
+-- Traces
그리고 장애가 발생하면:
1. Metrics
"문제가 있는가?"
2. Logs
"무슨 오류인가?"
3. Trace
"어디에서 문제가 발생했는가?"
4. Kubernetes
"Pod/Service/Node 상태는 정상인가?"
5. Root Cause
"왜 문제가 발생했는가?"
순서로 접근하면 된다.
Kubernetes 환경에서 애플리케이션이 복잡해질수록 단순한 kubectl logs만으로는 장애 원인을 찾기 어려워진다.
특히 다음과 같은 구조에서는 더욱 그렇다.
Ingress
↓
Nginx
↓
FastAPI
↓
Redis
↓
PostgreSQL
하나의 사용자 요청이 여러 서비스를 통과하기 때문이다.
이때 Distributed Tracing을 사용하면:
사용자 요청
|
v
Trace ID
|
+-- Nginx
|
+-- FastAPI
|
+-- Redis
|
+-- PostgreSQL
전체 요청 흐름을 하나의 Trace로 확인할 수 있다.
이번 실습에서 구축한 핵심 구조는 다음과 같다.
FastAPI
|
v
OpenTelemetry SDK
|
v
OpenTelemetry Collector
|
v
Tempo
|
v
Grafana
그리고 기존 시스템과 결합하면:
Kubernetes
|
+-----------+-----------+
| | |
v v v
Metrics Logs Traces
| | |
Prometheus Loki Tempo
| | |
+-----------+-----------+
|
v
Grafana
|
v
장애 원인 분석
이제 Kubernetes Observability의 핵심 세 가지인 Metrics + Logs + Traces가 모두 연결되었다.
가장 중요한 것은 각각의 기술을 별도로 사용하는 것이 아니라 서로 연결하는 것이다.
Prometheus
"500 오류가 증가했다"
↓
Loki
"Database timeout 로그가 발생했다"
↓
Tempo
"PostgreSQL Span에서 2.8초가 소요되었다"
↓
Kubernetes
"PostgreSQL Pod와 연결 상태를 확인한다"
↓
Root Cause
"Database Connection Pool 부족"
이렇게 Metrics → Logs → Traces → Kubernetes → Root Cause로 연결해서 장애를 분석할 수 있다면 Kubernetes 운영에 필요한 Observability의 핵심 개념을 제대로 이해한 것이다.