카테고리 없음

응답속도 향상을 위한 캐싱 적용기 (+ 언제 캐싱을 쓰는게 좋을까?)

scsi to. 2024. 12. 28. 16:38

A. 캐싱을 언제 사용하면 좋을까? 

     레디스를 이용해서 캐싱을 사용하게 되면 RAM에서 캐싱된 데이터를 가져오게 됩니다. RAM에서 가져오는 데이터는 HDD나 SSD같은 보조 기억장치에서 가져오는 것보다 속도가 월등히 빠르기에 RAM에 캐싱된 정보는 매우 빠르게 가져올 수 있습니다. 하드디스크에서 가져오는 것과 RAM에서 데이터를 가져오는 것은 속도가 50배 정도의 속도 차이가 생깁니다(Gecko & Fly). Memcached와 Postgre SQL의 db cache와 비교한 실험에서는 읽기 속도가 3배까지 차이가 난다고 합니다. (Kreitech) 이런 차이가 생기는 이유는 CPU와의 근접도도 있지만 데이터 조회 동작에 우선순위를 둔 DRAM의 설계와 저장에 우선 순위를 둔 SSD의 NAND 플래시나 하드디스크의 설계 상 차이 때문입니다. (진종문 교수님, hongreat님)

from Gecko & Fly

 

    이렇게 빠르게 데이터를 가져올 수 있는 캐싱이지만 캐시가 본래 속도만큼 성능을 내려면 캐시 적중률(Cache Hit Rating)이 높아 "캐시 히트"되는 경우가 많아져야 합니다. (Twojun님, 황심지님)

 

캐시히트(Cache Hit): 프로세스가 데이터를 요청했을때 캐시메모리에 존재하는 경우.

 

캐시 미스 (Cache miss) : 프로세서가 특정 데이터 요청 했을때, 캐시 메모리에 존재하지 않아서 보조 기억장치에서 가져와야 하는 경우. 

 

    캐시 미스가 자주 발생 한다면, 사실상 캐시를 사용하지 않는 것과 비슷하기에 평균 접근 시간은 캐싱의 속도가 아닌 DB에 접근하는 속도, 제 경우에는 API 요청을 보내는 속도와 비슷할 것입니다. (Twojun님) 그렇기에 캐시는 캐싱된 값이 여러 요청 또는 작업에서 사용할 수 있어야 사용하기에 적합합니다. 요청별로 고유한 결과를 필요로 하는 경우에는 캐싱의 적중률이 낮아서 사용할 이유가 없습니다. (Matt Brinkley, Jas Chabra 

 

B. 캐싱 사용시 유의할점

1. 데이터 일관성

 

    캐싱된 데이터는 시간이 지나면서 원본 데이터와 차이가 나기때문에 최종적 일관성을 얼마나 요구하는지에 따라서 선택되어야 합니다. 특히, 여러 지역에 걸쳐서 로컬캐시가 존재할 경우 데이터의 일관성을 만드는 일은 어려울 수 있습니다. (이원석님, Matt Brinkley, Jas Chabra) 원본 데이터가 얼마나 정적인지에 따라 데이터가 얼마나 일관성을 쉽게 지킬 수 있을지가 결정됩니다. (Matt Brinkley, Jas Chabra). 

 

   캐시된 데이터를 갱신하는 전략은 A.데이터 쓰기시 캐시와 DB 동기 업데이트하는 Write-Through, B. 데이터 쓰기시 캐시에만 쓰고 DB는 나중에 업데이트 하는 Write-Back, C. 데이터를 DB에 먼저 쓰고 읽기 요청이 들어오면 캐시를 갱신하는 Write-Around 전략등이 있습니다.(곰민님) 이를 데이터 특성에 맞게 적용하여서 데이터의 일관성을 얼마나 엄격히 관리할지 선택합니다. 

 

2. 데이터 만료 정책

 

    TTL(Time to Live) 변수를 통해 만료 정책을 적용할 수 있습니다. 만료 기간이 너무 짧으면 다시 갱신을 해야해서 평균 접근 시간이 늘어 캐시의 효율이 떨어 질 수 있으나 데이터의 일관성을 높일 수 있습니다. 만료시간은 클라이언트에서 데이터가 얼마나 정적인지, 얼마나 오래 데이터가 허용 되는지의 요구사항에 따라 달라지며 변경 속도가 느린 데이터는 더 오래 캐싱될 수 있습니다. 이상적으로는 요청의 예상 볼륨과 요청에서 얼마나 캐싱된 데이터가 자주 필요한지에 맞추어서 캐시 히트를 최대한 보장 하도록 하는 시간을 계산합니다. (Matt Brinkley, Jas Chabra)

 

    만료 정책의 종류에 따라서 읽기의 방법이 달라질 수 있는데 대표적인 읽기 방법으로는 A. 어플리케이션이 주체가 되어 캐시가 있으면 캐시에서 가져오고 아니면 DB에서 가져와 캐시에 저장하는 Look Aside(또는, Cache Aside)방법이 있습니다. 

 

3. 장애 대응 전략

 

   읽기 방법에 따라 데이터가 없을때 대응하는 전략이 다른데 대표적인 읽기 방법으로는 A. 캐시 미스가 발생하면 캐시가 DB에서 조회를 하여 캐시를 업데이트 시켜주는 Read-through 방법이 있습니다. 이 경우 캐싱 프로그램이 캐시미스 상황을 책임져 주기는 하지만 캐싱 되는 데이터는 DB와 같아야 하는 제약이 있습니다. B. 어플리케이션이 주체가 되어 캐시가 있으면 캐시에서 가져오고 아니면 DB에서 가져와 캐시에 저장하는 Look Aside(또는, Cache Aside)방법이 있습니다. 이 경우 어플리케이션이 주체가 되기에 DB와 다른 포맷의 정보도 캐싱할 수 있습니다. (xellos님)

 

    이러한 읽기 전략에도 불구하고 캐시 서버를 한대만 두면 단일 장애 지점(Single Point of Failure, SPOF)가 될 수 있습니다. 로컬서버 글로벌서버를 두는 1차, 2차 캐시를 사용하여 분산 서버를 두어 장애에 대응 할 수도 있고 (제리님, 기록은 재산이다님), 코드레벨에서도 일정한 응답시간이상 지연되면 서킷브레이커를 작동시켜 fallback 전략을 통해 데이터를 받아 올 수 있도록 합니다(Hyuk님, tossTech, 제리님)

 

4. 데이터 방출 정책

 

    캐시에 사용할 수 있는 메모리에는 한계가 있기에 메모리를 확보하려면 데이터를 방출하는 전략이 필요합니다. 이를 데이터 방출 정책이라고 하고 종류로는 A. 가장 오래된 데이터를 방출하는 LRU(Least, Recently Used), B. 사용빈도가 낮은 데이터를 방출하는 LFU(Least Frequently Used), C. 선입선출 방식의 FIFO(First In First Out) 등이 있습니다.(짱호님) 가장 일반적인 방법은 LRU 정책 입니다(Matt Brinkley, Jas Chabra)

 

C. 캐싱 적용

    기존의 임시 DB상황보다 캐시히트적중률이 높아질 수 있는 경우에 캐싱을 적용하여 조회 성능을 끌어 올려 보려고 합니다.

  • 캐시의 적용은 자주 사용되면서 변경가능성이 적은 데이터에 적용하는 것이 좋습니다.
  • 이번에는 '도움'을 조회하는 기능에 캐싱을 적용하여 성능을 높이려고 합니다. 
    • 이미 조회된 도움은 짧은 시간에 다시 확인할 가능성이 높아 캐싱적중률은 높지만 변경 가능성이 조금 있어서 고민되었습니다.
    • 짧은 시간에 다시 사용되고 이후에는 필요하지 않기에 TTL을 3분으로 짧게 설정하였습니다.
    • 데이터 일관성을 위해 Write Through 전략을 사용하였습니다. 
    • 또한, DB와 같은 포맷이 아닌 조회를 하는 DTO를 캐싱할거라 Look Aside 읽기 전략을 사용하였습니다. 
  • 설정 
    • 매핑 방법 및 직렬화, 역직렬화 설정 + TTL 설정
    • ObjetMapper
      • setVisibility
        • 필드와 클래스 정보에 접근하기 위한 설정입니다.
      • activeDefaultTyping :
        • JSON에 타입정보를 기록해주기 위하여 사용합니다.
        • Cacheable은 등록하는 DTO의 정보를 기록하지만 이 경우 DTO가 다른 클래스를 가지고 있는 복합 구현체라 필요합니다.
        • 타입 정보가 없으면 Redis는 역직렬화시 Linked Hash Map으로 구현하고 Class Cast Exception이 발생한다. 
    • RedisCacheConfiguration
      • Spring Data Redis 사용시 RedisCacheManager 자동 설정해 주지만 Redis를 사용시에는 RedisCacheConfiguration을 설정해 주어야 합니다.  
      • entryTtl: 캐시 만료 시간입니다.
      • serializeKeysWith: Key 를 직렬화할 때 사용하는 규칙입니다.
      • serializeValuesWith: Value 를 직렬화할 때 사용하는 규칙입니다.
  @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
        // Jackson 라이브러리 설정 
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        objectMapper.activateDefaultTyping(
                objectMapper.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.EVERYTHING,
                JsonTypeInfo.As.PROPERTY
        );
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        // 캐싱 직렬화, 역직렬화 설정
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer =
                new GenericJackson2JsonRedisSerializer(objectMapper);

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer));

        //캐싱 종류별 TTL 설정
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        redisCacheConfigurationMap.put("help_searched", redisCacheConfiguration.entryTtl(Duration.ofMinutes(3)));

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(connectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .withInitialCacheConfigurations(redisCacheConfigurationMap)
                .build();
    }
  • DTO 캐싱 (Look Aside)
    @Cacheable(cacheNames = "help_searched", key = "'checkIn_' + #id")
    public CheckInService.CheckInSelected selectCheckIn(Long id) {
        return checkInService.findCheckIn(id);
    }

 

  • 업데이트시 Write Through
@Transactional
    @CachePut(cacheNames = "help_searched", key = "'checkIn_' + #result")
    public CheckInService.CheckInSelected updateCheckIn(CheckInService.Update dto) {
        return checkInService.update(dto);
    }

 

 

  • 캐싱 적용시 조회 성능 변화
    • PostMan으로 테스트 결과 DB에서 바로 가져오는 경우(적용 전) 80-126ms 정도 걸렸지만 캐싱을 사용시(적용 후) 9-15ms으로 약 14배 정도 성능이 개선 되었습니다.

적용 전
적용 후

 

 

======

출처. 

12 Free RAMDisk vs SSD – 10x Faster Read Write Speed via RAM Virtual Disk by Gecko & Fly

Memcached vs DB cache Comparison by Kreitech

진종문 교수님의 [반도체 특강] 디램(DRAM)과 낸드플래시(NAND Flash)의 차이

hongreat님의 캐시와 데이터베이스는 성능차이가 왜 일어나는가?

램과 ssd 속도 차이가 나는 원인이 뭔가요? - quasar zone 질문

황심지님의 캐싱은 언제 적용하는게 좋을까? 

Twojun님의 [6주 차] - 메모리(데이터) 참조 지역성, 캐시(Cache) 메모리

tossTech - 김신님의 캐시 문제 해결 가이드 - DB 과부하 방지 실전 팁

제리님의 [무신사 watcher] 캐시 서버에 장애가 생긴다면?

기록은 재산이다님의 Spring Cache 장애 대응 방안

짱호님의 [대규모 시스템 설계] 응답시간 개선하기

이원석님의 대규모 트래픽을 감당하는 역략 - (캐시 메모리)

곰민님의 cash말고! 데이터 베이스 캐시(Database Cache) 활용 전략(2)

 

xellos님 캐싱 전략 (원제: Caching Strategies and How to Choose the Right One)