
야구보구 모니터링

현재 야구보구 모니터링 스택은 Grafana, Prometheus, Loki, Promtail, Tempo로 구성되어 있다.
기존에는 cloudwatch를 사용했는데, cloudwatch의 여러 한계점을 만나게 되었다.
cloudwatch를 사용하면 Log Insights로 엔드포인트별 응답 시간 정도는 볼 수 있었지만, 상태 코드나 비즈니스 지표(구장별 실시간 톡 메시지 수, 하루 동안 전체 체크인 발생 수, 승리요정 랭킹 등) 같은 세밀한 커스텀 메트릭을 코드 단에서 커스텀하기가 어려웠다.
또한 인프라, 데이터베이스, 애플리케이션 로그가 각기 다른 대시보드에 분산되어 있어 하나의 화면에서 통합적으로 관찰하기 어려웠고,
API가 느려질 때 DB 쿼리/외부 API/서버 중 어디가 원인인지도 알 수 없어서 사실상 모니터링의 역할을 다하지 못했다.
Grafana Tempo만큼 AWS x-ray도 비용이 저렴하고 커스텀 메트릭 정의를 잘 할 수 있다는 것을 알고 X-Ray 도입을 고려했지만, IAM 권한이 없어서 사용할 수 없었고, 결국 Prometheus + Grafana + Tempo 기반의 통합 모니터링 스택으로 이전해 문제를 해결했다.
하나의 Grafana 대시보드 안에서 서버 메트릭, API 성능, 로그, 트레이싱을 모두 확인할 수 있도록 구축했다.
아래는 몇 가지 대시보드를 가져왔다.
Spring Boot 3.x Statistics Dashboard
이 대시보드는 Spring Boot Actuator와 Prometheus 연동으로 수집한 내부 메트릭을 시각화한 것이다.
애플리케이션이 얼마나 효율적으로 동작 중인지를 실시간으로 관찰할 수 있다.

Spring Boot Observability

이 대시보드는 Spring Boot Actuator와 Prometheus가 수집한 요청(Request) 단위 지표를 시각화한 것이다.
어떤 API가 얼마나 자주 호출되고, 응답 상태(2xx, 4xx, 5xx)가 어떤 비율로 분포하는지를 한눈에 볼 수 있다.
야구보구 모니터링 스택을 꾸리다
내가 처음 하고 싶었던 건 단순했다.
야구보구 백엔드 서버랑 부하 테스트(k6) 결과를 하나의 모니터링 시스템에서 깔끔하게 보고 싶었다.
먼저 우리 야구보구 모니터링의 스택은 다음과 같다.
- Grafana - 대시보드, 시각화
- Prometheus - 메트릭 저장소
- Loki, Promtail - 로그 수집, 검색
- Tempo - 트레이싱
처음에 작성했던 docker-compose.yml (모니터링 서버)는 대략 다음과 같은 구조였다.
services:
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-remote-write-receiver'
loki:
tempo:
grafana:
ports:
- "80:3000"
여기서 두 가지의 중요한 포인트가 있다.
1. Prometheus 9090 포트가 외부로 노출되어 있다.
기본적으로 Prometheus는 포트 9090에서 실행된다. 다른 서버(ex k6)에서 Prometheus에 데이터 보내거나 접근하려면 이 포트를 통해 통신해야 한다.
2. --web.enable-remote-write-receiver 옵션이 켜져 있다.
보통 Prometheus는 pull 방식(스크레이프)으로 작동하는데, 이 옵션을 켜면 push 방식으로도 데이터를 받을 수 있다.
이 상태에서 백엔드 서버, k6용 스크레이프 설정을 prometheus.yml에 하나씩 추가하기 시작했다.
첫 번째: K6를 Prometheus에서 직접 스크레이프 하려다가 막힘
처음엔 k6를 Prometheus가 긁어가는 pull 방식으로 생각했다.
그래서 prometheus.yml에 이런 블록을 추가했다.
- job_name: k6
metrics_path: /metrics
static_configs:
- targets:
- '10.0.0.238:80'
labels:
app: k6
environment: k6
나는 k6 서버가 /metric 엔드포인트를 열어줄테니까 Prometheus가 주기적으로 메트릭을 가져올 수 있을 것이라고 생각했다.
그리고 모니터링 서버에서 다음과 같이 확인해보았다.
curl http://<모니터링 ip>:80/metrics
결과는 다음과 같았다..
{"timestamp":"2025-11-04T08:23:59.707+00:00","status":404,"error":"Not Found","path":"/metrics"}
K6는 기본적으로 Prometheus가 pull 하지 못하게 되어 있다. 즉, /metrics를 열어주는 exporter 역할이 기본 탑재되어 있지 않다. 그래서 내가 /metric 엔드포인트를 열었더라도 exporter가 없기 때문에 프로메테우스 형식으로 응답을 반환하지 않는다.
여기서 이런 깨달음을 얻었다.
k6를 프로메테우스처럼 /metrics로 긁어오길 기대했는데, 실제로 k6는 기본적으로 /metric이라는 서버를 띄우지 않는다. 즉, prometheus가 긁을 대상이 없다.
이걸 해결하려면 다음의 두 가지 방법이 있다.
1. xk6-prometheus-extension를 설치해서 k6가 /metric 열어주도록 하자.
2. k6에 내장된 remote write 기능 사용하기. k6가 프로메테우스한테 데이터를 직접 post로 밀어넣는 방식.
그래서 Pull 방식 대신 k6가 프로메테우스로 데이터 넣는 push 방식으로 생각해보게 되었다.
두 번째: k6 → Prometheus remote write로 보내기
프로메테우스쪽은 이미 이런 설정이 있는 상태였다.
모니터링 서버의 ~/monitoring$ cat docker-compose.yml
command:
- '--web.enable-remote-write-receiver'
즉, Prometheus는 POST /api/v1/write 엔드포인트를 열어놓고 remote write 포맷으로 들어오는 데이터를 받을 준비가 되어 있었다.
여기서 /api/v1/write 엔드포인트는 프로메테우스 안에 이미 구현되어 있는 공식 API이다. 평소에는 이 엔드포인트가 꺼져있다가, 위의
--web.enable-remote-write-receiver 옵션을 주면 활성화된다.
push 방식이기 때문에 k6 실행도 다음과 같이 해야 했다.
k6 run vps.js \
-e BASE_URL='http://<k6서버 ip>.80:80' \
-e API_PATH='/api/v1/games?date=2025-05-25' \
-o experimental-prometheus-rw=http://<Prometheus 주소>:9090/api/v1/write
Docker로 k6를 띄웠을 때는 docker-compose.yml이 대략 이런 느낌이었다.
k6:
image: grafana/k6:latest
volumes:
- ./k6-scripts:/scripts
command:
- run
- --out
- experimental-prometheus-rw=http://localhost:9090/api/v1/write
- /scripts/script.js
실행해보니 k6로그에 이런 에러가 떴다.
Failed to send the time series data to the endpoint
error="HTTP POST request failed: Post \"http://localhost:9090/api/v1/write\": dial tcp [::1]:9090: connect: connection refused"
이 문제의 원인은 간단했다.
프로메테우스는 모니터링 서버에 있지만, k6는 다른 부하테스트용 서버에서 실행됐다. k6에서 localhost:9090을 사용하면? 그건 그냥 자기 자신을 가리킨다. 즉, k6는 자기 서버 안에서 9090을 찾고 있었고, 그 안엔 프로메테우스가 없었기 때문에자기(k6컨테이너) 안에는 9090포트가 없는데? 하고 연결을 거절한 것이다.
그래서 이렇게 바꿨다.
-o experimental-prometheus-rw=http://<k6 서버ip>:9090/api/v1/write
드디어 Grafana 대시보드가 바뀌었다.

무슨 일이 일어난건가?
1. 모니터링 서버 안에서 k6 run ... 명령을 실행했다. 즉, 모니터링 서버 안에서 k6 서버에 부하를 보냈다.
2. BASE_URL = 'http://<k6 server ip>.80:80'으로 지정했기 때문에 부하의 목표는 k6서버에 있는 api이다.
3. 실제 트래픽 흐름은 다음과 같다. (보내는 쪽) <모니터링서버ip>:k6 → (받는 쪽) <k6서버ip>.80:80/api/v1/games)
4. 동시에 자신의 결과를 같은 서버의 프로메테우스(localhost:9090)로 전송한 것.
궁금증
Q: 부하를 준 서버에서 트래픽이 나올텐데 어떻게 그걸 안 거치고 바로 localhost에서 부하를 쏠 수 있는가?
A: 부하를 받은 서버는 아무 데이터도 프로메테우스로 보내지 않는다. 부하를 보낸 쪽이 테스트 결과를 프로메테우스로 직접 push 하는 구조. 즉, k6가 자기 손에 쥐고 있는 데이터를 바로 프로메테우스에 전송하는 구조.
커스텀 패널
기존에는 k6 부하테스트 결과 보기 위해 prometheus에서 제공하는 공식 템플릿을 사용했는데, 전체 평균 응답시간만 볼 수 있어서 어떤 API가 느린지 한눈에 파악하기 힘들었다.
이번에 Grafana 패널을 API별로 분리해서 응답시간(p99) / 실패율 / RPS를 API별로 따로 보는 대시보드를 만들었다.
API 별로 성능을 나누어 보기 위해 k6 스크립트에 태그를 추가하고 Prometheus 메트릭에서 그 태그를 기준으로 그룹화했다.
// 예시: 체크인 생성 API
http.post(`${BASE}/api/v1/create-checkin`, payload, {
headers: { 'Content-Type': 'application/json' },
tags: { api: 'create-checkin' },
});
// 예시: 승리요정 랭킹 API
http.get(`${BASE}/api/v1/victory-fairy`, {
tags: { api: 'victory-fairy' },
});
이렇게 태그를 달아두면, Prometheus에는 k6_http_req_duration_p99{api="create-checkin"}, k6_http_req_failed_rate{api="victory-fairy"} 처럼 API 이름이 라벨로 붙은 메트릭이 들어오게 된다.
이후 Grafana 패널에서는 k6가 보내준 메트릭을 그대로 사용해서 각 패널마다 다음 쿼리로 API별 지표를 분리해서 그렸다.
1. API별 99% 응답시간 (p99)
k6_http_req_duration_p99
(메트릭 자체가 이미 api 라벨을 포함하고 있어서, 그래프 옵션에서 Legend를 {{api}}로 두고 라인 색으로만 구분해 사용했다.)
2. API 요청 실패율
avg(k6_http_req_failed_rate) by (api)
각 API별 실패율을 평균 내서, 어떤 API가 특히 많이 실패하는지 한눈에 볼 수 있게 했다.
3. API별 요청 실패율
avg(k6_http_req_failed_rate) by (api)
k6_http_reqs_total를 1분 단위 rate로 환산해서, API별로 실제 몇 req/s까지 올라갔는지 확인할 수 있었다.

이렇게 분리해서 보니까, 같은 시점에 부하를 줬어도 특정 API 별로 나누어 볼 수 있어, 나중에 p99가 튀거나 실패율이 급증하는 api를 판단할 수 있을 것 같아서 좋았다.
결론
최종적으로 야구보구의 모니터링 구조는 다음과 같다.
k6 → Prometheus: 데이터를 push 한다(remote write)
Prometheus → Spring boot 서버: pull 방식
Grafana → Prometheus, Loki, Tempo: 시각화
원래는 k6서버에서 promtail을 돌려서 로그를 모니터링 서버로 전송하려고 했는데, 지금 구조에서는 그럴 필요가 없다는 것을 알았다. 왜냐하면 k6서버는 부하를 보내는 역할만 하고 서비스 로직이 없기 때문.
'개발괴발 > Devops' 카테고리의 다른 글
| [AWS] CloudWatch 알람 Discord로 보내기 (SNS + Lambda 활용) (3) | 2025.09.08 |
|---|---|
| [Infra] Nginx와 Docker로 구현하는 단일 서버 Blue-Green 무중단 배포 (0) | 2025.08.29 |
| [GitHub Actions] EC2에서 막힌 CI/CD, Self-hosted Runner로 자동화하기 (5) | 2025.08.01 |
| [Github Actions] CI/CD, 배포 자동화하기 (3) | 2025.07.22 |
| [Jenkins] ERROR: Error fetching remote repo 'origin' (0) | 2024.11.29 |