From 50c1bcb502d07f365fd6d80498c09897d88c300a Mon Sep 17 00:00:00 2001 From: tasoo park Date: Tue, 26 May 2026 17:05:47 +0000 Subject: [PATCH] feat: configure load test environment --- tests/load/.env.example | 14 ++++ tests/load/README.md | 61 +++++++++++++++--- tests/load/submission-load.js | 116 +++++++++++++++++++++++++--------- 3 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 tests/load/.env.example diff --git a/tests/load/.env.example b/tests/load/.env.example new file mode 100644 index 0000000000..6d176b180f --- /dev/null +++ b/tests/load/.env.example @@ -0,0 +1,14 @@ +BASE_URL=https://codedang.com/api +USERNAME= +PASSWORD= +PROBLEM_ID=395 + +# Reduced default load. Override as needed. +NORMAL_VUS=20 +VILLAIN_VUS=2 +RAMP_UP_DURATION=30s +STEADY_DURATION=2m +RAMP_DOWN_DURATION=30s + +# Enable with: --out experimental-prometheus-rw +# K6_PROMETHEUS_RW_SERVER_URL=https://prometheus.codedang.com/api/v1/write diff --git a/tests/load/README.md b/tests/load/README.md index c955073252..2720a4f36c 100644 --- a/tests/load/README.md +++ b/tests/load/README.md @@ -15,10 +15,12 @@ k6 (외부) | 시나리오 | VU | 설명 | |----------|---:|------| -| `normal_users` | 140 | 언어별 정답 코드 제출 (A+B) | -| `villain_users` | 10 | 자원 소모 악성 코드 제출 (무한루프, 메모리폭탄 등) | +| `normal_users` | 20 | 언어별 정답 코드 제출 (A+B) | +| `villain_users` | 2 | 자원 소모 악성 코드 제출 (무한루프, 메모리폭탄 등) | -**단계**: 2분 ramp-up → 10분 steady → 2분 ramp-down (총 14분) +**기본 단계**: 30초 ramp-up → 2분 steady → 30초 ramp-down (총 3분) + +VU 수와 시간은 `.env` 또는 `k6 run -e`로 조정할 수 있습니다. ## 사전 준비 @@ -29,16 +31,54 @@ k6 (외부) ## 실행 +로컬 설정 파일을 만듭니다. `.env`는 git에 커밋되지 않습니다. + ```bash cd tests/load +cp .env.example .env +``` + +`.env` 예시: + +```dotenv +BASE_URL=https://codedang.com/api +USERNAME=tasoo1118 +PASSWORD=your-password +PROBLEM_ID=395 + +NORMAL_VUS=20 +VILLAIN_VUS=2 +RAMP_UP_DURATION=30s +STEADY_DURATION=2m +RAMP_DOWN_DURATION=30s +``` + +실행: + +```bash +cd tests/load +k6 run \ + --out "json=results/$(date +%Y%m%d-%H%M%S).json" \ + submission-load.js +``` + +`-e`로 넘긴 값은 `.env`보다 우선합니다. + +```bash +k6 run \ + -e NORMAL_VUS=5 \ + -e VILLAIN_VUS=0 \ + -e STEADY_DURATION=30s \ + --out "json=results/$(date +%Y%m%d-%H%M%S).json" \ + submission-load.js +``` + +Prometheus remote write를 함께 사용할 때: + +```bash k6 run \ - -e BASE_URL=https://codedang.com/api \ - -e USERNAME=loadtest-user \ - -e PASSWORD=your-password \ - -e PROBLEM_ID=123 \ --out experimental-prometheus-rw \ --out "json=results/$(date +%Y%m%d-%H%M%S).json" \ - -e K6_PROMETHEUS_RW_SERVER_URL=https://prometheus.codedang.com/api/v1/write \ submission-load.js ``` @@ -60,6 +100,11 @@ k6 run \ | `USERNAME` | O | 로그인 사용자명 | | `PASSWORD` | O | 로그인 비밀번호 | | `PROBLEM_ID` | O | 테스트 대상 문제 ID | +| `NORMAL_VUS` | | 일반 사용자 VU 수 (기본: `20`) | +| `VILLAIN_VUS` | | 악성 사용자 VU 수 (기본: `2`, `0`이면 비활성화) | +| `RAMP_UP_DURATION` | | ramp-up 시간 (기본: `30s`) | +| `STEADY_DURATION` | | steady 시간 (기본: `2m`) | +| `RAMP_DOWN_DURATION` | | ramp-down 시간 (기본: `30s`) | | `K6_PROMETHEUS_RW_SERVER_URL` | | Prometheus remote write URL (`--out` 사용 시) | ## Grafana 대시보드 가이드 diff --git a/tests/load/submission-load.js b/tests/load/submission-load.js index 06689e9e59..6d62d8c697 100644 --- a/tests/load/submission-load.js +++ b/tests/load/submission-load.js @@ -8,18 +8,99 @@ const submissionErrors = new Counter('submission_errors') const submissionDuration = new Trend('submission_duration', true) // ── Environment variables ─────────────────────────────────────── -const BASE_URL = __ENV.BASE_URL || 'https://codedang.com/api' -const USERNAME = __ENV.USERNAME -const PASSWORD = __ENV.PASSWORD -const PROBLEM_ID = __ENV.PROBLEM_ID +function loadDotEnv() { + try { + return parseDotEnv(open('./.env')) + } catch (_) { + return {} + } +} + +function parseDotEnv(content) { + const env = {} + for (const line of content.split(/\r?\n/)) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + + const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/) + if (!match) continue + + let value = match[2].trim() + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1) + } + + env[match[1]] = value + } + return env +} + +const dotenv = loadDotEnv() + +function env(name, fallback) { + const value = __ENV[name] !== undefined ? __ENV[name] : dotenv[name] + return value !== undefined && value !== '' ? value : fallback +} + +function numberEnv(name, fallback) { + const value = Number(env(name, fallback)) + if (!Number.isFinite(value) || value < 0) { + throw new Error(`${name} must be a non-negative number`) + } + return value +} + +const BASE_URL = env('BASE_URL', 'https://codedang.com/api') +const USERNAME = env('USERNAME') +const PASSWORD = env('PASSWORD') +const PROBLEM_ID = env('PROBLEM_ID') + +const NORMAL_VUS = numberEnv('NORMAL_VUS', 20) +const VILLAIN_VUS = numberEnv('VILLAIN_VUS', 2) +const RAMP_UP_DURATION = env('RAMP_UP_DURATION', '30s') +const STEADY_DURATION = env('STEADY_DURATION', '2m') +const RAMP_DOWN_DURATION = env('RAMP_DOWN_DURATION', '30s') + +if (NORMAL_VUS === 0 && VILLAIN_VUS === 0) { + throw new Error('At least one of NORMAL_VUS or VILLAIN_VUS must be greater than 0') +} if (!USERNAME || !PASSWORD || !PROBLEM_ID) { throw new Error( 'Required env vars: USERNAME, PASSWORD, PROBLEM_ID\n' + - 'Usage: k6 run -e USERNAME=x -e PASSWORD=x -e PROBLEM_ID=123 submission-load.js' + 'Usage: create .env or run k6 with -e USERNAME=x -e PASSWORD=x -e PROBLEM_ID=123 submission-load.js' ) } +function rampingScenario(exec, vus, tags) { + return { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: RAMP_UP_DURATION, target: vus }, + { duration: STEADY_DURATION, target: vus }, + { duration: RAMP_DOWN_DURATION, target: 0 } + ], + exec, + tags + } +} + +const scenarios = {} +if (NORMAL_VUS > 0) { + scenarios.normal_users = rampingScenario('normalSubmission', NORMAL_VUS, { + user_type: 'normal' + }) +} +if (VILLAIN_VUS > 0) { + scenarios.villain_users = rampingScenario('villainSubmission', VILLAIN_VUS, { + user_type: 'villain' + }) +} + // ── Payloads ──────────────────────────────────────────────────── const normalPayloads = new SharedArray('normal', () => [ { lang: 'C', code: open('./payloads/normal/solution.c') }, @@ -39,30 +120,7 @@ const villainPayloads = new SharedArray('villain', () => [ // ── Scenarios ─────────────────────────────────────────────────── export const options = { - scenarios: { - normal_users: { - executor: 'ramping-vus', - startVUs: 0, - stages: [ - { duration: '2m', target: 140 }, - { duration: '10m', target: 140 }, - { duration: '2m', target: 0 } - ], - exec: 'normalSubmission', - tags: { user_type: 'normal' } - }, - villain_users: { - executor: 'ramping-vus', - startVUs: 0, - stages: [ - { duration: '2m', target: 10 }, - { duration: '10m', target: 10 }, - { duration: '2m', target: 0 } - ], - exec: 'villainSubmission', - tags: { user_type: 'villain' } - } - }, + scenarios, thresholds: { http_req_duration: ['p(95)<5000'], submission_errors: ['count<50']