Spring Async and Lettuce Async

Redis Developer Course Redis Technical Support Redis Enterprise Server

Spring Async

Spring Boot에서 제공하는 @EnableAsync, @Async 어노테이션을 이용해서 비동기(asynchronous)로 메소드 실행 방법입니다.
소스 코드에 대한 일반적인 내용은 위 문서 [Connection Pool & Thread]를 보시기 바랍니다.

Thread 설정

Thread 개수를 입력 받아 실행합니다. 각 thread 당 1000번 save(set 명령)을 실행합니다.

StringController.java

Repository 설정

Class에 @EnableAsync 어노테이션을 추가하고, 메소드에 @Async 어노테이션을 추가합니다.
@EnableAsync 어노테이션은 아래와 같이 main 메소드가 있는 class에 추가해도 됩니다.

StringRepository.java


Lettuce Async

Spring Boot에서 제공하는 @Async 어노테이션을 이용하지 않고 Lettuce에서 제공하는 비동기(asynchronous)로 메소드 실행 방법입니다.
@GetMapping("/save/sync/{size}")는 동기로 입력할 때 사용합니다.
@GetMapping("/save/async/{size}")는 비동기로 입력할 때 사용합니다.

StringController2.java

StringRepository2.java

RedisConfig.java

Lettuce로 레디스 서버에 직접 연결하기 위해 RedisClient를 리턴하는 메소드를 추가해습니다.


[Spring 비동기(@Async)]와 [Lettuce 비동기(Async)] 성능 비교

Spring @Async 어노테이션을 이용한 방법[Spring 비동기(@Async)]와 [Lettuce 비동기(Async)] 성능 비교
레디스 서버와의 연결은 1개를 사용했다. setShareNativeConnection(true)

Spring 비동기(@Async)

  • @Async 작동 방식에 대한 오해: 명령을 최대한 빨리 레디스 서버에 보내고 응답을 비동기로 받는 것으로 생각했다. 스레드 1개에서는 생각과 같이 작동해서 동기입력일 때 8초였는데 비동기에서 3초로 줄었다. 그런데 스레드를 10개로 늘려서 실행해보니 소요시간이 동기입력(9초)보다 늘어서 12초가 나왔다. 스레드 100개 일 때는 102초로 동기입력(14초)보다 훨씬 느려졌다.
    Java에서 스레드별로 for loop 종료 후 소요시간을 찍어본 결과 위 경우 모두 1초 이내로 나온다.
    StringController.java
    for (int i=0; i<count; i++) {
        stringRepository.saveAsync(threadName+"-"+i, "value-"+threadName+"-"+i);
    }
    log.info("{} 소요시간: {}회 -> {} ms (시작:{} ~ 종료: {})", threadName, count, ...
    레디스 서버에서 redis-cli --stat로 명령 실행 상태를 확인해보니 초당 약 900~1000개 명령을 실행한다.
  • 정리: @Async를 사용하면 스레드 개수와 관계없이 해당 메서드를 초당 약 900~1000번 실행한다. 즉, 스레드 개수를 늘려도 전체 성능이 나아지지 않는다.

Lettuce 비동기(Async)

  • 레터스의 비동기 실행은 최대한 빨리 서버로 명령을 보내고 응답을 비동기로 기다린다. 그러므로 전체 처리 시간에서 동기입력에 비해서 훨씬 빠르다.
    StringRepository2.java
    private RedisClient redisClient;
    private StatefulRedisConnection<String, String> connection;
    private RedisCommands<String, String> syncCmd;
    private RedisAsyncCommands<String, String> asyncCmd;

    public StringRepository2(RedisClient redisClient) {
        this.redisClient = redisClient;
        this.connection = redisClient.connect();
        this.syncCmd = this.connection.sync();
        this.asyncCmd = this.connection.async();
    }
  • 스레드 1개: 동기입력 8초 -> 비동기입력 1초
    스레드 10개: 동기입력 9초 -> 비동기입력 2초
    스레드 100개: 동기입력 14초 -> 비동기입력 3초
    스레드 1000개: 동기입력 62초 -> 비동기입력 10초
  • 당연히 Spring 비동기(@Async) 입력에 비교해도 훨씬 빠르다.
  • 결론: 대량의 데이터를 레디스 서버에 입력할 경우에는 Lettuce 비동기 방식을 사용할 것을 추천합니다.

[Spring 비동기(@Async)]와 [Lettuce 비동기(Async)] 테스트 결과

동기입력은 이전 테스트 결과를 사용했다.

스레드 입력건수 소요시간(sec)
동기입력 Spring 비동기(@Async) Lettuce 비동기(Async)
1 1,000 8 3 1
10 10,000 9 12 2
100 100,000 14 102 3
500 500,000 34 테스트 생략 7
1000 1,000,000 62 10
5000 5,000,000 288 37
  • Spring 비동기(@Async)의 스레드 500 이상은 결과가 예상되므로 테스트를 생략했다.

[Lettuce 비동기(Async)] 스레드 5000

명령 실행 횟수가 초당 20만에 가까이 도달했다. 서버 CPU는 60% 정도 사용했다.
해당 서버를 redis-benchmark로 테스트 시 초당 11만 정도 처리했다.
redis-benchmark는 스레드를 50개 처리한다. 스레드 개수를 늘려도 성능이 나아지지 않는다.
정리 일반적으로 표준으로 삼고 있는 redis-benchmark 테스트보다 Java Lettuce 비동기 테스트가 성능이 더 좋게 나왔다.

Thread 5000 Lettuce 비동기(Async)

Redis-benchmark 테스트

redis-benchmark
SET 명령 100만회 실행
$ redis-benchmark -n 1000000 -r 1000000000 set keyA___rand_int__ valueA___rand_int__

Spring 비동기(@Async)

다음은 각 테스트 동안 redis-cli --stat를 이용해서 서버 처리 상태를 모니터링 한 것이다.

Spring 비동기(@Async), 스레드 1: 처리시간 3초

requests ()괄호 안에 있는 숫자가 레디스 서버에서 명령을 실행한 횟수이다.

Spring 비동기(@Async), 스레드 10: 처리시간 12초

Spring 비동기(@Async), 스레드 100: 처리시간: 102초

Lettuce 비동기(Async)

Lettuce 비동기(Async), 스레드 1: 처리시간 1초

requests ()괄호 안에 있는 숫자가 레디스 서버에서 명령을 실행한 횟수이다.

Lettuce 비동기(Async), 스레드 10: 처리시간 2초

Lettuce 비동기(Async), 스레드 100: 처리시간 3초

Lettuce 비동기(Async), 스레드 500: 처리시간 7초

Lettuce 비동기(Async), 스레드 1000: 처리시간 10초

Lettuce 비동기(Async), 스레드 5000: 처리시간 37초


<< Connection Pool Async DB select >>

Email 답글이 올라오면 이메일로 알려드리겠습니다.