Lua Script in Redis + Local Cache 관리를 통한 성능 개선

shinny·2024년 7월 14일

[성능 개선]

목록 보기
5/8

1️⃣ 다른 방법 찾기

Locust로 부하 테스트를 진행했을 때, 1000명의 users, 3 workers, 300 ramp-up인 상황에서 RPS는 여전히 300 정도에 머물렀다. 다른 방법이 없는지 더 찾아보다가, 이전 RedissonClient로 락을 걸고 해제하는 과정을 학습하면서 배웠던 Lua Script가 생각났다.

Scripting with Lua

Redis에서 Lua 실행하기(Redis Docs)
Redis는 사용자들이 서버에서 직접 Lua 스크립트를 업로드하고 실행할 수 있도록 허용한다. 스크립트는 서버에서 실행되기 때문에, 스크립트로 데이터를 읽고 쓰는 것은 매우 효율적이다. Redis는 스크립트의 원자적 실행을 보장하며, 스크립트가 실행되는 동안 모든 서버의 활동은 중단된다.(단일 스레드 실행)

Lua 스크립트 작업은 다음과 같은 특징을 지닌다.

  • 데이터가 있는 곳에서 로직을 실행함으로써 로컬성을 제공한다. 이 데이터의 로컬성은 전체 지연 시간을 줄이고 네트워킹 비용을 절약해준다.
  • 스크립트의 원자적 실행을 보장하는 블로킹을 의미한다.
  • Redis에 기본적으로 없는 간단한 기능을 추가하거나 너무 특수해서 포함되지 않은 기능을 구성할 수 있게 한다.

여기서 첫 번째와 두 번째의 특징에 착안하여, 기존의 Redis 분산락 기능을 제거하고, Redis에서 실행될 모든 기능을 스크립트화하여 한번에 실행해보려고 한다.

2️⃣ 코드 작성

String code = redisTemplate.execute(
                issueScript,
                List.of(issueRequestKey, issueRequestQueueKey),
                String.valueOf(userId),
                String.valueOf(totalIssueQuantity),
                objectMapper.writeValueAsString(couponIssueRequest)
    	);
        
// issueScript
private RedisScript<String> issueRequestScript() {
    String script = """
            if redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 then
                return '2'
            end
                                
            if tonumber(ARGV[2]) > redis.call('SCARD', KEYS[1]) then
                redis.call('SADD', KEYS[1], ARGV[1])
                redis.call('RPUSH', KEYS[2], ARGV[3])
                return '1'
            end
                                
            return '3'
            """;
    return RedisScript.of(script, String.class);
}

위와 같이 직접 Lua Script를 작성하였고, 이 스크립트를 redisTemplate을 통해 직접 실행하는 것으로 코드를 수정하였다.

3️⃣ 결과

  • RPS : 4283, Average Response Time : 0.2 sec
  • Redis CPU Usage : 29%

현재까지 시도한 방법 중 RPS가 가장 높은 것은 300 전후였다. 그것이 4000대까지 올라왔으니 RPS가 거의 1400% 증가했다고 평가해야하며, 평균 응답 시간의 경우도 가장 빠른 것이 3.6초였으니 거의 1/20로 줄었다고 평가할 수 있다.
Redis 사용량은 30% 정도로 증가하였으나, 아직 충분히 여유가 있다.

4️⃣ Local Cache 적용

  • caffeine 의존성 추가
implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
  • LocalCacheManager를 Bean으로 등록
@Bean
public CacheManager localCacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager();
    cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(Duration.of(10, ChronoUnit.SECONDS))
                .maximumSize(1000));
    return cacheManager;
}
  • cacheManager로 LocalCacheManager 등록
@Cacheable(cacheNames = "coupon", cacheManager = "localCacheManager")
public CouponRedisEntity getCouponLocalCache(long couponId) {
    return proxy().getCouponCache(couponId);
}

@CachePut(cacheNames = "coupon", cacheManager = "localCacheManager")
public CouponRedisEntity putCouponLocalCache(long couponId) {
    return proxy().putCouponCache(couponId);
}

public CouponCacheService proxy() {
    return (CouponCacheService) AopContext.currentProxy();
}

5️⃣ 평가

  • RPS : 5311, Average Response Time : 0.19 sec

로컬 캐시 사용으로 RPS는 4283에서 5311로 증가했고, 평균 응답 속도도 조금 더 줄었다.

profile
꾸준히, 성실하게, 탁월하게 매일 한다

0개의 댓글