사전 Locust
구현체

Locust

gabury1

Locust 는 서버에 부하를 걸어 보는 도구입니다. 사람 여럿이 한꺼번에 들어오는 상황을 흉내 내서 대상이 얼마나 받아내는지 잽니다. 흉내 낼 사용자가 무엇을 하는지는 파이썬 코드로 적습니다. 도는 동안 나오는 수치는 웹 화면에서 그대로 봅니다.

쉽고 빠른 이해

무슨 일을 하는 물건인가 — 파이썬 코드로 적은 가짜 사용자를 여럿 띄워 서버에 요청을 쏟아붓고 얼마나 버티는지 재는 도구입니다. locustfile.py 에 적은 대로, 예를 들어 /hello 같은 경로에 요청을 보냅니다.

왜 이렇게 하나 — 화면이나 장황한 설정 파일로 시나리오를 짜게 하는 기존 도구에 대한 불만에서 Locust 가 나왔다고 문서가 밝힙니다. 그래서 화면·설정 파일 대신 파이썬 코드를 쓰게 했고, 코드라서 반복·조건·계산이 필요할 때도 반복문·조건문·라이브러리를 그대로 씁니다.

어떻게 도나

  1. locustfile.py 에 흉내 낼 사용자가 할 일을 파이썬 코드로 적습니다.
  2. 실행하면 가짜 사용자 수만큼 각자 따로 돌리고, 사용자마다 할 일 하나를 골라 실행한 뒤 정해진 시간만큼 쉬었다가 다시 고릅니다.
  3. 웹 화면에서 보거나, 화면 없이 명령줄에서 결과를 글로 받습니다.

대가 — 진짜 브라우저처럼 페이지 속 이미지·스크립트를 알아서 더 받아오지는 않습니다. 그리고 프로세스 하나는 컴퓨터의 코어 하나만 쓰므로, 코어를 다 쓰려면 프로세스를 여러 개 띄워야 합니다.

상세

공식 문서는 Locust 를 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜) 를 비롯한 여러 프로토콜을 상대로 쓰는 오픈소스 성능·부하 테스트 도구라고 적습니다. 그리고 테스트를 평범한 파이썬 코드로 정의하게 해 주는 것이 이 도구의 접근이라고 덧붙입니다. 명령줄에서도 돌고 웹 기반 UI(User Interface, 사용자 인터페이스)로도 돕니다. 개요가 말한 웹 화면이 이것이고, 이 문서에서는 이하 웹 UI 라고 줄여 씁니다. 처리량과 응답 시간과 오류를 실시간으로 보거나 나중에 분석하려고 내보낼 수 있습니다. 설치는 pip install locust 한 줄입니다. locust -V 를 치면 판 번호와 파이썬 판이 함께 찍힙니다. 라이선스는 MIT(Massachusetts Institute of Technology, 매사추세츠 공과대학교) 라이선스입니다.

대본의 뼈대

부하 대본은 locustfile.py 라는 파일에 적습니다. 파일이 유효한 locustfile 이 되려면 User 를 상속한 클래스가 최소 하나 있어야 합니다. HttpUser 를 상속하면 사용자마다 client 속성이 생깁니다. HttpSession 인스턴스이고, 이걸로 대상 서버에 요청을 보냅니다. 이 호출은 Locust 의 통계에 기록됩니다.

@task 로 표시한 메서드가 대본의 알맹이입니다. 테스트가 시작되면 Locust 는 시뮬레이션하는 사용자 수만큼 이 클래스의 인스턴스를 만듭니다 — 앞의 요약 상자에서 가짜 사용자라고 부른 것이 이 인스턴스이고, 부하 테스트 분야에서는 흔히 가상 사용자라고 부릅니다. 사용자는 선언된 태스크 중 하나를 골라 실행하고, 끝나면 정해진 대기 시간만큼 쉬었다가 다시 하나를 고릅니다. 이 고르기·실행·대기가 사용자 하나가 테스트 내내 도는 주기입니다.

stateDiagram-v2
    [*] --> 태스크고르기
    태스크고르기 --> 실행 : 무작위로 하나를 고른다
    실행 --> 대기 : 태스크가 끝난다
    대기 --> 태스크고르기 : 대기 시간이 끝난다

고르기는 무작위지만 가중치를 줄 수 있습니다. @task(3) 은 그 태스크가 뽑힐 확률을 세 배로 만듭니다. 태스크 안의 코드는 순서대로 돕니다. 평범한 파이썬 코드라서, 앞 요청의 응답이 오기 전에는 뒤 요청이 안 나갑니다.

대기 시간은 wait_time 속성이 정합니다. wait_time = between(1, 5) 면 태스크 하나가 끝날 때마다 1초에서 5초 사이를 쉽니다.

사용자 하나에 실행 단위 하나

Locust 는 모든 사용자를 각자의 그린렛 안에서 돌립니다. 그린렛은 Locust 가 쓰는 라이브러리 gevent 가 주는 오버헤드가 작은 실행 단위로, 문서는 이걸 코루틴이나 마이크로스레드라고 풀고, 다른 자리에서는 gevent 가 만든 그린 스레드라고도 부릅니다. 이 구조 덕분에 콜백(응답이 왔을 때 실행할 함수를 미리 등록해 두는 방식) 같은 장치를 쓰지 않고 평소처럼 블로킹(요청을 보내면 응답이 올 때까지 다음 줄로 넘어가지 않고 기다리는 방식)하는 파이썬 코드로 테스트를 쓸 수 있다고 문서가 적습니다.

flowchart TD
    F["locustfile.py"] --> C["User 를 상속한 클래스"]
    C --> G1["그린렛 · 사용자 1"]
    C --> G2["그린렛 · 사용자 2"]
    C --> G3["그린렛 · 사용자 N"]
    G1 --> S["대상 서버"]
    G2 --> S
    G3 --> S

동작 방식은 gevent 를 쓴 이벤트 기반(요청을 기다리는 동안 자리를 붙들지 않고, 이벤트가 오면 그때 처리하는 방식)이고, 그래서 프로세스 하나가 수천 명의 동시 사용자를 감당할 수 있다고 문서가 적습니다.

배경

파이썬 코드로 사용자를 정의하는 이 접근은 기존 도구에 대한 불만에서 나왔다고 문서가 밝힙니다. 페이지마다 사용자에게 다른 내용을 내주는 동적인 웹사이트에 현실적인 부하를 걸 만큼 잘 갖춰진 도구가 없었고, 기존 도구들은 화면 인터페이스나 장황한 설정 파일로 테스트 시나리오를 적게 했다고 짚습니다. Locust 는 그 대신 설정 형식이나 화면 없이, 사용자의 동작을 파이썬 코드로 직접 적는 프레임워크 쪽을 택했습니다.

이름은 메뚜기(locust)라는 곤충 종에서 왔습니다. 문서는 이 곤충이 떼 지어 몰려다니는 습성으로 알려졌다고 적습니다.

포기한 것

화면으로 시나리오를 짜는 것

문서는 대부분의 다른 도구와 달리 테스트 설계가 GUI(Graphical User Interface, 그래픽 사용자 인터페이스) 나 도메인 특화 언어(Domain-Specific Language, DSL)에 갇히는 일이 없을 거라고 적습니다.

포기한 자리는 코드 없이 시나리오를 만드는 통로입니다. 대본이 파이썬 코드라서 대본을 쓰려면 코드를 써야 합니다. 대신 얻은 것은 파이썬이 주는 제어 구조입니다. 반복하고 싶으면 반복문을, 조건에 따라 다르게 굴리고 싶으면 조건문을, 계산이 필요하면 계산식을 그대로 씁니다. 평범한 파이썬 라이브러리를 가져다 쓸 수 있고, 평소 쓰는 IDE(Integrated Development Environment, 통합 개발 환경)로 쓰고, 다른 코드처럼 형상 관리에 넣습니다. XML(eXtensible Markup Language, 확장 마크업 언어)이나 이진 형식을 쓰는 다른 도구와 갈리는 자리가 여기라고 문서가 짚습니다.

브라우저 기록에서 시작하고 싶은 쪽을 위한 우회로는 따로 있습니다. HAR(HTTP Archive, HTTP 아카이브) 파일을 받아 locustfile 을 만들어 주는 har2locust 입니다. 다만 아직 베타라 언제나 올바른 locustfile 을 만들지 못할 수 있고, 판마다 인터페이스가 바뀔 수 있다고 문서가 적습니다.

브라우저 노릇

HttpUser 는 진짜 브라우저가 아닙니다. 그래서 HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어) 응답을 파싱해서 리소스를 더 받아오거나 페이지를 렌더하지 않습니다. 쿠키는 계속 추적합니다.

포기한 자리는 실제 브라우저가 만들어 내는 부수 요청입니다. 페이지 하나를 열 때 브라우저가 뒤이어 받아가는 이미지·스타일시트·스크립트가 이 도구에서는 저절로 생기지 않습니다. 그 요청까지 재려면 대본에 직접 적어야 합니다. 대신 얻은 것은 요청 하나가 곧 통계 한 줄이 되는 단순함과, 사용자마다 드는 비용이 작다는 것입니다. 문서는 각 Locust 사용자의 오버헤드가 낮아서 동시성이 높은 작업 부하를 테스트하기에 적합하다고 적습니다.

프로세스 하나로 코어를 다 쓰는 것

파이썬은 프로세스 하나로 코어 하나 이상을 온전히 쓰지 못합니다. 문서는 그 이유로 GIL(Global Interpreter Lock, 전역 인터프리터 잠금)을 지목합니다. 그래서 컴퓨팅 파워를 다 쓰려면 프로세서 코어마다, 사용자를 실제로 돌리는 프로세스인 워커 인스턴스를 하나씩 돌려야 한다고 못 박습니다. 현실에서는 CPU(Central Processing Unit, 중앙처리장치) 코어당 Locust 프로세스를 하나 돌려야 한다는 것입니다.

포기한 자리는 프로세스 하나로 기계 전체를 쓰는 것입니다. 대신 얻은 것은 프로세스를 늘리는 옵션이 제품 안에 들어 있다는 것입니다. --processes 가 그 자리입니다.

같은 하드웨어에서 초당 요청 수를 최대로 뽑는 것

동시 사용자 수 쪽에는 사실상 제한이 없습니다. 문서는 워커 하나에 몇 명을 돌릴 수 있는지에 거의 제한이 없다고 적습니다. Locust 와 gevent 는 프로세스 하나에서 수천 명, 수만 명까지도 무리 없이 돌린다고 합니다. 조건이 하나 붙습니다. 그 사용자들의 총 초당 요청 수(RPS, Requests Per Second)가 너무 높지 않아야 합니다.

천장이 옮겨간 자리가 그 초당 요청 수입니다. 기본 HTTP 클라이언트는 python-requests 입니다. 문서는 많은 파이썬 개발자에게 익숙한 API(Application Programming Interface, 응용 프로그램 인터페이스)를 주고 관리도 잘 되고 있지만, 처리량이 아주 높은 테스트를 제한된 하드웨어에서 돌릴 계획이라면 때때로 충분히 효율적이지 않다고 적습니다.

그래서 FastHttpUser 가 함께 들어 있습니다. 이쪽은 geventhttpclient 를 씁니다. 비슷한 API 를 주면서 CPU 시간을 훨씬 적게 쓰고, 주어진 하드웨어에서 초당 최대 요청 수를 때로는 5배에서 6배까지 늘린다고 문서가 적습니다. Locust 를 돌리는 쪽의 하드웨어가 모자라 CPU 가 천장에 닿는 자리가 바로 이 개선이 값을 내는 자리입니다. 다만 FastHttpUser 의 응답 시간 자체는, Locust 를 돌리는 쪽의 CPU 가 포화 상태가 아닌 한 HttpUser 와 거의 같습니다 — 개별 요청을 더 빠르게 만들지는 않는다고 문서가 못 박습니다.

값으로도 적어 두었습니다. 최선의 경우, 즉 while True 반복문 안에서 작은 요청을 던지는 경우에, 코어 하나에 묶인 Locust 프로세스 하나가 FastHttpUser 로 초당 약 16000 요청, HttpUser 로 4000 요청을 낸다고 합니다. 2021년 M1 맥북 프로(원문 표기는 2021 M1 MacBook Pro)와 파이썬 3.11 에서 잰 값입니다. 다만 특정 하드웨어와 특정 테스트 계획에서 몇 요청이 나올지는 말할 수 없으니 직접 재 봐야 한다는 단서가 붙어 있습니다. 요청과 무관한 계산을 대본이 많이 한다면 개선 폭이 더 작아질 수도 있습니다.

이 자리를 문서 스스로 정리한 문장이 있습니다. 주어진 하드웨어에서 초당 더 많은 요청을 낼 수 있는 다른 도구가 있을 수 있지만, 각 Locust 사용자의 낮은 오버헤드가 동시성이 높은 작업 부하를 테스트하기에 적합하게 만든다는 것입니다. 초당 요청 수의 정점을 내주고 사용자 수 쪽을 가져간 셈입니다.

예시

가장 짧은 locustfile

Python
from locust import HttpUser, task

class HelloWorldUser(HttpUser):
    @task
    def hello_world(self):
        self.client.get("/hello")
        self.client.get("/world")

공식 안내서가 첫 테스트로 싣는 코드입니다. 이 사용자는 /hello 로 요청을 하나 보내고, 그다음 /world 로 보내고, 다시 반복합니다. 두 경로를 대상 서버의 실제 경로로 바꾸고 locustfile.py 라는 이름으로 현재 디렉토리에 두면 준비가 끝납니다.

터미널
$ locust
[2021-07-24 09:58:46,215] .../INFO/locust.main: Starting web interface at http://0.0.0.0:8089
[2021-07-24 09:58:46,285] .../INFO/locust.main: Starting Locust 2.46.4

인자 없이 locust 만 치면 웹 UI 가 뜹니다. 로그가 그 주소를 적어 줍니다. 브라우저로 http://localhost:8089 를 열고 대상 서버의 호스트 이름을 넣으면 시작합니다.

웹 UI 없이 도는 실행 한 줄

터미널
$ locust --headless --users 10 --spawn-rate 1 -H http://your-server.com
[2021-07-24 10:41:10,947] .../INFO/locust.main: No run time limit set, use CTRL+C to interrupt.
[2021-07-24 10:41:10,947] .../INFO/locust.main: Starting Locust 2.46.4
[2021-07-24 10:41:10,949] .../INFO/locust.runners: Ramping to 10 users using a 1.00 spawn rate
Name              # reqs      # fails  |     Avg     Min     Max  Median  |   req/s failures/s
----------------------------------------------------------------------------------------------
GET /hello             1     0(0.00%)  |     115     115     115     115  |    0.00    0.00
GET /world             1     0(0.00%)  |     119     119     119     119  |    0.00    0.00
----------------------------------------------------------------------------------------------
Aggregated             2     0(0.00%)  |     117     115     119     117  |    0.00    0.00

--headless 는 웹 UI 를 끄고 즉시 테스트를 시작합니다. --users(-u 와 같음)가 최대 동시 사용자 수, --spawn-rate(-r 과 같음)가 초당 몇 명씩 늘릴지이고, -H 로 대상 서버의 주소를 줍니다. 결과는 화면 대신 글로 나옵니다. 표의 칸은 요청 이름, 요청 건수, 실패 건수, 평균·최소· 최대·중앙값 응답 시간, 그리고 초당 요청 수와 초당 실패 수입니다. 맨 아랫줄 Aggregated 가 전체 합계입니다.

클라이언트를 바꾸는 대본

Python
from locust import task, FastHttpUser

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

기본 클라이언트가 감당이 안 될 때 쓰는 형태입니다. HttpUser 대신 FastHttpUser 를 상속하는 것이 전부라고 문서가 적습니다. 대본의 나머지는 그대로 둡니다.

운영

웹 UI 와 시작 방식

웹 UI 는 실행하면 기본으로 붙습니다. 로그에 찍히는 주소가 http://0.0.0.0:8089 입니다. 어느 네트워크 인터페이스에 붙일지는 --web-host 가 정하고 기본값은 모든 인터페이스를 뜻하는 * 입니다. 포트는 --web-port 또는 -P 로 바꿉니다.

옵션 무엇을 하나
--headless 웹 UI 를 끄고 즉시 테스트를 시작합니다. 사용자 수와 시간은 -u 와 -t 로 줍니다
--autostart 즉시 시작하되 웹 UI 는 끄지 않습니다
-u, --users 최대 동시 사용자 수(앞서 말한 시뮬레이션하는 사용자 수). --headless 로 도는 동안 터미널에서 키보드로 바꿀 수 있습니다. w 는 1명, W 는 10명 추가, s 와 S 는 그 반대입니다
-r, --spawn-rate 초당 몇 명씩 늘릴지
-t, --run-time 300s · 20m · 3h · 1h30m 처럼 줍니다. 기본은 끝없이 도는 것입니다
-s, --stop-timeout 종료할 때 도는 태스크를 몇 초 기다릴지. 기본은 즉시 종료입니다

--run-time 의 시간은 테스트 시작 시점부터 셉니다. 사용자를 다 늘린 시점부터가 아닙니다.

분산 실행

프로세스 하나로 부족하면 여러 프로세스, 나아가 여러 기계로 펼칩니다. --master 로 하나를 띄우고 --worker 로 하나 이상을 띄웁니다.

마스터가 웹 UI 를 돌리고 워커에게 사용자를 언제 늘리고 멈출지 알립니다. 사용자를 실제로 돌리는 쪽은 워커이고, 워커는 통계를 마스터로 보냅니다. 마스터 자신은 사용자를 하나도 돌리지 않습니다.

flowchart TD
    M["마스터"] --> U["웹 UI"]
    M -->|"늘리고 멈추라는 지시"| W1["워커 1"]
    M -->|"늘리고 멈추라는 지시"| W2["워커 N"]
    W1 -->|"통계"| M
    W2 -->|"통계"| M
    W1 -->|"요청"| S["대상 서버"]
    W2 -->|"요청"| S
옵션 기본값과 쓰임
--master-host 워커가 붙을 마스터의 호스트. 기본값은 127.0.0.1 입니다
--master-port 마스터의 포트 번호. 기본값은 5557 입니다
--master-bind-host 마스터가 어느 인터페이스에 바인드할지. 기본값은 모든 인터페이스를 뜻하는 * 입니다
--master-bind-port 마스터가 어느 포트를 들을지. 기본값은 5557 입니다
--expect-workers --headless 로 마스터를 띄울 때 씁니다. 지정한 수의 워커가 붙을 때까지 기다렸다가 시작합니다
--processes locust 프로세스를 몇 번 포크할지. --worker 와 같이 쓰면 워커만 띄우고, 혼자 쓰면 마스터와 워커를 한꺼번에 띄웁니다

locust --processes 4 는 마스터 하나와 워커 넷을 띄웁니다. locust --processes -1 은 기계의 논리 코어 수를 알아내서 코어마다 워커를 하나씩 띄웁니다. 이 기능은 fork()(프로세스를 통째로 복제하는 운영체제 호출)에 기대기 때문에 윈도우에서는 안 돕니다. 문서는 이 옵션을 실험적이라고 표시해 두었습니다.

워커 기계 쪽에서는 대본을 마스터에서 받아올 수 있습니다. locust -f - --worker --master-host <마스터> --processes 4 꼴입니다. -f - 가 로컬 파일 대신 마스터에서 locustfile 을 받아오라는 뜻입니다. 단일 locustfile 일 때만 됩니다.

무엇을 보고 아나

원하는 부하가 안 나올 때, Locust 는 로그에 남는 경고로 원인을 가릅니다.

CPU 자원이 거의 바닥나면 Locust 가 로그에 경고를 남깁니다. 이 경고가 뜨면 컴퓨팅 파워 쪽이 천장이라는 뜻입니다 — 앞서 다룬 초당 요청 수 천장과 같은 자리입니다.

열린 파일 수 한도도 걸리는 자리입니다. 사용자와 HTTP 연결마다 파일을 하나, 정확히는 파일 디스크립터를 하나 엽니다. 많은 운영체제가 동시에 열 수 있는 파일 수의 기본 한도를 낮게 잡아 둡니다. Locust 가 이걸 자동으로 조정해 보지만 운영체제가 허용하지 않는 경우가 많고, 그때는 로그에 경고가 뜹니다. 그러면 직접 올려야 합니다.

CPU 경고와 열린 파일 수 경고는 서로 다른 곳을 가리킵니다. CPU 경고는 컴퓨팅 파워 쪽이 이미 천장에 닿았다는 뜻이고, 열린 파일 수 경고는 운영체제가 파일 디스크립터 한도를 자동으로 올려 주지 않았다는 뜻입니다.

설정과 결과 파일

옵션을 파일로 둘 수 있습니다. Locust 는 기본으로 ~/.locust.conf 와 ./locust.conf 와 ./pyproject.toml 을 찾습니다. --config 로 파일을 하나 더 지정합니다. 값이 겹치면 나중 것이 이깁니다. 순서는 ./pyproject.toml → ./locust.conf → --config 로 지정한 파일 → 환경변수 → 명령행 인자입니다.

결과는 --csv <파일이름> 으로 CSV(Comma-Separated Values, 쉼표로 구분한 값) 세 벌이 나옵니다. _stats.csv 와 _stats_history.csv 와 _failures.csv 입니다. --html <파일이름> 은 HTML 리포트를 남깁니다. 실패한 표본이 하나라도 있으면 프로세스는 종료 코드 1 로 끝납니다. 이 동작은 --exit-code-on-error 로 바꿉니다.

관련 항목

부하 테스트에서 쓰는 용어

부하 테스트 · 성능 테스트 · 가상 사용자 · 초당 요청 수 · 처리량 · 응답 시간 · 램프업 · 동시성

이 물건을 이루는 구성 요소

Python · gevent · 그린렛 · 코루틴 · GIL · python-requests · geventhttpclient · HTTP · 쿠키 · 파일 디스크립터 · fork · 웹 UI · 마스터-워커 · har2locust

다른 이름: locust · locustfile · HttpUser