[Study] 기술 부채 - Redis

9 분 소요

Study : 내가 부족한 기술(Redis)에 대해 정리한다.

Redis

[우아한테크세미나] 우아한레디스 참고

  • 단일 쓰레드로 동작하며, GET/SET 같은 명령어는 초당 10만개 정도의 처리가능
  • Keys와 같은 처리가 오래걸리는 명령어 수행 시 기다리는 명령어들에 타임아웃 발생 가능
  • Redis 메모리 한계는 maxmemory 값으로 설정. 다 찬 경우 max memroy policy에 따라 추가 메모리 확보

mexmemory-policy 설정값

  • noeviction : 기존 데이터 삭제 X(꽉찬 경우 OOM 반환 후 새로운 데이터 버림)
  • allkeys-lru : LRU(Least Recently Used)라는 페이지 교체 알고리즘을 통해 데이터 삭제하여 공간 확보
  • volatile-lru : expire set을 가진 것 중 LRU로 삭제하여 메모리 공간 확보
  • allkeys-random : 랜덤으로 데이터 삭제
  • volatile-random : expire set 중 랜덤 삭제
  • volatile-ttl : expire set 가진 것 중 TTL이 짧은 것부터 삭제
  • allkeys-lfu : 가장 적게 엑세스한 키 제거
  • volatile-lfu : expire set을 가진 것 중 가장 적게 엑세스한 키 제거

Redis > INFO 명령어를 통해 삭제된 데이터 갯수 확인 가능(evicted_keys : 0)

Redis는 기본적으로 쓰기 작업을 할 때 COW(Copy On Write) 방식을 통해 작동한다.

COW는 쓰기 요청에 대해 OS로부터 fork()를 통해 자식 프로세스를 생성하여 다른 가상 메모리 주소를 할당받은 후 쓰기 작업을 진행한다. 때문에 쓰기 작업 시 가상으로 할당받기 위한 여분의 Memory 공간(free memory)이 필요하다.

Redis 설치 시 /proc/sys/vm/overcommit_memory값을 1로 설정하지 않아 장애가 발생할 때가 있음.

overcommit_memory = 0이면 OS는 주어진 메모리량보다 크게 할당할 수 없음. 즉, fork()시 OS가 충분한 메모리가 없다고 판단하기 때문에 에러가 발생

overcommit_memory = 1로 설정해서 OS한테 일단 over 해서 메모리 할당할 수 있도록 한 후 max memory에 도달한 경우 policy에 따라 처리하도록 설정하는 것이 좋다.

또한 redis memory 수치 중 used_memory_rss 값을 살펴볼 필요가 있음. RSS는 데이터를 포함한 레디스가 사용중인 메모리인데, 이 값이 실제 사용중인 used_memory 값보다 클 수 있음.

used_memory_rss
Redis 서버 프로세스가 실제 운영체제의 물리적 메모리에 사용하고 있는 총 RSS(Resident Set Size) 메모리를 나타냄
Redis가 실제 사용중인 메모리 양을 알려줌
$ redis-cli info memory 명령어로 확인 가능(결과값은 바이트 단위로 표시됨)

이런 현상의 이유는 OS가 메모리를 할당할 때 page 사이즈의 배수만큼 할당하기 때문. 예를들어 page size = 4096인데 요청 메모리 사이즈가 10이라고 하면 OS는 4096만큼 할당한다.

Page
운영체제에서 메모리 할당은 고정 크기의 블록단위로 이루어짐(4KB, 8KB, 16KB 등)
운영체제는 메모리를 페이지 단위로 관리하며 프로세스에 메모리를 할당할 때 페이지의 배수 단위로 할당함.
Redis가 3KB의 메모리를 요청한 경우에도 페이지에 따라 4KB의 페이지를 할당하게 됨(실제 사용중인 메모리가 아닌 점유중인 물리적 메모리 크기가 used_memory_rss에 표기됨)
메모리 단편화를 최소화 하고 효율적으로 메모리 관리를 가능하게 함.
OS에 따라 다르지만 일반적으로 4KB

이를 Fragmentation(파편화) 현상이라고 한다. 이것이 실제 사용한 메모리랑 할당된 메모리가 다른 원인이 된다.

Redis 쓰기 처리 방식

  1. 클라이언트 요청을 받음
  2. 메모리 버퍼링 사용
    • 쓰기 요청을 메모리 버퍼에 기록
      • 버퍼는 서버 메모리에 위치함
      • 쓰기 작업은 버퍼에 일시적으로 저장됨
      • 이를 통해 쓰기 작업을 효율적으로 처리할 수 있음
  3. 파일 시스템 캐시 사용
    • 메모리 버퍼에 기록한 데이터는 운영 체재의 파일 시스탬 캐시로 전달됨
    • OS는 파일 시스템에 대한 접근을 빠르게 하기 위해 파일 시스템 캐시를 사용
    • Redis의 데이터는 주로 디스크에 저장됨.
    • 쓰기 작업은 파일 시스템 캐시에 기록됨.
    • Redis는 디스크에 직접 접근하는 것보다 훨씬 빠르게 데이터를 기록할 수 있음.
  4. 백그라운드 저장 작업
    • 주기적으로 데이터를 디스크에 저장하기 위해 백그라운드 저장 작업을 수행
    • 디스크에 저장되지 않은 쓰기 작업을 처리
    • Redis의 지속성 보장
  5. 파일 동기화
    • 파일 동기화 작업을 통해 메모리 버퍼에 있는 쓰기 작업을 디스크로 영구적으로 저장
    • 이는 파일 시스템에 쓰기 작업을 수행하고 변경된 데이터를 디스크에 동기화 한다.
  6. 응답 전송
    • 쓰기 작업이 성공적으로 처리되었다는 응답을 보냄

파일 시스템 케시는 운영 체제에서 관리되는 메모리 영역으로 Redis 는 직접 파일 시스템 캐시에 접근하거나 제어하지 않음.

단지 Redis는 쓰기 작업을 메모리 버퍼에 저장하고 운영 체제가 알아서 파일 시스템 캐시를 활용해서 디스크에 쓰는 것임.

정리

  1. 쓰기 요청
  2. 메모리 버퍼에 쓰기 작업 기록
  3. 메모리 버퍼 -> 파일 시스탬 캐시
  4. 파일 시스탬 캐시 -> 디스크로 전달
  5. 디스크 내 영구 보관

Redis Replication

레디스 구성 방법 중 Read 분산과 데이터 이중화를 위한 Master/Slave 구조가 있음

Master 노드는 쓰기/읽기 전부 수행, Slave 는 읽기만 가능

이렇게 되면 Slave가 Master의 모든 데이터를 가지고 있어야 함.

이를 위한 과정이 Replication

Replication
마스터 데이터를 복제하여 Slave에 옮기는 작업

Sync 작업 과정

  1. Slave Configuration 쪽에 “replicaof " 설정하거나 REPLICAOF 명령어를 통해 마스터에 데이터 Sync 요청
  2. Master는 백그라운드에서 RDB파일(Redis Database 파일 약자, Redis의 데이터 스냅샷 형식 중 하나. 현재 메모리 상태를 담은 파일) 생성을 위한 프로세스를 진행.
    • 이때 마스터는 fork()를 통해 메모리를 복사 후 현재 메모리 정보를 디스크에 덤프 뜨는 작업 진행 (스냅샷 복제)
  3. 2번 작업과 동시에 마스터는 이후부터 들어오는 쓰기 명령들을 버퍼에 저장
    • AOF : Append-Only File, Redis에서 사용되는 데이터 변경 로그를 저장하는 메커니즘
      • Redis의 모든 쓰기 연산을 기록. Redis가 비정상 종료되는 경우에도 최근의 데이터 변경 사항을 손실 없이 복구 가능.
      • AOF가 꽉차면 자동으로 백그라운드에서 재생성함. 기존 AOF를 분석하여 최소한의 명령어로 압축하고 새로운 AOF 파일 생성하여 이전의 데이터 변경 로그를 대체
  4. 덤프작업이 완료되면 마스터는 슬레이브에 해당 RDB 파일을 전달하고, 슬레이브는 디스크에 저장후 메모리로 로드
  5. 3번에서 모아두었던 쓰기 명령들을 전부 슬레이브로 보냄(증분 복제)

-> 복제 과정은 비동기로 진행되며 스넵샷 복제 후 증분 복제를 수행한다.

위 과정에서 보았듯 Master가 fork 하는 부분에서 자신이 쓰고 있는 메모리 만큼이 추가로 필요해짐. 따라서 Replication 할 때 OOM이 발생하지 않도록 주의 필요.

Master 죽은 경우 Slave는 마스터를 잃고 Sync 에러를 냄. 이 상태에서는 쓰기는 불가능하고 읽기만 가능.

따라서, Slave를 Master로 승격시켜야 함. 매번 대응하기 어려우니 다양한 방법으로 이를 구현

예를들어, DNS 기반으로 failover 대응이 가능. 클라이언트는 마스터의 도메인을 계속 바라보고, 마스터 장애 발생하면 슬레이브에 마스터 DNS를 맵핑.

장애 감지 방식

  1. 장애감지 : 슬레이브는 주기적으로 마스터에 헬스 체크(ping)를 보냄. 헬스체크 응답을 받지 못하면 마스터 노드의 장애를 감지
  2. 슬레이브의 PROMOTE 요청 : 마스터 노드 장애 감지후 자신이 마스터 노드로 승격되고자 PROMOTE 요청을 보냄
  3. 다수의 슬레이브에서 PROMOTE 요청 수신 : 마스터 노드 장애를 감지한 슬레이브 노드는 PROMOTE 요청을 수신한 다른 슬레이브 노드와 의견을 조율. 이때 다수의 슬레이브가 동의하면 슬레이브 노드 중 하나를 새로운 마스터로 승격 시키는 결정을 내림
  4. 슬레이브 승격 : 결정된 슬레이브 노드는 자신을 새로운 마스터로 승격 시킴. 이 과정에서 슬레이브 노드는 자신의 데이터를 사용하여 승격된 마스터 노드로부터 누락된 데이터를 복제함
  5. 클라이언트 재연결 : 승격된 새로운 마스터 노드는 클라이언트에게 장애 발생 및 승격 사실을 알리고 클라이언트는 새로운 마스터 노드에 재연결하여 작업을 지속 함.

승격 요청 처리방식

  1. 슬레이브 노드의 PROMOTE 요청
  2. 다수의 슬레이브의 의견 수렴
    • PROMOTE 를 수신한 각 슬레이브 노드는 해당 요청에 대한 의견을 내놓음.
    • 마스터 노드의 상태와 자신의 상태를 비교하고 마스터로 승격될 가능성에 대해 판단을 내림
  3. 의견 조율
    • 다수의 슬레이브 노드가 각자 의견을 내놓은 후, Redis는 이들 의견을 조율하여 승격 여부를 결정
    • 다양한 요인에 의해 동의 여부가 결정됨.
      • ex) 다수의 슬레이브 노드가 마스터 노드의 응답을 받지 못하고 마스터의 장애를 감지하면 승격에 동의할 수 있음
  4. 승격
    • 결정된 슬레이브 노드가 마스터로 승격 됨

의견 조율 방식

  1. 투표 방식 : 슬레이브 노드는 각자의 의견을 표명하고 투표를 진행. 동의 or 거부와 같은 형태로 투표가 진행 됨.
  2. 마스터 노드 상태 확인 : 마스터 노드가 응답하지 않거나 장애로 판단되면 승격에 동의할 가능성이 높아짐
  3. 슬레이브 노드의 상태 확인 : 각 슬레이브 노드는 자신의 상태를 확인. 데이터 복제 진행중인지, 동기화가 완료된 상태인지 등을 고려
  4. 의견 조율 및 결정 : 다수결을 통해 의견을 조율하고 최종 결정을 내림. ex) 슬레이브 노드 중 과반 이상이 승격에 동의하면 승격 진행

동의 기준

  1. 복제 지연 시간 : 복제 지연 시간이 가장 낮은 슬레이브 노드가 선호될 수 있음
  2. 데이터 일관성 : 다른 슬레이브들과 데이터 일관성이 가장 잘된 노드 선호
  3. 네트워크 대역폭 : 네트워크 대역폭을 고려하여 성능 및 네트워크 부하를 최소화
  4. 하드웨어 및 용량 : 성능이 우수하고 충분한 용량을 가진 슬레이브 노드가 선호됨
  5. 운영 정책 : 조직의 운영 정책에 따라 결정. ex. 특정 노드에 우선순위 부여. 특정 지리적 영역에 있는 슬레이브 노드 선호 등

기타

Persistent Store로 사용 시 반드시 레디스 슬레이브에서 데이터 백업(migration) 작업(= 메모리 더 큰 곳으로 이동) 수행.

Redis Cluster

  • 레디스 클러스터는 failover를 위한 대표적인 구성방식 중 하나
  • 레디스 클러스터는 여러 노드가 Hash 기반의 slot을 나눠가지면서 클러스터를 구성하여 사용하는 방식
  • 전체 slot16384이며, hash 알고리즘CRC16을 사용
  • Key를 CRC16으로 해시 후 이를 16384로 나누면 해당 key가 저장될 slot이 결정 됨

구성

  • 클라이언트
    • Master1 - Slave
    • Master2(장애) - Slave(마스터 승격)
    • Master3 - Slave

클러스터를 구성하는 각 노드들은 마스터 노드로 자신만의 특정 slot range를 갖음

하나의 클러스터는 여러 마스터 노드로 구성되며 한 마스터 노드는 여러 슬레이브를 가지는 구조

특정 마스터 노드가 죽으면 해당 노드의 슬레이브 노드중 하나가 마스터로 승격

Tips

  • Maxclient 설정 50000 (높게 잡아야함)
    • 들어가서 확인하고 싶을 때 낮게 잡으면 꽉차서 못할때 있기 때문
  • RDB/AOF 설정 off
    • 성능, 안정상 유리
    • 마스터에서는 off, 필요시 slave에서 on
  • 특정 Commands disable
    • Kyes
    • AWS의 ElasticCache는 이미 하고 있음
  • 전체 장애의 90% 이상이 Keys와 Save(= ex) 1분안에 만개가 들어오면 dump 해) 설정을 해서 발생

자료구조

Sorted Set : O(log N)

레디스의 두 가지 데이터 구조 : zip list vs skip list

선택 기준

  1. 멤버수
    • 멤버 수가 128 이하 = zip
    • 멤버 수가 129 이상 = skip
  2. 값의 길이
    • 64byte 이하 = zip
    • 65byte 이상 = skip (여러 맴버 중 하나라도 65 이상이면 skip으로 변환)

두 조건은 OR 조건으로 하나만 만족해도 레디스가 바꿔버림

이는 redis.confADVANCDE CONFIG 파트에 있으며 변경할 수 있다.

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

레디스는 Sorted set 을 Zset 라고 부름

  • zip list : 메모리 절약에 최적화
  • skip list : 주로 해당 자료구조로 사용 됨

skip list

참고한 블로그 저자가 참고한 논문

  • 정렬 상태 유지하며 삽입, 삭제, 탐색 가능한 구조체
  • (정렬된 상태를 유지하는)링크드 리스트의 단점을 개선함

image

  • 위로갈수록 건너 뛰는 단위가 커지는 레벨이라는 개념이 존재
  • 레벨 단위로 비교하면서 연산 횟수를 줄임

이미 정해진 레벨 문제

그러나 추가 삭제가 일어나면 각 노드마다 연결된 레벨이 재배치 되는 문제가 있음.

동전 던지기 방식

위 문제를 해결하기 위해 각 노드마다 동전 던지기를 해서

  • 앞면이 나오면 해당 레벨에서 stop
  • 뒷면이 나오면 레벨 up 하고 다시 동전던지기 시도.(앞면 나올때까지)

이렇게 하면 확률에의해 각 노드들의 레벨이 결정되며 확률에 의거하여 레벨이 분배된다.

높은 레벨이 많이 나오는 것이 좋은 것이 아니다. 레벨이 높다는 것은 맵핑된 포인터 수가 많다는 것으로 이를 맵핑하기 위한 메모리 오버헤드가 발생하여 실제 저장되는 값에 비해 관리 비용이 더 많이 들게 된다.

이로써 독립적인 레벨 맵핑이 가능해졌으나, 고정 레벨일때와 달리 레벨을 통해 몇 번째 노드인지 확인할 수 없게 되었다.

ZRANK, ZRANGE 같은 명령어는 노드가 몇번째인지 바로 알아야 한다.

레디스는 이를 위해 레벨에 포인터와 순서를 가지는 필드를 만들어 저장하고 필드명을 SPAN이라고 한다. 이를 인덱스 스킵 리스트라고 함.

image

탐색 경로의 포인터 값을 더하면 해당 값의 위치를 얻을 수 있다. 값 20 -> 3 + 1 = 4번에 값 20이 위치

레벨마다 포인터 & 순서 저장으로 인한 오버헤드 줄이는 방법 : 주사위

레벨을 낮추면 많은 양의 데이터를 보관해야 하는 높은 레벨이 나올 확률이 줄어든다. 동전(1/2)에서 주사위(1/6)으로 낮추면서 높은 레벨이 나올 확률이 확 낮아진다. 그러나 그만큼 낮은 레벨이 많아 촘촘하기 때문에 탐색의 성능이 떨어지게 된다.

image

  • 논문에 나온 확률당 탐색 시간 및 평균 레벨

레디스에서는 1/4 체택

  • server.h
#define ZSKIPLIST_P   0.25       /* Skiplist P = 1/4 */

레디스에서의 Skip List

  • 최대 레벨 : 32
  • 스킵 리스트 자체를 저장하는 구조체에 최대 레벨, 노드 수(length), 헤더 노드, 마지막(tail) 노드의 포인터를 가지고 있음

image

  • 레디스 스킵 리스트 데이터 구조
    • zskiplist : 자체
    • zskiplistNode : 노드

image

레디스 셋, 해시, 리스트 모두 최대 멤버 수는 약 43억개. 그러나 Sorted Set은 나와있지 않음. (동일 레벨에 계속 쌓일 수 있기 때문이라는 것 같은데 이해가 안되었음)

제한이 있다면 길이 저장에 unsigned long을 사용했기에 1800경 만큼 저장 가능(사실상 메모리 제한만 있음)

메모리 오버헤드 cause by. Linux

  • 리눅스는 페이지 단위의 메모리 할당 방식을 사용
    • 확인 방법 : vmstat -m 또는 cat /proc/slabinfo, 페이지 사이즈 : getconf PAGESIZE = 4096(=4KB)
$ vmstat -m 

# Output
kmalloc-8192                 83     96   8192      4
kmalloc-4096                343    416   4096      8
kmalloc-2048                917    976   2048     16
kmalloc-1024              56282  63712   1024     16
kmalloc-512               42589  54064    512     16
kmalloc-256                4610   5680    256     16
kmalloc-192                5822   6237    192     21
kmalloc-128                7825  11424    128     32
kmalloc-96                12180  12180     96     42
kmalloc-64                44403  48960     64     64
kmalloc-32                28349  29312     32    128
kmalloc-16               107495 125184     16    256
kmalloc-8                 14336  14336      8    512

삭제 처리 부하는?

데이터 수가 많을 수록 오래걸리지만 감당가능한 수준이라고 함.(삭제 과정에서 노드를 해제하거나 메모리에서 삭제하는 free() 발생으로 인한 오버헤드 발생)

  • 삭제 절차
    • 삭제 대상 이전, 다음 노드 찾음
    • 둘을 연결하고 본인은 삭제 됨
    • 삭제된 노드를 메모리에서 해제(free())

결론

Sorted Set의 skip list의 경우 관리용 메모리 오버헤드리눅스 커널 메모리 할당 방식에 따른 오버헤드를 합쳐실제 값의 몇 배를 사용할 수 있다는 것을 인지하고 사용해야 한다.

Jedis vs Lettuce

결론, Lettuce를 쓰자.

  • 주요 특징
    • Lettuce
      • Netty 기반
      • 비동기 이벤트 기반 고성능 네트워크 프레임워크
      • spring-data-redis 포함되어있음
    • Jedis
      • 경량
      • 사용 쉬움
      • 동기 방식 클러스터 지원
  • 성능 비교
    • Jedis
      • 동기 방식으로 Connection Pool 설정해주어야 성능이 잘 나옴.
      • Redis CPU 사용량이 높음
    • Lettuce
      • 비동기 방식으로 커넥션을 많이 안잡음
      • Redis CPU 사용량이 낮음

댓글남기기