[WIL] wrk2

yoonn·2024년 8월 1일
post-thumbnail

Intro

본 포스팅에서는 Mac M2 Max환경에서 wrk2를 사용하여 Go로 작성된 간단한 웹 애플리케이션의 성능 테스트를 진행한 과정을 작성했습니다. 이 테스트의 주요 목적은 다음과 같습니다.
1. wrk를 이용한 웹 서버의 최대 RPS(Request Per Second) 측정
2. wrk의 다양한 설정 값(스레드 수, 연결 수, 요청 비율 등)을 조합하여 최적의 성능 구성을 탐색
3. 부하 테스트 중 시스템 한계 식별 및 극복 방법을 모색

이 실험을 통해 단일 머신에서 최대한의 부하를 안정적으로 생성하는 방법을 찾고, 부하 생성 시스템의 성능을 최적화하여 테스트 대상 시스템의 실제 한계를 측정할 수 있는 환경을 구축하고자 합니다. 또한, 대규모 트래픽을 시뮬레이션할 수 있는 효과적인 부하 테스트 전략을 찾고자 합니다.

테스트 환경

  • Spec : Apple M2 Pro, Mem 32GB
  • Start CPU Usage : 18%
  • Start MEM Usage : 72%

Setup

  1. 소스 코드 다운로드. wrk2 에서 소스 코드를 clone 합니다.
git clone https://github.com/giltene/wrk2.git
  1. Rosetta2 설치. M2 Mac은 ARM 아키텍처를 사용하지만, wrk2의 일부 종속성은 x86_64 아키텍처를 필요로 합니다. Rosetta 2를 설치하여 이 문제를 해결할 수 있습니다.
sudo softwareupdate --install-rosetta
  1. x86_64용 OpenSSL 설치
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
  1. Makefile 수정
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
  1. Makefile 빌드
make clean && make
  1. 실행 파일 시스템 경로 설정
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)

Test

간단한 웹 어플리케이션 작성

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를 이용한 웹 서버 성능 테스트 설정

wrk -t20 -c400 -d30s -R 100000 http://127.0.0.1:8080
  • -t20: 20개의 thread
  • -c400: 400개의 동시 HTTP 연결
  • -d30s : 30초 동안 테스트 실행
  • -R 100000 : 초당 10만개의 요청(목표 RPS)
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
  • Latency : 평균 0.92ms, 최대 14.26ms
    - 이 Latency는 실제로 처리된 요청에 대한 것으로, 목표 RPS와 실제 처리된 RPS에 영향을 받습니다.
    - 즉, 목표 RPS 대비 실제 처리된 RPS가 낮으면 Latency 값은 이보다 지연된 결과를 얻게됩니다. 이는 시스템이 처리할 수 있는 요청만 처리하고 나머지는 드롭하기 때문입니다.
  • 초당 요청 수 : 초당 평균 3.06k, 최대 5.22k
  • RPS : 58,201.61
  • 전체 처리량 : 30초동안 전체 1,745,910개의 요청 처리, 총 전송 데이터는 219.78MB, 초당 전송량은 7.33MB/s
  • 오류 및 실패 : 소켓 연결 오류 167개, 타임아웃은 2,338개로 총 실패한 요청은 2,505건. (전체 약 0.14%)
  • Thread calibration : 평균 Latency가 0.842ms - 0.984ms고 sampling interval는 10ms로 일관된 값을 보입니다. 즉, 각 스레드가 비슷한 성능을 보이고 있어 부하가 고르게 분산되고 있음을 알 수 있습니다.

피크 부하 테스트

옵션 값만 조금씩 변경하면서 적정한 최대 RPS를 찾는 테스트를 진행했다.

ThreadRequest (1s)HTTP ConnectionTimeoutLatency(avg)Req/Sec(avg)Total RequestRPSTPS
1100,000100089761.85ms26.55k730,02424,3343.06MB
1200,000100089761.98ms53.11k1,459,99248,6626.13MB
1300,000100089762.11ms79.66k2,190,08173,0019.19MB
1400,000100089763.06ms106.32k2,919,91997,31612.25MB
1500,000100089761.39s119.06k3,450,768115,02314.48MB
1600,000100089763.27s133.27k3,756,762125,22415.76MB
1700,000100089766.46s124.77k3,579,766119,32315.02MB
1800,000100089767.52s127.46k3,820,454127,34616.03MB
1800,000500322413.89s129.70k3,795,810126,52515.93MB
1900,000500322414.48s132.03k3,814,249127,14116.01MB
1800,000200016.93s121.38k3,597,938119,93115.10MB
2800,000200016.22s68.35k4,079,281135,97617.12MB
2900,000200016.96s67.14k4,000,723133,34516.79MB
2800,000100016.80s64.42k3,816,189127,20716.01MB
2800,00050016.32s65.15k3,793,171126,43915.92MB
4800,000200016.62s33.46k4,036,785135,56216.94MB
8800,000200016.44s17.24k4,057,330135,25117.03MB
16800,000200016.64s8.37k4,181,967139,40617.55MB
  1. Thread 수
    • thread 갯수가 증가하면 Req/Sec의 표준편차가 증가해했습니다. 이는 공유 자원에 대한 경쟁으로 인해 스레드 간 처리 속도에 편차가 발생하기 때문입니다.
    • 스레드 수를 1에서 2로 증가시켰을 때, Latency가 13.89s에서 16.22s로 증가했습니다. 이는 공유 자원(CPU, 메모리, 네트워크 대역폭 등)에 대한 경쟁이 심화되었기 때문입니다.
    • 하지만 전체적인 처리량은 3,795,810 → 4,079,281로 증가했습니다. 이는 여러 스레드가 병렬로 작업을 처리함으로써 전체적인 처리 능력이 향상되었기 때문입니다.
  2. HTTP connection 변화
    • 연결 수를 줄이면 타임아웃 발생이 감소하는 경향을 보입니다.
  3. 이 테스트에서는 2개의 스레드와 200개의 HTTP 연결을 사용할 때 최대 RPS(135,976)를 달성했습니다.

요청 수 늘리기

⌈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
  • ulimit : 프로세스당 열 수 있는 파일 디스크립터의 최대 개수(열린 파일, 소켓, 파이프)
    • 이 값이 작으면 “Too many open files” 에러가 발생할 수 있습니다.
  • 각 네트워크의 연결은 운영 체제에서 1개의 file descriptor로 취급됩니다. 따라서, ulimit -n으로 설정되는 파일 디스크립터 제한은 동시에 유지할 수 있는 네트워크 연결의 최대 수를 직접적으로 제한합니다.
  • 현재 설정되어있는 ulimit 값은 256 입니다. (최대 256개의 동시 연결만 가능합니다.)
sudo sh -c "ulimit -n 65536 && exec $SHELL"
  • 프로세스당 열 수 있는 파일 디스크립터의 최대 갯수를 65,536으로 증가시킵니다. 이로 인해 동시에 더 많은 네트워크 연결 처리가 가능해지고, 더 많은 파일을 동시에 열 수 있습니다. 또한 I/O 집약적 작업의 성능이 향상됩니다. 과도한 리소스가 사용될 수 있으므로 적절한 수를 조절해서 사용해야 합니다.
  • 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


Conclusion

이번 실험을 통해 초기에 설정한 세 가지 주요 목적에 대해 다음과 같은 결과와 인사이트를 얻을 수 있었습니다:

  1. wrk를 이용한 웹 서버의 최대 RPS(Request Per Second) 측정
  • "Hello, World!"만을 응답하는 단순한 애플리케이션의 경우 최적의 wrk 설정으로 최대 약 135,000 RPS 처리량을 달성했습니다.
  • 300ms의 인위적 지연을 추가한 후, 초기 642RPS로 급감했으나 최적화 후 38,916 RPS까지 향상시킬 수 있었습니다.
  • 이를 통해 부하 테스트 시 서버에서 제공하는 API의 성능을 충분히 고려해야 함을 확인했습니다.
  1. wrk의 다양한 설정 값을 조합해 최적의 성능 구성 탐색
  • 스레드 수, 연결 수, 요청 비율 등의 매개변수가 전체 성능에 큰 영향을 미치는 것을 확인했습니다.
  • 이번 테스트에서 단순 응답의 경우 2개의 스레드와 200개의 HTTP 연결이 최적의 구성이었습니다.
  • 고지연 환경에서는 더 많은 스레드(100개)와 연결(12,000)을 사용해 성능을 크게 향상시킬 수 있었습니다.
  • 이를 통해 테스트 대상 서버의 특성에 따라 wrk 설정을 세심히 조정해야 함을 확인했습니다.
  1. 부하 테스트 중 시스템 한계 식별 및 극복 방법 모색
  • "Too many open files"에러를 통해 시스템의 파일 디스크립터 한계를 식별했습니다.
  • ulimit 값을 65,536 으로 증가시켜 이 한계를 극복해 더 높은 동시성을 달성할 수 있었습니다.
  • 실시간 연결 수 모니터링 스크립트를 통해 테스트 중 시스템 상태를 지속적으로 관찰해 병목 지점을 식별했습니다.
  • 이러한 방법을 통해 시스템 한계를 파악하고 극복하는 과정이 정확한 부하 테스트에 필수적임을 확인했습니다.

Reference

0개의 댓글