본문 바로가기
Log/Experiment

exp001_TypeOrm_관계 참조 여부에 따른 쿼리 성능 비교 실험

by RIEM 2023. 2. 24.

Summary

  • 관계 옵션을 추가하면 당연하지만 성능이 느려진다.
  • DB CPU 사용률이 높아지는 동시에 Throughput이 감소했다. 반면 CPU 사용률은 떨어졌다. DB 쪽에 병목 현상이 있는 것으로 추정된다.

테스트 1 : 현재 상황 파악

  • Goal : 현재 상황 파악
  • test-number : test-20230223-001

Screen Shot 2023-02-23 at 5 16 31 PM

nest.js 어플리케이션이 담긴 EC2는 t2.micro로 맞췄고, controller, agent는 t2.large와 t2.medium으로 해주었다.

  • ngrinder controller : t2.large
  • ngrinder agent : t2.medium
  • nestjs application : t2.micro
  • DB : postgres db.t3.micro

controller, agent의 성능이 앱보다 더 뛰어난 상황이다. 우선 요청자의 성능을 높게 잡고, 처리 결과에 따라 어플리케이션 성능을 높여가면서 처리 성능 변화를 관측하고 싶어서 이러한 설정을 했다. 다른 개발자들에 따르면 controller, agent 성능에 따라서도 테스트의 결과가 달라질 수 있다고 한다.

nGrinder 권장 사양은 아래와 같다.

Controller Agent Monitor
Core 1-2 2 1
Ram 2GB 4GB(free memory 3GB) 50MB

참고한 한 2021년에 쓰여진 개발자 블로그 글에 따르면 Controller는 1개, Agent는 3~6개로 일반적으로 구성하는데, Controller는 t2.large, Agent는 t2.2xlarge를 권장한다고 한다(https://backtony.github.io/aws/2021-10-27-aws-10/).

부하테스트

Screen Shot 2023-02-23 at 4 22 14 PM|1000

40명의 가상 유저를 만들어 테스트한 결과, TPS(Transaction Per Second, 초당 트랜잭션 수)는 총 156.1을 기록했으며, 그중 가장 높은 수치는 175했다.

GET url/raffles

  async findAll() {
    const result = await this.raffleRepository.find({
      take: 10,
      order: {
        dateEnd: 'DESC'
      }
    });
    const count = result.length;
    return { count: count, data: result };
  }

테스트한 쿼리는 정말 단순한 쿼리다.
raffle DB에서 경매 이벤트를 가지고 오는 쿼리인데, 데이터는 총 10개를 가져오고, 옵션은 dateEnd(이벤트가 끝나는 날) 내림차순으로 즉, 마감 임박이 가장 높은 이벤트부터 요청한다.

{
    "count": 10,
    "data": [
        {
            "createdAt": "2023-02-22T08:36:57.121Z",
            "updatedAt": "2023-02-22T08:36:57.121Z",
            "deletedAt": null,
            "raffleId": 65,
            "dateStart": "2022-09-07",
            "dateEnd": "2022-12-30",
            "isClosed": true,
            "closedPrice": 373919,
            "usersId": 98556,
            "productId": 2269,
            "bid": []
        },
        ...
        ,
        {
            "createdAt": "2023-02-22T08:37:08.467Z",
            "updatedAt": "2023-02-22T08:37:08.467Z",
            "deletedAt": null,
            "raffleId": 2226,
            "dateStart": "2022-08-21",
            "dateEnd": "2022-12-30",
            "isClosed": true,
            "closedPrice": 598532,
            "usersId": 25358,
            "productId": 6531,
            "bid": []
        }
    ]
}

리턴 값은 이와 단순 JSON 형태로 나와있다.

Screen Shot 2023-02-23 at 5 10 46 PM

어플리케이션 서버의 CPU는 약 8.7% 사용했는데, 미미한 수치다.

Screen Shot 2023-02-23 at 6 36 16 PM

2번째 테스트를 진행하다가 알게된 사실인데, 위의 이미지는 빙산의 일각이었음을 알게되었다. 실제 서버의 CPU 사용률은 28.2%까지 올라가있었다. CPU를 모니터링을 실시간으로 해야겠다는 생각이 들었다.

Screen Shot 2023-02-23 at 5 18 18 PM

DB의 CPU는 54%까지 상승했다. 어플리케이션 서버의 CPU 사용률인 8.7% 대비 약 7배 이상 사용이 되고 있다.

부하테스트 하에서 서버 CPU는 안정적인데, DB의 CPU는 급격히 상승했다.

나는 findAll() 메소드에 limit, order by 외 특별한 옵션을 주지 않아서 DB에 큰 영향을 주지 않을 것이라 생각했는데, 생각보다 CPU 사용률이 높아 놀랐다.

여기서 하나 궁금증이 생겼다. 과연 전체 조회 시 관계를 설정하는 옵션을 추가하여 가져오게 된다면 어떻게 될까? 사실, 위의 fineAll() 쿼리는 기본적인 수준이었고, 실제 구현한 서비스에서는 관계맺은 다른 DB의 데이터를 추가로 가져와야 한다는 과제가 남아있다. 이런 상황에서 DB CPU 사용률은 어떨지 또 다른 지표들은 어떨지 궁금해졌다.

인사이트

  • 특별한 쿼리 옵션을 설정해주지 않은 채로 기본 쿼리 조회 시, CPU 대비 DB의 C

테스트 2 : 쿼리 관계 참조 옵션 추가

  • test-number : test-20230223-002
  • Goal : 관계 참조 옵션 추가할 시 쿼리 성능 변화 관찰

변경사항

수정 전

GET url/raffles

  async findAll() {
    const result = await this.raffleRepository.find({
      take: 10,
      order: {
        dateEnd: 'DESC'
      }
    });
    const count = result.length;
    return { count: count, data: result };
  }

수정 후

GET url/raffles

async findAll() {
    const result = await this.raffleRepository.find({
      take: 10,
      order: {
        dateEnd: 'DESC'
      },
      relations: {
        product: true,
        bid: true,
        user: true
      }
    });
    const count = result.length;
    return { count: count, data: result };
  }

쿼리와 관계를 맺은 테이블의 정보도 함께 조회하도록 설정해주었다.

받아오는 데이터는 아래와 같다.

{
    "count": 10,
    "data": [
        {
            "createdAt": "2023-02-22T08:36:57.121Z",
            "updatedAt": "2023-02-22T08:36:57.121Z",
            "deletedAt": null,
            "raffleId": 65,
            "dateStart": "2022-09-07",
            "dateEnd": "2022-12-30",
            "isClosed": true,
            "closedPrice": 373919,
            "usersId": 98556,
            "productId": 2269,
            "product": {
                "createdAt": "2023-02-22T08:35:47.666Z",
                "updatedAt": "2023-02-22T08:35:47.666Z",
                "deletedAt": null,
                "productId": 2269,
                "productImage": "https://source.unsplash.com/random",
                "productModel": "#7d79b1",
                "productName": "Eleven-banded armadillo (unidentified)",
                "productSize": 276,
                "productColor": "Turquoise",
                "productCategory": "Lane",
                "releasePrice": 162011,
                "releaseDate": "2022-05"
            },
            "bid": [],
            "user": {
                "createdAt": "2023-02-22T08:34:13.310Z",
                "updatedAt": "2023-02-22T08:34:13.310Z",
                "deletedAt": null,
                "usersId": 98556,
                "userId": "user98556",
                "password": "user98556",
                "raffle": [
                    {
                        "createdAt": "2023-02-22T08:36:57.121Z",
                        "updatedAt": "2023-02-22T08:36:57.121Z",
                        "deletedAt": null,
                        "raffleId": 65,
                        "dateStart": "2022-09-07",
                        "dateEnd": "2022-12-30",
                        "isClosed": true,
                        "closedPrice": 373919,
                        "usersId": 98556,
                        "productId": 2269
                    }
                ]
            }
        },
        ....
}

raffle 전체 조회하면 이것저것 다 긁어오는데, product, bid, user 등 관계를 맺은 테이블의 데이터도 잘 가지고 오는 것을 볼 수 있다.

문제는 user에 raffle 정보와 같이 불필요한 정보가 중복으로 조회되고 있다는 점이다. 그 이유는 래플과 유저는 M:1 관계를 맺고있어서, 1명의 유저가 여러 래플을 생성할 수 있기 떄문이다. 위의 래플 id 65번을 만든 userId 98556이 만든 raffle의 목록으로 래플 id 65번을 보여주고 있다.

프론트 서비스를 어떤 방식으로 구현하느냐에 따라 이 데이터 구조는 달라지겠지만 내 생각에 user가 어떤 래플을 생성했는지까지 보여줄 필요는 없어보인다. 다음 쿼리 튜닝 시에는 이를 수정해도 좋을 것 같다.

Screen Shot 2023-02-23 at 5 55 43 PM

test-20230223-002의 테스트 조건은 이전 테스트(test-20230223-001)와 동일한 조건으로 통제해주었다.

Screen Shot 2023-02-23 at 6 14 48 PM|1000

이번 테스트의 TPS는 27.5로 전 테스트의 TPS 156.1 대비 -128.6이나 감소했다. 물론 쿼리에 불필요한 정보들이 많은 것은 사실이지만 그렇다고 이정도까지 감소할 줄은 예상할 줄은 몰랐다.

Screen Shot 2023-02-23 at 6 36 16 PM

두번째 테스트의 경우(우측), 서버의 CPU 사용율은 8% 정도로 미미한 수준이었다.

Screen Shot 2023-02-23 at 6 39 17 PM

DB CPU 사용율은 이전 테스트의 약 55% 대비 약 15% 증가한 70%까지 증가했다. 확실히 관계 데이터까지 조회하면 DB의 부하도 더 많이 걸리는 것을 볼 수 있다.

인사이트

Screen Shot 2023-02-23 at 7 31 45 PM

  • 관계 옵션을 추가하면 당연하지만 성능이 느려진다. 여기서 내가 알고싶었던 점은 느려지는 성능으로 인해 어떤 지표들이 바뀌고, 그 지표들간의 상관관계에 대한 느낌을 잡고 싶었다.
  • DB CPU 사용률이 높아지는 동시에 Throughput이 감소했다. 반면 CPU 사용률은 떨어졌다. DB 쪽에 병목 현상이 있는 것으로 추정된다.
  • 다음 실험에서는 DB의 성능을 개선할 수 있는 작업을 수행할 것이다.

댓글