본 포스팅에서는 Mac M2 Max환경에서 wrk2를 사용하여 Go로 작성된 간단한 웹 애플리케이션의 성능 테스트를 진행한 과정을 작성했습니다. 이 테스트의 주요 목적은 다음과 같습니다.
1. wrk를 이용한 웹 서버의 최대 RPS(Request Per Second) 측정
2. wrk의 다양한 설정 값(스레드 수, 연결 수, 요청 비율 등)을 조합하여 최적의 성능 구성을 탐색
3. 부하 테스트 중 시스템 한계 식별 및 극복 방법을 모색
이 실험을 통해 단일 머신에서 최대한의 부하를 안정적으로 생성하는 방법을 찾고, 부하 생성 시스템의 성능을 최적화하여 테스트 대상 시스템의 실제 한계를 측정할 수 있는 환경을 구축하고자 합니다. 또한, 대규모 트래픽을 시뮬레이션할 수 있는 효과적인 부하 테스트 전략을 찾고자 합니다.
git clone https://github.com/giltene/wrk2.git
sudo softwareupdate --install-rosetta
arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
arch -x86_64 /usr/local/bin/brew install openssl
cd wrk2
vi Makefile
Makefile에서 CFLAGS 및 LDFLAGS 변수를 다음과 같이 수정합니다.
CFLAGS := -std=c99 -Wall -O2 -I/usr/local/opt/openssl/include
LDFLAGS := -L/usr/local/opt/openssl/lib
make clean && make
cp wrk /usr/local/bin
이제 wrk2를 어느 경로에서나 실행할 수 있는 상태가 되었습니다!
> wrk --version
wrk 4.0.0 [kqueue] Copyright (C) 2012 Will Glozer
Usage: wrk <options> <url>
Options:
-c, --connections <N> Connections to keep open
-d, --duration <T> Duration of test
-t, --threads <N> Number of threads to use
-s, --script <S> Load Lua script file
-H, --header <H> Add header to request
-L --latency Print latency statistics
-U --u_latency Print uncorrected latency statistics
--timeout <T> Socket/request timeout
-B, --batch_latency Measure latency of whole
batches of pipelined ops
(as opposed to each op)
-v, --version Print version details
-R, --rate <T> work rate (throughput)
in requests/sec (total)
[Required Parameter]
Numeric arguments may include a SI unit (1k, 1M, 1G)
Time arguments may include a time unit (2s, 2m, 2h)
go를 이용해 localhost:8080을 호출하면 Hello, World를 출력하는 간단한 어플리케이션을 작성해보겠습니다.
⌈code⌋ main.go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServe(":8080", nil)
}
wrk -t20 -c400 -d30s -R 100000 http://127.0.0.1:8080
Running 30s test @ http://127.0.0.1:8080
20 threads and 400 connections
Thread calibration: mean lat.: 0.936ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.934ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.872ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.889ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.929ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.953ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.898ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.897ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.910ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.951ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.887ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.984ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.935ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.919ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.926ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.915ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.958ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.842ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.909ms, rate sampling interval: 10ms
Thread calibration: mean lat.: 0.880ms, rate sampling interval: 10ms
Thread Stats Avg Stdev Max +/- Stdev
Latency 0.92ms 490.83us 14.26ms 70.16%
Req/Sec 3.06k 240.24 5.22k 65.49%
1745910 requests in 30.00s, 219.78MB read
Socket errors: connect 167, read 0, write 0, timeout 2338
Requests/sec: 58201.61
Transfer/sec: 7.33MB
옵션 값만 조금씩 변경하면서 적정한 최대 RPS를 찾는 테스트를 진행했다.
| Thread | Request (1s) | HTTP Connection | Timeout | Latency(avg) | Req/Sec(avg) | Total Request | RPS | TPS |
|---|---|---|---|---|---|---|---|---|
| 1 | 100,000 | 1000 | 8976 | 1.85ms | 26.55k | 730,024 | 24,334 | 3.06MB |
| 1 | 200,000 | 1000 | 8976 | 1.98ms | 53.11k | 1,459,992 | 48,662 | 6.13MB |
| 1 | 300,000 | 1000 | 8976 | 2.11ms | 79.66k | 2,190,081 | 73,001 | 9.19MB |
| 1 | 400,000 | 1000 | 8976 | 3.06ms | 106.32k | 2,919,919 | 97,316 | 12.25MB |
| 1 | 500,000 | 1000 | 8976 | 1.39s | 119.06k | 3,450,768 | 115,023 | 14.48MB |
| 1 | 600,000 | 1000 | 8976 | 3.27s | 133.27k | 3,756,762 | 125,224 | 15.76MB |
| 1 | 700,000 | 1000 | 8976 | 6.46s | 124.77k | 3,579,766 | 119,323 | 15.02MB |
| 1 | 800,000 | 1000 | 8976 | 7.52s | 127.46k | 3,820,454 | 127,346 | 16.03MB |
| 1 | 800,000 | 500 | 3224 | 13.89s | 129.70k | 3,795,810 | 126,525 | 15.93MB |
| 1 | 900,000 | 500 | 3224 | 14.48s | 132.03k | 3,814,249 | 127,141 | 16.01MB |
| 1 | 800,000 | 200 | 0 | 16.93s | 121.38k | 3,597,938 | 119,931 | 15.10MB |
| 2 | 800,000 | 200 | 0 | 16.22s | 68.35k | 4,079,281 | 135,976 | 17.12MB |
| 2 | 900,000 | 200 | 0 | 16.96s | 67.14k | 4,000,723 | 133,345 | 16.79MB |
| 2 | 800,000 | 100 | 0 | 16.80s | 64.42k | 3,816,189 | 127,207 | 16.01MB |
| 2 | 800,000 | 50 | 0 | 16.32s | 65.15k | 3,793,171 | 126,439 | 15.92MB |
| 4 | 800,000 | 200 | 0 | 16.62s | 33.46k | 4,036,785 | 135,562 | 16.94MB |
| 8 | 800,000 | 200 | 0 | 16.44s | 17.24k | 4,057,330 | 135,251 | 17.03MB |
| 16 | 800,000 | 200 | 0 | 16.64s | 8.37k | 4,181,967 | 139,406 | 17.55MB |
⌈code⌋ main.go
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Millisecond * 300)
fmt.Fprintf(w, "Hello, Worlzzd!")
})
API 응답 시간을 늘려 실제 서비스와 유사한 환경을 시뮬레이션했습니다.
wrk -t2 -c200 -d30s -R 100000 http://127.0.0.1:8080
우선, 이전 실험에서 얻었던 최적 구성으로 테스트를 진행했습니다. 결과는,
> wrk -t2 -c200 -d30s -R 100000 http://127.0.0.1:8080
Running 30s test @ http://127.0.0.1:8080
2 threads and 200 connections
Thread calibration: mean lat.: 4793.276ms, rate sampling interval: 16793ms
Thread calibration: mean lat.: 5166.052ms, rate sampling interval: 18546ms
Thread Stats Avg Stdev Max +/- Stdev
Latency 19.54s 5.62s 29.70s 57.62%
Req/Sec 332.00 1.00 333.00 100.00%
19434 requests in 30.26s, 2.45MB read
Requests/sec: 642.14
Transfer/sec: 82.78KB
RPS가 135,976에서 642로 크게 감소했습니다. 이는 각 요청에 300ms 지연이 추가되어 서버의 처리 능력이 크게 제한되었기 때문입니다.
각 요청의 처리에 많은 시간이 소요되면서 스레드가 대부분의 시간을 I/O 바운드로 인한 대기 상태로 소요하고 있습니다. 이에 따라, 병렬 처리량을 증가시키기 위해 스레드 및 연결 수를 더 증가시키는 것을 고려해볼 수 있습니다.
요청 수를 늘리기 위해 스레드와 연결 수를 증가시켰지만, 다음과 같이 시스템 제한에 도달했습니다:
> wrk -t1000 -c1000 -d30s -R 800000 http://127.0.0.1:8080
unable to create thread 128: Too many open files
이 문제를 해결하기 위해서는 시스템 설정을 조정해주어야 합니다.
> ulimit -n
256
sudo sh -c "ulimit -n 65536 && exec $SHELL"
설정 조정 후 RPS를 확인해보면 다음과 같습니다.
> wrk -t1000 -c1000 -d30s -R 800000 http://127.0.0.1:8080
Thread Stats Avg Stdev Max +/- Stdev
Latency 19.94s 5.72s 29.74s 57.58%
Req/Sec 3.00 0.00 3.00 100.00%
98997 requests in 30.02s, 12.46MB read
Requests/sec: 3297.97
Transfer/sec: 425.13KB
> wrk -t100 -c12000 -d30s -R 800000 http://127.0.0.1:8080
Latency 18.97s 5.32s 28.39s 57.57%
Req/Sec 398.22 0.86 403.00 96.00%
1174533 requests in 30.18s, 147.86MB read
Socket errors: connect 0, read 85, write 0, timeout 0
Requests/sec: 38916.46
이 결과는 시스템 설정 조정과 부하 테스트 도구의 매개변수 최적화를 통해 높은 지연 시간 환경에서도 상당한 RPS 증가를 달성할 수 있음을 보여줍니다.
추가로, 실시간 연결 수 다음과 같이 모니터링을 위한 스크립트를 사용해 테스트 중 시스템 상태를 지속적으로 관찰했습니다.
⌈code⌋ monitoring.sh
#!/bin/bash
while true
do
count=$(lsof -i | wc -l)
echo "$(date): $count active connections"
sleep 1
done
이번 실험을 통해 초기에 설정한 세 가지 주요 목적에 대해 다음과 같은 결과와 인사이트를 얻을 수 있었습니다: