Locust + OpenTelemetry로 부하 테스트를 “관측 가능”하게 만든 기록
부하 테스트 결과를 수치에서 끝내지 않고, 병목 원인까지 연결해 본 과정
Locust + OpenTelemetry로 부하 테스트를 “관측 가능”하게 만든 기록
부하 테스트 결과를 수치에서 끝내지 않고, 병목 원인까지 연결해 본 과정
🎈 도입
이전에는 PortfoGram에서 K6를 사용해 부하 테스트를 진행했습니다. 그 과정에서 “어디서 느려지는지”를 찾는 데는 어느 정도 성과가 있었지만, 한 가지 한계가 남았습니다.
부하 테스트 도구는 보통 다음과 같은 결과 지표를 잘 보여줍니다.
- 요청 수(RPS)
- 응답시간(p50, p95, p99)
- 에러율
이 숫자만으로도 “느리다”는 사실은 알 수 있습니다. 하지만 왜 느린지까지는 알기 어렵습니다.
예를 들어 p95나 p99가 갑자기 튀는 상황을 발견해도,
- DB 쿼리가 느린지
- 커넥션 풀 대기가 발생하는지
- Redis 구간이 밀리는지
- 애플리케이션 내부에서 다른 블로킹이 있는지
는 수치만으로 바로 알 수 없습니다.
PortfoGram은 실제 운영 트래픽을 받는 서비스는 아니기 때문에, 실사용자 대신 부하 테스트를 통해 시스템 반응을 검증하고 있습니다. 또한 관측 도구로는 이미 Grafana Tempo + OpenTelemetry를 도입해둔 상태였습니다.
문제는 여기서 끝나지 않았습니다.
Tempo에는 애플리케이션 내부 trace가 들어오고 있었지만, 그 trace가 방금 실행한 부하 테스트 요청인지를 trace 자체만으로 명확하게 식별하기는 어려웠습니다.
느린 /auth/login trace를 하나 찾더라도, 그 요청이 현재 부하 테스트 중 발생한 것인지, 수동 테스트 요청인지, 혹은 다른 호출인지 바로 구분하기는 쉽지 않았습니다.
즉, 응답시간이 느리다는 사실은 확인할 수 있었지만, 그 지연을 특정 테스트 요청과 직접 연결해 해석하기에는 한계가 있었습니다.
이번 작업은 이 한계를 줄이기 위한 시도였습니다.
부하 테스트 결과를 단순한 수치가 아니라, trace와 함께 해석할 수 있는 형태로 만들 수 없을까?
! 그 전에, 뒤에서 계속 등장할 trace와 span을 짧게 정리하고 가겠습니다.
이 글에서는 trace와 span이라는 표현이 계속 나옵니다. 뒤에서 계속 등장하는 개념이라, 여기서 짧게 정리하고 가겠습니다.
- trace: 요청 한 번의 전체 흐름
- span: 그 흐름 안에 포함된 개별 작업 단위
예를 들어 로그인 요청 하나가 trace라면,
- Controller 진입
- DB 조회
- Redis 저장
같은 단계 하나하나가 span입니다.
즉, trace를 보면 “이 요청이 전체적으로 어디를 지나갔는지”를 볼 수 있고, span을 보면 “각 단계가 얼마나 걸렸는지”를 나눠서 볼 수 있습니다.
🤔 기존 방식의 한계 — 느린 건 보이는데, 왜 느린지는 보이지 않았다
기존 Locust 결과에서는 /auth/login 요청의 응답시간이 비정상적으로 길어지는 구간이 있었습니다.
통계 화면 기준으로 보면, 중앙값은 약 28초였고 상위 지연 구간은 30초 이상까지 늘어났습니다. 에러율은 0%였습니다..

즉, 요청이 실패한 것은 아니었습니다. 서비스가 완전히 멈춘 상태도 아니었고, 알람이 울릴 정도의 에러도 보이지 않았습니다. 다만 요청이 결국 처리되기는 하지만, 지나치게 느리게 처리되고 있는 상태였습니다.
이런 상황에서는 자연스럽게 몇 가지 가설이 떠오릅니다.
- DB 쿼리가 느린가?
- 커넥션 풀 대기가 발생하는가?
- Redis 구간이 밀리는가?
문제는, 기존 방식으로는 이 가설들을 한 번에 이어서 확인하기 어려웠다는 점입니다.
- Locust에서는 응답시간과 에러율 같은 결과 지표를 보고
- Grafana에서는 메트릭을 따로 확인하고
- Tempo에서는 trace를 따로 열어봐야 했습니다
각 도구는 분명 유용했습니다. 하지만 하나의 느린 요청을 공통 기준으로 연결해서 보기 어렵다는 한계가 있었습니다.

특히 Tempo를 열면 애플리케이션 내부 trace는 보였지만, 그 요청의 출발점인 Locust 자체는 trace에 포함되지 않았습니다.
그래서 느린 /auth/login trace를 하나 찾더라도, 그 요청이
- 지금 방금 실행한 부하 테스트 요청인지
- 수동으로 호출한 요청인지
- 다른 내부 요청인지
를 trace 자체만으로는 명확하게 구분하기 어려웠습니다.
즉, “느리다”는 사실은 보이는데, 그 느린 요청이 내가 만든 부하 테스트 요청인지까지는 바로 연결되지 않는 상태였습니다.
staging처럼 비교적 격리된 환경에서 테스트를 수행하면 노이즈는 줄일 수 있습니다. 하지만 그것만으로는 요청의 출발점이 trace 안에 기록되지는 않습니다.
이번 작업의 목적은 단순히 테스트 환경을 분리하는 것이 아니라, 부하 발생기부터 애플리케이션 내부까지 하나의 trace로 연결해서, 응답시간 수치와 내부 병목을 같은 흐름 안에서 분석할 수 있게 만드는 것이었습니다.
이 지점에서 질문이 분명해졌습니다.
Locust가 보내는 요청도 trace의 시작점으로 포함시킬 수 없을까?
💬 K6에서 Locust로 바꾼 이유
이전 글에서는 K6로 부하 테스트를 진행했습니다. 하지만 이번에는 Locust로 바꿨습니다. 이유는 두 가지였습니다.
- Python 기반 계측의 단순함
OpenTelemetry Python SDK와
opentelemetry-instrumentation-requests를 활용해 HTTP 요청에 trace context를 주입하는 방식이, Python 기반인 Locust에서는 훨씬 자연스럽습니다. 반면 K6는 JavaScript 기반이라, 같은 구성을 만들기 위해 별도의 확장 구성이나 추가 작업을 더 고려해야 했습니다. - Kubernetes Operator 지원 Locust는 공식 Kubernetes Operator를 제공하기 때문에, CRD 기반으로 테스트를 선언형으로 실행할 수 있습니다. 이미 EKS 환경에서 운영 중이었기 때문에, 이 방식은 테스트를 일회성 실행이 아니라 반복 가능한 리소스 형태로 관리하는 데도 잘 맞았습니다.
즉, 이번 전환은 단순한 도구 변경이 아니라,
- trace 계측을 더 자연스럽게 붙이고
- Kubernetes 환경에서 반복 가능한 테스트 구조를 만들기 위한
선택에 가까웠습니다.
💡부하 발생기를 trace의 시작점으로 만들자
문제를 풀기 위한 핵심 아이디어는 단순했습니다.
Locust가 보내는 요청도 trace의 일부가 아니라, 아예 trace의 시작점이 되게 만들자
HTTP 통신에는 traceparent라는 표준 헤더가 있습니다. 이 헤더는 “이 요청이 어떤 trace에 속하는지”를 다음 서비스에 전달하는 역할을 합니다.
Spring Boot에 OpenTelemetry Java Agent가 적용되어 있으면, 애플리케이션은 이 헤더를 읽고 같은 trace 안에서 span을 이어받습니다.
그렇다면 Locust가 요청을 보낼 때도 같은 trace context를 함께 보내면, 이전에는 애플리케이션 내부에서만 시작되던 trace를 다음처럼 확장할 수 있습니다.
- Locust 요청 시작
- Spring Boot 진입
- Controller
- Repository
- DB
- Redis
즉, 부하 발생기부터 애플리케이션 내부 처리까지 하나의 trace로 연결되는 구조입니다.
이번 작업에서는 Locust 쪽에 OpenTelemetry Python SDK를 적용하고, requests 계층을 계측해서 이 흐름을 만들었습니다.
핵심은 RequestsInstrumentor().instrument() 설정입니다.
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.requests import RequestsInstrumentor
OTEL_ENDPOINT = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://otel-collector:4317")
_provider = TracerProvider(
resource=Resource.create({"service.name": "locust-portfogram"})
)
_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint=OTEL_ENDPOINT, insecure=True),
max_export_batch_size=512,
schedule_delay_millis=1000,
)
)
trace.set_tracer_provider(_provider)
RequestsInstrumentor().instrument() # 이 한 줄이 모든 HTTP 요청에 traceparent 헤더를 자동으로 주입합니다
이 설정을 적용하면 Locust가 내부적으로 사용하는 Python requests 라이브러리가 계측되고, Locust가 보내는 HTTP 요청에 trace context가 자동으로 포함됩니다.
덕분에 기존 부하 테스트 시나리오 자체를 크게 바꾸지 않고도, 요청의 출발점을 trace 안에 포함시킬 수 있었습니다.
여기서 한 가지 더 확인해야 할 부분이 있었습니다.
Q. 테스트가 끝나는 순간, 마지막 trace가 저장되기 전에 사라질 수 있지 않나 ?
OpenTelemetry는 span을 바로바로 보내지 않고, 잠시 모아서 한 번에 전송할 수 있습니다. 이 방식이 성능에는 유리하지만, 테스트가 끝나자마자 프로세스가 종료되면 마지막 span 일부가 전송되기 전에 종료될 가능성이 있습니다.
그래서 이번 구성에서는 테스트 종료 시점에 exporter가 정리(flush / shutdown)될 수 있도록 종료 흐름도 함께 고려했습니다. 즉, 테스트 막바지에 생성된 trace가 유실되지 않도록 종료 방식까지 포함해 설계한 것입니다.
Q. Locust Worker가 여러 개면, 서로 다른 요청 trace가 섞이거나 충돌하지 않나?
각 Worker는 독립적으로 trace를 생성하고, OpenTelemetry는 요청마다 고유한 trace ID를 만듭니다. 그래서 Worker 수가 늘어나더라도, 서로 다른 요청이 같은 trace로 섞일 가능성은 매우 낮습니다.
다만 Tempo에서는 모든 Worker가 같은 service.name(locust-portfogram)으로 보이기 때문에, 개별 Worker를 따로 보기보다는 하나의 부하 발생기 서비스처럼 묶어서 조회하는 방식에 가깝습니다.

아래 Tempo Search 화면에서도 이 점을 확인할 수 있습니다.
service.name 기준으로 보면, 부하 발생기 쪽은 locust-portfogram, 애플리케이션 쪽은 spring-boot로 구분됩니다.
이 구조 덕분에 두 가지를 동시에 얻게 됐습니다.
- 연결: 하나의 요청을 Locust부터 애플리케이션 내부까지 같은 trace로 추적 가능
- 구분: Tempo에서 테스트 트래픽을
locust-portfogram기준으로 더 명확하게 식별 가능
즉, 이전에는 애플리케이션 내부 trace만 보였다면, 이제는 “이 요청이 어디서 시작됐는가”까지 포함된 trace를 볼 수 있게 된 것입니다.
🛠️ 구현 방식 — Locust + OpenTelemetry 연동
구현에 들어가기 전에, 이번 부하 테스트 환경이 어떤 구성으로 연결되어 있는지 먼저 정리했습니다.
이번 구조의 핵심은 단순히 “Locust로 요청을 보낸다”가 아니라, 부하 발생 → trace context 주입 → trace 수집 → trace 저장/조회 → 메트릭 교차 확인까지 한 흐름으로 묶는 것이었습니다.
구성 요소를 역할 기준으로 나누면 다음과 같습니다.

즉, 이번 구조는 “부하 테스트 도구” 하나만 추가한 것이 아니라, 부하 테스트 결과를 trace와 메트릭으로 함께 해석할 수 있도록 연결한 관측 파이프라인에 가깝습니다.
📦 커스텀 Locust 이미지
Locust 기본 이미지 위에 OpenTelemetry 관련 패키지를 추가해, 요청 계층을 계측할 수 있도록 구성했습니다.
FROM locustio/locust:2.20.0
USER root
RUN pip install --no-cache-dir \
opentelemetry-api==1.20.0 \
opentelemetry-sdk==1.20.0 \
opentelemetry-exporter-otlp-proto-grpc==1.20.0 \
opentelemetry-instrumentation-requests==0.41b0
USER locust
중요한 점은, 기존 테스트 시나리오를 전면 수정한 것이 아니라 Locust가 내부적으로 사용하는 HTTP 요청 계층에 trace context를 실어 보낼 수 있게 만든 것입니다.
즉, 기존 locustfile.py의 사용자 행동 로직은 유지하면서도,
각 요청이 trace 안에서 출발점을 갖도록 만들었습니다.
☸️ Kubernetes Operator로 선언형 배포
Locust는 Kubernetes Operator를 제공하기 때문에, 이번 테스트는 선언형으로 실행했습니다.
apiVersion: locust.io/v1
kind: LocustTest
spec:
image: minimeisme/locust-otel:v1.0
workerReplicas: 1
configMap: locust-scripts
masterCommandSeed: >-
--locustfile /lotest/src/locustfile.py
--autostart -u 50 -r 5 --run-time 10m
이 방식의 장점은 다음과 같습니다.
- 테스트 실행 조건을 매니페스트로 남길 수 있고
- 마스터/워커 구성을 반복 가능한 형태로 관리할 수 있으며
- 부하 테스트를 “수동 실행 스크립트”가 아니라 운영 가능한 리소스 형태로 다룰 수 있다는 점입니다
즉, 한 번 임시로 돌리는 테스트가 아니라 재실행 가능한 부하 테스트 환경으로 정리했다는 데 의미가 있었습니다.
🎭 시나리오 구성 — 단순 반복이 아니라, 읽기/쓰기 패턴을 섞기
부하 테스트 시나리오는 단순한 단일 API 반복 호출로 구성하지 않았습니다. 대신 실제 서비스에서 발생할 법한 행동 패턴을 단순화해서, 3가지 사용자 클래스로 나눴습니다.

즉, 이번 시나리오의 목적은 “실제 트래픽을 완벽히 복제”하는 것이 아니라, 부하가 걸릴 때 어떤 요청 패턴에서 커넥션 풀이 먼저 압박받는지 보기 위한 최소한의 행동 모델을 만드는 것이었습니다.
Q . 이 비율이 정말 실제 트래픽을 반영한다고 볼 수 있나?
완전히 그렇다고 말할 수는 없습니다.
PortfoGram은 실제 운영 트래픽이 있는 서비스가 아니기 때문에, 이 비율은 어디까지나 운영을 가정한 테스트용 모델입니다.
이번 설정에서 중요했던 것은 “정확한 실사용 비율 재현”보다, 읽기 위주 / 혼합형 / 쓰기 집중형 요청이 함께 존재할 때 커넥션 풀이 어떤 패턴으로 소비되는지 확인하는 것이었습니다.
그래서 6:3:1 비율은 다음 판단 기준으로 사용했습니다.
- 포트폴리오형 SNS 서비스 특성상 조회 비중이 가장 높을 것
- 로그인 이후 상호작용 요청은 그보다 적을 것
- 댓글/재조회 같은 쓰기 중심 흐름은 상대적으로 적을 것
즉, 이 비율은 “실제 트래픽 재현”이라기보다, 병목이 드러날 만큼 현실적인 요청 혼합을 만들기 위한 테스트 비율에 가깝습니다.
이번 글의 목적도 “실제 운영 트래픽과 100% 동일한 분포를 증명”하는 것이 아니라, 이런 혼합 패턴 안에서 유의미한 병목이 재현되는지를 확인하는 데 있었습니다.
정리하면
이번 구현의 핵심은 세 가지였습니다.
- Locust 요청이 trace 안에서 출발점을 갖도록 만들기
- 테스트를 Kubernetes 환경에서 반복 가능하게 실행하기
- 읽기/쓰기 패턴이 섞인 시나리오로 병목이 드러나는 조건 만들기
즉, 단순히 부하를 주는 것이 아니라 “관측 가능한 부하 테스트”가 성립하도록 실행 구조와 시나리오를 함께 설계한 것이 이번 구현의 핵심이었습니다.
🔍 실제로 무엇이 보이는가
구성이 갖춰진 뒤에는, 실제로 무엇이 보이는지를 확인해야 했습니다.
테스트를 실행하면서 Grafana 대시보드와 Locust Charts를 함께 확인했습니다. 이번에 보고 싶었던 것은 단순히 “느리다”는 결과가 아니라, 느려지는 시점에 시스템 내부에서 어떤 변화가 함께 나타나는가였습니다.

먼저 Grafana 대시보드에서는 HikariCP 커넥션 풀 지표와 HTTP 응답 지표를 한 화면에 올려두고 관찰했습니다. 이렇게 보면 응답시간이 흔들리는 구간과 커넥션 풀 사용 패턴을 같은 시간축으로 비교할 수 있습니다.

HikariCP 패널을 보면 active 커넥션이 한동안 높은 수준으로 유지되며, pool 상한에 가까워지는 구간이 보입니다.
이 장면만으로 “풀이 완전히 고갈됐다”고 단정할 수는 없지만, 커넥션 풀이 높은 점유 상태로 유지되고 있다는 점은 분명히 읽을 수 있습니다.

동시에 Locust Charts를 보면, 테스트 초반 응답시간이 크게 치솟았다가 이후 점차 내려오는 패턴이 나타납니다.
여기서 중요한 점은, 이 차트는 p99 차트가 아니라 50th percentile과 95th percentile 기준 차트라는 것입니다.
즉, 이 화면에서 정확히 말할 수 있는 것은 초반 상위 지연 구간(95th percentile)이 급격히 악화되었다는 사실입니다.
이 두 화면을 함께 보면 이런 가설이 생깁니다.
응답시간이 급등하는 시점과, 커넥션 풀 사용률이 높아지는 시점이 겹친다. 따라서 커넥션 풀 대기가 병목 후보일 수 있다.
이전 방식이었다면 분석은 여기서 멈췄을 가능성이 큽니다. 가설은 생기지만, 그 가설을 요청 단위로 확인할 수 있는 연결 고리가 없었기 때문입니다. 이번에는 여기서 Tempo로 넘어가, 같은 시점의 느린 요청을 trace로 직접 확인했습니다
🔎 Tempo에서 trace 열기
Locust 쪽 span까지 포함된 뒤에는, Tempo에서 테스트 요청을 훨씬 명확하게 좁혀 볼 수 있었습니다.
이전에는 Spring Boot 내부 trace만 보여서 “느린 요청”은 찾을 수 있어도, 그 요청이 방금 부하 테스트로 만든 요청인지를 trace 자체만으로 확신하기 어려웠습니다.
이제는 locust-portfogram을 기준으로 테스트 트래픽을 먼저 좁힌 뒤,
그 안에서 /auth/login 요청만 선택해 waterfall을 확인할 수 있었습니다.
resource.service.name="locust-portfogram" && span.http.url=~".*auth/login.*"

목록에서 하나를 눌러서 waterfall을 봅니다.

trace를 열면 가장 위에는 locust-portfogram span이 보이고,
그 아래로 Spring Boot 내부 span이 이어집니다.
즉, 하나의 요청 안에서 다음 흐름을 한 번에 볼 수 있게 됩니다.
- Locust 요청 시작
- Spring Boot 진입
TokenController.loginfindByEmailSELECTSETEX
이 구조 덕분에, 이전에는 하나의 “응답시간”으로만 보이던 지연을 이제는 구간별 시간으로 나눠서 해석할 수 있게 됐습니다.
🚨 핵심 발견 — SQL이 아니라, 요청 내부의 긴 대기 구간
이번 분석에서 가장 중요한 지점은 여기였습니다.
trace를 보면 상위 span은 매우 길지만, 하위 span인 DB 조회와 Redis 처리 시간은 상대적으로 짧습니다.



예를 들어 한 요청에서는:
- 전체 요청 시간은 20초 이상으로 길고
TokenController.loginspan도 길게 유지되지만findByEmail은 상대적으로 짧고- 실제
SELECT실행 시간도 수 ms ~ 수십 ms 수준이며 SETEX역시 상위 span 전체에 비하면 짧은 편입니다
즉, 요청 전체는 오래 걸리는데, 계측된 하위 작업들의 합은 그보다 훨씬 짧다는 패턴이 보였습니다.
이 말은 곧, 요청 내부 어딘가에 하위 span으로 직접 설명되지 않는 긴 대기 구간이 있다는 뜻입니다.
여기서 주의해야 할 점이 있습니다.
이 차이값만 보고 바로 “이게 정확히 HikariCP 커넥션 acquire 대기 24.6초다” 라고 단정하면 과한 해석이 됩니다.
trace 하나만으로 확실히 말할 수 있는 것은 다음까지입니다.
- 상위 span이 길다
- 하위 span 합은 짧다
- 따라서 긴 비계측 공백 구간이 존재한다
다만, 이 구간을 같은 시점의 HikariCP 메트릭과 함께 보면 커넥션 풀 사용률이 높게 유지되던 시점과 겹칩니다. 그래서 이번 글에서는 이 구간을 커넥션 풀 대기 후보 구간으로 해석했습니다.
즉, 이번 분석의 핵심은 다음과 같습니다.
- SQL 자체는 상대적으로 짧았다
- 요청 전체는 매우 길었다
- 그 차이가 커넥션 풀 고점유 시점과 함께 나타났다
이전 방식이었다면 “DB가 느리다”고 오해하고 쿼리 튜닝부터 시작했을 가능성이 컸습니다. 하지만 이번에는 trace와 메트릭을 함께 보면서, 병목 후보를 DB 실행 자체가 아니라 요청 중간의 대기 구간으로 더 정확하게 좁힐 수 있었습니다.
또 한 가지 흥미로운 점은, locust-portfogram span과 spring-boot span 시작 시점 사이에도 짧은 차이가 보인다는 점입니다.
이 값은 요청이 부하 발생기에서 출발한 뒤, 애플리케이션이 첫 span을 생성하기 전까지의 전단 구간으로 볼 수 있습니다.
여기에는 네트워크, 로드밸런서, Ingress, 애플리케이션 진입 전 처리 시간이 함께 포함될 수 있습니다.
즉, trace를 연결한 뒤에는 애플리케이션 내부뿐 아니라 “애플리케이션에 도달하기 전” 구간도 분리해서 볼 수 있는 여지가 생겼습니다.
Q. TokenController와 findByEmail 사이의 긴 시간 차이를 꼭 HikariCP 대기라고 볼 수 있나?
아닙니다. trace의 span 차이만으로는 “정확히 HikariCP 대기”라고 단정할 수 없습니다. 이 차이는 단지 상위 span은 길지만, 하위 span으로 설명되지 않는 긴 대기 구간이 있다는 사실을 보여줄 뿐입니다.
그래서 이 글에서는 trace만 보고 결론을 내리지 않고, 같은 시점의 HikariCP 메트릭을 함께 확인했습니다.
- 커넥션 풀 사용률이 높게 유지되는지
- active 커넥션이 상한에 가까워지는지
- (있다면) pending 대기가 증가하는지
이 신호가 같은 시간대에 함께 나타났기 때문에, 이 구간을 커넥션 풀 대기 후보로 해석했습니다.
즉, 이번 해석은 trace 하나의 숫자가 아니라, trace와 메트릭을 함께 보고 내린 판단입니다.
Q. OpenTelemetry 계측 자체가 응답시간을 늘린 건 아닐까?
계측은 분명 약간의 오버헤드를 추가합니다. 다만 이번에 관찰된 지연은 수 ms 수준이 아니라 수 초~수십 초 단위였기 때문에, 계측 오버헤드만으로 설명하기는 어렵습니다.
또한 같은 계측이 적용된 상태에서 pool 설정을 조정했을 때 응답 패턴이 달라졌기 때문에, 이번 분석에서는 OTel 자체보다 애플리케이션 내부 대기 구간과 커넥션 풀 상태가 더 큰 원인이라고 판단했습니다.
📊 개선 — pool 크기 조정과 검증
병목 후보가 커넥션 풀 대기 구간으로 좁혀진 뒤에는, HikariCP pool size를 조정해 같은 시나리오로 다시 검증했습니다.
기존 pool=10에서는 동시 요청이 몰릴 때 커넥션 풀 사용률이 빠르게 높아졌고,
응답시간 급등 구간도 함께 나타났습니다.


이를 pool=25로 조정한 뒤 다시 테스트해 보니, 이전보다 응답시간의 급등 폭이 줄고, 그래프상 흔들림도 완화되는 패턴이 보였습니다.
또한 trace에서 보이던 긴 대기 구간도 이전보다 덜 두드러지게 나타났습니다.
여기서 중요한 것은 “숫자를 키우니까 좋아졌다”가 아닙니다.
핵심은
- 같은 시나리오
- 같은 관측 방식
- 같은 비교 기준
으로 다시 측정했다는 점입니다.
즉, 이번 개선은 관측 결과를 바탕으로 설정을 바꾸고 다시 검증한 실험이었습니다.

표로 정리하면 변화는 더 명확해집니다.
findByEmail응답시간 감소SETEX지연 감소- 응답시간 급등 구간 완화
- 에러율은 유지
trace에서 보이던 긴 비계측 대기 구간이 더 이상 크게 드러나지 않았고, 메트릭상 커넥션 풀 사용 패턴도 이전보다 안정적으로 보였습니다.
Q. SETEX도 함께 빨라졌다면, Redis가 원인이었던 것은 아닐까?
Redis는 커넥션 풀 자체와 직접 연결된 병목 지점은 아닙니다.
다만 pool=10 상태에서는 TokenController 구간에서 커넥션을 기다리는 시간이 길어지면서, 전체 요청 흐름 자체가 뒤로 밀렸습니다. 그 과정에서 Redis 연결 재사용이나 처리 타이밍도 함께 영향을 받았고, 결과적으로 SETEX 구간 역시 더 느리게 관측됐습니다.
반대로 pool=25로 조정한 뒤에는 커넥션 대기 구간이 줄어들면서 TokenController 흐름이 앞당겨졌고, Redis 작업도 재연결이나 불필요한 지연 없이 더 빠르게 수행됐습니다.
즉, 이번 결과에서 Redis 지연 감소는 Redis 자체가 1차 병목이었다기보다, 앞단의 DB 커넥션 병목이 완화되면서 함께 개선된 간접 효과로 보는 편이 더 정확하다고 생각합니다.
⚠️ pool을 무조건 크게 잡으면 왜 안될까 ..
커넥션 풀은 크게 잡는다고 항상 좋은 것이 아닙니다.
풀 크기를 키우면 애플리케이션 내부 대기는 줄어들 수 있지만, 반대로 DB가 동시에 감당해야 하는 연결 수는 증가합니다.
실제로 더 큰 값으로 실험했을 때는, 애플리케이션 쪽 대기 구간은 줄어드는 대신 DB에서 Too many connections가 발생했습니다.
이 지점에서 중요한 것은, 문제가 “해결”된 것이 아니라 병목 위치가 애플리케이션에서 DB로 이동할 수 있다는 점입니다.
즉, 커넥션 풀 크기는 단순히 크게 잡는 값이 아니라 다음 요소를 함께 고려해서 정해야 합니다.
- DB 최대 연결 수
- 애플리케이션 인스턴스 수
- 예상 동시성
- 순간적인 부하 특성
이번 테스트에서는 pool=25가 “무조건 정답”이라기보다,
현재 조건에서 애플리케이션 대기와 DB 연결 한도 사이의 균형점에 가까운 값으로 보였습니다.
그래서 이번 조정은 단순한 성능 튜닝이 아니라, Capacity Planning 관점에서 적정 설정을 찾는 과정이기도 했습니다.
✅ 정리
이번 개선 과정을 겪으면서 가장 크게 달라진 것은 숫자보다 관점이었습니다.
기존에는 부하 테스트를 통해 “응답시간이 얼마나 늘어나는가”를 확인하는 데 집중했습니다. 즉, 결과를 보는 테스트였습니다.
이번에는 한 단계 더 나아가, 그 시간이 요청 내부의 어느 구간에서 소비되는지를 trace로 좁혀 볼 수 있게 됐습니다.
정리하면 이번 변화는 이렇습니다.

- 이전에는 “p95가 높다”는 사실만 보였다면
- 이제는 느린 요청을 trace로 열어
- 같은 요청의 span 계층을 보고
- 메트릭과 교차 검증해
- 병목 후보를 좁히고
- 설정 변경 후 다시 검증할 수 있게 됐습니다
즉, 부하 테스트가 단순한 결과 측정에서 끝나는 것이 아니라, 병목을 추적하고 설정을 검증하는 분석 루프로 확장된 것입니다.
이번 작업의 핵심을 한 문장으로 정리하면 이렇습니다
Locust와 OpenTelemetry를 연결해, 부하 테스트를 “응답시간 수치”가 아니라 “병목 추적 가능한 관측 데이터”로 바꿨다.
이 변화 덕분에, “얼마나 느린가”를 넘어서 “왜 느린가”에 더 가까이 다가갈 수 있었습니다.
추가 Q. 이 방식은 프로덕션 환경에도 그대로 적용할 수 있을까?
원칙적으로는 가능하지만, 그대로 적용하기보다는 테스트 트래픽과 일반 트래픽이 섞이지 않도록 운영 경계를 분명히 두는 것이 더 중요하다고 생각합니다.
이 구조에서는 service.name="locust-portfogram" 기준으로 테스트 트래픽을 Tempo에서 분리 조회할 수 있기 때문에, 스테이징 환경에서는 비교적 명확하게 테스트 흐름만 골라 분석할 수 있습니다.
다만 프로덕션 환경에서는 실제 사용자 요청과 테스트 트래픽이 동시에 존재할 수 있기 때문에, 부하 테스트를 직접 수행하는 방식은 훨씬 더 신중해야 합니다. 가능하다면 스테이징이나 별도 전용 환경에서 실행하고, 프로덕션에서는 추적 구조와 관측 흐름만 재사용하는 편이 안전합니다.
또한 프로덕션처럼 trace 양이 많아질 수 있는 환경에서는 OTel Collector의 memory_limiter 같은 보호 장치를 함께 설정해, trace 수집 부하가 Collector 자체의 OOM으로 이어지지 않도록 대비해야 합니다.
즉, 이 방식의 핵심은 “프로덕션에 직접 부하를 주는 것”이 아니라, 테스트 트래픽을 구분 가능한 형태로 남기고, 안전한 환경에서 병목을 추적 가능한 구조를 만드는 것에 있습니다.
메타데이터
- post_id
- f66a893eaa70
- slug
- locust-opentelemetry로-부하-테스트를-관측-가능-하게-만든-기록-f66a893eaa70
- url
- https://medium.com/@minina1868/locust-opentelemetry%EB%A1%9C-%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EA%B4%80%EC%B8%A1-%EA%B0%80%EB%8A%A5-%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A0-%EA%B8%B0%EB%A1%9D-f66a893eaa70
- canonical_url
- https://medium.com/@minina1868/locust-opentelemetry%EB%A1%9C-%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EA%B4%80%EC%B8%A1-%EA%B0%80%EB%8A%A5-%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A0-%EA%B8%B0%EB%A1%9D-f66a893eaa70
- author_url
- https://medium.com/@minina1868
- status
- ok
- fetched_at
- 2026-06-09 18:04:40