Redis 야무지게 사용하기

파이 ఇ·2024년 11월 7일
1
post-thumbnail

이 글은 NHN Cloud의 Redis 야무지게 사용하기 를 정리한 글 입니다.

1. Redis 캐시로 사용하기

먼저 캐싱이란? 사용자의 입장에서 데이터의 원래 소스보다 더 빠르고 효율적으로 액세스 할 수 있는 임시 데이터 저장소이다.

캐시의 활용

동일한 데이터에 대해 반복적으로 액세스하는 상황에 캐시를 사용하는 것이 좋다. 즉, 데이터의 재사용 횟수가 1회 이상이여야 캐시가 의미있게 된다. 또한, 잘 변하지 않는 데이터일 수록 캐시를 사용하는게 더 효율적이다.

캐시로서의 redis는 가장 널리 사용되는 캐싱 솔루션이다.

  • 단순한 key-value 구조
  • In-memory 데이터 저장소(RAM)
  • 빠른 성능
    • 평균 작업속도 < 1ms
    • 초당 수백만 건의 작업 가능

캐싱 전략 (Caching Straregies)

redis를 캐시로 사용할 때 어떻게 배치하느냐에 따라 시스템의 성능에 큰 영향을 끼친다. 이를 캐싱 전략이라고도 부르며, 캐싱 전략은 데이터의 유형과 해당 데이터에 대한 액세스 패턴을 잘 고려해서 선택해야 한다.

먼저 애플리케이션에서 데이터의 읽는 작업이 많을 때 사용하는 전략을 살펴보자.

읽기 전략 : Look-Aside


이 구조는 redis를 캐시로 쓸 때 가장 일반적으로 사용되는 방법이다. 애플리케이션은 데이터를 찾을 때 캐시에 먼저 확인하고 캐시에 데이터가 존재하면 데이터를 가져오는 작업을 반복한다.

만약, 캐시에 데이터가 존재하지 않아 cache miss가 일어나게 되면 애플리케이션은 DB에 접근해서 직접 데이터를 가지고 온 뒤, 캐시에 저장 및 반환하는 작업을 한다.

이 구조는 redis가 다운되더라도 바로 장애로 이어지지 않고, DB에서 데이터를 가지고 올 수 있다.

여기서 주의할 점은 하지만 캐시에 붙어있던 커넥션이 많았다면, 그 커넥션이 모두 DB로 붙기 때문에 DB에 갑자기 많은 부하가 몰릴 수 있다. 그래서 이런 경우 캐시를 새로 투입하거나, redis가 다운되어 DB에만 새로운 데이터를 저장했다면 처음에 cache miss가 많이 발생해서 성능에 저하를 가지고 올 수 있다.

이럴 때 미리 DB에서 캐시로 데이터를 밀어넣어주는 작업을 할 수 있는데, 이를 cache warming이라고 한다.

쓰기 전략 : Write-Around, Write-Through


Write-Around 전략DB에만 데이터를 저장한다. 일단 모든 데이터는 DB에 저장되고 cache miss가 발생한 경우 캐시에 데이터를 적재한다. 이 경우 캐시 내의 데이터와 DB 내의 데이터가 다를 수 있다는 단점이 있다.

Write-Through 전략DB에 데이터를 저장할 때 캐시에도 함께 저장하는 방법이다. 캐시는 항상 최신 정보를 가지고있다는 장점이 있지만, 저장할 때 마다 두 단계의 step을 거쳐야 하기 때문에 상대적으로 느리다고 볼 수 있다. 또한 저장하는 데이터가 재사용되지 않을 수 도있는데 무조건 캐시에 넣어버리기 때문에 일종의 리소스 낭비라고도 볼 수 있다.

따라서 이렇게 데이터를 저장할 때는 몇 분, 혹은 몇 시간동안만 데이터를 보관하겠다는 의미인 expire time을 설정해주는 것이 바람직하다.

2. Redis의 데이터 타입 활용하기

redis는 자체적으로 많은 자료구조를 제공하고 있다.

특정 상황에서 어떻게 자료구조를 효율적으로 사용할 수 있는지 알아보자.

2-1. Best Practice - Counting

String

String의 Increment 함수를 사용하면 아주 간단하게 카운팅 할 수 있다.

위 예시를 살펴보자. score:a에 10이라는 값을 저장 한 후 INCR 함수를 사용하면 1이 증가한다. INCRBY 사용하면 직접 카운트할 개수를 지정해 그 값만큼 증가한다.

Bit

Bit를 이용하면 저장 공간을 굉장히 절약할 수 있다.

예를 들어, 우리 서비스의 오늘 접속한 유저 수를 세고 싶을 때 날짜 키 하나를 만들어 놓고 유저ID에 해당하는 bit를 1로 올려주면 된다. 한 개의 비트가 한 명을 의미하므로 천만 명의 유저는 천만 개의 비트로 표현할 수 잇고, 천만 개의 비트는 곧 1.2MB밖에 차지하지 않는다.

예제에서 처럼 SETBIT로 비트를 설정할 수 있고 BITCOUNT를 통해 1로 설정된 값을 모두 카운팅 할 수 있다.

하지만 이 방법을 이용하려면 모든 데이터를 정수로 표현할 수 있어야 한다.
즉, 유저ID 같은 값이 0이상의 정수 값일 때만 카운팅이 가능하며, 그런 sequential한 값이 없을 때에는 이 방법을 사용할 수 없다.

HyperLogLogs

마지막으로 알아볼 카운팅 방법은 HyperLogLogs를 사용하는 것이다. HyperLogLogs는 모든 String 데이터 값을 유니크하게 구분할 수 있다. HyperLogLogs는 set과 비슷하지만 저장되는 데이터 개수와 저장되는 값이 몇백만, 몇천만 건 이든 상관없이 모든 값이 12KB로 고정되어 있기 때문에 대량의 데이터를 카운팅 할 때는 훨씬 더 유용하게 사용된다.

예를 들어, 우리 웹사이트에 방문한 IP가 유니크한게 몇 개가 되는지 혹은 하루종일 크롤링한 URL의 개수가 몇 개인지, 우리 검색 엔진에서 검색된 유니크한 단어가 몇 개가 되는지 등등 유니크한 값을 계산할 때 아주 적절하게 사용할 수 있다.

PFADD 커맨드로 데이터를 저장하고 PFCOUNT 커맨드로 유니크하게 저장된 값을 조회할 수 있다.

만약 일별로 데이터를 저장했는데 일주일 치를 취합해서 보고싶다면 PFMERGE 커맨드로 키들을 머지해서 확인할 수 있다.

2-2. Best Practice - Messaging

Lists

Lists는 메세지 큐로 사용하는데 적합하다. 특히 자체적으로 blocking 기능을 제공하기 때문에 이를 적절히 사용하면 불필요한 polling 프로세스를 막을 수 있다.

클라이언트 A가 BRPOP 커맨드를 통해 myqueue에서 데이터를 꺼내오려하는데 현재 리스트 안에는 데이터가 없어 대기를 하고 있는 상황이다.

이 때 클라이언트 B가 hi라는 값을 넣어주면 클라이언트 A에서 바로 이 값을 확인할 수 있다.

또한, LPUSHX나 RPUSHX같은 커맨드를 사용하면 키가 존재할 때만 Lists에 데이터를 추가하는데 이 기능도 잘 사용하면 굉장히 유용하게 사용할 수 있다.
키가 이미 존재한다면 예전에 사용했던 큐라는 것이고, 사용했던 큐에만 메세지를 넣어줄 수 있기 때문에 비효율적인 데이터의 이동을 막을 수 있다.

인스타그램, 페이스북, 트위터 등과 같은 SNS에는 각 유저별로 타임라인이 존재하고 그 타임라인에 내가 팔로우한 사람들의 데이터가 보여지게 된다. 트위터에서는 각 유저의 타임라인에 보일 트윗을 캐싱하기위해 redis의 Lists를 사용하는데 이 때, RPUSHX 커맨드를 사용하여 트위터를 자주 이용하던 유저의 타임라인에만 새로운 데이터를 미리 캐시해 놓을 수 있다. 자주 사용하지 않는 유저는 캐싱 키 자체가 존재하지 않기 때문에 자주 사용하지 않는 유저를 위해 데이터를 미리 쌓아놓는 비효율적인 작업을 방지할 수 있다.

Stream

stream은 로그를 저장하는 가장 적절한 자료구조라고 볼 수 있다. 실제 서버에 로그가 쌓이는 것처럼 모든 데이터는 append-only 방식으로 저장되며 중간에 데이터가 변경되지 않는다.

예제에서는 XADD 커맨드를 이용해 mystream이라는 키에 데이터를 저장한다. 키 이름 옆에 *는 ID 값을 의미하며 ID 값을 직접 저장할 수도 있지만 일반적으로 *로 입력하면 redis가 알아서 저장하고 ID 값을 반환해준다.
반환되는 ID 값은 데이터가 저장된 시간을 의미한다. 이 값 뒤로는 hash처럼 key-value 쌍으로 데이터가 저장되는데, 예제에서는 sensor-id 값에 1234를 온도에는 19.8을 저장한 것을 의미한다.

3. Redis에서 데이터를 영구적으로 저장하려면?

redis는 In-memory 데이터 스토어이다. 모든 데이터가 메모리에 저장되어 있기 때문에 서버나 redis 인스턴스가 재시작되면 모든 데이터는 유실된다.

복제 구조를 사용하고 있더라도 데이터 유실에 있어 안전하다고 볼 수는 없다.
코드상의 버그가 있거나 혹은 휴먼 에러로 데이터를 날린 경우에는 바로 복제본에도 똑같이 적용되기 때문에 이런 경우에는 데이터를 복원할 수 없다.

따라서 redis를 캐시 이외의 용도로 사용한다면 적절한 데이터의 백업이 필요하다.

Redis Persistence Option

redis에서는 데이터를 영구적으로 저장하는 두 가지 방법을 제공한다.

  • AOF : 데이터를 변경하는 커맨드가 들어오면 커맨드를 그대로 모두 저장한다.
  • RDB : RDB는 스냅샷 방식으로 동작하기 때문에 저장 당시 메모리에 있는 데이터 그대로 파일로 저장한다.

예제를 살펴보면 AOF에는 key1에 a가 저장되었다가 apple로 변경되었고, key2가 들어왔다가 삭제된 기록 모두 남아있다. 하지만 RDB에는 key1이 apple인 값만 남아있다.

AOF 파일은 Append Only하게 동작하기 때문에 데이터가 추가되기만 해서 대부분 RDB 파일보다 커지게 되기 때문에, AOF 파일은 주기적으로 압축해서 재작성되는 과정을 거쳐야 한다.

자동 / 수동 파일 저장 방법

RDB와 AOF 파일 모두 커맨드를 이용해 직접 파일을 생성할 수 있으며, 원하는 시점에 파일이 자동 생성되도록 설정할 수도 있다.

  • AOF - Append Only File
    • 데이터 변경 커맨드까지 모두 저장한다.
    • 자동 저장시 : redis.conf 파일에서 auto-aof-rewrite-percentage 옵션으로 크기 기준으로 저장 가능
    • 수동 저장시: BGREWRITEAOF 커맨드를 이용해 CLI창에서 수동으로 AOF 파일 재작성이 가능하다
  • RDB - snapshot
    • 저장 당시 메모리에 있던 데이터들만 그대로 저장
    • 자동 저장시 : redis.conf 파일에서 SAVE 옵션으로 시간 기준으로 저장할 수 있다.
    • 수동 저장시 : BGSAVE 커맨드를 이용해 cli 창에서 수동으로 RDB파일을 저장할 수 있다.

RDB vs AOF 선택 기준

  • 백업은 필요하지만 어느 정도의 데이터 손실이 발생해도 괜찮은 경우
    • RDB 단독 사용
    • redis.conf 파일에서 SAVE 옵션을 적절히 사용
    • 예) SAVE 900 1 : 900초 동안 한 개 이상의 키가 변경되었을 때 RDB 파일을 재 작성하라는 의미.
  • 장애 상황 직전까지의 모든 데이터가 보장되어야 할 경우
    • AOF 사용(appendonly yes)
    • APPENDFSYNC 옵션이 everysec인 경우 최대 1초 사이의 데이터 유실 가능 (기본 설정)
  • 제일 강력한 내구성이 필요한 경우
    • RDB & AOF 동시에 사용하라고 redis 공식 문서에 가이드 되어 있다.

Redis의 아키텍처 선택 노하우 (Replication vs Sentinel vs Cluster)

redis의 아키텍처는 크게 3가지로 나눌 수 있다.

Replication

단순히 복제만 연결된 상태를 말한다.

  • replicaof 커맨드를 이용해 간단하게 복제 연결
  • 비동기식 복제
  • HA 기능이 없으므로 장애 상황 시 수동 복구
    • replicaof no one
    • 애플리케이션에서 연결 정보 변경

Sentinel

자동 페일오버 가능한 HA 구성 (High Availability)

  • sentinel 노드가 다른 노드 감시
  • 마스터가 비정상 상태일 때 자동으로 페일 오버
  • 연결 정보 변경 필요 없음
  • sentinel 노드는 항상 3대 이상의 홀수로 존재해야 함
    • 과반수 이상의 sentinel이 동의해야 페일오버 진행

Cluster

스케일 아웃과 HA 구성 (High Availability)

  • 키를 여러 노드에 자동으로 분할해서 저장 (샤딩)
  • 모든 노드가 서로를 감시하며, 마스터가 비정상 상태일 때 자동 페일오버
  • 최소 3대의 마스터 노드 필요

아키텍처 선택 기준

5. Redis 운영 Tip과 장애 포인트

사용하면 안되는 커맨드

redis는 싱글 스레드로 동작하기 때문에 한 사용자가 오래 걸리는 커맨드를 실행한다면 나머지 모든 요청들은 수행하지 못하고 대기하게 된다. 이로 인한 장애도 빈번하게 발생한다.

  • keys * -> scan으로 대체
    • keys는 모든 키를 보여주는 커맨드인데 주로 개발할 때 자주 사용하다 운영환경에서 실수로 손이 먼저 나간다. scan을 사용하면 재귀적으로 키들을 호출할 수 있다.
  • Hash나 Sorted Set 등 자료구조
    • Hash나 Sorted Set 같은 자료구조는 내부에 여러 개의 아이템을 저장할 수 있는데, 키 내부에 아이템이 많아질 수록 성능이 저하된다. 만약 좋은 성능을 원한다면 하나의 키에 최대 백만개 이상은 저장하지 않도록 키를 적절히 나누는 것이 좋다.
  • del -> unlink
    • 키에 많은 데이터가 들어있을 때 del로 지우면 key를 지우는동안은 아무런 동작을 할 수 없다. 이때, unlink를 사용하면 백그라운드로 지워준다.

변경하면 장애를 막을 수 있는 기본 설정 값

  • STOP-WRITE-ON-BGSAVE-ERROR = NO
    해당 옵션의 기본 설정값은 YES이다. 의미는 RDB 파일이 정상적으로 작동하지 않았을 때 redis로 들어오는 모든 모든 WRITE를 차단하는 기능이다. 만약 redis 서버에 대한 모니터링을 적절히 하고 있다면 이 기능은 꺼두는게 불필요한 장애를 막을 수 있는 방법이다.
  • MAXMEMORY-POLICY = ALLKEYS-LRU
    해당 기본 설정에 대한 설명을 하기 앞서 중요한 얘기를 하자면 redis를 캐시로 사용할 때는 키에 대한 expire time 설정을 꼭 해야한다. 메모리는 한정되어 있기 때문에 expire time을 설정하지 않는다면 데이터가 금세 MaxMemory까지 가득차버리게 된다. 데이터가 가득 차게 되면 MAXMEMORY-POLICY 정책에 의해 데이터가 삭제된다.
    • NOEVICTION : 기본값. 메모리가 가득 차면 더 이상 레디스는 새로운 키를 저장하지 않는다. 새로운 데이터를 입력하는게 불가능하기 때문에 이는 장애 상황으로 발생할 수 있다.
    • VALATILE-LRU : 가장 최근에 사용하지 않았던 키부터 삭제한다. 이 때 expire 설정이 있는 key값만 삭제한다. (만약 메모리에 expire 설정이 없는 key만 남아있다면 위와 같은 장애가 발생할 수 있다.)
    • ALLKEYS-LRU : 모든 키에 대해 LRU 방식으로 키를 삭제한다. 이 설정에서는 적어도 데이터가 가득 참으로 인한 장애가 발생할 가능성은 없다.

Cache Stampede

대규모 트래픽 환경에서 TTL(저장된 데이터의 만료시간)값을 너무 작게 설정한 경우 cache stampede 현상이 발생할 가능성이 존재한다. 1장에서 봤던 look-aside 패턴에서는 redis의 데이터가 없다는 응답을 받은 서버가 직접 DB로 데이터를 요청한 뒤 다시 redis에 저장하는 과정을 거친다. 하지만 키가 만료되는 순간 많은 서버에서 이 키를 같이 보고 있었다면? 모든 애플리케이션 서버들이 DB에 가서 같은 데이터를 찾게되는 duplicate read가 발생한다. 또 읽어온 값을 redis에 각각 write하는 duplicate write도 발생하게 된다. 이러한 상황을 Cache Stampede라고 한다. 이는 굉장히 비효율적인 상황이고, 이런 상황이 발생하면 처리량도 느려질 뿐 아니라 불필요한 작업들이 늘어나 장애로까지 이어질 수 있다.

MAXMEMORY 설정 시 주의할 점

redis의 데이터를 파일로 저장할 때 포크를 통해 자식 프로세스를 향상한다. 자식 프로세스로 백그라운드에서는 데이터를 파일로 저장하고 있지만, 원래 프로세스는 계속해서 일반적인 요청을 받아 데이터를 처리한다. 이게 가능한 이유는 copy on write라는 기능으로 메모리를 복사해서 사용하기 때문이다. 이로 인해 서버의 메모리 사용률은 2배로 증가하는 상황이 발생할 수 있다. 만약에 데이터를 영구 저장하지 않는다고 해도 복제 기능을 사용하고 있다면 주의해야 한다. 복제 연결을 처음 시도하거나, 혹은 연결이 끊겨 재시도를 할 때에 새로 RDB 파일을 저장하는 과정을 거치기 때문이다. 따라서 이런 경우에는 MAXMEMORY 값은 실제 메모리의 절반 정도로 설정해 주는 것이 바람직하다. 예상치 못한 상황에 메모리가 가득차서 장애가 발생할 가능성이 매우 높기 때문이다.

모니터링시 유의해야 할 점

redis는 메모리를 사용하는 저장소이기 때문에 메모리 관리가 운영에서 제일 중요하다. 모니터링할 때도 유의해야할 점이 있는데 모니터링할 때 used_memory값이 아닌 user_memory_rss값을 보는게 더 중요하다. used_memory는 논리적으로 레디스에 얼만큼의 데이터가 저장되어 있는 지를 나타내고 used_memory_rss는 OS가 실제로 redis에 얼만큼의 메모리를 할당했는지를 보여준다. 따라서 실제 저장되어 있는 값은 적은데 rss 값은 큰 상황이 발생할 수 있고, 이 차이가 클 때 fragmentation이 크다고 할 수 있다. 주로 삭제되는 키가 많을 때 fragmentation이 증가한다. 예를 들어, 특정 시점에 키가 피크를 치고 다시 삭제되는 경우 혹은 TTL로 인해 삭제가 과도하게 많을 경우에 발생한다.

위 그래프는 피크를 친 시점에 그래프인데 초록색 used 그래프는 확 내려간 반면 노란색 rss 그래프는 아직 많이 차있는 것을 볼 수 있다.
이때 activefrag 라는 기능을 잠시 켜두면 도움이 된다. 실제 공식 문서에서도 이 값을 항상 켜두는 것보다는 단편화가 많이 발생했을 때 켜두는 것을 권장하고 있다.

[ref] https://www.youtube.com/watch?v=92NizoBL4uA

profile
⋆。゚★⋆⁺₊⋆ ゚☾ ゚。⋆ ☁︎。₊⋆

0개의 댓글