Published on

ab(Apache Bench)로 간단하게 부하테스트하고 결과 읽는 방법

Authors

업무에서 특정 조회 API가 동시 요청을 어느정도 받아줄 수 있는지 간단하게 확인할 일이 있었습니다.

부하테스트라고 하면 k6, Locust, nGrinder 같은 도구들이 먼저 떠오르는데, 시나리오를 짜고 스크립트를 작성할 정도의 일은 아니었고 단일 엔드포인트의 체감 수치만 빠르게 보고 싶었습니다.

그래서 mac에 기본으로 깔려있는 ab(Apache Bench)를 사용했는데, 생각보다 출력 결과에 읽을거리가 많아서 항목별로 해석한 내용을 기록합니다.

1. ab는 언제 쓰기 좋은가

ab는 아파치 재단에서 만든 HTTP 서버 벤치마킹 도구입니다.

mac에는 기본으로 설치되어 있고, 리눅스에서도 apache2-utils 정도만 설치하면 바로 사용할 수 있습니다.

which ab
# /usr/sbin/ab

개인적으로 느낀 ab의 포지션은 아래와 같습니다.

  • 단일 엔드포인트에 대해 "동시 요청 n개를 견디는지" 정도를 빠르게 확인할 때
  • 최적화 작업 전후로 같은 조건을 반복 실행해서 상대 비교할 때
  • 시나리오(로그인 → 조회 → 결제 같은 흐름)가 필요 없는 경우

반대로 유저 시나리오 기반의 본격적인 부하테스트가 필요하다면 그때는 Locust나 k6로 넘어가는 것이 맞다고 생각합니다.

2. 기본 사용법

제가 실행한 명령어의 형태는 아래와 같습니다. 운영 환경이 아닌 staging 환경에 테스트를 진행했습니다.

ab -n 100 -c 10 \
  -H "Authorization: Bearer <JWT토큰>" \
  "https://staging.example.com/api/some-list/?month=11&year=2025"
  • -n 100 : 총 100개의 요청을 보낸다
  • -c 10 : 동시에 10개씩 보낸다 (동시성 수준)
  • -H : 요청 헤더 추가. 인증이 필요한 API 라면 JWT 토큰을 이렇게 넣어주면 됩니다.

여기서 주의할 점 두가지가 있습니다.

첫번째는 URL에 쿼리스트링(?, &)이 있으면 반드시 따옴표로 감싸야 합니다. 안그러면 쉘이 &를 백그라운드 실행으로 해석해버립니다.

두번째는 URL이 /로 끝나지 않는 경우입니다. ab는 path가 없는 URL(https://example.com)을 넘기면 ab: invalid URL 에러를 냅니다. Django 프로젝트라면 어차피 trailing slash가 있어서 크게 신경쓸 일은 없었습니다.

3. 실행 결과 전체 읽어보기

실제 실행 결과입니다. (호스트 등 일부 정보는 가렸습니다)

Server Software:        nginx/1.18.0
Server Hostname:        staging.example.com
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128

Document Path:          /api/some-list/?month=11&year=2025
Document Length:        6133 bytes

Concurrency Level:      10
Time taken for tests:   3.368 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      642500 bytes
HTML transferred:       613300 bytes
Requests per second:    29.69 [#/sec] (mean)
Time per request:       336.819 [ms] (mean)
Time per request:       33.682 [ms] (mean, across all concurrent requests)
Transfer rate:          186.28 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       39   77  31.5     69     193
Processing:    81  226  67.6    225     375
Waiting:       81  226  67.6    225     375
Total:        145  303  67.9    305     492

Percentage of the requests served within a certain time (ms)
  50%    305
  66%    336
  75%    345
  80%    358
  90%    379
  95%    429
  98%    475
  99%    492
 100%    492 (longest request)

항목이 많아 보이지만 실제로 챙겨볼 것은 몇개 안됩니다.

3.1 Failed requests

가장 먼저 볼 것은 실패 여부입니다. 이번 테스트에서는 100건 모두 성공했습니다.

한가지 함정이 있는데, ab는 응답 body의 길이가 첫 응답과 다르면 실패(Length)로 카운트합니다. 응답에 타임스탬프같은 동적인 값이 포함된 API라면 멀쩡한데도 Failed로 잡힐 수 있습니다. 이럴때는 -l 옵션을 주면 length 차이를 무시합니다.

3.2 Requests per second (RPS)

Requests per second:    29.69 [#/sec] (mean)

이 서버(staging)가 이 엔드포인트에 대해 초당 약 30개의 요청을 처리했다는 의미입니다.

절대적인 수치보다는 조건을 고정하고 상대 비교하는 용도로 보는것이 좋다고 생각합니다. 예를들어 쿼리 최적화나 캐시 적용 전후로 같은 명령어를 실행해보면 개선폭이 RPS로 바로 보입니다.

3.3 Time per request가 두 개인 이유

처음에 헷갈렸던 부분입니다.

Time per request:       336.819 [ms] (mean)
Time per request:       33.682 [ms] (mean, across all concurrent requests)
  • 첫번째(336ms)는 클라이언트(유저) 입장에서 요청 하나가 걸린 평균 시간입니다.
  • 두번째(33.6ms)는 서버 입장의 처리량 관점으로, 전체 시간을 전체 요청 수로 나눈 값입니다. (3.368s / 100)

동시성이 10이기 때문에 정확히 10배 차이가 납니다. "유저가 체감하는 응답속도"를 보고 싶다면 첫번째 값을 보면 됩니다.

3.4 Connection Times와 Percentile

Connect는 TLS 핸드셰이크를 포함한 연결 시간이고, Processing이 실제 서버 처리+응답 수신 시간입니다.

그리고 개인적으로 mean보다 중요하게 보는 것이 아래의 percentile 분포입니다.

  50%    305
  95%    429
  99%    492

평균이 괜찮아도 p95, p99가 튀는 API는 일부 유저가 느린 응답을 겪고 있다는 뜻입니다. 이번 결과는 p99(492ms)와 median(305ms)의 차이가 크지 않아서, 응답이 비교적 고르게 나온다고 판단했습니다.

APM(Datadog 등)에서 p95를 기준으로 성능을 보는 이유와 같은 맥락입니다.

4. 사용하면서 주의했던 점

  • 운영(prod) 환경에는 쏘지 않았습니다. 부하테스트는 그 자체로 부하이기 때문에, staging에서만 진행했습니다. staging 이라도 외부 서비스(PG, 알림 발송 등)를 호출하는 API는 대상에서 제외하는게 안전합니다.
  • JWT 토큰의 만료시간을 확인해야 합니다. 토큰이 만료된 상태로 돌리면 401 응답도 "Complete requests"로 집계되어서, 낮은 응답크기와 비정상적으로 높은 RPS가 나옵니다. 다행히 2xx가 아닌 응답이 있으면 결과에 Non-2xx responses: 100 같은 줄이 추가로 출력되기 때문에, 이 줄이 보이면 테스트 자체가 잘못된게 아닌지 먼저 의심해야 합니다. Document Length가 정상 응답 크기인지 확인하는 것도 좋은 습관입니다.
  • -c 값을 올릴때는 점진적으로 올리는게 좋습니다. 10 → 30 → 50 처럼 올려보면 어느 지점부터 percentile이 무너지는지 보입니다.
  • keep-alive 테스트가 필요하면 -k 옵션을 사용합니다. 기본값은 매 요청마다 새 연결을 맺습니다.

간단한 확인 용도로는 ab로 충분하다는 결론을 얻었습니다.

도구를 세팅하는 시간보다 결과를 해석하는 눈이 더 중요하다고 느꼈고, 특히 Time per request 두 값의 차이나 Failed requests의 length 함정 같은 것들은 알아두면 다른 도구를 쓸때도 그대로 통용되는 개념들이라 정리해두었습니다.

다음에는 최적화 전후 비교에 ab를 활용한 사례도 기록해보려고 합니다.

참고 자료

  • hongreat 블로그의 글을 봐주셔서 감사합니다!^^
  • 내용에 잘못된 부분이나 의문점이 있으시다면 댓글 부탁 & 환영 합니다~!
  • (하단의 버튼을 누르시면 댓글을 보거나 작성할 수 있습니다.)
Buy Me A Coffee