트레이스 기반 용량 계획: 스레드/커넥션 풀 USE 지표와 자동 스케일링 설계
장애는 CPU 100%에서만 오지 않습니다. 웹 스레드 풀이 꽉 차서 대기열이 길어지거나, DB 커넥션 풀이 포화되어 요청이 기다리다가 타임아웃으로 끝나는 경우가 더 흔합니다. 이 글은 OpenTelemetry(Trace)와 Prometheus(메트릭)를 함께 써서 USE 모델(Utilization·Saturation·Errors)로 스레드 풀/DB 커넥션 풀을 관찰하고, 그 지표로 자동 스케일링을 설계하는 방법을 정리합니다.
1) USE 모델을 스레드/커넥션 풀에 매핑하기
- Utilization: 사용률.
busy_threads / max_threads,active_connections / max_pool_size - Saturation: 포화도.
request_queue_length,connection_waiters,wait_time_p95 - Errors: 실패.
timeout_count,rejected_execution,SQL timeout
핵심은 대기열입니다. CPU는 여유가 있어도 큐가 길면 사용자 체감은 급격히 나빠집니다.
2) Spring Boot에서 계측 활성화(메트릭 + 트레이스)
2-1) 의존성
// build.gradle
dependencies {
implementation("io.micrometer:micrometer-registry-prometheus")
implementation("org.springframework.boot:spring-boot-starter-actuator")
runtimeOnly("io.opentelemetry.javaagent:opentelemetry-javaagent:2.7.0")
}
2-2) Actuator 노출
# application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: order-api
2-3) 톰캣/히카리 지표
Spring Boot는 기본으로 Tomcat, HikariCP 지표를 Micrometer로 노출합니다(Undertow/Jetty도 유사).
- Tomcat:
tomcat.threads.current,tomcat.threads.busy,tomcat.global.sent/received - Hikari:
hikaricp.connections.active,hikaricp.connections.idle,hikaricp.connections.pending,hikaricp.connections.timeout.total
2-4) OTel 에이전트(트레이스)
# 실행 환경변수
JAVA_TOOL_OPTIONS="-javaagent:/opt/otel/opentelemetry-javaagent.jar"
OTEL_EXPORTER_OTLP_ENDPOINT="http://127.0.0.1:4317"
OTEL_SERVICE_NAME="order-api"
트레이스에는 HTTP 서버 스팬과 DB/외부 API 스팬의 구간 지연이 기록됩니다. 이후 대기열 지표와 함께 상관분석합니다.
3) Prometheus 기록식(Recording Rules)로 USE 파생지표 만들기
# /etc/prometheus/rules/use.yml
groups:
- name: use.threadpool
rules:
- record: svc:thread:utilization
expr: (sum by (application) (tomcat_threads_busy)) / ignoring() (sum by (application) (tomcat_threads_config_max))
- record: svc:thread:saturation
expr: clamp_max(sum by (application) (tomcat_threads_current) - sum by (application) (tomcat_threads_config_max), 0)
- record: svc:thread:errors_rate
expr: rate(tomcat_rejected_connections_total[5m])
- name: use.hikari
rules:
- record: db:pool:utilization
expr: (sum by (pool) (hikaricp_connections_active)) / ignoring() (sum by (pool) (hikaricp_connections_max))
- record: db:pool:saturation
expr: sum by (pool) (hikaricp_connections_pending)
- record: db:pool:errors_rate
expr: rate(hikaricp_connections_timeout_total[5m])
utilization은 0~1 범위로 직관적이고, saturation은 대기자/대기열을 직접 나타냅니다.
에러는 거부/타임아웃 카운터의 속도로 봅니다.
4) Grafana 패널 설계(현장에서 바로 쓰는 구성)
- 상단: 스레드 사용률(라인), 대기열 길이(막대), 거부율(라인, 보조축)
- 중단: DB 풀 사용률(라인), pending(막대), timeout rate(라인)
- 하단: 트레이스 기반 p95 구간 지연(웹/DB/외부API) — Tempo 링크 연결
라인+막대 조합으로 사용률↑ → 대기열↑ → 지연↑의 인과관계를 한눈에 보이게 만듭니다.
5) 경보 규칙: SLO와 연결
# /etc/prometheus/rules/use-alerts.yml
groups:
- name: use.alerts
rules:
- alert: ThreadPoolSaturated
expr: svc:thread:utilization > 0.85 and svc:thread:saturation > 0
for: 10m
labels: {severity: warning}
annotations:
summary: "웹 스레드 풀 포화"
description: "사용률 >85%이며 대기열 발생. 임시 스케일아웃 또는 max-threads 상향 검토"
- alert: HikariPendingHigh
expr: db:pool:saturation > 10
for: 10m
labels: {severity: warning}
annotations:
summary: "DB 커넥션 대기자 과다"
description: "pending > 10. 커넥션 풀/DB 스케일 확인"
- alert: HikariTimeouts
expr: db:pool:errors_rate > 0
for: 5m
labels: {severity: critical}
annotations:
summary: "DB 커넥션 타임아웃 발생"
description: "사용자 오류 노출 가능. 즉시 원인 파악 필요(Tempo로 트레이스 이동)"
6) 자동 스케일링 지표 설계(HPA/수동 스케일 기준)
Kubernetes HPA를 예로 들지만 VM/베어메탈에서도 동일 지표를 수동 스케일 트리거로 쓸 수 있습니다.
- 주 지표:
svc:thread:utilization(목표 0.6~0.7),svc:thread:saturation(0 유지) - 보조 지표:
db:pool:utilization(목표 0.6),db:pool:saturation(0 유지), p95 응답시간
6-1) HPA 예시(Prometheus Adapter)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-api
spec:
minReplicas: 2
maxReplicas: 10
metrics:
- type: Pods
pods:
metric:
name: svc_thread_utilization
target:
type: AverageValue
averageValue: "0.70"
- type: Pods
pods:
metric:
name: svc_thread_saturation
target:
type: AverageValue
averageValue: "0"
Prometheus Adapter에서 svc:thread:utilization을 svc_thread_utilization 등의 커스텀 메트릭 이름으로 노출해야 합니다.
7) 톰캣/히카리 튜닝 가이드(실전 값)
- Tomcat:
server.tomcat.threads.max를 CPU 코어×2~4로 시작. 포화 시 스케일아웃 우선. - HikariCP:
maximumPoolSize는 DB가 허용하는 동시 연결 한계를 고려(인덱스/버퍼캐시/락 경합). - 타임아웃:
connectionTimeout은 2~5초,socketTimeout은 p95+α. - 스레드+커넥션 밸런스: 웹 스레드가 DB 동시 연결 수를 지속 초과하면 대기열이 필연적으로 생깁니다.
8) 트레이스와 USE의 상관분석(Tempo ↔ Grafana)
- 대시보드에서
svc:thread:saturation이 튄 시점 클릭 - Templating/데이터 링크로 Tempo 이동 → 해당 시간대 트레이스 집합 조회
- 느린 스팬(외부 API/DB)을 열어 대기 시간(queue/wait)을 확인
대기열이 증가하면서 DB 호출 스팬의 duration이 늘어나면 풀 병목이 맞습니다.
9) 부하 테스트로 임계 확인(짧은 루틴)
# 10분, 초당 200요청까지 증가하며 p95/대기열 추적(예: hey)
hey -z 10m -q 200 -c 100 https://app.example.com/api/orders/1
테스트 동안 사용률→대기열→p95가 어떻게 연동되는지 그래프를 스냅샷으로 남겨 팀 기준선을 만듭니다.
10) 운영 체크리스트(복붙)
- Micrometer/Actuator Prometheus 노출 확인(
/actuator/prometheus) - Tomcat/Hikari 지표 수집 OK, 기록식(
use.yml) 적용 - Grafana 패널: 사용률·대기열·타임아웃을 한 화면에
- Alert: 대기열 > 0 이면 경고, 타임아웃 발생 시 즉시 알림
- HPA/수동 스케일 기준: 사용률 0.7 목표, 대기열 0
- Tempo 링크: 느린 시점 → 트레이스 드릴다운
결론: CPU가 아니라 대기열을 보며 스케일합니다. USE 지표로 스레드/커넥션 풀의 병목을 조기에 발견하고, 트레이스로 바로 원인을 확인하세요. 그러면 “사용자 체감”에 맞춘 용량 계획과 자동 스케일링이 가능합니다.