성능개선 - 이미지 최적화 및 로드테스트(Locust)

민찬홍·2024년 5월 29일

성능개선

목록 보기
1/1

Locust란

python으로 작성된 오픈 소스 부하 테스트 도구이다.

Locust를 사용하면 분산 시스템에서 여러 사용자를 시뮬레이션하여 웹 어플리케이션의 성능을 측정할 수 있다.
또한 매우 사용하기 쉽고 확장성이 뛰어나며, 사용자 시나리오를 코드로 작성할 수 있어 유연성이 높다.
대시보드를 통해 실시간으로 결과를 모니터링 할 수 있어 테스트 중에 성능 이슈를 발견하고 조치할 수 있다.

로드 테스트란

시스템이 얼마만큼의 부하를 견딜 수 있는지 파악하기 위한 테스트

Locust는 파이썬 기반이라 파이썬을 설치해야된다. 설치 후에 pip intall locust 명령어를 써야되는데 pip가 작동하지 않았다. 이 경우 해결하는 방법은 아래와 같다.

  1. Terminal에서 curl로 설치 파일 다운로드한다.
    curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
    -> 위 코드 실행

  2. 파이썬으로 파일 실행한다
    python3 get-pip.py
    -> 위 코드 실행

  3. pip를 사용해본다.

위에 순서대로 진행하면 pip를 사용해 Locust가 제대로 설치되는 것을 알 수 있다.

스크립트 생성

이제 python과 locust를 설치했으면 python으로 이루어진 스크립트를 짜고 이를 locust -f locustfile.py 명령어로 실행해야된다. 나같은 경우에는 이미지 최적화를 했을 때, 이미지의 용량이 늘어나면 상세 글 조회를 하거나 전체 글 조회를 해서 페이지에 렌더링 시킬 때 이미지 렌더링이 늦어진다는 문제 상황을 가정한다. 이러한 문제 상황을 가정하고 이미지 최적화를 진행하지 않았을 때의 전체 글 조회, 상세 글 조회의 성능을 측정한 다음 이를 이미지 최적화를 진행한 후와 비교해보는 작업을 진행할 것이다.

그래서 짠 기본 스크립트는 아래와 같다.

from locust import HttpUser, TaskSet, task, between
import random

class ArticleTasks(TaskSet):

        @task(2)
        def get_articles(self):
                with self.client.get("/api/article/all",catch_response=True) as response:
                        if response.status_code == 200:
                                articles = response.json()


        @task(1)
        def get_article(self):
                article_id = random.randint(1,45)
                self.client.get(f"/api/article/detail/{article_id}", name="/article/[id]")

class WebsiteUser(HttpUser):
        tasks = [ArticleTasks]
        wait_time = between(1,5)

우선 ArticleTask라는 큰 항목으로 작업단위를 나눠준 다음에 @task를 통해 작업 순서를 정했다. 현재 DB에는 테스트 데이터로 들어가있는 게시물(이미지 포함) 이 45개가 들어가있다. 따라서 1부터 45사이에 난수를 만들어서 detail한 게시물을 요청한다. 또 전체 게시물을 보기 위해서 get_articles라는 스크립트를 짰다.

locust 접속

스크립트를 짜고 그 파일을 터미널로 실행한 다음에 http://localhost:8089 주소로 접속하면 페이지를 볼 수 있다.

여기서 부하테스트에 대한 설정을 진행해야되는데 값들이 무엇을 의미하는지 살펴보면

  • Number of User : 말그대로 유저 수이다. 유저의 수가 늘수록 트래픽이 증가하니 원하는대로 유저 수를 조절하면 된다.

  • Ramp up : 일반적으로 Ramp Up 시간을 의미하며, Locust에서는 Spawn Rate으로 표현될 수 있음. 테스트 시작부터 설정된 전체 사용자 수에 도달하기까지의 시간 또는 사용자가 점진적으로 증가하는 속도이다. 사용자 수가 점진적으로 증가하는 상황을 모델링하여, 애플리케이션이 사용자 증가 속도에 어떻게 대응하는지 평가한다.

  • Host : 테스트를 원하는 서버의 url을 입력하면 된다. 나같은 경우에는 따로 프론트를 띄워놓고 하는게 아니라서 백 서버 주소를 입력하고 진행했다.

  • Advanced Options: 시간입력과 같은 추가사항을 기입할 수 있다.

우선 1차적으로 잘 돌아가는지 확인하기 위해 user 10명에 ramps up을 1로 설정하고 진행했다. 위는 그 결과고 fail 값과 로그를 확인해보니 에러 없이 잘 돌아간 것을 확인할 수 있었다.

User수와 Ramp up 조정

위에 내가 잘 돌아가는지 확인하기 위해 설정했던 user수는 10명이고 ramp up은 1이었다. 근데 여기서 조금 더 대규모 트래픽을 일으키려면 user와 ramp up을 어떻게 조정해야할까.

  • user 수의 증가 : 이 방법은 가장 직관적인 방법이다. 시스템에 요청을 보내는 가상 사용자 수를 늘리는건데, 이렇게되면 더 많은 부하를 생성하며 시스템의 성능과 안정성을 테스트할 수 있다.

  • ramp-up 시간 조정 : ramp-up 시간은 특정 사용자 수를 시스템에 도달하게 하는데 걸리는 시간이다. 예를 들어 사용자 수를 100명으로 설정하고, ramp-up 시간을 10초로 설정했다면, 시스템은 매 초마다 10명의 사용자를 시뮬레이션하여 점차적으로 부하를 증가시킨다. 이 시간을 조정하여 사용자가 시스템에 접근하는 속도를 늦추거나 빠르게 할 수 있다.

따라서 대규모 트래픽을 생성하고자 한다면 가장 직관적인 방법은 user수를 늘리는 것이다. 하지만 부하 증가 속도에 대한 반응을 테스트하고자 한다면 ramp-up 시간도 조정해야한다. 두 방법을 적절히 조합해서 사용해야한다.

위에는 기존보다 user를 100명으로 늘리고 ramp-up도 10으로 조정한 결과이다.

첫번째 차트는 RPS(current Requests Per Seconds)와 초당 Failures를 보여준다.

두번째 차트는 Median Response Time과 95% percentile을 보여준다.

지표의 의미

차트말고 표를 보면 각 지표마다 결과값이 떠 있는 것을 볼 수 있다.

  • Type : 요청의 유형 또는 카테고리, 예를 들어 GET,POST 등의 HTTP 메서드

  • Name : 요청의 이름, 일반적으로 API 엔드포인트나 테스트 시나리오의 이름

  • Requests : 해당 요청이 수행된 총 횟수

  • Fails : 요청이 실패한 총횟수. 실패는 서버 응답 코드가 4xx나 5xx일때, 또는 타임아웃이 발생했을 때로 정의

  • Median(ms) : 요청 응답 시간의 중앙값. 중앙값은 응답 시간을 오름차순으로 정렬했을 때, 중간에 위치하는 값. 이 값은 평균보다 왜곡이 적어 성능의 대표값으로 유용

  • 95%ile(ms) : 요청 응답 시간의 95번째 백분위수. 즉 전체 요청 중 95%의 요청이 이 시간 이하로 응답되었다는 것을 의미. 성능의 일관성을 평가하는데 유용

  • 99%ile(ms) : 요청 응답 시간의 99번째 백분위수. 전체 요청 중 99%가 이 시간 이하로 응답되었음을 의미. 성능의 최악의 경우를 평가하는데 유용

  • Average(ms) : 요청의 평균 응답 시간. 모든 요청의 응답 시간을 합산한 후 요청 수로 나눈 값

  • Min,Max(mx): 가장 짧은, 긴 요청 시간

  • Average size(bytes) : 요청의 평균 응답 크기. 모든 응답 크기를 합산한 후 요청수로 나눈 값

  • Current RPS : 현재 초당 요청 수 (Request per Second). 성능 테스트의 부하를 평가하는데 사용

    • RPS가 높은 경우: 이는 시스템이 높은 부하 하에서도 많은 요청을 효과적으로 처리할 수 있음을 의미. 높은 RPS 값은 웹 서버, API, 어플리케이션이 높은 트래픽을 처리할 수 있는 능력을 가지고 있음을 나타내므로 긍정적으로 평가될 수 있음

    • RPS가 낮은 경우: 시스템이 초당 처리할 수 있는 요청의 수가 낮은 것. 이는 시스템의 성능이 기대에 못 미칠 수 있음을 나타낸다. 낮은 RPS는 시스템이 트래픽 증가에 잘 대응하지 못하거나, 리소스가 충분하지 않거나, 최적화가 필요함을 나타낼 수 있다.

  • Current Failures/s : 현재 초당 실패 수. 성능 테스트 중 실패율을 평가하는데 사용

이미지 최적화

이제 이미지 최적화 전의 성능을 살펴봤으니 이미지 최적화 후의 성능도 살펴보도록 할 것이다. 일단 우리 서비스 특성상 이미지의 품질변화가 있어서는 안된다. 유저가 이미지를 다운로드 받을 때, 원래 품질보다 낮은 이미지가 다운될 수도 있기 때문이다. 따라서 무손실 최적화를 위주로 알아보았다. 무손실 최적화를 진행하기 위해서는 우선 이미지 포멧을 일괄적으로 변경할 필요가 있다.

 @Transactional
    public File convertMultipartFileToPngFile(MultipartFile multipartFile) throws  IOException {
        // 파일명에서 확장자를 제외한 부분과 새로운 UUID를 결합하여 PNG 파일명 생성
        String originalFileName= Objects.requireNonNull(multipartFile.getOriginalFilename()).split("\\.")[0];
        String newFileName = UUID.randomUUID() + "_" + originalFileName + ".png";
        File convertedFile = new File(newFileName);


        // 멀티파트 파일을 BufferedImage로 변환
        BufferedImage image = ImageIO.read(multipartFile.getInputStream());

        // BufferedImage를 PNG 형식으로 파일에 쓰기
        ImageIO.write(image, "PNG",convertedFile);

        return convertedFile;
    }
@Transactional
    public Image create(Article article, MultipartFile multipartFile) throws IOException {

        //저장 경로, 파일 이름 설정
        String imgPath = setImagePath();

        // 멀티파트 파일을 PNG 형식의 파일로 전환, PNG 파일의 이름을 받아옴
        File pngFile = convertMultipartFileToPngFile(multipartFile);
        String pngFileName = pngFile.getName();

        //Object storage에 업로드
        try {
            s3.putObject(new PutObjectRequest(s3Util.getBucketName(), imgPath + "/" + pngFileName, pngFile)
                    .withCannedAcl(CannedAccessControlList.PublicRead));
        } catch (AmazonS3Exception e) {
            e.printStackTrace();
        } catch(SdkClientException e) {
            e.printStackTrace();
        }

        //이미지 객체 생성
        Image image = Image.builder()
                .article(article)
                .fileName(pngFileName)
                .path(imgPath)
                .build();

        //저장
        imageRepository.save(image);

        //로컬에 생성된 파일 삭제
        pngFile.delete();

        return image;

    }

위와 같이 우선 이미지 포멧을 전부 png로 변경해주었다. png는 무손실 압축을 사용하는 이미지 포멧이다. 즉, 이미지를 PNG 포멧으로 변환하면 이미지 데이터가 손실 없이 압축된다.

이렇게 이미지 포멧을 변경한 후 locust로 한 번 더 성능을 측정해봤다.

결과를 보면 알 수 있듯이 전체 요청처리량은 비슷하지만 개별 요청의 평균 응답시간이 게시물 전체 조회에서는 120.27ms -> 31.93 로 감소하였고, 게시물 단건 조회에서도 28.96ms -> 13.94ms 로 감소하였다.

여기서 추가적으로 PNG 이미지의 크기를 더 줄이기 위해 추가적인 무손실 압축을 적용할 수 있다. 예를 들어, PNG 이미지를 최적화하는 도구나 라이브러리를 사용하여 이미지의 파일 크기를 더 줄일 수 있다.

나는 TwelveMonkeys라는 라이브러리를 사용하여 추가적인 이미지 최적화를 진행하였다. 아래는 이미지 최적화를 진행한 후 코드이다.

@Transactional
    public File convertMultipartFileToPngFile(MultipartFile multipartFile) throws  IOException {
        // 파일명에서 확장자를 제외한 부분과 새로운 UUID를 결합하여 PNG 파일명 생성
        String originalFileName= Objects.requireNonNull(multipartFile.getOriginalFilename()).split("\\.")[0];
        String newFileName = UUID.randomUUID() + "_" + originalFileName + ".png";
        File convertedFile = new File(newFileName);

        BufferedImage image = ImageIO.read(multipartFile.getInputStream());

        // PNG 형식으로 파일에 쓰기 (TwelveMonkeys 라이브러리 사용)
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
        if(!writers.hasNext()) {
            throw new IllegalArgumentException("No writers : png");
        }
        ImageWriter writer = writers.next();

        try (ImageOutputStream ios = ImageIO.createImageOutputStream(convertedFile)) {
            writer.setOutput(ios);

            ImageWriteParam writeParam = writer.getDefaultWriteParam();
            // 무손실 압축이므로 압축 품질을 설정하지 않음

            writer.write(null, new IIOImage(image, null, null), writeParam);
        } finally {
            writer.dispose();
        }

        return convertedFile;
    }

위처럼 TwelveMonkeys를 사용해보았다. 무손실 압축을 진행할 것이기 때문에 압축 품질을 따로 정하지 않았다.결과적으로 무손실 압축으로 이미지 품질에는 손실 없이 용량만 압축할 수 있다. 아래는 TwelveMonkeys를 사용한 이후에 Locust로 성능측정을 한 결과이다.

위에서 PNG 포멧 일괄 변경때와 같이 전체 요청처리량에는 큰 변화가 없지만 각 요청에서 평균 요청 응답시간이 더 개선된 것을 확인할 수 있다.

결과적으로 처음 이미지 최적화를 진행하기 전보다 게시물 전체 조회에서는 120.27ms -> 30.42ms로 요청의 평균 응답 시간이 74.68% 개선되었고 게시물 단건 조회에서도 28.96ms -> 11.68ms로 평균 응답 시간을 59.64% 개선할 수 있었다.

profile
백엔드 개발자를 꿈꿉니다

0개의 댓글