번외: 부하테스트로 관측 스택을 실전 검증하기 - k6 + HPA + MSA 파이프라인
시리즈 6에서 만든 관측 스택(Prometheus/Grafana/Alertmanager)을 인위적 장애가 아니라 실제 트래픽 증가로 검증한다. k6로 이미지 업로드→비동기 추론 파이프라인 전체에 부하를 걸고, 큐 길이 기반 HPA 오토스케일링, 알림 임계치, 병목 구간까지 확인하는 절차.
목표
시리즈 4에서 관측 스택을 세우고 노드 다운·OOMKill 같은 장애를 "인위적으로" 주입해 탐지 시간을 쟀다. 하지만 실제 운영에서 가장 흔하게 벌어지는 상황은 노드가 죽는 게 아니라 트래픽이 늘어나는 것이다 - 이 경우엔 아무것도 "고장"나지 않았는데도 큐가 쌓이고, 응답이 느려지고, 어느 시점부턴가 임계치를 넘는다. 이 시리즈는 k6로 파이프라인 전체(api-server → RabbitMQ → inference-worker → PostgreSQL)에 실제 부하를 걸어서, 관측 스택이 가짜 장애가 아니라 진짜 트래픽 증가에서도 제 역할을 하는지 검증한다. 부수적으로, 지금까지 수동(kubectl scale)으로만 조정하던 워커 수를 큐 길이 기준 자동 확장(HPA/KEDA)으로 바꾸고, 그게 실제로 작동하는지도 이 부하테스트로 증명한다.
개념: 장애 주입과 부하테스트는 다른 질문에 답한다
시리즈 4의 장애 주입 실험은 "시스템이 죽었을 때 알림이 얼마나 빨리 오는가"를 잰다 - 답은 있음/없음에 가깝다. 부하테스트는 다른 질문을 던진다 - **"트래픽이 늘어날 때 시스템이 어디서부터, 얼마나 천천히 무너지는가"**다. 두 실험은 서로 대체되지 않는다. 장애 주입만 해봤다면 알림 규칙이 "완전히 죽었을 때"는 검증됐어도 "서서히 나빠질 때"는 검증되지 않은 상태다. 관측 스택 없이 부하테스트만 했다면 p95가 올랐다는 건 알아도 그게 게이트웨이인지, 큐 대기인지, 워커 처리인지, DB인지 알 수 없다 - 이번 시리즈는 관측 스택이 있는 상태에서 부하테스트를 하기 때문에 그 질문에 답할 수 있다는 게 핵심이다.
실제 구축 절차
1. 부하 시나리오 설계 - 파이프라인 진입점을 향해
fastapi-appendix-k6에서 다룬 건 단일 CRUD API였다면, 이번엔 비동기 파이프라인 전체를 통과시키는 시나리오가 필요하다 - 요청 하나가 끝나는 지점이 HTTP 응답이 아니라 "결과가 저장되어 조회 가능해지는 시점"이기 때문이다.
// tests/load/pipeline-ramp.js
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'https://api.local';
export const options = {
stages: [
{ duration: '2m', target: 10 }, // 평상시 수준
{ duration: '3m', target: 10 },
{ duration: '2m', target: 80 }, // 급증 - 큐가 쌓이기 시작하는 지점을 넘김
{ duration: '5m', target: 80 }, // 고부하 유지 - 오토스케일이 반응할 시간
{ duration: '2m', target: 0 }, // 회복 확인
],
thresholds: {
http_req_failed: ['rate<0.02'],
http_req_duration: ['p(95)<2000'], // 업로드 접수 자체는 빨라야 함(비동기이므로)
},
};
export default function () {
const res = http.post(`${BASE_URL}/jobs`, JSON.stringify({ image_ref: `sample-${__VU}-${__ITER}.tif` }), {
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${__ENV.TOKEN}` },
});
check(res, { 'job accepted (202)': (r) => r.status === 202 });
sleep(1);
}
업로드 API는 작업을 큐에 넣고 즉시 202 Accepted로 응답하므로(시리즈 2), http_req_duration이 빨라도 파이프라인 뒷단은 여전히 밀릴 수 있다 - 그래서 이 시나리오의 진짜 결과는 k6 리포트가 아니라 다음 단계의 Grafana 대시보드에서 읽는다.
2. 큐 길이 기반 오토스케일 연결 - 수동 스케일을 걷어낸다
시리즈 2까지는 kubectl scale deployment inference-worker --replicas=N으로 수동 조정했다. 이번엔 RabbitMQ 큐 길이를 기준으로 자동 확장되게 KEDA를 붙인다 - CPU 사용률 기준 HPA보다 "큐에 일이 얼마나 쌓였는지"가 이 파이프라인의 실제 부하를 더 정확히 반영하기 때문이다.
# keda-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: inference-worker-scaler
spec:
scaleTargetRef:
name: inference-worker
minReplicaCount: 1
maxReplicaCount: 15 # 위성 플랫폼 회고에서 실제로 확인한 수평 확장 한도와 동일하게 맞춤
triggers:
- type: rabbitmq
metadata:
queueName: inference-queue
host: amqp://rabbitmq.default:5672
queueLength: "20" # 큐에 20건 이상 쌓이면 워커 1개 추가
queueLength: "20" 하나로 "쌓인 작업 수 ÷ 20 = 목표 워커 수"가 계산된다 - 이 숫자를 몇으로 잡느냐가 "얼마나 빨리 늘리고, 얼마나 자원을 아낄지"의 트레이드오프다. 이번 부하테스트로 이 값이 적절한지 실측한다.
3. Grafana 대시보드 - 5개 지표를 한 화면에
시리즈 6에서 이미 Prometheus/Grafana는 있으므로, 이번엔 패널만 새로 구성한다.
| 패널 | 쿼리 | 보는 것 |
|---|---|---|
| 큐 길이 | rabbitmq_queue_messages{queue="inference-queue"} | 부하가 쌓이는 속도, 워커가 이를 소진하는 속도 |
| 워커 파드 수 | kube_deployment_status_replicas{deployment="inference-worker"} | KEDA가 실제로 스케일아웃하는 시점 |
| api-server 응답시간 | histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job="api-server"}[1m])) | 업로드 접수 자체가 밀리는지(게이트웨이/DB 병목 신호) |
| DB 커넥션 풀 사용률 | pg_stat_activity_count / pg_settings_max_connections | 워커가 늘어날 때 DB가 병목이 되는지 |
| 알림 상태 | Alertmanager ALERTS | 정의된 알림이 실제로 Firing 상태로 전환되는 시점 |
4. 부하 실행 + 5개 시각 기록
k6 run --env BASE_URL=https://api.local --env TOKEN=$TOKEN tests/load/pipeline-ramp.js
실행하면서 Grafana를 같이 열어두고, 아래 5개 시각을 반드시 기록한다 - 시리즈 6의 "탐지 공백" 개념을 그대로 가져오되, 이번엔 원인이 장애가 아니라 부하다.
- 부하 급증 시작 시각 (k6 stage가 80 VU로 전환된 시각)
- 큐 길이가
queueLength임계치(20)를 넘긴 시각 - KEDA가 워커를 스케일아웃한 시각
- Alertmanager 알림이 도착한 시각 (큐 길이 또는 지연 관련 규칙)
- 큐가 다시 정상 수준으로 소진된 시각
23번 사이 간격이 "오토스케일 반응 지연"이고, 24번 사이 간격이 "이번 버전의 탐지 공백"이다 - 시리즈 6의 알림 규칙이 사람이 아니라 실제 트래픽 증가에도 적절한 시점에 울리는지가 이 시리즈의 통과 기준이다.
5. (선택) OTel 트레이스로 병목 구간 특정
시리즈 4의 선택 확장으로 OpenTelemetry를 붙였다면, 부하가 걸린 동안 임의의 요청 하나를 골라 trace ID로 조회한다 - 게이트웨이 진입, 큐 대기, 워커 처리, DB 기록 각 구간이 몇 ms인지 한 화면에서 비교하면, http_req_duration만으로는 안 보이던 "정확히 어느 구간이 늘어났는지"가 드러난다. 예를 들어 큐 대기 구간만 늘어났다면 병목은 워커 처리 속도가 아니라 오토스케일 반응 속도 쪽이다.
검증 방법
- k6 리포트:
http_req_failed,p(95)등이 정의한 threshold를 통과하는지 - 큐 길이 → 스케일아웃 → 알림 도착까지 5개 시각이 실제로 기록되고, 각 구간의 지연이 납득 가능한 수준인지
- 부하가 걷힌 뒤 워커가
minReplicaCount까지 자동으로 축소되는지 (스케일인 확인 - 스케일아웃만 확인하고 끝내지 않는다) - (선택) 트레이스로 병목 구간이 특정되는지
체크리스트
- 파이프라인 진입점(작업 생성 API)을 향한 k6 ramping 시나리오 작성
- RabbitMQ 큐 길이 기준 KEDA ScaledObject 적용, 수동 스케일 제거
- 큐 길이·워커 파드 수·api-server 응답시간·DB 커넥션·알림 상태를 한 대시보드에 구성
- 부하 실행하며 5개 시각(부하 시작/임계치 초과/스케일아웃/알림 도착/정상화) 기록
- 스케일인(축소)까지 확인
- (선택) OTel 트레이스로 병목 구간 특정, 로그/메트릭만으로 봤을 때와 비교