Django - Locust를 이용한 로드 테스트(Load test)
Web/Backend

Django - Locust를 이용한 로드 테스트(Load test)

로드 테스트: 시스템이 얼마만큼의 부하를 견뎌낼 수 있는 가에 대해 파악하기 위해 진행하는 테스트
 
 
What is Locust? - Locust 1.5.1 documentation
If you want your users to loop, perform some conditional behaviour or do some calculations, you just use the regular programming constructs provided by Python. Locust runs every user inside its own greenlet (a lightweight process/coroutine). This enables you to write your tests like normal (blocking) Python code instead of having to use callbacks or some other mechanism.
https://docs.locust.io/en/stable/what-is-locust.html

 

 

차트1 (Total Requests per Second) green line: RPS, red line: Failures/s

차트2 (Response Times (ms)) green line: Medium Response Time, yellow line: 95% percentile

차트3(Number of Users) green line: Users

 

실제 라이브 서비스를 위해 서버 산정을 할 때, 서비스 규모에 걸맞는 트래픽을 감당하기 위한 테스팅은 필수적

 

Load Testing (부하 테스트)

말그대로 시스템이 얼마만큼의 부하를 견뎌낼 수 있는 가에 대한 테스트를 말한다. 보통 Ramp up 이라하여 낮은 수준의 부하부터 높은 수준의 부하까지 예상 트래픽을 꾸준히 증가시키며 진행하는 테스트로, 한계점의 측정이 관건이며, 그 임계치를 높이는 것이 목적이라 할 수 있다. 일반적으로 "동시접속자수" 와 그정도의 부하에 대한 Response Time 으로 테스트를 측정한다. 예를들어 1분동안 10만명의 동시접속을 처리할 수 있는 시스템을 만들수 있는지 여부가 테스트의 주요 관건이 된다.

 

Stress Testing (스트레스 테스트)

스트레스 테스트는 "스트레스받는 상황에서 시스템의 안정성" 을 체크한다. 가령 최대 부하치에 해당하는 만큼의 많은 동시접속자수가 포함된 상황에서 시스템이 얼마나 안정적으로 돌아가느냐가 주요 관건이 된다. 스트레스 상황에서도 시스템의 모니터링 및 로깅, 보안상의 이슈나 데이터의 결함 등으로 서비스에 영향이 가서는 안되므로 결함에 대한 테스트가 일부 포함된다. 가령 메모리 및 자원의 누수나 발생하는 경우에 대한 Soak Test 나 일부러 대역폭 이상의 부하를 발생시키는 Fatigue Test 가 포함된다. 이처럼 부하가 심한 상황 또는 시스템의 복구가 어려운 상황에서 얼마나 크래시를 견디며 서비스가 운영될 수 있으며 빠르게 복구되는지, 극한 상황에서도 Response Time 등이 안정적으로 나올 수 있는지 등을 검사하게 된다. 예를들어 10만명의 동시접속 상황에서 크래시율을 얼마나 낮출 수 있는가, 혹은 데이터 누락을 얼마나 방지할 수 있는가 에 대한 테스팅이 있을 수 있다.

 

Performance Testing (퍼포먼스 테스트)

Load Test 와 Stress Test 의 모집합 격인 테스트의 종류이다. 리소스 사용량, 가용량, 부하 한도(Load Limit) 를 포함해서 응답시간, 에러율 등 모든 부분이 관건이 된다. 특정 상황에서 어떤 퍼포먼스를 보이는지에 대한 측정이 주가 되며, 서비스의 안정성보다는 퍼포먼스 자체에 집중한다. 주로 서비스적 관점에서는 Performance Test 보다는 Load Test 나 Stress Test 가 더 중점이 되며, 시스템 전체적인 성능의 양적 측정과 같은 관점에서 Performance Test 로 분류한다. 보통 그럴 때에 수행하는 테스트로 Benchmark 라고 하기도 한다.

 

스트레스트 테스팅을 위한 도구

  • Apache Jmeter
  • SoapUI
  • Locust

 

Locust

  • 현재 규모의 프로젝트에 적합한 규모의 도구다
  • Python으로 테스트 코드를 작성한다.
  • 빠르게 테스트 환경을 구축하고 실행해볼 수 있다.
  • 설치와 사용이 편리하고, 테스트 시나리오를 파이썬 스크립트로 작성을 하기 때문에 다양한 시나리오 구현이 가능하다.

 

python3 -m pip install locustio
locust -f locust_files/my_locust_file.py — host=http://example.com

 

부하 스크립트 생성

locust 부하테스트는 파이썬 스크립트를 실행하는 방식으로 하기 때문에, 부하테스트용 스크립트 파일을 작성해야 한다.

from locust import HttpLocust,TaskSet,task,between  
class MyTaskSet(TaskSet):   
    @task  
    def index(self):  
        self.client.get("/")
class MyLocus(HttpLocust):  
    task_set = MyTaskSet   
    wait_time = between(3,5)  

 

 

Locust 실행

locust 실행은 locust -f 형식

locust 서버가 기동되고, 부하를 줄 준비가 된 상태가 된다. 실제 부하 생성은 locust 웹콘솔에 접속해서 해야 한다.  아래 스크립트는 8080 포트로 locust 웹 클라이언트를 기동하도록 한 명령이다.

%locust -f ./locustfile.py --port 8080

http://localhost:8080

으로 접속하면 아래와 같이 웹 콘솔이 나온다.

첫번째 인자는, 몇개의 클라이언트를 사용할 것인지를 정의.

두번째는 Hatch rate 라고 해서, 초마다 늘어나는 클라이언트 수. 처음에는 클라이언트가 1대로 시작되서, 위의 설정의 경우 초마다 하나씩 최대 30개까지 늘어난다.

그리고 마지막은 테스트할 웹 사이트 주소를 입력한다.

Start Swarming 버튼을 누르게 되면 부하테스트가 시작되서 아래와 같이 부하 테스트 화면이 실시간으로 출력된다.

몇 위의 화면은 Total Request Per Second로, 초당 처리량이고, 두번째는 응답 시간, 그리고 마지막은 현재 클라이언트 수 를 모니터링 해준다.

 

Locust 종류

 

스크립트는 Locust (클라이언트)를 정의해서, Locust의 행동 시나리오 (TaskSet)를 정의해야 한다.

지원되는 Locust 종류는 HttpLocust와, Locust 두가지가 있다. HttpLocust는 웹 부하테스트용 클라이언트이고 범용으로 사용할 수 있는 클라이언트 (예를 들어 데이타 베이스 테스트)용으로 Locust라는 클래스를 제공한다. 하나의 부하테스트에서는 한 타입의 클라이언트만이 아니라 여러 타입의 클라이언트를 동시에 만들어서 실행할 수 있다.

예를 들어서 하나의 부하 테스트에서 안드로이드용 클라이언트와, iOS용 클라이언트를 동시에 정의해서 부하 비율을 정의해서 테스트(안드로이드,iOS = 7:3으로) 하는 것이 가능하다.

Locust Class

locust 클래스는 하나의 유저에 해당

locust 클래스에서 정의해야할 속성

  • task_set
  • min_wait & max_wait

task_set은 TaskSet 클래스의 인스턴스를 저장

min_wait & max_wait은 의미대로 접속 대기 시간의 최소 & 최대 값

from locust import Locust, TaskSet, taskclass 
MyTaskSet(TaskSet): 
    @task
    def my_task(self):  
        print(“executing my_task”)class MyLocust(Locust):
        task_set = MyTaskSet 
        min_wait = 5000 
        max_wait = 15000

 

TaskSet Class TaskSet 클래스는 task의 집합입니다. 즉, 테스트하고자 하는 어플리케이션의 기능들을 해당 클래스로 정의할 수가 있습니다.

하나의 task는 @task 데코레이터를 사용해 정의됩니다.

 

from locust import Locust, TaskSet, task 
class MyTaskSet(TaskSet):  
    min_wait = 5000 
    max_wait = 15000 
    @task(3)  
    def task1(self): 
    	pass 
    @task(6)  
    def task2(self):
    	pass
        
class MyLocust(Locust):
	task_set = MyTaskSet

@task 데코레이터는 인자로 weight 값을 받을 수 있습니다.

위 예시에서는, task2 태스크는, task1 태스크보다 2배 더 많이 실행됩니다.

 

Taskset

from locust import Locust, TaskSetdef 
my_task(l): 
	pass
class MyTaskSet(TaskSet): 
	tasks = [my_task]
class MyLocust(Locust): 
	task_set = MyTaskSet

TaskSet들은 아래와 같이 구조적으로 중첩될 수 있습니다.

 

TaskSequence class

TaskSet들의 실행 순서 조절

class MyTaskSequence(TaskSequence): 
@seq_task(1)  
def first_task(self): 
	pass 
@seq_task(2) 
def second_task(self):
	pass 
@seq_task(3) 
@task(10)  
def third_task(self): 
	pass

@seq_task 데코레이터의 인자 값을 통해 Task 실행 순서를 정의

 

 

Nesting

Locust에서 TaskSet은 다른 TaskSet을 호출(Nest) 하는 것이 가능하다.

예를 들어 부하 테스트 시나리오에서, 게시판에 글을 쓰는 시나리오와 상품을 구입하는 시나리오가 하나의 클라이언트에서 동시에 일어난다고 했늘때, 게시판에 글을 쓰는 TaskSet과, 상품을 구입하는 TaskSet을 정의하고 전체 시나리오에서 이 두 TaskSet을 호출하는 것과 같이 반복적이고 재사용적인 시나리오를 처리하는데 사용할 수 있다.

아래 예제를 보자, 아래 예제는 UserBahavior TaskSet에서 Nested 라는 TaskSet을 호출하는 예제이다.

from locust import Locust,TaskSet,task,between  
class Nested(TaskSet):         
@task(1)        
def task1(self):              
	print("task1")    
@task(1)      
def task2(self):             
	print("task2")      
@task(1)    
def stop(self):           
	print("stop")           
	self.interrupt() 
class UserBehavior(TaskSet):  
tasks =          
@task       
def index(self):       
	print("user behavior task")

UserBehavior에는 index라는 Task와 Nested TaskSet이 정의되어 있고, Nested TaskSet은 가중치가 2로 되어 있기 때문에, index task에 비해서 2배 많이 호출된다.

Nested TaskSet은 task1,task2,stop을 가지고 있는데, 각각 가중치가 1로 이 셋중 하나가 랜덤으로 실행되는데, Nested TaskSet이 실행이 시작되면, 계속 Nested TaskSet 안의 task들만 실행이 되고, 그 상위 TaskSet인 UserBehavior가 원칙적으로는 다시 호출되지 않는다. 즉 Nested TaskSet의 task들로 루프를 도는데, 그래서 stop task에 self.interrupt를 호출해서, Nested TaskSet 호출을 멈추고, UserBehavior TaskSet으로 리턴하도록 하였다.

 

Hook

TaskSet은 실행 전후에, Hook을 정의할 수 있다.

setup & teardown

setup과, teardown은 TaskSet이 생성되었을때와 끝나기전에 각각 한번씩만 수행된다.

locust가 실행되면, TaskSet 별로 정의된 setup 메서드가 실행되고, 프로그램을 종료를 하면, 종료하기전에 teardown 메서드가 실행된다.

setup과 teardown은 테스트를 위한 준비와 클린업등에 사용할 수 있는데, 예를 들어 테스트를 위한 데이타 베이스 초기화 등에 사용할 수 있다. 주의 할점은 클라이언트를 여러개 만든다고 하더라도, TaskSet의 setup과 teardown은 각각 단 한번씩만 실행이 된다.

on_start,on_stop

setup과 teardown이 전체 클라이언트에서 한번만 실행된다면, 클라이언트마다 실행되는 Hook은 on_start와  on_stop이 된다. on_start는 클라이언트가 생성될때 마다 한번씩 실행된다.

from locust import Locust,TaskSet,task,between  class UserBehavior(TaskSet): 	def setup(self): 		print("nested SETUP");  	def teardown(self): 		print("nested TEAR DOWN");  	def on_start(self): 		print("nested on start");  	def on_stop(self): 		print("nested on stop");  	@task(1) 	def task1(self): 		print("task1")  	@task(1) 	def task2(self): 		print("task2")  class User(Locust): 	task_set = UserBehavior 	wait_time = between(1,2)

코드를

%locust -f ./locust.py --no-web -c 2 -r 1

명령으로 2개의 클라이언트를 실행하게 되면 on_start는 단 두번 실행이 된다.

 

Task 생명주기

React의 component, Android의 activity의 Life-Cycle 처럼 Locust에도 Life Cycle이 있습니다. (공식 document에서는 Life Cycle이라고 표현하지는 않습니다. “Order of events”라고 표현함)

Locust setup, TaskSet setup과 같은 event들은 다음과 같은 순서를 가집니다.

1. Locust setup

2. TaskSet setup

3. TaskSet on_start

4. TaskSet tasks…

5. TaskSet on_stop

6. TaskSet teardown

7. Locust teardown

따라서 위의 event들을 활용한다면, 유저의 activity를 일련의 순서대로 작성할 수 있습니다.

Ex)유저 로그인 -> 프로필 페이지 접속 -> 로그아웃

 

HTTP requests

Http request는 HttpLocust 클래스를 통해 정의될 수 있습니다.

예시)

from locust import HttpLocust, TaskSet, taskclass
MyTaskSet(TaskSet): 
    @task(2)
    def index(self): 
    	self.client.get(“/”)
    @task(1) 
    def about(self): 
    	self.client.get(“/about/”)

class MyLocust(HttpLocust):  
    task_set = MyTaskSet 
    min_wait = 5000 
    max_wait = 15000

 

 

분산 부하 테스트

locust는 여러개의 worker를 이용하여, 부하를 대량으로 발생 시키는 분산 부하 테스트가 가능하다. 특히 분산 클러스터 구성 설정이 매우 간단하다는 장점을 가지고 있다.

마스터 노드의 경우에는 아래와 같이 --master 옵션을 지정하여 마스터 노드로 구동하면 되고,

% locust -f --host= --master

워커 노드의 경우에는 실행 모드를 slave로 하고, 마스터 노드의 주소만 명시해주면 된다.

% locust -f --host= --slave --master-host=

이렇게 클러스터를 구성하면 아래와 같은 구조를 갖는다

특히 워크노드를 추가할때 별도의 설정이 필요없이 워커노드를 추가하면서 마스터 노드의 주소만 주면 되기 때문에, 스케일링이 편리하다.

 

 

Task 코드 예시

import string
import random

from locust import HttpUser, task, between

def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
    return ''.join(random.choice(chars) for _in range(size))

class QuickstartUser(HttpUser):
    wait_time = between(1, 5)
    device_id_list = []

    @task(3)
    def get_search(self):
        self.client.get("/notice/all?target=cse+main&q=장학")

    @task(3)
    def get_notices(self):
        for iin range(1,11):
            self.client.get(f"/notice/all?target=cse+main&page={i}")

    @task(1)
    def put_subs(self):
        with self.client.put(
            "/accounts/device",
            json={
                "id":random.choice(self.device_id_list),
                "subscriptions":"cse+main"}) as response:
            ifnot response.ok:
                print(response)

    @task(1)
    def put_keywords(self):
        with self.client.put(
            "/accounts/device",
            json={
                "id":random.choice(self.device_id_list),
                "keywords":"장학+등록"}) as response:
            ifnot response.ok:
                print(response)

    @task(1)
    def put_alarm(self):
        with self.client.put(
            "/accounts/device",
            json={
                "id":random.choice(self.device_id_list),
                "alarm_switch":random.choice([True,False])}) as response:
            ifnot response.ok:
                print(response)

    @task(1)
    def get_notice_list(self):
        for iin range(5):
            self.client.get("/notice/list")

    def on_start(self):
        device_id = id_generator(30)
        self.device_id_list.append(device_id)
        self.client.post("/accounts/device", json={"id":device_id})

 

 

728x90