Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ docker-compose.yml
!*.tfvars.example
crash.log
override.tf
override.tf.json
override.tf.json
### k6 결과 덤프 ###
loadtest/results/*.json
27 changes: 27 additions & 0 deletions loadtest/capacity-contention.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import http from 'k6/http';
import { sleep } from 'k6';
import { Counter } from 'k6/metrics';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js';

const BASE_URL = __ENV.BASE_URL || 'http://localhost';
const HOT_SUBJECTS = parseInt(__ENV.HOT_SUBJECTS || '1'); // 경쟁 대상 과목 수 (1 또는 2)
Expand Down Expand Up @@ -69,3 +70,29 @@ function formHeaders(userId) {
return { headers: { userId: String(userId), 'Content-Type': 'application/x-www-form-urlencoded' } };
}
function safeJson(res) { try { return res.json(); } catch (e) { return null; } }

// 터미널엔 전체 요약 + 정합성 지표 강조, 파일엔 JSON 저장(비교용)
export function handleSummary(data) {
const ts = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19);
return {
stdout: textSummary(data, { indent: ' ', enableColors: true }) + highlight(data),
[`loadtest/results/capacity-contention-${ts}.json`]: JSON.stringify(data, null, 2),
};
}
function highlight(data) {
const m = data.metrics;
const cnt = (k) => (m[k] ? m[k].values.count : 0);
const hot = parseInt(__ENV.HOT_SUBJECTS || '1');
const p95 = m.http_req_duration ? m.http_req_duration.values['p(95)'] : 0;
const failed = m.http_req_failed ? (m.http_req_failed.values.rate * 100).toFixed(2) : '0.00';
return [
'\n========== 정합성 검증 (경합) ==========',
`신청 성공(정원 내) : ${cnt('enroll_success')} (기대: 30 x ${hot} = ${30 * hot})`,
`정원 초과 거절 : ${cnt('capacity_full')}`,
`기타 거절 : ${cnt('other_reject')}`,
`응답시간 p95 : ${p95.toFixed(0)} ms`,
`HTTP 실패율 : ${failed} %`,
'>>> 각 과목 registeredNum <= 30 인지 서버에서 추가 확인',
'=======================================\n',
].join('\n');
}
Empty file added loadtest/results/.gitkeep
Empty file.
29 changes: 29 additions & 0 deletions loadtest/throughput-baseline.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import http from 'k6/http';
import { sleep } from 'k6';
import { Counter } from 'k6/metrics';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js';

const BASE_URL = __ENV.BASE_URL || 'http://localhost';
const SUBJECT_COUNT = parseInt(__ENV.SUBJECT_COUNT || '30'); // 시드된 과목 수
Expand Down Expand Up @@ -75,3 +76,31 @@ function formHeaders(userId) {
}
function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
function safeJson(res) { try { return res.json(); } catch (e) { return null; } }

// 터미널엔 전체 요약 + 핵심 지표 강조, 파일엔 JSON 저장(비교용)
export function handleSummary(data) {
const ts = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19);
return {
stdout: textSummary(data, { indent: ' ', enableColors: true }) + highlight(data),
[`loadtest/results/throughput-baseline-${ts}.json`]: JSON.stringify(data, null, 2),
};
}
function highlight(data) {
const m = data.metrics;
const cnt = (k) => (m[k] ? m[k].values.count : 0);
const succ = cnt('enroll_success');
const total = succ + cnt('enroll_rejected');
const rate = total > 0 ? ((succ / total) * 100).toFixed(1) : '0.0';
const p95 = m.http_req_duration ? m.http_req_duration.values['p(95)'] : 0;
const p99 = m.http_req_duration ? m.http_req_duration.values['p(99)'] : 0;
const rps = m.http_reqs ? m.http_reqs.values.rate : 0;
const failed = m.http_req_failed ? (m.http_req_failed.values.rate * 100).toFixed(2) : '0.00';
return [
'\n========== 핵심 지표 (처리량 baseline) ==========',
`신청 성공 / 시도 : ${succ} / ${total} (성공률 ${rate}%)`,
`처리량(http_reqs) : ${rps.toFixed(1)} req/s`,
`응답시간 p95 / p99 : ${p95.toFixed(0)} / ${p99.toFixed(0)} ms`,
`HTTP 실패율 : ${failed} %`,
'================================================\n',
].join('\n');
}
1 change: 1 addition & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ locals {
# EC2에 전달할 .env 내용
env_content = <<-EOT
DOCKER_USERNAME=${var.docker_username}
SPRING_PROFILES_ACTIVE=${var.spring_profiles_active}
MYSQL_ROOT_PASSWORD=${var.mysql_root_password}
DB_NAME=${var.db_name}
DB_USERNAME=${var.db_username}
Expand Down
3 changes: 3 additions & 0 deletions terraform/terraform.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ db_username = "course"
mysql_root_password = "change-me-root-password"
db_password = "change-me-app-password"

# Spring 프로파일 (평소 prod / 부하테스트 시 prod,loadtest 로 변경하면 헤더 기반 userId 활성화)
# spring_profiles_active = "prod,loadtest"

# SSH 허용 IP를 직접 지정하려면 주석 해제 (비우면 apply 시 현재 공인 IP 자동 감지)
# ssh_ingress_cidr = "1.2.3.4/32"
6 changes: 6 additions & 0 deletions terraform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ variable "docker_username" {
type = string
}

variable "spring_profiles_active" {
description = "Spring 프로파일 (평소 prod / 부하테스트 시 prod,loadtest)"
type = string
default = "prod"
}

variable "db_name" {
description = "MySQL 데이터베이스명"
type = string
Expand Down
Loading