Redis를 이용하여 현재 게임 웹서버 프로젝트를 진행 중에, 의문 점이 하나 생겼다.
현재 조건은
결국 DB에 60ms 안에 Upload가 완료되어야 하고, 사용자가 Map Data를 다운 받을 때 무사히 다운받아야 하는 것이다.
고로 성능이 굉장히 중요하므로, 현재 Memory를 사용하여 먼저 메모리에 저장 후 DB에 업로드 하는 방식으로 고민해보고있다.
그런데, 만약 Redis에서 Map Data를 Upload하는 도중에 사용자가 요청이 날라오게 되면 어떻게 될지가 갑자기 궁금해지면서, Redis의 쓰레드와 Transaction 동작 방식이 궁금해졌다. 이에 대해 찾아보다보니 Redis에 대한 이해가 부족하다고 느꼈고, 대략적으로 궁금했던 내용들을 정리해보려고 한다.
Redis는 Single Thread라고 많이 알려져 있는데, 어디서는 Multi Thread라고 한다. 과연 무엇이 맞는 것일까?
Redis 6 rings in a new era: while it retains a core single-threaded data-access interface, I/O is now threaded.
출처 : Redis.com
그리고 기타 다른 블로그들을 참고하여 Version 별로 요약해보면 이렇다.
즉, 부분적으로 Multi Thread라는 것이고 명령의 실행 자체는 Single Thread 인 것이다. 고로 Redis의 특징인 Atomic은 Single Thread를 유지함으로써 여전히 보장이 되는 것이다.
여기서 말하는 AOF란 도대체 뭘까?
Redis는 영속성을 유지(백업)하기 위해 AOF와 RDB 두 가지 방식을 제공하게 된다. RDB는 우리가 MySQL을 쓸 때 많이 보았던 특정 시점에 snapshot을 생성하는 방식이다.
AOF란 Append Only File의 약자로, 명령이 실행될 때마다 해당 명령이 파일에 기록되는 방식을 말한다. (조회 명령은 제외)
기본적으로 appendonly.aof
파일에 기록되며, 특정 시점에 데이터 전체를 다시 쓰는 rewrite
기능이 있다. 왜냐하면, 계속 추가되면서 기록되기 때문에 점점 파일 사이즈가 커지게 되며, 너무 커져버리면 OS File Size 제한이 걸려 기록이 되지 않을 수도 있고 Redis 서버 시작 시 로드 시간이 많이 걸릴 수 있기 때문이다.
그렇다면 이 기능이 어떻게 파일 사이즈를 작게 할까? 만약에 SET 명령으로 똑같은 key에 대해 5번 수행을 하게 된다고 하면, 메모리에는 마지막 수행값만 남게 될 것이므로, 앞에 존재하는 4번의 수행값은 전혀 의미가 없어지게 된다. 고로 Rewrite 시에는 마지막 SET 명령어만 남게 되고, 이런 식으로 다른 명령어들도 rewrite를 시켜준다.
appendonly.aof
파일은 텍스트 파일이므로 수정이 가능하다. 따라서 명령어를 잘못 사용하여 실수로 데이터를 모두 날리게 된다면 즉시 해당 파일에서 해당 명령어를 제거하고 재부팅을 하게 되면 데이터 손실을 막을 수 있다.
UNLINK가 Sub Thread에서 처리해 준다고 했는데, 그렇다면 UNLINK가 무엇이길래 중요한 것일까?
This command is very similar to DEL: it removes the specified keys. Just like DEL a key is ignored if it does not exist. However the command performs the actual memory reclaiming in a different thread, so it is not blocking, while DEL is. This is where the command name comes from: the command just unlinks the keys from the keyspace. The actual removal will happen later asynchronously.
출처 : Redis Doucments
UNLINK 명령어는 DEL 명령어와 같이 특정 key 값을 지워주는 명령어로, Version 4.0에서 추가된 명령어이다.
DEL과 다른 점은 key가 존재하지 않을 때 DEL은 Block되지만 Unlink 명령어는 Block되지 않는다.
Key값 삭제는 Sync 로 동작하고 값 삭제를 별도의 Thread에서 비동기로 처리한다고 한다. 그래서 value가 굉장히 클 때 이 방법이 엄청 효율적이라고 한다.(DEL은 value가 다 삭제될 때까지 block하므로 오래 기다려야 한다.)
그렇다면 UNLINK 명령어를 무조건 사용하는 게 맞을까?
대부분의 경우에는 UNLINK가 거의 안전하다. 왜냐하면 UNLINK도 무조건 non-blocking/async로 동작하는 것이 아니라, value가 매우 작으면 DEL과 거의 비슷하게 동작하므로, UNLINK를 사용하는 경우가 거의 대부분 효율적이고 안전할 것이다. 하지만 만약 thread sync 문제가 발생한다면, DEL 을 사용하는 것을 권장한다고 한다.
StackOverflow에 똑같은 문제로 질문이 있었다. 링크 : Is UNLINK always better?
이 부분은 제대로 이해를 하지 못했다.
링크 : https://charsyam.wordpress.com/2020/05/05/입-개발-redis-6-0-threadedio를-알아보자/
해당 링크에 설명이 되게 자세히 나와 있다.
요약해보면,
이 두개가 Threaded IO가 적용이 되었다는 것 까지는 이해를 했는데, 이 코드 구조 자체는 이해를 하지 못했다.
결국 저 두개가 Threaded IO가 적용이 되어서 Redis 6는 약 2.5배 속도가 향상 되었다는 것이다.
위에서 배운 내용을 요약해보면, 결국 Redis 내부에서 핵심 동작들은 Single Thread로 동작하게 되는 것이므로, Atomic을 유지할 수 있다. 되게 좋은 건 알겠는데, 그렇다면 Single Thread로 어떻게 수백만명의 유저들이 빠른 속도로 Redis를 이용할 수 있는 것일까?
하나의 Data에 대해서 수백만 명이 요청을 날리게 된다면, 그걸 어떻게 몇 ms로 응답을 할 수 있을 것인가?
이러한 궁금증을 나만 갖고 있던 것이 아니었다.
StackOverflow - how Redis do concurrentI/O?
Tistory - Redis 동시성, 고립성
정말 똑같은 생각을 가진 두 링크를 찾을 수 있었다.
결론은 Concurrency(동시성)과 Parallelism(병렬성)의 차이이다.
Redis는 동시성은 있지만 병렬성이 없는 서비스이다.
그렇다는 즉슨, 동시성으로 인한 문제가 발생할 수 있다는 것인데, Redis에서는 이를 어떻게 해결할까?
Docs : Redis Transaction
Redis Documents를 보면, Redis는 안전한 Transaction을 위해 여러 명령어를 제공한다.
MULTI
, EXEC
, DISCARD
, WATCH
명령어를 제공한다. MULTI와 EXEC를 이용해서 Transaction 을 실행하게 되는데, 단순히 이 명령어들만 사용하게 되면 동시성 문제가 발생하게 된다.
그래서 Optimistic Locking을 사용하기 위해 WATCH 명령어를 사용하고, 해당 명령어가 안전한 Transaction을 보장하게 된다고 한다.
그렇다면 Spring에서는 Redis를 연결하기만 하면 해당 문제를 예방해줄까?
그렇지 않다. Spring에서 공식적으로 사용하는 Redis Client인 Jedis와 Lettuce에서는 PlatformTransactionManager
구현체를 제공하지 않기 때문에 JDBC의 DataSoruceTransactionManager
를 사용하거나 JPA의 JpaTransactionManager
를 사용해야 한다고 한다.
Docs : Spring Data Redis Docs
또는 SessionCallBack을 사용하여 직접 Redis 명령어를 사용하여 Transaction을 설정할 수 있다고 한다.
해당 방법에 대해서는 나중에 실사용하게 되면 실험을 해보려고 한다.
이 방법에 대해 설명이 잘 나와있는 블로그가 있다.