[Scouter] 캐시적용후 성능측정

한호성·2024년 5월 17일
0

성능측정

목록 보기
4/7

성능향상을 위해 캐시를 적용한 과정과 성능 테스트한 결과를 작성한 글입니다.

Index

  • 어떤 캐시를 사용했는가?
  • 어플리케이션 적용한 곳 ?
  • 적용했을 때의 코드 작성 및 캐시 설정 이유
  • 성능측정 결과

어떤 캐시를 사용했는가?

  1. Global Cache vs Local Cache
  • 현재 진행하는 프로젝트의 규모상 서버의 scale out을 할일이 적다고 1차적으로 판단하였고, 만약 scale out이 필요하더라도, local cache의 ehcache의 clustering을 활용하여 처리하 수 있겠다고 판단하였습니다.
  • global cache를 사용할 경우 외부에 서버를 띄어야하는데, 현재 프로젝트가 단일 서버로 진행되기 때문에 오버스펙이라고 판단하였습니다.
  1. Ehcache vs Caffeine
  • 이 중 대중적으로 많이 사용하고 있는 Ehcache를 사용하였습니다.
    관련자료가 많고, 이미 검증되었다는 뜻으로 판단하였습니다.
  • 처음 Local Cache를 사용해보는 상황에서 가장 대중적인 것으로 감을 잡는것도 좋은 방법이라 생각 하였습니다.
  • 추후에, Terracotta Server(분산 캐시)를 사용해서 scale out에 대비할 때를 고려하여 선택하였습니다.

어플리케이션 캐싱 적용한 곳

캐싱 기준

  • 유저에 의해 자주 호출되는 Resource
  • 내용이 자주 변하지 않는 Resource

캐싱한 Resource

  • 사전예약 시스템에서 위의 캐싱 기준을 적용해서 3가지 API의 Service layer에 캐싱 처리를 하였습니다.
    • 상품 전체 조회
    • 상품 단일 조회
    • 주문 단일 조회

캐싱 설정 및 코드

  • 3가지의 cache 설정을 기본적으로 동일하게 진행하였습니다.
  • 설정 중 유의미하게 고려한점은, memoryStoreEvictionPolicy 과 timeto** 옵션들입니다.
    • timeToIdleSeconds : 마지막으로 접근한 후로 몇초동안 유지할 것인가
    • timeToLiveSeconds : 캐싱처리 된후 몇초동안 유지할 것인가
    • memoryStoreEvictionPolicy : 캐싱 메모리가부족할 때, 어떤 기준으로 제거할 것인가
      • LFU: 가장 사용빈도가 적은 것을 제거한다.
#### Cache 설정 config
    <cache
            name="productCache"
            maxEntriesLocalHeap="10000"
            maxEntriesLocalDisk="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LFU"/>

    <cache
            name="productDetailCache"
            maxEntriesLocalHeap="10000"
            maxEntriesLocalDisk="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LFU"/>

    <cache
            name="optionDetailCache"
            maxEntriesLocalHeap="10000"
            maxEntriesLocalDisk="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LFU"/>

어플리케이션 코드

  • 스프링에서 지원하는 AOP @Cacheable @CacheEvict을 활용해서 사용하였습니다.
  • 상품 전체조회를 할 때 캐싱 key pageNumber 와 pageSize를 통해 캐싱처리 하였습니다.
  • 새 상품이 등록, 수정될 때, 상품전체조회 캐시를 모두 제거하여서 , 재등록되게끔 하였습니다.
    • 다른 2개의 API 상품 단일조회, 주문단일조회에도 같은 방식으로 추가하였습니다.
    //ProductFacadeService.java 
    @Cacheable(cacheNames = CacheString.PRODUCT_CACHE, key = "#pageable.getPageNumber" + "-"+"#pageable.getPageSize")
    @Transactional(readOnly = true)
    public Page<ProductViewDto> getProductList(Pageable pageable) {

        assert (pageable != null);

        Page<ProductDto> productList = productService.getProductList(pageable);
        List<ProductViewDto> productViewDtoList = productList.stream().map(productMapper::changeToProductViewDto).collect(Collectors.toList());

        return new PageImpl<>(productViewDtoList, pageable, productList.getTotalElements());
    }
    
    
    
    //ProductService.java
    @CacheEvict(value = CacheString.PRODUCT_CACHE, allEntries = true)
    public Product register(ProductDto productDto) {

        assert (productDto != null);

        Product product = mapper.changeToProduct(productDto);

        return productRepository.save(product);

    }
    
		 //ProductService.java
    @CacheEvict(value = CacheString.PRODUCT_CACHE, allEntries = true)
    public Product updateProduct(ProductDto productDto) {
        assert (productDto != null);
        assert (productDto.getId() != null);

        Product product = productRepository.findById(productDto.getId()).orElseThrow(InvalidArgumentException::new);

        product.updateData(productDto.getName(), productDto.getCategory(), productDto.getPrice());

        return product;
    }

성능측정 결과

Test1 - 캐싱 처리 vs 캐싱처리 x vs 캐싱처리( 주기적으로 Evict)

측정시나리오

  • 50명의 유저가 1분동안 지속적으로 상품정보 Get 요청을 하는 경우

결과

  • case1 : 캐싱처리를 한 경우
  • case2 : 캐싱처리를 하지 않는 경우
  • case3: 주기적으로 상품 insert api호출해서, 재캐싱하도록 세팅

case1

TPS: 13k

상위 99% 처리시간: 7ms

case2

TPS : 700

상위 99% 처리시간: 132ms

case3

TPS : 2.4k

상위 99% 처리시간: 399ms

(처리시간의 대부분을 insert 시간을 db쿼리에 사용함)

결론:

  • 캐싱처리를 했을 때, 시간차이와 처리량 차이각 날줄은 알았지만, 이렇게 심하게 날줄은 알지 못했다..
  • 실 어플리케이션 운영할 때, 트래픽이 몰리는 상황에, 캐싱은 필수라고 생각하게 되었다.
  • 캐싱 메모리 관리를 어떻게 해줘야할지가 또다른 과제가 될것으로 생각된다.
    • scale out 할 때의 동기화 이슈, 메모리라는 자원의 제한을 어떻게 타파할 것인지 등등
profile
개발자 지망생입니다.

0개의 댓글