본문 바로가기
Database

[Redis - 실시간 랭킹 가이드 01] 캐시와 레디스

by !!e!! 2022. 2. 16.

 

레디스는 대표적인 인메모리 데이터 저장소로 빠르고 편리한 도구입니다. 또한 매우 쉽기 때문에 당장 학습하고 사용하는 것도 어렵지 않습니다. 하지만 실무에서 레디스를 좋은 도구로 활용하기 위해서는 레디스를 깊이 있게 이해하는 것이 중요합니다.

 

레디스에 대한 좋은 세미나와 글은 많지만 머리로만 이해하다보니 내것이 아닌 느낌이 들때가 많습니다. 그래서 실무에서 레디스를 사용하고 학습한 내용들을 토대로 레디스에 대해 깊게 알아보고 간단한 실시간 랭킹을 구현해보는 시리즈를 만들어 보려합니다. 레디스를 깊이 있게 이해하고 활용할 수 있는데 도움이 되기를 바라며 시작해보도록 하겠습니다!

 

 

 

레디스가 무엇인지 설명하기 좋은 장면인것 같아서 직접 만들어본 짤입니다. 이런거 대체 누가 만드나 했는데 그게 저일줄은..ㅎㅎ

 

 

캐시의 배경

 

백엔드 개발자로서 서비스 개발과 운영을 하다보면 서비스 내에서 가장 병목이 많은 지점이 DB 라는 것을 경험적으로 알 수 있습니다. 실제로 대규모 서비스 트래픽의 병목 원인 중 80% 이상이 DB 관련 latency라고 합니다.

그렇다면 왜 유독 DB에서 병목이 많이 발생하는 것일까요?

결론부터 말씀드리자면 DB는 데이터를 영속하기 위해 하드 디스크(HDD) 혹은 SSD에 접근하기 때문입니다.

 

 

컴퓨터 세계에서는 정직하게도 용량이 크고 저렴한것들은 느리고, 용량이 작고 비싼것들은 빠른 특징을 보입니다. 위 그림상으로 디스크와 메모리는 한 간격 차이이지만 성능에서는 매우 큰 차이를 보입니다. 동일 조건에서 MySQL과 레디스의 성능을 비교한 결과를 보면 2배정도 차이나게 됩니다. (MySQL - 메모리 + 디스크, Redis - 메모리)

 

 

데이터를 저장한 장소가 디스크인지 메모리인지 만으로도 이런 차이를 보인다면, 애플리케이션(코어)과 DB(디스크) 간의 속도차는 당연히 발생할 수 밖에 없습니다. 그나마 다행이라면 애플리케이션은 너무 바빠서 DB랑만 어울리지는 않는다는 것입니다.

 

DB가 너무 느리다고 탓 할 수 만은 없습니다. DB의 본래 목적은 저렴한 비용으로 데이터를 영속하는 것이기 때문입니다. 그래서 DB의 본래 목적을 해치지 않으면서도 성능 향상을 기대할 수 있는 방법들을 고민하게 됩니다.

 

최소한의 변경으로 큰 성능 향상을 기대하기 위해서는 가장 많이 문제가 되는 부분을 개선하는 것이 합리적일 것입니다. 대부분의 서비스는 데이터를 추가, 수정, 삭제하는 것보다 데이터를 조회하는 비율이 압도적으로 높습니다. 그렇다 보니 DB에서도 조회 성능을 개선하는 것이 전체 서비스 성능 개선에 유의미한 결과를 가져다 줄 가능성이 높습니다. 조회 성능을 개선할 수있는 방법에도 여러가지가 있습니다.

 

 

첫번째는 DB의 인덱스를 활용하는 것입니다.

인덱스는 대표적인 DB 조회 성능 개선 방법입니다. 인덱스를 설명할때 책의 목차를 많이 이야기하는데 실제로 인덱스의 사전적 의미와 원리가 목차와 동일하기 때문입니다. 인덱스는 별도의 공간에 할당되어 추가, 수정, 삭제 시에 함께 수정되기 때문에 저장된 데이터가 많고 인덱스의 개수가 적정해야 그 효용을 누릴 수 있습니다. 수행이 느린 쿼리를 찾아 인덱스를 추가하거나 인덱스가 걸려있는 쪽으로 조인 순서를 변경하여 쿼리 튜닝을 해볼 수 있습니다. 하지만 아무리 인덱스를 활용하여 빠른 색인을 하더라도 매번 동일한 요청에 대한 디스크 접근은 해결할 수 없습니다.

 

두번째 조회 성능 개선 방법은 이번 글의 주제이기도 한 캐시입니다.

실제 대규모 서비스에서 동일한 api를 유사 시간내에 호출하는 경우는 50% 이상이라고 합니다. 동일한 리소스 접근을 매번 수행이 느린 디스크를 타고 접근하는 것 보다 수행이 빠른 하드웨어에 로드해 놓고 제공한다면 latency 뿐만 아니라 DB의 부하도 줄일 수 있게 됩니다.

서비스 마다 조금씩 편차는 있지만 많은 경우 캐시를 통해 latency를 크게 줄일 수 있습니다. 앞에서 캐시를 조회 성능 개선 방법이라고 소개했지만 캐싱 전략에 따라 쓰기 latency와 DB 부하도 크게 줄일 수 있습니다.

 

 

HDD / SSD 에 저장하지 않고 빠르게 ACID를 만족할 방법은 없을까요? 아쉽게도 지속성을 만족하기 위해서는 필연적으로 디스크를 거쳐야합니다. 따라서 latency를 줄이기 위해서는 얼마나 디스크 접근을 최소화하느냐가 관건입니다. 실제 대부분의 DB는 내부적으로 접근 빈도가 높은 테이블 혹은 데이터를 메모리에 올려 놓는 등의 최적화를 진행합니다. 특히 빠르다고 알려진 Aurora, DynamoDB 과 같은 경우 정확한 메커니즘은 알 수 없지만 빠른 만큼 SSD를 사용하거나 디스크 접근을 최소화하기 위한 알고리즘, idle time에 디스크에 접근하는 방식 등을 사용하게 됩니다. 하지만 디스크 보다 빠른 하드웨어에 데이터를 적재하는 시간이 길어질 수록 비용은 증가하게 됩니다. 결국 latency와 비용 사이에는 Trade off가 존재합니다. 잘 생각해보면 모든 데이터를 빠르게 제공할 필요는 없습니다. 개발자로서 한가지 기억해야할것이 있다면 접근이 잦은 데이터는 비싼 비용을 지불하더라도 latency가 최소화되는 곳에 저장하고, 반대로 접근 빈도가 낮은 데이터는 latency가 커지더라도 비용이 최소화되는 곳에 저장한다는 것입니다.

 

 

캐시와 레디스

 

레디스를 캐시 용도로 많이 사용하기는 하지만 정확히 말하면 레디스는 캐시의 역할만 하지는 않습니다. 그럼에도 저는 개인적으로 레디스의 정체성을 캐시로 보는것이 적합하다고 생각합니다. (그 이유는 밑에서 말씀드리겠습니다)

우선 위키백과의 캐시 정의를 보면 '데이터나 값을 미리 복사해 놓는 임시장소로 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다' 고 되어 있습니다. 정의만 보면 시간을 절약하기 위한 데이터는 모두 캐싱 가능하다고 생각할 수도 있습니다. 하지만 실무에서 캐싱하는 데이터는 암묵적으로 몇가지의 의미가 더 내포되어 있습니다.

 

 

캐시 데이터의 암묵적 의미

 

반복적으로 동일한 결과를 반환하는 데이터

 

외부 캐시의 경우 네트워크 비용도 고려해야하기 때문에 캐시 Hit 하는 것이 유의미합니다. 어차피 매번 바뀌는 데이터를 캐싱한다면 되려 캐시를 조회하고 적재하는 네트워크 비용만 추가되는 상황이 발생합니다.

 

유실될 가능성이 있는 데이터

 

실무에서 캐싱되는 데이터는 유실될 가능성이 있다는 것을 항상 염두해 두어야 합니다. 왜냐하면 개발자가 캐시하는 데이터 적재 장소가 메모리이기 때문입니다. 메모리는 속도가 빠르지만 전원이 꺼지면 휘발되는 특징이 있습니다. 그래서 앞선 캐시의 정의에서도 유실 가능성을 어느정도 내포한 '임시장소'라는 표현이 있는 것을 확인할 수 있습니다.

 

학부시절 열심히 공부하신 분들 중에서는 비휘발성 메모리도 있다는 것을 아실겁니다. 하지만 비휘발성 메모리는 휘발성 메모리보다 속도가 느리기 때문에 이상적인 메모리의 역할을 하지 못합니다. (DRAM vs NAND Flash)

 

유실될 가능성을 염두한다는 것은 캐싱이 안 되는 상황이 대비 되어 있어야 한다는 의미입니다. 즉, 캐시에 데이터가 없더라도 백업로직을 통해 문제가 없어야 합니다. 또한 캐시가 동작하지 않을때 DB, 외부요청 등 데이터를 가져오거나 가공하는 로직에 트래픽 급격히 몰릴 수 있다는 것까지도 고려해야합니다.

 

저의 경우 레디스 서버의 메모리가 부족해 DB가 먹통이었던적이 있습니다. 앞서 이야기한것 처럼 캐시가 제대로 동작하지 않는 상황이 발생하자 모든 트래픽이 DB로 몰리면서 발생한 문제라고 할 수 있습니다.

 

이러한 장애 상황에서 할 수있는 대응은 우선 사용하지 않는 데이터를 삭제하는것 입니다. 하지만 레디스는 삭제뿐만 아니라 운영과정에서 사용하면 안되는 명령어가 있기 때문에 유의해야합니다. 데이터 삭제 이후에는 메모리 부족을 야기한 원인을 찾아 잘못된 레디스 설정을 수정하거나 메모리 용량을 증설하여 장애 가능성을 줄일 수 있습니다. 레디스 운영과 관련된 내용은 추후 더 자세히 알아보도록 하겠습니다.

 

 

레디스의 정체성

 

경쟁이 치열한 치킨 시장에서 치킨과 떡볶이를 조합해 먹는 수요가 늘고있다보니 최근 교촌치킨에서도 떡볶이를 판매하고 있습니다. 그렇다면 교촌 치킨에서 떡볶이를 판다고 해서 엽기떡볶이, 신전떡볶이 같은 분식집의 수요를 모두 가져올 수 있을까요? 치킨 가게에서 떡볶이를 판다고 떡볶이 가게라고 할 수 있을까요?

 

 

레디스 공식 홈페이지를 보면 첫 줄에서 스스로를 데이터베이스, 캐시 및 메시지 브로커로 사용되는 오픈소스 인메모리 데이터 구조 저장소라고 소개합니다. 레디스 역시 치열한 DB, NoSQL 시장에서 살아남기 위해 초기의 key-value 스토어에서 다양하게 변화해왔습니다. 그 과정에서 레디스는 기존 RDB(관계형 데이터베이스)와 다른 NoSQL에 비해 데이터 영속에 대한 안정성이 떨어진다는 약점을 보완하기 위해 몇가지 Persistence 옵션을 제공하기 시작했습니다.

 

 

RDB (Redis Database) 영속 방식

 

특정시점(snapshot)의 메모리에 있는 데이터 전체를 바이너리 파일로 저장.

 

AOF (Append Only File) 영속 방식

 

서버가 수신한 모든 쓰기 작업 로그를 기록하고 서버 시작 시 로그를 기반으로 원본 데이터 세트를 재구성. 로그가 너무 커지면 백그라운드에서 다시 쓸 수 있음.

 

 

Persistence 옵션이 제공되던 초기에 레디스를 Persistent Store로 사용하며 백업로직을 고려하지 않기도 하고 RDB(관계형 데이터베이스)를 대신해서 사용하는 경우도 있었다고 합니다. 하지만 기대보다 성능이 좋지 않았고 장애도 자주 발생하게 되었습니다. 분명 메모리를 사용하는 레디스인데 왜 Persistence 옵션을 사용하면 성능 저하와 장애가 발생하는 것일까요?

 

우선 레디스 장애의 대부분은 메모리 부족에서 발생합니다.

Persistence 옵션이 문제가 되는 이유는 데이터를 영속하는 과정에서의 성능 저하 뿐만 아니라 CoW(Copy on Write)를 유발해 메모리 부족을 일으키기 때문입니다.

 

Copy on Write (CoW)

 

OS(리눅스)에서 새로운 프로세스를 생성하기 위해서는 프로세스 스스로를 복제(fork)후 그 프로세스를 덮어쓰게됩니다. 우리가 사용하는 컴퓨터도 실행 시점에 최초의 프로세스 한개가 fork를 통해 무수히 많은 프로세스로 분화된것 입니다.

 

 

복제된 프로세스를 자식 프로세스라고 하는데 자식 프로세스는 기본적으로 부모 프로세스의 메모리 공간을 공유하게 됩니다. 그런데 자식 프로세스가 생성된 시점에 부모 프로세스에 변경이 생기면 자식 프로세스와 공간을 공유할 수 없게 됩니다. 그래서 부모 프로세스는 공유할 수 없는 데이터를 메모리의 다른 공간에 복사하고 이를 수정하게 됩니다. 이것을 Copy on Write 라고 합니다.

 

 

레디스 서버의 메모리 용량이 16GB 이고 실 데이터가 8GB 적재되어있다고 가정해보겠습니다.

최악의 경우 레디스에 적재된 데이터에 전체에 대해 CoW가 발생한다고 하면 8GB + 8GB 로 메모리 용량이 가득차게 됩니다. 심지어 이 계산에는 메모리 단편화와 레디스 운영에 필요한 메모리 점유를 고려하지 않았기 때문에 결국 Swap이 발생하게 됩니다.

 

이러한 특징 때문에 안정적인 레디스 운영을 위해서는 메모리를 여유롭게 사용하는 것이 중요합니다. 정말 안정적인 운영을 원한다면 메모리 공간의 50% 이상을 사용하지 않는 것이 좋습니다. 또한, 큰 메모리의 인스턴스 하나보다는 작은 메모리의 인스턴스 여러개가 더 안전하게 됩니다.

 

 

레디스 CoW 유발하는 요인들

 

  • Persistence의 RDB(Redis Database) 옵션 관련
    • redis.conf 의 save 파라미터 (default: save 60 10000)
    • BGSAVE 명령어
  • Persistence의 AOF 옵션 관련
    • redis.conf 의 auto-aof-rewrite-percentage 파라미터 (default: auto-aof-rewrite-percentage 100)
    • BGREWRITEAOF 명령어
  • 리플리케이션 신규 노드 연결시 RDB 파일 생성

 

레디스 CoW 대응 방법

 

  • RDB, AOF 옵션 자체를 사용하지 않는다 (특히 마스터는 절대! 꼭 사용해야 한다면 리플리케이션에만)
  • 리플리케이션 시 새 슬레이브 연결은 부하가 적은때에 진행한다
  • 기존 슬레이브가 fail over 하는 과정에서 전체 데이터를 복제(Full resync, RDB 파일 생성)하는 것은 어쩔 수 없다

 

위의 내용을 요약하자면 아래와 같습니다.

 

  • 레디스는 데이터 영속을 위한 Persistence 옵션을 제공한다
  • 레디스 Persistence 옵션은 CoW(Copy on Write)를 유발한다
  • CoW는 메모리 부족을 야기하고 Swap이 발생할 수 있다
  • 레디스 Persistence 옵션 → 성능 저하와 장애 유발

 

 

결론

 

앞서서 제가 레디스의 정체성을 캐시로 봐야한다고 이야기했었습니다.

레디스를 데이터 영속을 위한 Persistent Store, 즉 DB로 생각하고 사용한다면 유의해야할 것들이 너무 많아지게 됩니다.

또한 원활한 사용을 위해 정기적인 마이그레이션을 진행하는 등 추가적인 운영비용이 발생합니다.

 

교촌치킨에서 떡볶이를 출시했다고 해서 떡볶이 가게로 볼 수 없는것 처럼

레디스도 본래의 태생이 캐시인 만큼 Persistence 옵션은 옵션일뿐 캐시로 사용하는 것이 바람직하다고 생각합니다.

AWS도 레디스의 정체성을 캐시로 보는 걸까요? 고심한 이름일텐데 ElastiCache 로 부르고 있네요ㅎㅎ

 

 

 

참고링크

 

MySQL Vs Redis Performance Experiment

[반도체 특강] 디램(DRAM)과 낸드플래시(NAND Flash)의 차이

redis Copy-on-Write

redis Persistence Introduction

redis Performance 레디스 성능

[우아한테크세미나] 191121 우아한레디스 by 강대명님