ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring 성능 개선 - 캐시 사용하기 (Enhcache)
    카테고리 없음 2024. 8. 22. 21:42

     

    1. Ehcache  추가 

     

    1.1 의존성 추가 

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
    </dependency>

     

    1.2 ehcache.xml 추가

     

    <?xml version="1.0" encoding="UTF-8"?>
    <ehcache>
    
        <defaultCache
                maxElementsInMemory="1000"
                maxElementsOnDisk="0"
                eternal="false"
                statistics="false"
                timeToIdleSeconds="10"
                timeToLiveSeconds="10"
                overflowToDisk="false"
                diskPersistent="false"
                memoryStoreEvictionPolicy="LRU"/>
    
        <cache
                name="NoticeReadMapper.findAll"
                maxElementsInMemory="10000"
                maxElementsOnDisk="0"
                eternal="false"
                statistics="false"
                timeToIdleSeconds="10"
                timeToLiveSeconds="10"
                overflowToDisk="false"
                diskPersistent="false"
                memoryStoreEvictionPolicy="LRU"/>
    
        <cache
                name="NoticeReadMapper.findByPage"
                maxElementsInMemory="10000"
                maxElementsOnDisk="0"
                eternal="false"
                statistics="false"
                timeToIdleSeconds="10"
                timeToLiveSeconds="10"
                overflowToDisk="false"
                diskPersistent="false"
                memoryStoreEvictionPolicy="LRU"/>
    
    </ehcache>
    • name : 캐시명
    • maxElementsInMemory : 메모리에 저장할 수 있는 최대 요소 수 
    • maxElementsOnDisk : 디스크에 저장할 수 있는 최대 요소 수 
    • eternal : 캐시 항목이 영원히 유지되는지 여부 -> false로 설정되어 있긴하다만 캐시 항목은 유효기간(timeToLiveSeconds) 혹은 유휴기간(timeToIdleSeconds)이 지나면 제거된다.
    • statistics : JMX 통계정보 갱신 옵션
    • timeToIdleSeconds: 설정된 시간 동안 유휴상태시 갱신 -> 캐시된 데이터가 사용 되지 않은 채로 유지되는 최대 시간
    • timeToLiveSeconds: 설정된 시간 동안 유지 후 갱신 -> 캐시 된 데이터의 전체 수명
    • overflowToDisk : 메모리에 캐시된 데이터가 메모리 한계를 초과하는 경우 디스크로 넘길지 여부 지정
    • diskPersistent: 디스크에 저장된 데이터가 시스템 재시작 후에도 유지되어야 하는지 여부 
    • memoryStoreEvictionPolicy: 메모리가 꽉 찼을 때 데이터 제거 알고리즘 옵션 

    1.3 ehcache Configuration 추가

     

    EhCache를 사용할 수 있도록 EhCacheManagerFactoryBean과 EhCacheCacheManager를 Bean으로 등록

    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.cache.ehcache.EhCacheCacheManager;
    import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.core.io.ClassPathResource;
    
    @Configuration
    @EnableCaching
    public class EhcacheConfiguration {
    
        @Bean
        @Primary
        public CacheManager cacheManager(EhCacheManagerFactoryBean ehCacheManagerFactoryBean) {
            return new EhCacheCacheManager(ehCacheManagerFactoryBean.getObject());
        }
    
        @Bean
        public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
            EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
            ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
            ehCacheManagerFactoryBean.setShared(true);
            return ehCacheManagerFactoryBean;
        }
    
    }

    2. 캐시 사용하기 

     

    @Cacheable 사용하기 : value속성은 캐시의 이름, ehcache.xml 구성 파일에서 <cache>엘리먼트의 name과 일치시키도록 하자 

    @Override
    @Cacheable(value = "NoticeReadMapper.findAll")
    @Transactional
    public List<Notice> getAllNotices() {
        return noticeReadMapper.findAll();
    }

     

    @Cacheable 옵션 활용

     

    @Override
    @Cacheable(value = "NoticeReadMapper.findByPage", key = "#request.requestURI + '-' + #pageNumber", condition = "#pageNumber <= 5")
    public List<Notice> findByPage(HttpServletRequest request, int pageNumber) {
        int startIdx = (pageNumber - 1) * 10;
        return noticeReadMapper.findByPage(startIdx);
    }

     

    NoticeReadMapper.findByPage" 로 캐시 이름은 동일한데 1page에 대한 캐시인지 2page에 대한 캐시인지 어떻게 구분할까

    • 캐시의 키를 동적으로 생성하기 위한 SpEL식을 지정한다. 
    • 메소드의 파라미터를 이용하여 특정 파라미터 값을 기반으로 캐시 키를 생성할 수 있다.
    • condition속성은 캐시가 적용되기 위한 추가적인 조건을 지정할 때 사용한다. -> true인 경우에만 캐시적
      • 여기선 pageNumber가 5이하인 경우에만 캐싱처리 

    2. nGrinder로 캐시 전후 성능테스트 1 - 요청당 5000건 조회 

     

    - 요청 당 5000건에 공지사항 전체 데이터를 조회

    • uri : get /api/notices
    • 캐시 적용 전 
      • Vuser:10
      • Duration: 1분
    • 캐시 적용 후 
      •  Vuser:10
      • TTL 20초
      • Duration 1분 
    <defaultCache
                maxElementsInMemory="1000"
                maxElementsOnDisk="0"
                eternal="false"
                statistics="false"
                timeToIdleSeconds="20"
                timeToLiveSeconds="20"
                overflowToDisk="false"
                diskPersistent="false"
                memoryStoreEvictionPolicy="LRU"/>
    
        <cache //이하생략
    • TTL을 20초로 설정했기 때문에 20초로 수정했다. (원래 10초였다)

    2.1 캐싱 적용 전 

     

    먼저 캐시 어노테이션에 주석 처리 

     @Override
        //@Cacheable(value = "NoticeReadMapper.findAll")
        public List<Notice> getAllNotices() {
            return noticeReadMapper.findAll();
        }

     

    다음으로 스크립트를 셋팅할 것이다.

     

    아래와 같이 스크립트와 테스트를 셋팅한다. 

     

    1분 테스트 결과 아래와 같은 정보를 얻을 수 있다.

     

    환경에 따라 테스트 결과가 달라질 수 있지만, 꽤 무거운 API이므로, 그리 높은 TPS수치를 보여주진않는다.

    이제 캐시를 적용한 후 동일한 테스트를 진행해보자 

     

    2.2 캐시 적용 후 

     

    캐시 적용 후 TPS 

     

    한 눈에 봐도 TPS 수치가 많이 좋아진 것을 확인할 수 있다. 

     


    3. nGrinder로 캐시 전후 성능테스트 1 - 1~10 page 랜덤 조회

     

    --> page에 대한 요청을 랜덤으로 보내기 위해 스크립트에 api 요청을 살짝 수정했다.

    @Test
    	public void test() {
    		def page = new Random().nextInt(10)+1
    		def apiUrl = "http://127.0.0.1:8081/api/notices/${page}"
    		HTTPResponse response = request.GET(apiUrl, params)
    
    		if (response.statusCode == 301 || response.statusCode == 302) {
    			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
    		} else {
    			assertThat(response.statusCode, is(200))
    		}
    	}

     

     

    - 성능 테스트 환경

    • uri : get /api/notices/{page}
    • 캐시 적용 전 
      • Vuser:10
      • Duration: 1분
    • 캐시 적용 후 
      •  Vuser:10
      • TTL 20초
      • Duration 1분 

     

    3.1 캐시 적용 전 성능 

    - 요청당 10건의 데이터만 가져오므로, 꽤 준수한 TPS 수치를 보여준다.

     

    3.2 캐시적용 후 

     

     

    - 캐싱은 최신 데이터를 위주로 보는 특성을 고려하여 5page까지만 적용했다. 

     

    평균  TPS : 980 -> 1326 대략 35% 증가

    Peek TPS : 1302 - > 1930 대략 48% 증가 

    Mean Test Time : 9 -> 6 대략 33%감소 

    Exected Tests: 53,296 -> 74,966 대략 40% 증가 

     


     

    4. 모니터링 툴을 통한 수치 변화 - 공지사항 전체

     

    - 캐시 사용 전 

     

     

    X로그 (맨 오른쪽), TPS를 보면 3초 이내에 응답을 하고있음을 볼 수 있다.

    Heap Used -> 증가했다, 줄어들었다를 반복 -> 이는 자바의 GC가 조회한 데이터를 응답하고 사용하지않는 객체를 free하기 때문 (GC count 탭을 추가하면 확인 가능)

     

    *실무에서 모니터링 툴 확인 팁 

      CPU -> 60% ~40% 이하 유지 [CPU 사용량이 꾸준하게 80~90%라면 위험할 수 있다]

                  XLog를 통해 10초 혹은 그 이상 걸리는 트랜잭션이 있으면, 이유를 찾는다 -> 스카우터를 통해 어느정도 확인 가능

     

    - 캐시 사용 후 

     

    캐시 적용 후 에는 0.2초 이내에 빠르게 응답함을 볼 수 있다. 

    GC는 이전에 비해 더 많이 발생하게 된다. 

    또한 1분동안 더 많은 응답을 처리하므로 CPU 사용량이 더 크다.

Designed by Tistory.