---
id: testing-load-k6-locust
title: Load Testing — k6 / Locust / Artillery
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [testing, load, performance, vibe-coding]
tech_stack: { language: "JS / Python", applicable_to: ["Backend"] }
applied_in: []
aliases: [load testing, k6, Locust, Artillery, JMeter, Gatling, capacity, soak test]
---

# Load Testing

> Production 의 load 견디는지 검증. **k6 (modern), Locust (Python), Artillery, JMeter, Gatling**. Smoke / load / stress / soak / spike.

## 📖 핵심 개념
- Load test ≠ stress test ≠ soak test.
- VU (virtual user) = 동시 사용자.
- RPS = req per sec.
- p95/p99 latency 가 mean 보다 중요.

## 💻 코드 패턴

### k6 (modern, Go)
```js
// load.js
import http from 'k6/http';
import { sleep, check } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 100 },   // ramp up
    { duration: '5m',  target: 100 },   // steady
    { duration: '30s', target: 0 },     // ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  const r = http.get('https://api.example.com/users');
  check(r, { 'status 200': (r) => r.status === 200 });
  sleep(1);
}
```

```bash
k6 run load.js
```

### Output
```
✓ status 200

http_req_duration..: p95=412ms p99=890ms
http_req_failed....: 0.05%
http_reqs..........: 30000  100/s
vus................: 100
```

### Smoke test (1-5 VU, 1 min)
```js
options = { vus: 1, duration: '1m' };
```

→ Sanity check.

### Load test (typical)
```js
stages: [
  { duration: '2m', target: 100 },
  { duration: '5m', target: 100 },
  { duration: '2m', target: 0 },
]
```

### Stress test (capacity 한계)
```js
stages: [
  { duration: '2m', target: 100 },
  { duration: '5m', target: 200 },
  { duration: '5m', target: 500 },
  { duration: '5m', target: 1000 },
  { duration: '5m', target: 2000 },
]
```

→ 어디서 깨지는지 발견.

### Soak test (장시간)
```js
options = {
  vus: 100,
  duration: '4h',
};
```

→ Memory leak / resource exhaustion 발견.

### Spike test
```js
stages: [
  { duration: '10s', target: 100 },
  { duration: '1m', target: 100 },
  { duration: '10s', target: 5000 },  // spike
  { duration: '3m', target: 5000 },
  { duration: '10s', target: 100 },
  { duration: '3m', target: 100 },
]
```

→ 갑작스런 traffic 처리?

### Auth 가진 scenario
```js
import http from 'k6/http';
import { check } from 'k6';

export function setup() {
  const login = http.post('https://api.example.com/auth', JSON.stringify({
    email: 'test@example.com',
    password: 'pw',
  }));
  return { token: login.json('token') };
}

export default function (data) {
  const r = http.get('https://api.example.com/users', {
    headers: { Authorization: `Bearer ${data.token}` },
  });
  check(r, { '200': (r) => r.status === 200 });
}
```

### 다중 endpoint
```js
import { group } from 'k6';

export default function () {
  group('list users', () => {
    http.get('/users');
  });
  
  group('create order', () => {
    http.post('/orders', JSON.stringify({ ... }));
  });
}
```

### Test data
```js
import { SharedArray } from 'k6/data';

const users = new SharedArray('users', () => JSON.parse(open('./users.json')));

export default function () {
  const user = users[Math.floor(Math.random() * users.length)];
  http.post('/login', JSON.stringify(user));
}
```

→ 100k user 가 1번씩 로그인 = 진짜 traffic.

### Distributed (k6 Cloud / OSS)
```bash
k6 cloud load.js
# 또는 self-host
k6 run --distributed
```

→ 1 machine = ~10k VU. 더 = distributed.

### Locust (Python)
```python
# locustfile.py
from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 5)
    
    @task(3)
    def list_users(self):
        self.client.get('/users')
    
    @task(1)
    def view_profile(self):
        self.client.get('/profile')
    
    def on_start(self):
        self.client.post('/login', json={...})
```

```bash
locust -f locustfile.py --host=https://api.example.com
# Web UI: http://localhost:8089
```

→ Python script + UI.

### Artillery (YAML)
```yaml
# load.yml
config:
  target: https://api.example.com
  phases:
    - duration: 60
      arrivalRate: 10
    - duration: 300
      arrivalRate: 100
scenarios:
  - flow:
      - get: { url: '/users' }
      - think: 1
      - post: { url: '/orders', json: { ... } }
```

```bash
artillery run load.yml
```

### Gatling (Scala)
```scala
class LoadSim extends Simulation {
  val httpProtocol = http.baseUrl("https://api.example.com")
  
  val scn = scenario("Users").exec(
    http("list users").get("/users")
  )
  
  setUp(scn.inject(rampUsers(100) during (30 seconds)).protocols(httpProtocol))
}
```

→ JVM. 큰 enterprise 가 사용.

### JMeter (legacy GUI)
```
- XML config
- Thread group
- Plugin ecosystem
- 큰 enterprise

→ 옛 — Gatling / k6 가 모던.
```

### CI 통합
```yaml
# .github/workflows/load.yml
- run: k6 run load.js
```

→ PR 가 load test (smoke). 매일 staging full load.

### Threshold (auto fail)
```js
thresholds: {
  http_req_duration: ['p(95)<500'],  // 95% < 500ms
  http_req_failed: ['rate<0.01'],     // 1% 이하 fail
}
```

→ k6 가 exit code != 0.

### Metrics export
```js
// k6 → InfluxDB / Prometheus / DataDog
options = {
  ext: {
    loadimpact: { projectID: 123 },
  },
};
```

```bash
k6 run --out influxdb=http://...
```

### Backend monitoring 동시
```
Load test 중:
- App metric (CPU, memory, p99)
- DB metric (query count, lock)
- Network (latency, dropped)
- Queue (depth)

→ Bottleneck 식별.
```

### Production-like environment
```
Test env = prod env / 10 (size).

Production:
- 100 instance × 4 CPU = 400 CPU
- 1000 RPS

Test:
- 10 instance × 4 CPU = 40 CPU
- 100 RPS

→ Scale linear. 결과 외삽.
```

### Realistic load
```
Pareto: 80% read / 20% write.
실제 prod log → top endpoint % 추출.

읽기 많음 ≠ 쓰기 적음 (cache 매번 hit).
```

### Test data lifecycle
```
- Setup: 100k user 생성 (한 번)
- Test: 매번 random 추출
- Teardown: 안 — load test data 영구 (다음 가)

또는:
- 매 test = 새 DB schema (격리)
```

### Bottleneck 식별
```
RPS 늘림 → p99 latency ↑ → 어디?

1. CPU bound: app instance 가 100% CPU
2. DB bound: query 가 long, conn 다
3. Network: bandwidth 한계
4. Memory: GC 폭발
5. External: Stripe / S3 가 throttle
```

→ Profiler / APM (Datadog, NewRelic) 동시.

### Service mesh / sidecar overhead
```
Istio + Envoy = 매 hop 가 1-5ms.
- Service A → mesh → Service B
- 매 RPC 가 5-20ms 더.

→ Load test 결과 가 prod 에 가까운지 검증.
```

### Cost
```
1 hour 1000 RPS = ~3.6M req.
Cloud egress + storage = $.

→ Load test 도 budget.
```

## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| 모던 default | k6 |
| Python team | Locust |
| YAML 친화 | Artillery |
| JVM enterprise | Gatling |
| GUI 필요 | JMeter |
| Smoke test | k6 (1 VU) |
| Capacity | Stress (k6) |
| Memory leak | Soak (4h+) |
| Black Friday 대비 | Spike |

## ❌ 안티패턴
- **No load test**: prod 가 처음 진실.
- **Local machine 만 = test**: 1 machine 한계.
- **No threshold**: pass / fail 모름.
- **Spike test 안 함**: traffic burst 깨짐.
- **Soak 안 함**: 1주 후 OOM.
- **DB / cache reset 없음**: cache hit rate 가짜.
- **Realistic 아닌 mix**: 모두 read = 가짜.

## 🤖 LLM 활용 힌트
- k6 가 modern default.
- Smoke / load / stress / soak / spike = 5 종류.
- p95/p99 가 핵심 metric.
- Bottleneck 식별 = profiler 동시.

## 🔗 관련 문서
- [[Testing_Test_Pyramid]]
- [[Backend_Rate_Limiting]]
- [[Perf_Node_Profiling]]
