Locust : 파이썬 기반 오픈 소스 로드 테스트
Locust
Locust는 사용이 쉽고, 스크립트 가능하고, 확장 가능한 성능 테스트 도구이다.
유저들의 행동을 파이썬 코드를 사용해서 정의할 수 있다.
Features
평범한 Python으로 사용자 테스트 시나리오 작성
Locust는 경량 코루틴인 greenlet 내에서 모든 유저를 실행한다. 그렇기 때문에 콜백이나 다른 메커니즘을 사용하지 않고 일반 python 코드와 같은 테스트를 작성할 수 있다.
분산 & 확장 가능 - 수 십만의 유저 지원
Locust를 사용하면 분산된 부하 테스트를 여러 시스템에 쉽게 동작 시킬 수 있다. gevent를 사용하는 이벤트 기반이기 때문에, 단일 프로세스에서도 수 천의 동접 유저 처리를 할 수 있다. 더 많은 요청을 수행할 수 있는 다른 도구가 있을 수 있지만, Locust의 각 유저의 낮은 오버헤드는 동시 부하를 테스트하는데 적합하다.
Web-based UI
Locust는 플라스크를 사용해서 webUI를 서빙한다. 따라서 web endpoint를 추가하기 쉽다. Flask Blueprints와 templates를 사용할 수 있다. Locust는 유저 친화적인 웹 인터페이스로 테스트를 실시간으로 보여 준다. UI 없이도 사용해서 CI/CD 테스트에서도 쉽게 사용할 수 있다.
from locust import events
@events.init.add_listener
def on_locust_init(web_ui, **kw):
@web_ui.app.route('/added_page")
def my_added_page():
return "Another page"
Can test any system
How to ~
설치 하기
| pip 을 사용한 설치
$ pip3 install locust
$ locust -v
실행 하기
| 특정 Path에 위치한 Locust 파일을 실행하기
- –master
- –worker
- –headless
$ locust -f locust_files/my_locust_file.py # http://127.0.0.1:8089
| Docker-Compose를 사용해 실행하기
$ docker-compose up --scale worker=4
Writing a locusfile
locustfile은 일반적인 python 파일이다. 최소 User 클래스를 상속 받는 클래스 하나면 필요하다.
User class
Locust는 각 시뮬레이트 될 각 유저 마다 User 클래스의 인스턴스를 생성한다.
User 클래스에서 정의해줘야 하는 공통 attribute들이 존재한다.
wait_time attribute
Users
클래스의 wait_time
메소드는 optional attribute로 가상 유저들이 대기해야하는 태스크들 사이의 시간이 필요할 때 사용한다. wait_time
이 정의되지 않으면 새로운 태스크는 대기 없이 바로 실행된다.
from locust import User, task, between
class MyUser(User):
@task
def my_task(self):
print("Executing my_task")
wait_time = between(0.5, 10)
weight attritubute
하나 이상의 User 클래스가 파일에 존재하거나, User 클래스들이 커맨드 라인에 구체화 되지 않았을 때 Locust는 각 User 클래스들을 동일한 수 만큼 생성한다. 동일한 파일 내에서 어떤 User 클래스를 특정할 지, 커맨드 라인에서 정할 수 있다.
$ locust -f locust_file.py WebUser Mobile User
weight attribute를 클래스에서 사용해서 더 많은 유저들로 시뮬레이트 할 수 있다.
class WebUser(User):
weight = 3
...
class MobileUser(User):
weight = 1
...
host attribute
커맨드 라인에서 --host
옵션을 사용할 수 있으나 WebUI에서 입력할 수 있다.
tasks attribute
User 클래스는 메소드로 정의된 태스크를 @task
데코레이터를 사용해서 선언할 수 있다.
tasks attribute를 사용해서도 할 수 있다.
environment attribute …
on_start & on_stop 메소드 User ( 그리고 TaskSets ) 가 선언할 수 있다.
- User :
on_start
메소드는 running 시작 시에,on_stop
메소드는 running 이 멈춘 뒤호출 된다. - TaskSet :
on_start
메소드는 TaskSet을 가상 유저가 실행 시에,on_stop
은 가상 유저의 TaskSet 실행이 멈추거나, 유저가 죽을 때 호출 된다.
Tasks
tasks attribute
task를 선언하는 다른 방법 중 하나이다.
from locust import User, constant
def my_task(user)
pass
class MyUser(User):
tasks = [my_task]
wait_time = constant(1)
@task decorator @tag decorator
Events
테스트의 한 파트로 일부 설정 코드를 사용하기에 locustfile에 모듈 레벨로 두는 것으로 충분하지만,
실행 중에 특정 시간에 작업을 하는 경우에는 Locust에서 제공하는 event 훅을 사용하면 된다.
test_start & test_stop
부하 테스트 start 혹은 stop 시, 일부 코드를 실행해야 하는 경우, test_start
와 test_stop
이벤트를 사용해야 한다. locust 모듈 수준에서 이러한 이벤트에 대한 리스너를 설정할 수 있다.
from locust import events
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
print("A new test is starting")
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
print("A new test is ending")
init
init
이벤트는 각 Locust 프로세스가 시작 될 때, 트리거 된다.
분산 모드에서 각 워커 프로세스가 초기화 시에 뭔가 해야 할 일이 필요한 경우에 특히 유용하다.
from locust import events
from locust.runner import MasterRunner
@events.init.add_listener
def on_locust_init(environment, **kwargs):
if isinstance(environment.runner, MasterRunner):
print("I'm on master node")
else:
print("I'm on worker or standalone node")
HttpUser class
Validating responses
리쿼스트 들은 HTTP response code 가 OK 시에 성공으로 판단된다. (< 400)
그러나 추가 validation이 필요한 경우에 사용 될 수 있다.
- catch_response 파라미터
- with 구문
- response.failure() 호출
with self.client.get("/", catch_response=True) as response:
if response.text != "Success":
response.failure("Got wrong response")
elif response.elapsed.total_seconds() > 0.5:
response.failure("Request took too long")
with self.client.get("/does_not_exist/", catch_response=True) as response
if response.status_code == 404:
response.success()
from json import JSONDecodeError
...
with self.client.post("/", json={"foo":42, "bar":None}, catch_response=True) as response:
try:
if response.json()['greeting'] != 'hello':
response.failure('Did not get expected value in greeting')
except JSONDecodeError:
response.failure("Response could not be decoded as JSON")
except KeyError:
response.failure("Response did not contain expected key 'greeting'")
for i in range(10):
self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")
Advanced
Locust의 성능을 향상 시키기 faster HTTP client
Locust의 기본 HTTP client는 python-request이다. 잘 관리되는 파이썬 패키지이고 사용하는 것이 권장 된다.
만약 굉장히 거대한 규모의 테스트를 계획한다면, 다른 HTTP Client를 대안으로 사용할 수 있다. FastHttpUser
를 사용해서 geventhttpclient로 requests를 하는 것이다.
class FastLargeScaleUser(FastHttpUser):
wait_time = between(2, 5)
@task
def index(self):
response = self.client.get("/")
커스텀 load 형태 생성
load spike를 생성하거나 지정한 시간에 늘리거나 줄이는 것을 원한다면, LoadTestShape
클래스를 사용해서 컨트롤 할 수 있다.
해당 클래스에서 tick()
메소드를 정의할 수 있는데 Locust는 메소드를 초당 한번 호출한다.
class MyCustomShape(LoadTestShape):
time_limit = 600
spawn_rate = 20
def tick(self):
run_time = self.get_run_time() # check time how long the test run for
# user_count = get_current_user_count()
if run_time < self.time_limit:
user_count = round(run_time, -2)
return (user_count, spawn_rate)
return None
로깅
Locust는 python의 빌트인 로깅 프레임워크를 사용해서 로그를 핸들링한다.
User class
class User(environment)
생성될 ‘user’를 나타내며 로드 테스트할 시스템을 공격한다. 이 유저의 행동은 task로 정의된다.
Tasks는 @task decorator
메소드를 사용하거나 tasks attribute
를 세팅함으로 선언된다.
-
abstract() : True면, 클래스가 서브 클래스가 된다. 테스트 중에 유저들을 생성하지 않는다.
-
on_start() : User가 running을 시작할 때 호출된다.
-
on_stop() : User가 running을 멈출 때 호출된다.
-
tasks() : python으로 호출 가능한 태스크셋 클래스들의 Collection
- list : 태스크가 랜덤으로 선택되어 동작된다.
- 두개 이상의 tuple(callable, int)로 구성된 list나 dict 시에 int 값으로 가중되게 선택되어 동작한다.
-
wait() : User.wait_time 함수로 정의된 시간동안 동작 중인 유저를 sleep
-
wait_time() : locusts 태스크들 실행 간 시간을 반환하는 메소드
TaskSet class
class TaskSet(parent)
User가 실행할 태스크들의 셋을 정의하는 클래스
태스크셋이 동작하기 시작했을 때, tasks attribute로 부터 task를 선택하고, 실행하고, wait_time 함수로 반환된 초 만큼 sleep 한다.
태스크셋은 중첩될 수 있다.
task decorator
task(weight=1)
User 또는 TaskSet에 대한 작업을 클래스에서 in-line으로 선언 할 수있는 편리한 데코레이터로 사용된다.
tag decorator
tag(*tag)
주어진 태그명으로 tasks와 TaskSets를 태깅하는 데코레이터
class ForumPage(TaskSet):
@tag('thread')
@task(100)
def read_thread(self):
pass
@tag('thread')
@tag('post')
@task(7)
def create_thread(self):
pass
SequentialTaskSet class
SequentialTaskSet(*args, **kwargs)
User가 실행할 태스크 들의 시퀀스를 선언하는 클래스
TaskSet 클래스 처럼 동작하지만, task weight가 무시된다. 모든 태스크들이 순서대로 실행된다.
빌트인 wait_time 함수들
between(min_wait, max_wait)
min_wait ~ max_wait 간 랜덤 넘버를 반환한다.
constant(wait_time)
wait_time으로 고정된 넘버를 반환한다.
constant_pacing(wait_time)
Task의 실행 시간을 트래킹하는 함수를 반환하고 호출 마다 Task 실행 시간 사이의 총 시간을 wait_time 인수에 지정된 시간과 동일하게 만들려고 하는 대기 시간을 반환한다. 만약 task 실행 시간이 선언된 wait_time 초과 되면, 다음 task 실행 시 까지 wait가 0가 된다.
class MyUser(User):
wait_time = between(3.0, 10.5)
#wait_time = constant(3)
#wait_time = constant_pacing(1)
@task
def my_task(self):
time.sleep(random.random())
HttpSession class
WHAT?
Response class
이 클래스는 python-requests 라이브러리 내에 위치, request 문서에서 확인할 수 있음.
HTTP request에 대한 서버의 응답을 담고 있는 Response
object
class Response
- property
- apparent_encoding
- is_permanent_redirect
- is_redirect
- content ( 바이트 형태의 response content )
- close()
- cookies = None
- elapsed = None ( request를 보내고 response 도달 까지의 시간 )
- encoding = None ( r.text를 디코딩 하기 위한 인코딩 )
- headers = None
- history = None
- iter_content(chunk_size=1, decode_unicode=False)
- response 데이터에 대해 iterate stream=True일 때, 긴 response에 대해 content를 메모리를 한번에 올리지 않는다. chunk_size 만큼 메모리에 올리고, 지정된 unicode로 decode )
- iter_lines(chunk_size=1, decode_unicode=False, delimeter=None)
- 한번에 한 라인
- json(**kwargs)
- json 인코딩된 response의 content를 반환한다.
- Parameters
- Raises
- property
- links
- next
- ok
- text
- raise_for_status()
- raw = None
- reason = None
- request = None
- status_code = None
- url = None
- json 인코딩된 response의 content를 반환한다.
ResponseContextManager class
class ResponseContextManager(response, request_access, request_failure)
HTTP 요청이 Locust 통계에서 성공 또는 실패로 표시되어야 하는지 여부를 수동으로 제어하는 기능을 제공하는 context manager 역할도 하는 response class
이 클래스는 Response
의 서브 클래스로 success와 failure 두가지 메소드가 추가 되었다.
failure(exc)
with self.client.get("/", catch_response=True) as response:
if response.content == b"":
response.failure("No data")
success(exc)
with self.client.get("/does/not/exist", catch_response=True) as response:
if response.status_code == 404:
response.success()