Backend/Redis

Redis 알아보기

olsohee 2024. 2. 16. 13:05

Redis 캐시로 사용하기

캐시란 데이터의 원래 소스보다 더 빠르고 효율적으로 액세스할 수 있는 임시 데이터 저장소이다. 이때 자주 액세스되고, 자주 변하지 않는 데이터가 캐시로 적합한 데이터이다.

 

레디스는 캐시로 적합한데 그 이유는 다음과 같다.

  • 레디스는 인메모리 기반의 데이터 저장소로, 처리 속도가 빠르다. 따라서 단순한 Get/Set 명령어의 경우 초당 10만 TPS를 갖는다(1초당 10만개의 명령 처리 가능). (*인메모리: 데이터를 메인 메모리(=랜덤 액세스 메모리(RAM))에 저장하여 처리하는 방식)
  • 레디스는 다양한 자료구조를 지원한다. 따라서 적절한 자료구조를 사용하여 데이터를 용이하게 관리할 수 있다.

캐싱 전략

읽기 전략 - Look Aside

  • 데이터를 찾을 때 우선 캐시를 조회한다(Cache Hit). 캐시에 데이터가 없으면 DB를 조회하고 해당 데이터를 캐시에 저장한다.
  • 따라서 찾는 데이터가 캐시에 없을 때 DB에서 조회해서 캐시에 저장하기 때문에 이를 Lazy Loading이라고도 한다.
  • 장점
    • 레디스가 다운되더라도 DB에서 데이터를 가져올 수 있어서 장애로 이어지지 않을 수 있다.
    • 대신 캐시에 붙어있던 커넥션이 많았다면, 레디스가 다운된 순간 순간적으로 DB로 커넥션이 몰려 부하가 발생할 수 있다.
  • 단점
    • 캐시와 DB 간 정합성 문제가 발생할 수 있다.
    • 만약 DB에 새로운 데이터 다량을 추가하면, Cache Miss가 발생해서 성능저하가 올 수 있다. 따라서 DB에서 캐시로 미리 데이터를 넣어주는 Cache Warming 작업을 하는 방법이 있다. (*Cache Warming: DB의 데이터를 캐시로 미리 넣어두는 작업을 의미한다. 이 작업을 수행하지 않으면 서비스 초기에 대량의 Cache Miss가 발생하여 데이터베이스 부하가 급증할 수 있다. 다만 캐시는 용량이 크지 않아 무한정 데이터를 들고 있을 수 없어서 데이터의 expire 시간을 설정해야 하는데, 이 expire 시간을 잘 설정해야 한다.)

쓰기 전략 - Write Around

  • 데이터를 DB에 우선 저장한다.
  • Cache Miss가 발생하면 DB의 데이터를 캐시에도 저장한다.
  • 장점
    • 데이터를 DB에만 저장하므로 Write Through 방식보다 빠르다.
  • 단점
    • 캐시에 저장된 데이터와 DB에 저장된 데이터가 다르다.

쓰기 전략 - Write Through

  • 데이터를 DB와 캐시에 함께 저장한다.
  • 장점
    • 데이터를 DB와 캐시에 함께 저장하므로 캐시의 데이터는 항상 최신 상태로 유지되고, 데이터 정합성 문제가 발생하지 않는다.
  • 단점
    • 저장할 때 캐시와 DB 두 단계를 거치므로 상대적으로 느리다.
    • 저장되는 데이터가 재사용되지 않을 수도 있음에도 캐시에 저장하기 때문에 리소스 낭비가 일어날 수 있다.

Redis의 데이터 타입

레디스는 Key-Value 구조로 데이터를 저장하는데, Value로 다양한 데이터 타입을 가질 수 있다. 레디스는 다양한 데이터 타입을 제공하므로 개발 편의성이 증가한다.

이때 데이터 타입 사용 시 주의사항은 다음과 같다.

  • 하나의 컬렉션에 너무 많은 아이템을 담으면 좋지 않다. 10,000개 이하의 몇 천개 수준으로 유지하는 것이 좋다.
  • expire(TTL)은 컬렉션의 아이템 개별에 걸리지 않고 전체 컬렉션에 대해 걸린다. 따라서 10,000개의 아이템을 가진 컬렉션에 TTL이 걸려있으면 그 시간 후에 10,000개의 아이템이 모두 삭제된다.

Redis 데이터를 영구 저장하려면? (AOF vc RDB)

레디스는 인메모리 데이터 스토어로, 서버를 재시작하면 기존의 데이터는 모두 휘발된다. 따라서 레디스를 캐시 이외의 용도로 사용하려면 데이터 백업이 필요하다. 레디스는 메모리에 있는 데이터를 디스크로 백업하는 기능을 다음 두 가지 방법으로 제공한다.

  • AOF(Append Only File)
    • 데이터가 변경되는 입력/수정/삭제 명령이 실행될 때마다 해당 명령어들을 파일에 저장한다.
    • 장점
      • 단순히 명령어를 저장하므로 저장 속도가 빠르다.
      • RDB와 달리 실시간 데이터 백업이 가능하여 데이터 손실이 거의 없다.
    • 단점
      • 명령 실행 기록을 모두 기록하기 때문에 RDB보다 파일 크기가 크다.
      • 데이터 자체를 저장하는 것이 아니기 때문에 복원 소요 시간이 길다.
  • RDB(Redis Database Backup)
    • 특정 간격으로 레디스 메모리에 존재하는 데이터의 스냅샷을 남기는 방식이다.
    • 장점
      • 압축하여 저장하기 때문에 AOF보다 크기가 작다.
      • 데이터 자체를 저장하기 때문에 로딩/복구 속도가 빠르다.
    • 단점
      • 특정 간격으로 저장하기 때문에 그 사이에 발생한 데이터는 유실 가능성이 있다.

각 백업 방식의 선택 기준은 다음과 같다.

  • 어느정도 데이터 손실이 발생해도 괜찮은 경우 ➡️ RDB
  • 장애 상황 직전까지의 모든 데이터가 보장되어야 하는 경우 ➡️ AOF
  • 강력한 내구성이 필요한 경우 ➡️ RDB + AOF
    • ex, 매일 7시마다 RDB 스냅샷을 생성하고, RDB 생성 이후에 변경되는 데이터는 AOF로 백업

레디스의 특징

싱글 스레드 + 이벤트 루프

레디스 내부에서 명령어를 수행하는 부분은 싱글 스레드 아키텍처로 구현되어 있다. 그리고 레디스는 사용자들이 실행한 명령어들을 이벤트 루프(event loop) 방식으로 처리한다. 즉, 클라이언트가 실행한 명령어들을 Event Queue에 적재하고 싱글 스레드로 하나씩 처리한다. 따라서 다음과 같은 장단점을 갖는다.

  • 장점
    • Context Switching 비용이 발생하지 않는다.
    • Deadlock이 발생하지 않는다. 
    • Race Condition이 발생하지 않는다.
  • 단점
    • 싱글 스레드이므로 O(N) 명령어와 같이 오버헤드가 큰 명령어를 처리하는 동안에 다른 명령어를 처리할 수 없다.

논블로킹 I/O

레디스는 싱글 스레드이기 논블로킹 방식으로 비동기적으로 I/O 작업을 수행한다. 따라서 클라이언트 1의 요청에 대해 I/O 작업을 처리하는 동안 단일 스레드는 클라이언트 2의 요청을 처리할 수 있다. 이를 통해 높은 처리량을 달성할 수 있다. 만약 싱글 스레드인 레디스가 블로킹 방식을 사용한다면 어떻게 될까? 요청한 I/O 작업이 완료될 때까지 싱글 스레드는 봉쇄 상태로 대기하게 된다. 따라서 그동안 다른 클라이언트의 요청을 처리하지 못하게 되어 처리량이 매우 낮아질 것이다.

 

그러면 이때 말하는 I/O 작업은 무엇일까? 레디스는 메모리 기반의 데이터 저장소로 메모리에 데이터가 삽입되고 조회된다. 따라서 메모리에서 이뤄지는 데이터 삽입/조회는 CPU가 하는 일이지, I/O 작업이 아니다. 레디스에서 말하는 I/O 작업은 다음과 같다.

  • 디스크를 대상으로 데이터를 삽입/조회하는 RDB, AOF와 같은 백업 기능
  • 네트워크 I/O 작업: 클라이언트와의 통신을 의미한다. 레디스 서버는 클라이언트로부터 요청을 받고, 응답을 보내는 과정에서 네트워크 I/O 작업이 일어난다. 

TTL

레디스에 저장되는 모든 데이터는 유효 기간을 설정할 수 있다. 유효 기간이 지난 데이터는 레디스가 메모리에서 해당 데이터를 삭제한다(-> 메모리를 효율적으로 사용 가능). 레디스는 메모리에 데이터를 저장하므로 저장 공간이 한정적이다. 따라서 레디스에 데이터를 저장할 때는 데이터의 유효 기간을 설정하는 것이 권장된다. 만약 유효 기간을 설정하지 않으면 직접 데이터를 삭제할 때까지 영원히 유지된다.

Redis 사용 용도

  • Remote Data Store
    • A, B, C 서버에서 같은 데이터를 공유하고 싶을 때 Remote Data Store로 Redis를 이용할 수 있다. 즉 글로벌 캐싱으로 이용할 수 있다.
    • (글로벌 캐싱: 여러 서버에서 한 캐시 서버에 접근하여 참조한다. 여러 서버에서 공통으로 공유해야 하는 정보를 담는 용도로 사용한다. 네트워크 트래픽을 사용해야 해서 로컬 캐시보다는 느리다.)
  • Local Data Store
    • 만약 서버 한대에서만 필요하다면 그냥 서버에서 전역 변수를 쓰면 되지 않을까라고 생각할 수 있지만 Redis 자체가 Atomic을 보장해주기 때문에 이를 사용하는게 좋다. 그리고 Redis는 Thread-Safe하고 Single Thread라서 이슈가 덜하다.
    • (로컬 캐싱: 서버마다 캐시를 따로 저장한다. 속도가 빠르다. 만약 여러 서버를 사용할 경우 데이터가 변경할 때마다 변경 사항을 전달해야 하므로 효율적이지 않다.)
  • 인증 토큰 저장
    • 웹 서비스를 개발할 때 인증 토큰을 Redis에 저장하여 많이 사용한다. (Strings 또는 Hash 형태로 저장)
  • 주 데이터 저장소
    • AOF, RDB 백업 기능과 레디스 아키텍처를 사용하여 주 저장소로 데이터를 저장할 수 있다. 하지만 메모리 특성상 용량이 큰 데이터 저장소로는 적절하지 않다.

Redis 사용 시 주의사항

주의할 명령어

  • 레디스는 싱글 스레드로 동작한다. 따라서 오래 걸리는 명령어 수행 시 나머지 명령들은 앞의 명령 수행이 완료될 때까지 대기해야 한다. 따라서 O(N) 관련 명령어는 주의해야 한다. O(N) 관련 명령어는 다음과 같다. 
    • keys: 모든 키를 보여주는 명령어이다. (keys 대신 scan 명령어를 사용하자. keys는 모든 키를 순회하지만 scan은 짧게 여러 번 가져온다. 따라서 그 사이에 Context Switching이 일어나서 다른 명령의 수행이 가능하다.)
    • delete collections, get all collections (컬렉션의 모든 아이템을 가져와야 한다면, 컬렉션의 일부만 가져오거나 큰 컬렉션을 작은 컬렉션으로 나눠서 저장하자. 한 컬렉션 내에 몇 천개 안쪽으로 저장하는 것이 좋다.)

메모리 관리

  • 레디스는 인메모리 기반으로, 메모리 관리가 매우 중요하다.
  • maxmemory(최대 사용할 수 있는 메모리 양) 
    • 레디스는 메모리 관리를 위해 maxmemory 옵션을 제공한다. 이 설정을 통해 그 이상의 메모리 양을 사용하지 않도록 제한할 수 있다. 
    • 만약 레디스가 maxmemory로 설정한 값 이상의 메모리 양을 사용하려고 하면?
      • OS는 스왑을 진행한다. (그러나 스왑으로 인한 성능 저하가 발생한다.)
      • 그러나 OS가 모든 상황에서 스왑을 사용할 수 있는 것은 아니다(ex, 스왑 공간이 충분하지 않은 경우, 스왑이 비활성화되어 있는 경우). 이런 경우, maxmemory를 초과하면 레디스는 OOM(Out of Memory) 에러를 발생시킨다.
    • 따라서 메모리 모니터링을 통해 적절한 maxmemory를 설정해야 한다.
  • 모니터링 지표: used_memory, used_memory_rss
    • used_memory: 레디스가 사용하는 메모리 사용량
    • used_memory_rss: OS가 레디스에 할당한 메모리 양
    • used_memory값에 비해 used_memory_rss 값이 큰 경우, 즉 실제 사용하는 메모리 양에 비해 OS가 할당한 메모리 양이 많은 경우 비효율적으로 낭비되는 메모리가 생긴다. 이런 경우 모니터링 지표 중 mem_fragmentation_ratio 수치가 올라간다.
    • 예를 들어 삭제되는 키가 많을 때(메모리가 해제될 때), used_memory 값은 감소하지만 used_memory_rss 값은 즉시 감소되지 않을 수 있다. 이런 경우 used_memory 값에 비해 used_memory_rss 값이 큰 경우가 발생할 수 있다. 

Cache Stampede

대규모 트래픽 환경에서 TTL 값을 너무 작게 설정하면 Cache Stampede가 발생할 수 있다. 레디스를 캐시 용도로 사용할 때 Look Aside 패턴 상황이라고 가정하자. 만약 많은 서버에서 하나의 키를 참조하고 있는데 그 키의 TTL이 만료되면, 많은 서버들이 그 키 값을 다시 가져오기 위헤 DB에 접근할 것이다(Duplicated Read 발생). 그리고 각 서버들 모두 읽어온 키를 레디스에 쓰는 작업이 발생할 것이다(Dulicated Write).


Reference