기타

서버 부하 테스트 - Locust 사용 (FastAPI, Springboot 비디오 스트리밍 성능 비교), FastAPI Media Streaming 코드 수록

Railly Linker 2025. 4. 23. 22:36

- 이번 포스팅에선 Locust 를 사용하여 서버에 인위적으로 부하를 가하고, 그 결과를 파악하는 방법을 알아보고,

FastAPI 와 Springboot 에서 비디오 스트리밍을 할 때의 성능을 비교하겠습니다.

 

(Locust 란)

- Locust는 분산형 성능 테스트 도구로, 주로 웹 애플리케이션이나 웹 서비스의 부하 테스트를 수행하는 데 사용됩니다.

Locust는 Python으로 이루어져 있으며, 그렇기에 Python 문법을 사용하여 테스트케이스를 작성해야 합니다.

ChatGPT 등의 생성형 AI 로 쉽게 테스트 케이스를 만들 수 있으므로 사용이 매우 쉽다고 할 수 있습니다.


- Locust의 주요 특징:
1. 사용자 정의 테스트 시나리오:
Locust는 사용자가 정의한 Python 코드를 기반으로 테스트 시나리오를 작성합니다.

프로그래밍 언어를 기반으로 동작하기에, 사용자 흐름을 매우 세부적으로 설정할 수 있습니다.
예를 들어, 로그인, 상품 조회, 장바구니에 상품 추가 등의 동작을 사용자 시나리오로 만들 수 있습니다.

2. 동시성 지원:
멀티스레딩 및 비동기 방식으로 다수의 가상 사용자를 시뮬레이션할 수 있습니다.
실제 환경에서 여러 사용자가 동시에 시스템을 사용하는 상황을 모델링할 수 있어 서버나 애플리케이션의 부하를 정확히 측정할 수 있습니다.

3. Web UI 제공:
테스트 결과를 실시간으로 모니터링할 수 있는 Web UI를 제공합니다.
사용자는 브라우저에서 기본 주소 http://localhost:8089 주소로 접속하여 테스트 상태 및 성능 지표를 확인할 수 있습니다.

4. 분산 테스트:
Locust는 분산형 성능 테스트를 지원하여 여러 대의 컴퓨터에서 동시에 테스트를 실행할 수 있습니다.
이로 인해 대규모 트래픽을 테스트할 수 있습니다.

5. 유연한 테스트 설정:
테스트는 스크립트로 작성되어 있기 때문에, 다양한 HTTP 요청뿐만 아니라 복잡한 사용자 시나리오를 자유롭게 구성할 수 있습니다.
사용자는 테스트를 커스터마이즈하여 자신이 테스트하고자 하는 시나리오를 정확히 구현할 수 있습니다.

 

- 위와 같은 Locust 의 특성에 따라 간단한 실습을 아래에 진행해 볼 것입니다.

분산 테스트 및 시나리오 테스트는 추후 따로 진행하겠습니다.

 

(Locust 분산 실습)

- Locust 를 사용하기 위해선 Python 환경이 필요합니다.

될수 있다면 Anaconda 를 설치하여 가상환경을 구축하고,

해당 가상환경에서,

 pip install locust

 

이렇게 locust 를 설치하면 끝입니다.

 

- locust 실행 파일 만들기

locust 는 python 언어로 테스트케이스 코드를 작성해야 합니다.

import subprocess
from locust import HttpUser, task, between


class VideoStreamingUser(HttpUser):
    @task
    def video_streaming_test(self):
        # GET 요청으로 비디오 스트리밍 엔드포인트에 요청
        self.client.get(
            "/api-test/video-streaming",
            params={"videoHeight": "H720"},
            headers={"Accept": "text/html"}
        )


if __name__ == "__main__":
    subprocess.run([
        "locust",
        "-f", "stress_test_locusts/locust_case1.py",  # locust 파일 경로
        "--host", "http://localhost:12006",  # 테스트 대상 호스트
        "--web-port", "12345",  # 다른 포트로 웹 UI를 실행
        "--headless",  # CLI 모드로 실행 (브라우저 UI 없이)
        "-u", "500",  # 사용자 수
        "-r", "30",  # 초당 몇 명씩 증가할지
        "--run-time", "1m",  # 총 실행 시간
    ])

 

저는 위와 같이 stress_test_locusts/locust_case1.py 파일을 만들었습니다.

 

main 부터 보자면,

locust 명령어를 커맨드로 입력하는 것입니다.

 

저는 간단히 python 명령어로 테스트 케이스 파일을 실행시키게 하려고 위와 같이 명령어를 코드 안에 포함시켰지만,

만약 커맨드창으로 입력하고 싶다면 main 코드를 지우고, 위와 같은 명령어를 커맨드창에 입력하면 됩니다.

 

커맨드 설명을 하자면,

locust 를 실행시키고, 테스트 케이스 파일을 실행시키며, 부하 테스트 요청을 보낼 host 를 입력한 것입니다.

이 locust 를 실행시키면 localhost 의 12006 포트가 부하를 받게 되겠죠.

 

아래는 웹으로 실행시킬지 커맨드창으로 실행할지를 결정하는 것입니다.

web-port 는 웹으로 실행시킬 때, 어느 포트로 locust 웹에 접근할지를 설정한 것으로, 기본은 8089 포트지만, 위와 같은 설정상으로는,

 http://localhost:12345

이렇게 접근하도록 되어있습니다.

 

만약 headless 설정이 되어있다면 웹으로는 접근이 안되며 커맨드창에 결과가 보여집니다.

 

headless 설정 아래에는 주석에 쓰여있듯 사용자 수, 초당 증가수 등을 설정한 것으로,

headless 모드에서는 이 설정이 그대로 사용되며,

web 모드에서는 웹에 접속 후 화면에서 위 값들이 기본값으로 표기됩니다.

 

테스트 케이스 코드는 간단합니다.

/api-test/video-streaming 이라는 위치에 요청을 보내는 것으로,

요청 시나리오를 작성합니다.

 

- 위와 같은 코드를 사용하여 제가 테스트 할 것은, FastAPI 에서의 비디오 스트리밍 방식과 springboot 에서 지원하는 비디오 스트리밍 방식의 성능을 비교할 것입니다.

 

먼저, 테스트할 fast api 서버를 12006 포트로 실행시킨 후,

위 locust 테스트 코드를 실행시키면 됩니다.

locust 실행 화면

 

web 모드이므로 위와 같이 동작함을 볼 수 있는데,

위 화면에 나온 주소로 들어가보면,

locust 화면

 

위와 같이 앞서 입력한 설정값이 기본값으로 설정된 locust 시작 화면을 볼 수 있습니다.

 

여기서 start 를 누르면, 해당 서버로 위와 같은 설정으로 부하 테스트가 진행될 것입니다.

부하 테스트 모니터링

 

테스트 결과를 모니터링하면 위와 같이 확인할 수 있습니다.

 

만약 headless 모드로 실행했다면,

headless 모드 모니터링

 

이렇습니다.

 

결과값은 스스로 분석 방법을 배우는 것이 좋을테지만,

ChatGPT 로 분석을 대리해도 좋습니다.

 

(성능 비교)

- 각 프레임워크 성능 테스트를 진행하겠습니다.

500 명의 사용자의 동시 요청 환경,

동일한 720 해상도의 영상 스트리밍

 

- FastAPI

Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /api-test/video-streaming?videoHeight=H720                                      2018     0(0.00%) |   7292     383   40587   5800 |   36.50        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      2018     0(0.00%) |   7292     383   40587   5800 |   36.50        0.00

[2025-04-23 22:14:10,760] LhwBigNotebook/INFO/locust.main: --run-time limit reached, shutting down
Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /api-test/video-streaming?videoHeight=H720                                      2130     0(0.00%) |   7501     383   42476   6000 |   37.20        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      2130     0(0.00%) |   7501     383   42476   6000 |   37.20        0.00

[2025-04-23 22:14:12,566] LhwBigNotebook/WARNING/locust.runners: CPU usage was too high at some point during the test! See https://docs.locust.io/en/stable/running-distributed.html for how to distribute the load over multiple CPU cores or machines
[2025-04-23 22:14:12,567] LhwBigNotebook/INFO/locust.main: Shutting down (exit code 0)
Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /api-test/video-streaming?videoHeight=H720                                      2132     0(0.00%) |   7501     383   42476   6000 |   34.81        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      2132     0(0.00%) |   7501     383   42476   6000 |   34.81        0.00

Response time percentiles (approximated)
Type     Name                                                                                  50%    66%    75%    80%    90%    95%    98%    99%  99.9% 99.99%   100% # reqs
--------|--------------------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
GET      /api-test/video-streaming?videoHeight=H720                                           6000   7100   7800   8100   9000  25000  35000  39000  42000  42000  42000   2132
--------|--------------------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
         Aggregated                                                                           6000   7100   7800   8100   9000  25000  35000  39000  42000  42000  42000   2132

 

1. 요청 수 (reqs)
총 요청 수 2132건
실패한 요청 0건

2. 평균 응답 시간 (Avg)
평균 응답 시간 7501ms

3. 응답 시간 범위 (Min, Max)
최소 응답 시간 383ms

최대 응답 시간 42,476ms

4. 중앙값 (Median)
중앙값(Med) 6000ms

5. 요청당 초당 처리율 (req/s)
초당 요청 처리율 34.81req/s

결론:

CPU 사용량 경고가 발생했으므로, 제 컴퓨터 성능의 한계 상황에서의 측정입니다.
서버의 평균 응답 시간은 7.5초로, 높은 응답 시간을 나타냅니다. 최대 42초의 응답 시간 역시 성능에 문제가 있으므로,

스케일업이나 스케일아웃을 고려해야 하네요.

 

- Springboot

Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /api-test/video-streaming?videoHeight=H720                                      2265     0(0.00%) |   6486     145   26642   6800 |   48.70        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      2265     0(0.00%) |   6486     145   26642   6800 |   48.70        0.00

Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /api-test/video-streaming?videoHeight=H720                                      2383     0(0.00%) |   6572     145   27012   6900 |   49.10        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      2383     0(0.00%) |   6572     145   27012   6900 |   49.10        0.00

Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /api-test/video-streaming?videoHeight=H720                                      2487     0(0.00%) |   6716     145   28666   7000 |   50.70        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      2487     0(0.00%) |   6716     145   28666   7000 |   50.70        0.00

Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /api-test/video-streaming?videoHeight=H720                                      2618     0(0.00%) |   6867     145   31359   7200 |   50.70        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      2618     0(0.00%) |   6867     145   31359   7200 |   50.70        0.00

[2025-04-23 22:23:09,486] LhwBigNotebook/INFO/locust.main: --run-time limit reached, shutting down
Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /api-test/video-streaming?videoHeight=H720                                      2664     0(0.00%) |   6913     145   33336   7200 |   51.00        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      2664     0(0.00%) |   6913     145   33336   7200 |   51.00        0.00

[2025-04-23 22:23:11,975] LhwBigNotebook/WARNING/locust.runners: CPU usage was too high at some point during the test! See https://docs.locust.io/en/stable/running-distributed.html for how to distribute the load over multiple CPU cores or machines
[2025-04-23 22:23:11,977] LhwBigNotebook/INFO/locust.main: Shutting down (exit code 0)
Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /api-test/video-streaming?videoHeight=H720                                      2664     0(0.00%) |   6913     145   33336   7200 |   43.43        0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                      2664     0(0.00%) |   6913     145   33336   7200 |   43.43        0.00

Response time percentiles (approximated)
Type     Name                                                                                  50%    66%    75%    80%    90%    95%    98%    99%  99.9% 99.99%   100% # reqs
--------|--------------------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
GET      /api-test/video-streaming?videoHeight=H720                                           7200   7800   8200   8400  11000  17000  21000  25000  31000  33000  33000   2664
--------|--------------------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
         Aggregated

 

1. 요청 수 (reqs)
총 요청 수 2664건
실패한 요청 0건

2. 평균 응답 시간 (Avg)
평균 응답 시간 6913ms

3. 응답 시간 범위 (Min, Max)
최소 응답 시간 145ms

4. 중앙값 (Median)
중앙값(Med) 7200ms

5. 요청당 초당 처리율 (req/s)
초당 요청 처리율 43.43req/s

결론:
평균 응답 시간은 6.9초로 약간 개선되었습니다. 최대 응답 시간도 33초로 개선되었네요.

 

- 결과 분석

제 생각과는 다른 결과가 나왔습니다.

FastAPI 가 가벼운만큼 무조건 빠르다고 생각했는데, Springboot 가 더 성능이 좋습니다.

이유는 복합적일테지만, 제가 작성한 코드상의 문제일 수도 있고, chunk size 와 같은 설정값의 차이일 수도 있습니다.(실제로 chunk size 를 높이면 속도가 빨라짐)

 

- 아래는 제가 작성한 FastAPI Media Streaming 코드입니다.

객체지향적으로 작성했으며, 비동기 처리 등을 적용하여 속도와 안정성을 최대한 끌어내려 했습니다.

비디오 뿐 아니라 오디오 파일도 스트리밍이 가능하므로 활용하세요.

# (미디어 스트리밍 응답 생성기)
class MediaStreamResponseBuilder:
    def __init__(self, file_path: str, chunk_size: int = 1024 * 1024):
        self.file_path = file_path
        self.chunk_size = chunk_size

        if not os.path.isfile(self.file_path):
            raise HTTPException(status_code=404, detail="Video file not found")

        self.file_size = os.path.getsize(self.file_path)
        self.content_type = mimetypes.guess_type(self.file_path)[0] or "application/octet-stream"

    async def _file_chunk_generator(self, start: int = 0, end: Optional[int] = None) -> AsyncGenerator[bytes, None]:
        loop = asyncio.get_event_loop()
        with open(self.file_path, "rb") as f:
            f.seek(start)
            remaining = (end - start + 1) if end else None

            while True:
                chunk_size = self.chunk_size if not remaining else min(self.chunk_size, remaining)
                data = await loop.run_in_executor(None, f.read, chunk_size)
                if not data:
                    break
                yield data
                if remaining:
                    remaining -= len(data)
                    if remaining <= 0:
                        break

    async def build_response(self, request: Request) -> StreamingResponse:
        range_header = request.headers.get("range")

        if range_header:
            range_value = range_header.strip().lower().replace("bytes=", "")
            try:
                range_start_str, range_end_str = range_value.split("-")
            except ValueError:
                raise HTTPException(status_code=400, detail="Invalid Range header format")
            range_start = int(range_start_str)
            range_end = int(range_end_str) if range_end_str.strip() else self.file_size - 1

            if range_start > range_end or range_end >= self.file_size:
                raise HTTPException(status_code=416, detail="Requested Range Not Satisfiable")

            content_length = range_end - range_start + 1

            headers = {
                "Content-Range": f"bytes {range_start}-{range_end}/{self.file_size}",
                "Accept-Ranges": "bytes",
                "Content-Length": str(content_length),
                "Content-Type": self.content_type,
            }

            return StreamingResponse(
                content=self._file_chunk_generator(range_start, range_end),
                status_code=206,
                headers=headers,
            )

        # Full file streaming
        headers = {
            "Accept-Ranges": "bytes",
            "Content-Length": str(self.file_size),
            "Content-Type": self.content_type,
        }

        return StreamingResponse(
            content=self._file_chunk_generator(),
            headers=headers,
        )

 

사용법

    builder = custom_util.MediaStreamResponseBuilder(full_path)

    return await builder.build_response(request)

 

위와 같이 객체지향적으로 미디어 파일의 전체 경로를 넣어서 객체화하고, 이를 이용해 응답 객체를 반환하면 됩니다.

 

- 이상입니다.

이번 게시글로는 부하 분석을 통한 서로 다른 프레임워크 분석을 해보았습니다.

이처럼 성능지표를 파악하여 문제를 미리 파악하고 개선할 부분을 찾아내어 해결할 수 있습니다.