API 부하 테스트를 위하여 리서치 후 찾게 된 Python 기반의 부하 테스트 라이브러리.
다른 Python 코드에서도 통합이 가능.
pip를 통해 간편한 설치 가능.
locustfile.py 파일 하나로 구성 가능.
RPS(Request Per Seconds), RT(Response Time) 제공.
싱글 머신에서도 충분한 부하 테스트가 가능하고, 필요하다면 분산 클러스터를 구성할 수 있으며 그 구성이 간단하게 가능.
구성 요소
# locustfile.py
...
# 동작을 실행할 User
# Web UI에서 User 지정 수 만큼 클래스 인스턴스가 생성됨.
# 각 클래스 인스턴스는 선택된 task를 co-routine 생성하여 실행.
# 각 co-routine은 task로 지정된 함수를 실행.
class User(FastHttpUser):
# 클래스 인스턴스가 스레드를 생성하여 함수를 실행한 뒤, 다음 실행까지 대기하는 시간.
wait_time = between(1, 1.5)
# 클래스 인스턴스가 스레드를 생성하여 실행하는 함수.
# @task 데코레이터를 통해 해당 함수가 task인지 지정 가능.
@task(1)
def api1(self):
self.client.get(path)
@task(10)
def api2(self):
self.client.get(path)
FastHttpUser
Locust에서 제공하는 기본 HTTP User는 HttpUser 클래스를 사용한다.
HttpUser 는 기본적으로 python-requests라는 라이브러리를 사용하는데, FastHttpUser는 geventhttpclient를 사용한다.
python-requests 보다 CPU 사용량이 적어 HttpUser 보다 7~8배 빠르다.
client
HttpSession 의 인스턴스이다. 쿠키 지원.
get(url, name, ...)
url : 요청을 할 URL
name : URL이 고정된 값이 아니고 여러 URL이 발생하게 된다면 그 URL들을 하나로 묶는 네임스페이스 역할을 한다.
클래스를 여러 개를 생성하게 될 경우, 다양한 유형의 유저가 API를 호출하는 시나리오를 쉽게 구성할 수 있다.
@task
@task라는 데코레이터로 지정한 함수만 실행할 task로 지정이 된다.
기본적으로 데코레이터로 지정한 task가 여러 개 라면, 인스턴스는 랜덤으로 하나의 task를 실행한다.
@task(1), @task(10) 이런 식으로 데코레이터에서 값을 파라미터로 사용할 수 있는데, 이는 가중치를 의미하며 만약 위와 같이 두 개의 task가 지정되었다면 총 실행하는 task 개수에서 @task(1)은 9.1% @task(10)은 90.9%의 비율로 실행되게 된다.
실행
pip를 이용하여 locust 설치 (python 3.7 이상)
pip install locust
CLI를 통해 실행.
기본 port 인 8089로 웹을 통해 모니터링이 가능하다.
locust
동작 방식
Number of Users
task를 실행하는 User 클래스 인스턴스의 수
locust 가 실행하는 thread는 커널 레벨의 thread 가 아닌 light-weighted thread인 co-routine이다.
실제로는 하나의 싱글 스레드에서 여러 개의 클래스 인스턴스가 co-routine으로 task를 실행한다.
co-routine vs thread - 동시성 처리 측면에서 코루틴은 하나의 스레드 내에서 함수를 호출하기 때문에, context-switching 비용과 메모리를 절약할 수 있는 장점이 있다. - gevent : co-routine 기반의 python networking library - python의 generator와 asyncio를 활용함. - https://www.gevent.org/
Spawn rate
초당 생성 할 유저의 수
Host
부하 테스트를 실시할 Host의 주소
Run time
테스트를 어느 시간만큼 실행할 것인지에 대한 설정
결과 분석
어느 시점에서 User가 늘어나도 RPS가 늘지 않는 이유
task가 서버에 get 요청을 할 때, response를 받지 않으면 task가 끝났다고 판단하지 않기 때문에 다시 task를 실행할 때까지 RT + wait_time 만큼 blocking이 된다.
RPS는 '초당 Request 수' 이기 때문에 response를 받지 못한 인스턴스들은 request를 날리지 못한다.
RPS는 유지되고 RT가 증가하면 API 서버 쪽에서 병목이 발생하는 것이 아닌가 의심을 해야 한다.
User가 늘어나도 RPS는 유지되고 RT도 증가하지 않는다면 부하 테스트를 실시하는 테스트 서버 측의 리소스 한계를 의심해야 한다.