본문 바로가기
Log/Experiment

exp002_TypeOrm_partial Selection 사용 여부에 따른 성능 변화 관찰

by RIEM 2023. 2. 24.

실험 개요

Screen Shot 2023-02-24 at 6 45 23 PM|1000

실험명 : test-pyramid-20230224-001
관련 실험 : test-pyramid-20230223-002
실험 상황 : 부하 테스트 툴 nGrinder을 활용한 서버 부하 테스트 진행중
실험 목적 : TypeORM partial Selection 사용 여부에 따른 성능 변화 관찰

실험 결론

  • 실험 조건 하, TypeOrm partial Selection 적용 시 그렇지 않은 쿼리보다 성능이 약 5배 정도 성능이 개선되었다
  • 관계 참조하지 않는 단순 find()보다 partial Selection 적용한 find()가 성능이 조금 부족하지만, 관계하고 있는 데이터를 함께 가져온다는 점에서 유의미한 성능을 나타낸다고 판단됨
  • 쿼리 성능을 개선하니 웹서버 CPU 사용률과 DB CPU 사용률 모두 증가하였는데, 웹서버 CPU 사용륭 증가율의 폭이 더 크다

테스트에 앞서

어제 테스트(2023223-001 ~ 002)를 통해 쿼리에 관계 옵션을 넣는 여부에 따라 throughput이 얼마나 감소되는지 확인했다. 주 요인은 관계 관련 쿼리가 partial selection이 아닌 전체 select로 되어있어서 문제가 생긴 것으로 보인다. 그리고 추가로 DB의 CPU 사용률이 급격히 증가하는 부분도 확인했다. 그렇다면 오늘은 쿼리 튜닝을 통해 조금씩 DB 관련 성능을 키워보자.

이번 실험은 부하 테스트 시 발생할 수 있는 문제 유형들 중 DB 이슈를 다룬다.

  • 인프라 이슈
    • 로드 밸런서
    • 네트워크 설정
    • 네트워크 대역
  • 웹서버 CPU/메모리 리소스 부족 이슈
  • 어플리케이션 이슈
    • 웹 프레임워크
    • 캐시 설정
    • 불필요한 요청
    • 로직
    • CPU/Mem 효율이 좋지 않은 코드
  • DB 이슈
    • 참조 SQL
    • 갱신 SQL
    • 적절한 실행 계획이 없음
    • 적절한 인덱스의 부재
    • CPU/메모리 리소스 부족
    • 락 발생
  • 미들웨어 설정 이슈
    ref : 책 <아마존 웹 서비스 부하 테스트 입문>

테스트 : partial Selection 적용 여부에 따른 성능 변화

이 테스트는 20230223-002 테스트에 의존하고 있다. 관련 코드를 쭉 이어서 개선하는 테스트해보는 과정이기 때문이다.

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

return result;

일단 현재 쿼리는 이러하다. 날짜순으로 take로 10개만 추출하는데, 관계된 product, bid, user 테이블의 데이터도 함께 조회한다. 그런데 이 쿼리는 불필요한 데이터를 많이 조회한다는 것이 흠이다.

[
    {
        "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
                }
            ]
        }
    },

그렇다면 데이터는 어떤 데이터를 가져와야할까? 그리고 어떻게 특정 데이터만 가져올 수 있을까?

const result = await this.raffleRepository
      .createQueryBuilder('raffle')
      .leftJoinAndSelect('raffle.product', 'product')
      .leftJoinAndSelect('raffle.bid', 'bid')
      .leftJoinAndSelect('raffle.user', 'user')
      .take(10)
      .getMany();

find() 대신 쿼리빌더 leftJoinAndSelect로 데이터를 가져와보았다.

[
    {
        "createdAt": "2023-02-22T08:36:57.121Z",
        "updatedAt": "2023-02-22T08:36:57.121Z",
        "deletedAt": null,
        "raffleId": 3,
        "dateStart": "2022-08-06",
        "dateEnd": "2022-10-26",
        "isClosed": true,
        "closedPrice": 808706,
        "usersId": 72316,
        "productId": 5644,
        "product": {
            "createdAt": "2023-02-22T08:36:09.418Z",
            "updatedAt": "2023-02-22T08:36:09.418Z",
            "deletedAt": null,
            "productId": 5644,
            "productImage": "https://source.unsplash.com/random",
            "productModel": "#2b503e",
            "productName": "Bontebok",
            "productSize": 295,
            "productColor": "Pink",
            "productCategory": "Terrace",
            "releasePrice": 264843,
            "releaseDate": "2022-09"
        },
        "bid": [],
        "user": {
            "createdAt": "2023-02-22T08:34:13.310Z",
            "updatedAt": "2023-02-22T08:34:13.310Z",
            "deletedAt": null,
            "usersId": 72316,
            "userId": "user72316",
            "password": "user72316"
        }
    },

이전과 차이는 참조한 user 내에 raffle 데이터가 중복으로 오던 것이 사라졌다는 점이다. 하지만 이것만으로 좀 아쉽다. createAt, updateAt, password 이런 불필요한 데이터는 제외하고 싶다. 어디까지만 가져올 것인가는 아까 언급했듯 프론트에 유저에게 어떤 정보를 보여줄것인가에 따라 달려있다.

Screen Shot 2023-02-24 at 4 53 39 PM


우선 와이어프레임이다. 현재 기준 프로젝트의 와이어 프레임을 좀 더 보완했다. 위의 화면에 불필요한 정보들은 제외해도 될 것이다.

    const result = await this.raffleRepository
      .createQueryBuilder('raffle')
      .leftJoinAndSelect('raffle.product', 'product')
      .leftJoinAndSelect('raffle.bid', 'bid')
      .select([
        'raffle.raffleId',
        'product.productImage',
        'product.productColor',
        'product.productModel',
        'product.productName',
        'product.releasePrice',
        'raffle.dateEnd',
        'bid.bidId'
      ])
      .orderBy('raffle.dateEnd', 'DESC')
      .addOrderBy('raffle.raffleId', 'DESC')
      .take(10)
      .getMany();

쿼리는 이와 같이 해주었다.

[
    {
        "raffleId": 10116,
        "dateEnd": "2022-12-30",
        "product": {
            "productImage": "https://source.unsplash.com/random",
            "productModel": "#233c2f",
            "productName": "Heron, goliath",
            "productColor": "Puce",
            "releasePrice": 307622
        },
        "bid": [
            {
                "bidId": 1
            },
            {
                "bidId": 2
            }
        ]
    },
    {
        "raffleId": 9116,
        "dateEnd": "2022-12-30",
        "product": {
            "productImage": "https://source.unsplash.com/random",
            "productModel": "#233c2f",
            "productName": "Heron, goliath",
            "productColor": "Puce",
            "releasePrice": 307622
        },
        "bid": []
    },
    ...

결과는 상품 이미지, 모델, 이름, 컬러, 비즈, 래플 종료일 그리고 입찰 배열을 잘 가지고 오고 있다. 입찰 배열도 데이터를 직접 가져오기보다는 count로 깔끔하게 가져오는 것이 좋을 것 같은데, 이 부분은 프론트에게 맡기는 걸로 일단 해두자. 일단 partial selection 만으로도 어느정도 효과가 나오는지만 보자.

실험 계획

Screen Shot 2023-02-24 at 5 45 43 PM


실험 조건은 쿼리에 partial Selection을 먹였냐 안먹였냐의 차이를 제외하고는 이전과 동일하다. 실험을 수행하겠다. pm2 monit으로 모니터링도 실시간 체크해주자.

실험 도중

Screen Shot 2023-02-24 at 5 48 09 PM|1000

 

Screen Shot 2023-02-24 at 5 48 31 PM|1000




실험 결과

Screen Shot 2023-02-24 at 6 00 28 PM|1000


partial Selection으로 쿼리 튜닝을 진행한 이번 테스트의 평균 TPS는 144.8로 쿼리 튜닝을 하지않고 모든 데이터를 다 가져오는 이전 테스트의 TPS 27.5 대비 약 117 TPS 증가했다.
놀라운 점은 관계 데이터를 참조하지 않는 단순 findAll을 적용한 test-20230223-001의 평균 TPS 156보다 약 12 낮은 TPS라는 점이다.

Screen Shot 2023-02-24 at 6 02 20 PM|1000


서버 CPU 사용율은 27.7%인데, 이는 처음 수행했던 관계 데이터 조회하지 않는 단순 findAll의 CPU 사용률 28.2%와 유사한 수준이다. 적당히 부하가 잘 걸리는 모습임을 알 수 있다.

Screen Shot 2023-02-24 at 6 03 13 PM|1000


DB CPU 사용률은 약 83% 정도다. 관계 참조하지 않는 find의 CPU 사용류은 54%, 관계를 참조하되 불필요한 데이터까지 모두 가져오는 find의 CPU 사용률은 70%이었다.

흥미로운 점은 쿼리를 튜닝하여 성능을 높이니 오히려 DB CPU 사용률(83%)이 올라갔는데, 그 수준이 이전 쿼리 성능이 좋지 않았던 테스트의 DB CPU 사용률(70%)보다 높았다는 점이다. 오히려 병목현상이 해소되어 DB를 더 자주 사용하게 된 것일까?

Screen Shot 2023-02-24 at 6 29 05 PM


지금까지의 테스트 결과다.

인사이트

  • parTial Selection 적용 시 그렇지 않은 쿼리보다 성능이 약 5배 정도 성능이 개선되었다
    • (partial Selection을 적용한 쿼리의 TSP 144.8, Latency는 251이며, 적용하지 않은 쿼리의 TSP 27.5, Latency 1304.66)
  • 관계 참조하지 않는 단순 find()보다 partial Selection 적용한 find()가 성능이 조금 부족하지만, 관계하고 있는 데이터를 함께 가져온다는 점에서 유의미한 성능을 나타내고 있음
    • 관계 참조하지 않는 단순 find() 대비 관계 데이터를 모두 가져오는 find()의 성능은 약 5배 급격히 떨어진다. 단, 여기에 partial Selection를 적용하여 불필요한 데이터를 가져오지 않으면 성능은 다시 회복된다.
  • 쿼리 성능을 개선하니 웹서버 CPU 사용률과 DB CPU 사용률 모두 증가하였는데, 웹서버 CPU 사용륭 증가율의 폭이 더 크다

참조

  • 책 <아마존 웹 서비스 부하 테스트 입문>

댓글