쿼카러버의 기술 블로그

[Python] 파이썬 부하테스터 Locust 개념 정리 및 꿀팁 소개 본문

[Infra & Server]/API

[Python] 파이썬 부하테스터 Locust 개념 정리 및 꿀팁 소개

quokkalover 2022. 8. 11. 18:20

필자는 현재 API 서비스를 개발하고 있다. 회사에서 가장 많은 트래픽을 받는 서비스의 주요 로직들을 변경하게 됐는데, 변경된 로직을 검증하기 위해 production level과 비슷한 수준의 트래픽, 그리고 스트레스 테스트 등을 진행해야 했다. 따라서 변경된 로직의 서비스를 production 환경과 동일하게 셋업하고 부하테스트를 진행하기로 했다.

일반적인 서비스 개발 후에 부하테스트는 간단하게 로컬 서버 한 대에서, docker를 활용해 여러 worker를 띄운뒤 테스트해볼 수 있었지만, 이번에는 워낙 트래픽을 좀 받는 서비스기도 하고, 테스트해야 하는 로직들이 CPU-bound 태스크가 좀 많아서, 실제로 약 10개정도의 노드를 띄워 분산테스트를 진행해야 했다. 그리고 아래의 옵션들을 만족할 수 있는 스트레스 테스터를 찾아야 했다.

 

테스터 조건

(1) Ethereum 관련 라이브러리 지원

(2) 메인 request보내기 전 다른 서비스에 http request날릴 수 있어야 함

(3) 내가 익숙한 언어여야 함 (go, python, node.js)

 

그리고 아래의 테스터를 조사해보았고,

 

  • Jmeter : java 기반, 간단한 UI로 분산테스트 가능. 하지만 ethereum 관련 라이브러리 지원 X
  • Artillery : nodejs가 사용 가능. 실제 요청을 처리하는 파트에서 node.js사용이 안됨, 라이브러리 지원 X
  • K6 : nodejs 사용 가능. 로딩할 수 있는 import 크기에 제한이 있고, os, fs 등 라이브러리 사용 불가능
  • ngrinder : python 사용 가능., 이미 활용해봐서 익숙하지만 package import가 번거로웠음
  • locust : python 사용 가능. 여러 패키지 import가능함

결론은 locust로 진행하자!! 였다. 일반적인 요청만 있는 부하테스트 였다면 유명하기도 하고 실제 리소스 대비 성능이 잘나왔던 Jmeter를 사용했겠지만, 내가 원하는 옵션에 적합한 부하테스트 도구는 locust라고 판단했다.

 

python으로 성능 테스트를 ?!

일단 python 기반의 성능테스터는 성능이 떨어진다는 인식이 높다. 파이썬은 GIL로 인한 병렬 처리에 한계가 존재하기 때문이다. 하지만 파이썬 커뮤니티는 이에 기죽지 않고 퍼포먼스를 개선하기 위해 동시성 처리 관련 라이브러리를 개발해왔다. 그 노력의 일환으로 많이 사용되는 라이브러리로 tornadoGevent가 있다.

 

Locust는 높은 성능을 보장하기 위해 Gevent를 사용한다. Geventlibdev기반의 비동기 라이브러리와greenlet이라는 경량 스레드를 활용하고 있는 라이브러리다. Gevent를 사용하면 코루틴을 스케줄링하고 event-loop 위에서 로직들을 실행 할 수 있다. Gevent가 무엇인지에 대해서는 참고자료에 적혀있는 문서를 참고하는 것을 추천하고, 요약하자면 Locust는 부하테스트를 위해 비동기 라이브러리를 활용해 HTTP 요청을 보낸다고 생각하면 되겠다.

 


 

 

따라서 본 글에서는 locust를 활용해 부하테스트를 진행하기 위해 내가 공부했던 것들을 정리하고, locust의 performance를 높일 수 있는 여러가지 튜닝 팁들을 정리해보려고 한다.

 

Locust 사용시 알아두어야 할 기본 개념

 

User Class

locust에서 User class는 한 명의 user를 의미한다. Locust가 지정한 유저 수만큼 User class의 인스턴스를 생성하고, 각 인스턴스가 요청을 보내게 된다. User class는 아래와 같은 attribute을 가진다. attrubute는 클래스 변수로 적어주면 된다.

wait_time attribute

  • constant for a fixed amount of time
  • between for a random time between a min and max value

weight or fixed_count

  • 특정 User class가 더 많은 작업을 하게 하고 싶거나, 요청 횟수를 지정해주고 싶으면 위 속성 활용
class WebUser(User):
    weight = 3
    ...

class MobileUser(User):
    weight = 1
    ...

on_start, on_stop

각 User class에는 on_starton_stop method를 통해 User class의 인스턴스가 생성돼서 task를 실행하기 시작할 때와 끝나고 나서 해야 할 작업들을 정해둘 수 있다.

참고로 locust 테스트를 작성하다보면, User Class가 로딩될때 실행되는 코드와, Master node에서만 실행되는 코드, 테스트 실행때만 실행되는 코드 등 여러 초기화 메소드들이 있기 때문에 사용에 주의해야 한다.

tasks

task는 분산테스트의 업무를 의미한다. 분산 테스트가 시작되면, 개발자가 지정한 수만큼 User를 생성하면 그 만큼의 green thread(user-level 스레드)가 생성되고, 그 수만큼 User class의 인스턴스가 정의된 tasks를 실행한다.

task를 정의하는 방법은 크게 두 가지가 있다.

(1) decorator사용

  • decorator의 argument는 task의 execution ratio를 넣고싶을 때 사용한다.
from locust import User, task, between

class MyUser(User):
    wait_time = between(5, 15)

    @task(3)
    def task1(self):
        pass

    @task(6)
    def task2(self):
        pass

(2) 함수 정의 후 tasks attribute에 해당 함수들 입력

from locust import User, constant

def my_task(user):
    pass

class MyUser(User):
    tasks = [my_task]
    wait_time = constant(1)

Sequential Task

task에 순서를 부여하고 싶은 경우는 아래처럼 sequential task set을 정의할 수 있다.

from locust import HttpUser, SequentialTaskSet, task, between
class User(HttpUser):    
    @task
    class SequenceOfTasks(SequentialTaskSet):
        wait_time = between(1, 5)
        @task
        def mainPage(self):
            self.client.get("/")
        @task
        def login(self):
            self.client.options("https://richet.com/login")
            self.client.post("url",json={"username":"haha"})        
        @task
        def clickProduct(self):
            self.client.options("https://richet.com/product")
            self.client.post("url",json={"username":"haha"})        
        @task
        def addToCart(self):
            self.client.options("https://richet.com/cart")
            self.client.post("url",json={"username":"haha"})        
        @task 
        def viewCart(self):
            self.client.options("https://richet.com/cart")
            self.client.post("url",json={"username":"haha"})

Events

테스트의 특정 시점에 setup code가 필요한 경우, 예를 들어 테스트의 시작과 끝에 어떤 setup code를 작성해야 한다면 아래와 같은 이벤트를 정의할 수 있다

test_start and test_stop

  • 위는 load test를 시작하고 끝낼 때 실행되는 코드
  • 분산환경에서 실행된다면 master node에서만 실행된다.
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

  • 만약 master node뿐 아니라 locust process가 실행될 때마다 초기화해야 하는 코드가 필요하다면 아래와 같이 정의할 수 있다
from locust import events
from locust.runners import MasterRunner

@events.init.add_listener
def on_locust_init(environment, **kwargs):
    if isinstance(environment.runner, MasterRunner):
        print("I'm on master node")
        elif isinstance(environment.runner, WorkerRunner):
                print("I'm on a worker node") 
    else:
        print("I'm on a standalone node")

Logging

Locust를 실행할 때 아래 옵션들을 활용해 로깅을 셋업할 수 있다.

-skip-log-setup : 직접 로그를 configure하고 싶은 경우 사용

-loglevel : 기본값은 INFO인데, 원할 경우 DEBUG/INFO/WARNING/ERROR/CRITICAL 중에 선택

-logfile : 로그 파일이 저장될 경로 선택. 만약 지정되지 않으면 stdout/stderr 로 로깅된다.

기본적으로 locust가 사용하는 로그 외에, 따로 로그를 적고 싶은 로직에서는 아래처럼 로그를 찍으면된다.

import logging
logging.info("this log message will go wherever the other locust log messages go")

성능 튜닝 팁

connection pool

위 세팅외에도 Connection Pool을 사용해서 생성되는 유저마다 자체적인 pool을 가지지 않고, 모든 유저가 connection pool을 공유하도록 하고 싶으면 아래와 같이 pool_manager를 정의할 수도 있다.

from locust import HttpUser
from urllib3 import PoolManager

class MyUser(HttpUser):

    # All users will be limited to 10 concurrent connections at most.
    pool_manager = PoolManager(maxsize=10, block=True)

FastHttpUser 클래스 사용

기본적으로 제공되는 HttpUser말고 아래와 같이 FastHttpUser를 상속받아 User Class를 정의하는게 좋다.

HttpUser는 코어당 850 RPS정도의 성능을 가지고, FastHttpUser는 코어당 5000RPS 정도의 성능을 가지기 때문이다. 필자도 성능테스트할 때 FastHttpUser를 사용했다.

FastHttpUser의 기본 attribute들은 아래 문서를 참고하자

https://docs.locust.io/en/stable/increase-performance.html#increase-performance

from locust import task, FastHttpUser

class MyUser(FastHttpUser):
    @task
    def index(self):
        response = self.client.get("/")

기본적인 proejct 구조

locust 테스트를 위한 프로젝트 구조는 아래와 같이 짜는 것을 추천한다.

  • Project root
    • common/
      • __init__.py
      • auth.py
      • config.py
    • locustfile.py
    • requirements.txt (External Python dependencies is often kept in a requirements.txt)
      import common.auth
    • common에 정의한 모듈을 사용할 경우 아래와 같이 import해서 사용하도록 하면 된다

자 이렇게 locust 개념에 대해 간단히 알아보았으니, 다음 글에서는 Locust를 활용해 어떻게 부하테스트를 할 수 있는지 간단한 예제코드를 소개하도록 하겠다.

 

 

 

참고자료

https://learn-gevent-socketio.readthedocs.io/en/latest/gevent.html

https://learn-gevent-socketio.readthedocs.io/en/latest/greenlets.html

https://docs.locust.io/en/stable/extending-locust.html

https://medium.com/dkatalis/distributed-load-testing-on-k8s-using-locust-6ee4ed6c7ca

Comments