Conversation
3-Phase 구조로 SADD/SMEMBERS/TTL 각각 측정 - PHASE 1: POST /api/v1/vacancy 100회 호출로 SADD 부하 - PHASE 2: k6/experimental/redis 로 SMEMBERS 응답시간 측정 - PHASE 3: TTL/EXISTS 로 키 자동 삭제 확인
- @transactional 제거: 자동 롤백으로 시드 유저 미반영 문제 해결 - Redis 클라이언트 조건부 초기화: subscribe phase 단독 실행 가능 - Date.now() -> performance.now(): sub-ms SMEMBERS 측정 정확도 개선
…cribe-load-test Chore/#101 vacancy subscribe load test
- 01·03·05·06·07번: event_spike(워밍업 후 스파이크) + ramp_up(점진 증가) 두 시나리오 적용 - 04번: ramp_up 단독으로 서킷브레이커 OPEN 시점 탐색 시나리오로 개선 - docker-compose.prod.yml: Prometheus remote write 수신 활성화 및 포트 외부 노출 - run.ps1: 기본값 운영 서버 주소로 변경 - k6-dashboard.json: event_spike 시나리오명 동기화, 심화 지표 패널 4개 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 05: district 파라미터 한글 encodeURIComponent 적용 - 07: setDate/getDate 타임존 혼용 → ms 연산으로 통일 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chore: k6 부하테스트 시나리오 고도화 및 모니터링 대시보드 개선
There was a problem hiding this comment.
Code Review
This pull request updates and adds several k6 load testing scripts to compare event spike and ramp-up scenarios, introduces a Grafana dashboard, and adds a token generator utility. Feedback on these changes highlights a critical security issue in docker-compose.prod.yml where Prometheus port 9090 is exposed publicly. Additionally, multiple k6 script issues were identified, including incorrect string-based timeout values in 04-circuit-breaker.js and 06-store-nearby.js, and a dynamically generated options block in 11-vacancy-test.js that violates k6's static analysis requirements.
| ports: | ||
| - "9090:9090" |
There was a problem hiding this comment.
운영 환경(docker-compose.prod.yml)에서 Prometheus 포트 9090을 외부(0.0.0.0:9090)에 그대로 노출하는 것은 심각한 보안 위험입니다.
특히 현재 설정에는 --web.enable-lifecycle과 --web.enable-remote-write-receiver 옵션이 활성화되어 있어, 인증되지 않은 외부 사용자가 다음과 같은 악의적인 행위를 수행할 수 있습니다:
POST /-/reload또는POST /-/quit호출을 통한 Prometheus 강제 종료 및 설정 변경- 원격 쓰기 수신기(
/api/v1/write)에 대량의 허위 메트릭을 전송하여 디스크 고갈 및 서비스 거부(DoS) 유발 - 내부 시스템 메트릭 및 민감한 인프라 정보 무단 조회
해결 방안:
- 외부 노출이 불필요하다면 루프백 주소로 바인딩하여 외부 접근을 차단하세요:
127.0.0.1:9090:9090 - 외부(예: 로컬 k6)에서 원격 쓰기가 필요하다면, 이미 동일한 compose 파일에 정의된 Nginx 프록시를 통해 기본 인증(Basic Auth) 및 SSL을 적용하여 안전하게 라우팅하는 것을 권장합니다.
ports:
- "127.0.0.1:9090:9090"| const res = http.post(`${BASE_URL}/api/v1/chat/messages`, payload, { | ||
| headers: HEADERS_AUTH, | ||
| timeout: 65000, // LLM 응답 최대 대기 (65초) | ||
| timeout: '65s', |
| `¢erLat=${lat}¢erLng=${lng}&limit=50`; | ||
|
|
||
| const res = http.get(url, { headers: HEADERS_JSON, timeout: 15000 }); | ||
| const res = http.get(url, { headers: HEADERS_JSON, timeout: '15s' }); |
There was a problem hiding this comment.
k6의 http 요청 매개변수에서 timeout 옵션은 **밀리초(ms) 단위의 숫자(number)**를 지정해야 합니다.
'15s' 문자열 대신 15000 숫자를 사용해 주세요. (참고로 106라인의 nearby 요청에서는 15000 숫자로 올바르게 설정되어 있습니다.)
| const res = http.get(url, { headers: HEADERS_JSON, timeout: '15s' }); | |
| const res = http.get(url, { headers: HEADERS_JSON, timeout: 15000 }); |
| export const options = (() => { | ||
| if (PHASE === 'subscribe') { | ||
| return { vus: N, iterations: N }; | ||
| } | ||
| if (PHASE === 'smembers') { | ||
| // 단일 VU 가 N 번 반복 - SMEMBERS 자체 응답시간 분포만 본다. | ||
| // 동시성 부하를 보려면 vus 를 늘릴 것. | ||
| return { vus: 1, iterations: N }; | ||
| } | ||
| if (PHASE === 'ttl') { | ||
| return { vus: 1, iterations: 1 }; | ||
| } | ||
| throw new Error(`Unknown PHASE: ${PHASE}`); | ||
| })(); |
There was a problem hiding this comment.
k6에서 options 객체는 정적 분석(Static Analysis)이 가능하도록 **정적 객체 리터럴(Plain Object)**로 내보내야(export) 합니다.
현재와 같이 즉시 실행 함수(IIFE)를 사용하여 options를 동적으로 생성하면, k6가 실행 전 테스트 계획을 수립하는 단계에서 구문 분석에 실패하거나 설정을 무시하여 스크립트가 정상적으로 실행되지 않을 수 있습니다.
개선 방향:
k6에서 여러 테스트 시나리오나 단계를 실행하는 가장 표준적이고 권장되는 방법은 Scenarios를 정적으로 모두 정의해 두고, 실행 시 CLI 플래그(--scenario)를 사용하여 원하는 시나리오만 선택적으로 실행하는 것입니다.
export const options = {
scenarios: {
subscribe: {
executor: 'shared-iterations',
vus: N,
iterations: N,
},
smembers: {
executor: 'shared-iterations',
vus: 1,
iterations: N,
},
ttl: {
executor: 'shared-iterations',
vus: 1,
iterations: 1,
},
},
};| import http from 'k6/http'; | ||
| import redis from 'k6/experimental/redis'; | ||
| import { check } from 'k6'; | ||
| import { Counter, Trend } from 'k6/metrics'; |
There was a problem hiding this comment.
시나리오별 분기를 위해 k6/execution 모듈을 임포트합니다.
| import http from 'k6/http'; | |
| import redis from 'k6/experimental/redis'; | |
| import { check } from 'k6'; | |
| import { Counter, Trend } from 'k6/metrics'; | |
| import http from 'k6/http'; | |
| import redis from 'k6/experimental/redis'; | |
| import exec from 'k6/execution'; | |
| import { check } from 'k6'; | |
| import { Counter, Trend } from 'k6/metrics'; |
| export default async function () { | ||
| if (PHASE === 'subscribe') subscribe(); | ||
| else if (PHASE === 'smembers') await smembers(); | ||
| else if (PHASE === 'ttl') await ttl(); | ||
| } |
There was a problem hiding this comment.
options를 정적 시나리오 구조로 변경한 후, 실행 중인 시나리오 이름(exec.scenario.name)에 따라 동적으로 실행할 함수를 분기하도록 개선할 수 있습니다.
이렇게 하면 환경 변수(PHASE)에 의존하지 않고 k6의 표준 시나리오 제어 방식을 활용할 수 있어 훨씬 깔끔하고 견고해집니다.
| export default async function () { | |
| if (PHASE === 'subscribe') subscribe(); | |
| else if (PHASE === 'smembers') await smembers(); | |
| else if (PHASE === 'ttl') await ttl(); | |
| } | |
| export default async function () { | |
| const phase = exec.scenario.name; | |
| if (phase === 'subscribe') subscribe(); | |
| else if (phase === 'smembers') await smembers(); | |
| else if (phase === 'ttl') await ttl(); | |
| } |
📢 기능 설명
필요시 실행결과 스크린샷 첨부
연결된 issue
연결된 issue를 자동을 닫기 위해 아래 {이슈넘버}를 입력해주세요.
close #{이슈넘버}
✅ 체크리스트