Skip to content

Commit 396d16c

Browse files
authored
♻️ Refactor - Alloy + Tempo 사용으로 비즈니스 병목을 판단하고 개선한다
♻️ Refactor - Alloy + Tempo 사용으로 비즈니스 병목을 판단하고 개선한다
2 parents 8a05639 + 202229e commit 396d16c

19 files changed

Lines changed: 661 additions & 41 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Blue-Green Deploy
22

33
on:
44
push:
5-
branches: [dev]
5+
branches: [dev, refactor/#105_tempo_alloy]
66

77
jobs:
88
build:

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@ out/
4141
src/main/resources/application-local.yml
4242
src/main/resources/application-prod.yml
4343

44+
45+
.claude

Claude.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# COMFIT Project - CLAUDE.md
2+
3+
## 프로젝트 개요
4+
AI 기반 의상 추천 서비스 (COMFIT)의 관측성(Observability) 강화 작업.
5+
"감으로 느리다"가 아닌, **정량적 데이터 기반으로 병목을 특정하고 개선**하는 것이 목표.
6+
7+
---
8+
9+
## 현재 인프라 상태
10+
11+
### 기술 스택
12+
- **Spring Boot 3.4.2** + Java (Virtual Thread 사용 중)
13+
- **AWS EC2** 단일 서버 배포
14+
- **PostgreSQL** — 메인 DB
15+
- **Redis** — BRPOP 기반 비동기 Job Queue + 캐싱
16+
17+
### 관측성 스택 (현재)
18+
| 구성 요소 | 상태 | 비고 |
19+
|-----------|------|------|
20+
| Prometheus | ✅ 동작 중 | 메트릭 수집 (HTTP 히스토그램, JVM, 시스템) |
21+
| Grafana | ✅ 동작 중 | 시각화 대시보드 |
22+
| Loki | ✅ 동작 중 | 로그 저장소 |
23+
| Promtail | ❌ 제거됨 | 설정 문제로 제거 → 로그가 Loki로 안 가는 중 |
24+
| 트레이싱 | ❌ 없음 | Span 단위 병목 특정 불가 |
25+
26+
### 기존 관측성의 한계
27+
- traceId를 MDC에 심어 로그에 찍고 있지만, Loki로 안 가므로 조회 불가
28+
- Prometheus 메트릭으로 "p99가 느리다"는 알 수 있지만, **어디서 느린지** 특정 불가
29+
- Grafana 대시보드는 있으나 트레이싱 데이터소스 없음
30+
- **Three Pillars(Metrics/Logs/Traces) 중 Logs와 Traces가 빠져있는 상태**
31+
32+
---
33+
34+
## 이번 작업 목표: Observability Three Pillars 완성
35+
36+
### 최종 아키텍처
37+
38+
```
39+
[Spring Boot App]
40+
41+
├── Micrometer Metrics ──→ Prometheus ──→ Grafana (메트릭 대시보드)
42+
43+
├── OTLP Traces ──→ Alloy ──→ Tempo ──→ Grafana (트레이스 뷰)
44+
45+
└── 로그 파일 ──→ Alloy ──→ Loki ──→ Grafana (로그 뷰)
46+
47+
traceId로 3개 연동
48+
(메트릭 ↔ 로그 ↔ 트레이스 원클릭 점프)
49+
```
50+
51+
### 기술 결정 기록
52+
53+
| 결정 | 선택 | 비교군 | 선택 이유 |
54+
|------|------|--------|-----------|
55+
| 로그 에이전트 | **Alloy** | Promtail, Fluent Bit, Vector, Filebeat | Promtail이 이미 빠진 상태라 새로 셋업 필요. Alloy는 로그+트레이스 통합 수집 가능, Grafana 생태계 네이티브. Fluent Bit/Vector는 범용이지만 Tempo 연동에서 추가 설정 필요 |
56+
| 트레이스 저장소 | **Tempo** | Zipkin, Jaeger | Grafana 네이티브 → Loki↔Tempo traceId 원클릭 연동. Zipkin은 독립 UI라 Grafana 통합이 약함. Jaeger는 기능 풍부하지만 현 목적 대비 오버 |
57+
| 트레이스 전송 | **OTLP** | Brave→Zipkin | OpenTelemetry 표준 프로토콜. 벤더 종속 없음, 나중에 백엔드 교체 시 앱 코드 변경 불필요 |
58+
| 트레이스 계측 | **Micrometer Tracing + OpenTelemetry Bridge** | Brave | Spring Boot 3.4.2 공식 지원, OTLP 전송에 OTel 브릿지가 자연스러움 |
59+
60+
### 비교군 내부 동작 이해 (면접 대비)
61+
62+
**Promtail 내부 동작:**
63+
- 로그 파일 tail → Pipeline Stages (정규식 파싱 → 레이블 추출 → 타임스탬프 변환)
64+
- `{labels: [(timestamp, logline)]}` 스트림 단위로 Loki에 HTTP push
65+
- 읽은 오프셋을 `positions.yaml`에 저장 → 재시작 시 중복 수집 방지
66+
- 데이터 모델: Loki 스트림 전용
67+
68+
**Alloy 내부 동작:**
69+
- River 설정 언어 기반, 컴포넌트를 DAG(방향 비순환 그래프)로 연결
70+
- 로그: `local.file_match``loki.source.file``loki.write`
71+
- 트레이스: `otelcol.receiver.otlp``otelcol.exporter.otlphttp` (→ Tempo)
72+
- 단일 에이전트로 로그 + 트레이스 동시 처리
73+
- 데이터 모델: OTLP (로그/메트릭/트레이스 통합)
74+
75+
비교 테이블:
76+
77+
| 도구 | 데이터 모델 | 특징 |
78+
|------|------------|------|
79+
| Promtail | Loki 스트림 ({labels}, entries[]) | Loki 전용, 경량, push |
80+
| Alloy | OTLP (로그+메트릭+트레이스 통합) | 범용, River 설정언어, Grafana 네이티브 |
81+
| Fluent Bit | MessagePack 이벤트 버퍼 | 경량, 멀티 output 지원 |
82+
| Filebeat | ECS JSON | ELK 전용 |
83+
| Vector | Event 타입 (log/metric/trace) | Rust 기반 고성능, 범용 |
84+
---
85+
86+
## Phase 1: 관측성 파이프라인 구축
87+
88+
### Step 1. Tempo 배포 (Docker)
89+
- EC2에 Tempo 컨테이너 추가
90+
- OTLP gRPC (4317) / HTTP (4318) 수신 포트 오픈
91+
- 로컬 스토리지 사용 (단일 EC2이므로)
92+
93+
### Step 2. Alloy 배포 및 설정 (Docker)
94+
- Promtail 대체: 앱 로그 파일 → Loki 전송 파이프라인
95+
- 트레이스 수신: OTLP → Tempo 전송 파이프라인
96+
- 두 파이프라인을 하나의 Alloy config에 구성
97+
98+
### Step 3. Spring Boot 앱 설정 (3.4.2 기준)
99+
100+
**의존성 (build.gradle):**
101+
```groovy
102+
// Micrometer Tracing + OpenTelemetry Bridge
103+
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
104+
// OTLP Exporter (Alloy → Tempo로 전송)
105+
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
106+
// Actuator (메트릭 + 트레이싱 자동 설정)
107+
implementation 'org.springframework.boot:spring-boot-starter-actuator'
108+
```
109+
110+
**application.yml 핵심 설정:**
111+
```yaml
112+
management:
113+
tracing:
114+
sampling:
115+
probability: 1.0 # 개발/테스트: 전 요청 트레이싱 (프로덕션은 0.1 등)
116+
otlp:
117+
tracing:
118+
endpoint: http://localhost:4318/v1/traces # Alloy OTLP HTTP 수신 주소
119+
120+
logging:
121+
pattern:
122+
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
123+
```
124+
125+
**Virtual Thread + Tracing Context Propagation 주의:**
126+
- Virtual Thread 사용 시 ThreadLocal 기반 Context가 유실될 수 있음
127+
- 이미 MDC + TaskDecorator를 구현해둔 상태
128+
- Tracing context(Span, TraceId)도 같이 전파되는지 반드시 검증
129+
- `ContextSnapshotFactory` 또는 기존 TaskDecorator 확장 필요할 수 있음
130+
131+
### Step 4. 커스텀 Span 추가
132+
133+
**자동 계측으로 잡히는 구간:**
134+
- HTTP 요청/응답 (Controller)
135+
- DB 쿼리 (JPA/JDBC)
136+
- Redis 호출
137+
138+
**수동 Span이 필요한 구간:**
139+
- AI API 호출 (OpenAI 등 외부 HTTP) → 전체 응답시간의 대부분 차지 예상
140+
- Redis BRPOP Job Queue 처리 구간
141+
- 비즈니스 로직 주요 단계
142+
143+
```java
144+
// 방법 1: @Observed 어노테이션
145+
@Observed(name = "ai.api.call", contextualName = "ai-recommendation")
146+
public RecommendationResult callAiApi(RequestDto request) {
147+
// AI API 호출 로직
148+
}
149+
150+
// 방법 2: ObservationRegistry 직접 사용 (세밀한 제어)
151+
Observation.createNotStarted("ai.api.call", registry)
152+
.observe(() -> {
153+
// AI API 호출 로직
154+
});
155+
```
156+
157+
### Step 5. Grafana 데이터소스 연결
158+
- Tempo 데이터소스 추가 (http://tempo:3200)
159+
- Loki 데이터소스에 **Derived Field** 설정:
160+
- traceId 정규식 매칭 → Tempo 링크 자동 생성
161+
- **핵심:** 로그에서 traceId 클릭 → Tempo 트레이스 뷰로 원클릭 점프
162+
- Prometheus 메트릭 → Exemplars 설정으로 트레이스 연결
163+
164+
---
165+
166+
## Phase 2: K6 부하 테스트 + 병목 특정
167+
168+
### Step 6. K6 부하 테스트
169+
- 기존 K6 스크립트 활용
170+
- 테스트 중 Grafana에서 실시간 모니터링
171+
172+
### Step 7. 병목 분석
173+
1. Grafana → Tempo에서 p95/p99 느린 트레이스 선택
174+
2. Span 트리에서 각 구간별 소요시간 확인
175+
3. 병목 구간 비율 계산 (예: "AI API 호출이 전체의 85%")
176+
4. **스크린샷 캡처** → 포트폴리오/자소서 근거
177+
178+
### Step 8. 개선 + 재측정
179+
- 병목 구간에 대한 최적화 적용
180+
- K6 재실행
181+
- Before/After 정량 비교
182+
- 결과 문서화
183+
184+
---
185+
186+
## 제약 조건
187+
- **타임라인**: 당근 자소서 제출 전까지 Phase 1~2 완료
188+
- **인프라**: 단일 EC2 → 메모리 여유 확인 필요 (Tempo, Alloy 추가 시)
189+
- **목적**: 프로덕션 운영이 아닌 **포트폴리오 + 면접 근거 확보**
190+
- **기존 코드 유지**: Virtual Thread, MDC traceId, Redis BRPOP 등 기존 구현 유지
191+
192+
---
193+
194+
## 작업 순서 요약
195+
1. Spring Boot 버전 확인 (3.4.2)
196+
2. Docker Compose에 Tempo + Alloy 추가
197+
3. Alloy config 작성 (로그→Loki, 트레이스→Tempo 파이프라인)
198+
4. Spring Boot 의존성 추가 + application.yml 설정
199+
5. Virtual Thread Context Propagation + Tracing 연동 검증
200+
6. AI API 호출 구간에 커스텀 Span 추가
201+
7. Grafana에 Tempo 데이터소스 + Loki Derived Field 설정
202+
8. K6 부하 테스트 → 트레이스 수집 확인
203+
9. 병목 분석 → 개선 → 재측정 → 문서화

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ dependencies {
9999

100100
implementation 'io.micrometer:context-propagation:1.1.1'
101101

102+
// Micrometer Tracing — OTel 브릿지 (Spring Boot 3.4.2 BOM 버전 관리)
103+
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
104+
// OTLP Exporter — Alloy로 트레이스 전송
105+
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
106+
// DB (JDBC/JPA) 쿼리 단위 Span — 각 SQL 쿼리를 child Span으로 계측
107+
implementation 'net.ttddyy.observation:datasource-micrometer-spring-boot:1.0.6'
108+
102109
//resilience4j
103110
implementation 'io.github.resilience4j:resilience4j-spring-boot3'
104111
implementation 'io.github.resilience4j:resilience4j-reactor'

docker-compose-agent.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
version: '3.8'
2+
3+
# ============================================================
4+
# EC2-1 (앱 서버) 전용 — Alloy 에이전트
5+
#
6+
# 역할:
7+
# 1. 로그: Docker socket → blue/green 로그 읽기 → EC2-2 Loki push
8+
# 2. 트레이스: Spring Boot OTLP 수신(4318) → EC2-2 Tempo 전달
9+
#
10+
# Spring Boot application-prod.yml:
11+
# management.otlp.tracing.endpoint: http://172.17.0.1:4318/v1/traces
12+
# ============================================================
13+
14+
services:
15+
alloy:
16+
image: grafana/alloy:latest
17+
container_name: alloy
18+
ports:
19+
- "4317:4317" # OTLP gRPC (Spring Boot → Alloy)
20+
- "4318:4318" # OTLP HTTP (Spring Boot → Alloy)
21+
- "12345:12345" # Alloy 관리 UI
22+
volumes:
23+
- ./observability/alloy/config.alloy:/etc/alloy/config.alloy:ro
24+
- /var/run/docker.sock:/var/run/docker.sock:ro
25+
command: run --server.http.listen-addr=0.0.0.0:12345 /etc/alloy/config.alloy
26+
user: root
27+
restart: unless-stopped

docker-compose-observability.yml

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
version: '3.8'
2+
3+
# ============================================================
4+
# Observability Stack — EC2-2 (172.31.47.238) 전용
5+
#
6+
# EC2-1(앱 서버)에서 오는 트래픽:
7+
# - Alloy → Loki : EC2-1 Alloy가 3100으로 로그 push
8+
# - Alloy → Tempo : EC2-1 Alloy가 4317로 트레이스 전달
9+
# - Prometheus : EC2-2 Prometheus가 EC2-1 8080/8081 스크래핑
10+
# ============================================================
11+
12+
services:
13+
14+
# ----------------------------------------------------------
15+
# Loki: 로그 저장소
16+
# EC2-1의 Alloy가 push
17+
# ----------------------------------------------------------
18+
loki:
19+
image: grafana/loki:3.4.2
20+
container_name: loki
21+
user: "0"
22+
ports:
23+
- "3100:3100"
24+
volumes:
25+
- ./observability/loki/loki.yml:/etc/loki/config.yml:ro
26+
- loki_data:/var/loki
27+
command: -config.file=/etc/loki/config.yml
28+
restart: unless-stopped
29+
30+
# ----------------------------------------------------------
31+
# Prometheus: 메트릭 수집
32+
# EC2-1의 blue(8080), green(8081) 스크래핑
33+
# ----------------------------------------------------------
34+
prometheus:
35+
image: prom/prometheus:latest
36+
container_name: prometheus
37+
ports:
38+
- "9090:9090"
39+
volumes:
40+
- ./observability/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
41+
- prometheus_data:/prometheus
42+
command:
43+
- '--config.file=/etc/prometheus/prometheus.yml'
44+
- '--storage.tsdb.retention.time=7d'
45+
- '--web.enable-remote-write-receiver'
46+
restart: unless-stopped
47+
48+
# ----------------------------------------------------------
49+
# Tempo: 트레이스 저장소
50+
# 4317: EC2-1 Alloy가 트레이스 전달 (외부 오픈)
51+
# 3200: Grafana가 쿼리 (내부 통신)
52+
# ----------------------------------------------------------
53+
tempo:
54+
image: grafana/tempo:2.6.1
55+
container_name: tempo
56+
ports:
57+
- "3200:3200"
58+
- "4317:4317" # EC2-1 Alloy → Tempo OTLP gRPC
59+
volumes:
60+
- ./observability/tempo/tempo.yml:/etc/tempo/config.yml:ro
61+
- tempo_data:/var/tempo
62+
command: -config.file=/etc/tempo/config.yml
63+
restart: unless-stopped
64+
65+
# ----------------------------------------------------------
66+
# Grafana: 시각화 (Prometheus + Loki + Tempo 통합)
67+
# SSH 터널링으로 접근: ssh -L 3000:localhost:3000 ubuntu@EC2-2-IP
68+
# ----------------------------------------------------------
69+
grafana:
70+
image: grafana/grafana:latest
71+
container_name: grafana
72+
ports:
73+
- "3000:3000"
74+
environment:
75+
- GF_SECURITY_ADMIN_USER=admin
76+
- GF_SECURITY_ADMIN_PASSWORD=admin
77+
- GF_FEATURE_TOGGLES_ENABLE=traceqlEditor
78+
volumes:
79+
- ./observability/grafana/provisioning:/etc/grafana/provisioning:ro
80+
- grafana_data:/var/lib/grafana
81+
depends_on:
82+
- prometheus
83+
- loki
84+
- tempo
85+
restart: unless-stopped
86+
87+
volumes:
88+
loki_data:
89+
prometheus_data:
90+
tempo_data:
91+
grafana_data:

0 commit comments

Comments
 (0)