Redis에 알아보기 전에 Cache의 개념에 대해 정리하고 가자.
Cache
Cache란 나중에 요청할 결과를 미리 저장해둔 후 빠르게 서비스해주는 것을 의미한다. 즉, 미리 결과를 저장하고 나중에 요청이 오면 그 요청에 대해서 DB 또는 API를 참조하지 않고 Cache를 접근하여 요청을 처리하는 기법이다.
이러한 캐시가 나온 배경에는 파레토 법칙이 있다. 파레토 법칙이란 80%의 결과는 20%의 원인으로 인해 발생한다는 뜻이다.
즉, 캐시는 모든 결과를 캐싱할 필요가 없으며 서비스를 할 때 많이 사용되는 20%만 캐싱함으로써 전체적으로 효율을 끌어올릴 수 있다는 것이다.
이외에도 메모리 측면의 캐시 개념으로 속도가 느린 장치와 빠른 장치에서 속도 차이로 인한 병목 현상을 줄이기 위한 메모리이다.
실제로 RAM과 CPU는 연산속도 편차가 심하기 때문에 캐시 메모리를 중간에 두는 것으로 속도 차이를 해결하고 있다. 비슷한 논리로 CPU와 HDD/SSD 속도 차이가 매우 심하여 RAM을 캐시메모리라고 부르기도 한다.
Cache를 고려하게 되는 순간
서비스를 처음 운영할 때는 WEB - WAS - DB 정도로 작게 인프라를 구축해도 되지만 사용자가 늘어나 트래픽이 늘어나게 되면 점점 DB에 무리가 가기 시작한다.
DB는 데이터를 물리 디스크에 직접 쓰기 때문에 서버에 문제가 발생해도 데이터가 손실되지는 않지만, 매 트랜잭션마다 디스크에 접근해야하므로 부하가 많아지면 성능이 떨어지게 된다.
그래서 사용자가 늘어나면 DB를 스케일 인 또는 스케일 아웃하는 방식 외에도 캐시 서버를 검토하게 된다.
Cache 사용 구조
기본적인 캐시 사용 과정은 다음과 같다. (여기서 서버와 DB를 예로 들었지만 서버를 CPU, 데이터베이스를 메모리로 봐도 무방하다.)
- 클라이언트로부터 요청을 받는다.
- Cache와 작업을 한다.
- 실제 DB와 작업한다.
- 다시 Cache와 작업한다.
클라이언트가 웹 서버에 요청을 보내면, 웹 서버는 데이터를 DB에서 가져 오기 전에 캐시에 데이터가 있는 지 확인하고, 있다면 바로 클라이언트에게 저장된 데이터를 반환한다. 이를 Cache Hit라고 한다.
반대로 캐시에 데이터가 없으면 DB에 데이터를 요청하여 원하는 데이터를 조회한 후 그 데이터를 클라이언트에게 제공하는데, 이를 Cache Miss라고 한다.
위 과정에서 캐시를 어떻게 사용하느냐에 따라서 대표적인 캐시 전략인 look aside cache와 write back으로 나뉜다.
Look Aside Cache
- 캐시에 데이터 존재 유무 확인
2-1. 데이터가 있다면 캐시의 데이터 사용
3-1. 데이터가 없다면 캐시의 실제 DB 데이터 사용
3-2. DB에서 가져온 데이터를 캐시에 저장
Look Asdie Cache는 캐시 저장소에 데이터가 있으면, 캐시에서 가져오고, 없다면 메인 DB에서 값을 가져오는 대표적인 캐시 전략이다.
하지만 이 전략은 데이터에 수정이 일어날 시 동기화를 해줘야 하며 다음과 같은 방식을 사용한다. - 애플리케이션이 새로운 데이터 쓰기 혹은 업데이트할 때 캐시와 DB 모두에 같은 작업을 실행하는 방법
- 애플리케이션의 모든 쓰기 작업은 DB에만 적용되고, 기존의 캐시 데이터를 무효화시키는 방법
Write Back
- 모든 데이터를 캐시에 저장
- 캐시의 데이터를 일정 주기마다 DB에 한꺼번에 저장 - 배치
- DB에 저장한 데이터를 캐시에서 제거
write back은 주로 쓰기 작업이 굉장히 많아서, 쿼리를 일일이 날리지 않고 한꺼번에 배치 처리를 하기 위해 사용된다. 예를 들어 게시물을 클릭했을 때 조회수가 올라가는 서비스가 있을 때, 여러 사람이 동시에 게시물을 클릭하면 DB에 갑작스럽게 엄청난 쓰기 요청이 몰리게 되어 DB서버가 죽거나 조회수가 유실되는 문제가 있을 수 있다. 이때 write back 기반의 캐시를 적용하면 캐시 메모리에 데이터를 저장해 놓고, 이후 DB 디스크에 업데이트 해주면 안전하게 쓰기 작업을 해줄 수 있는 것이다.
DB에서 디스크를 접근하는 횟수가 줄어들기 때문에 성능 향상을 기대할 수 있지만, DB에 데이터를 저장하기 전에 캐시가 죽어버리면 데이터가 유실된다는 문제점이 있다.
필자도 역시 프로젝트를 진행하며 태그 조회시 Look Aside Cache 방식을 사용하였다.
그 이유는 쓰기 성능 개선보단 조회 성능 개선하기 위해서는 Look Aside Cache방식이 더 적합했기 때문이다.
동기화를 위해 태그가 생성되었을 때 해당 하는 유저의 태그 목록을 캐시에서 삭제하는 식으로 구현하였다.
Memcached
대표적인 In-Memory DB로써, 분산 메모리 캐싱 시스템이다. DB의 부하를 줄여 동적 웹 어플리케이션의 속도 개선을 위해 사용하며, DB나 API 호출 등으로부터 빧아오는 결과 데이터를 Key-Value 형태로 메모리에 저장한다.
장점
시스템의 사용되지 않는 일부 메모리를 활용할 수 있어 남는 자원을 효율적으로 사용해 성능을 향상 시킬 수 있다.
초장기 캐시 시스템은 [그림1]과 같이 각노드가 완전히 독립적으로 운영되어 데이터를 조회/저장시 어느 서버를 이용할 지 관리되어야 하며 용량의 제한으로 인해 비생산적이고 자원 낭비적인 시스템으로 구성되었다.
memcached는 이러한 제약사항의 해결을 위한 메커니즘을 제공 하는데 [그림2]와 같이 consistent hash 알고리즘을 사용하여 물리적인 별도의 캐시서버를 로직상 하나의 서버로 보고 사용할 수 있도록 한다. 즉, 개발자는 서버가 몇 대든 상관없이 한 개의 객체만을 활용하여 저장 및 조회할 수 있으므로 능률적이고 대용량의 캐시 시스템을 갖게 되는 것이다. 또한, 기업의 입장에서는 오래된 저사양 서버의 남는 메모리를 활용할 수 있기 때문에 비용면에서 효율적일 수 있다.
또한, Consistent hash로 인해 서버가 추가되거나 장애가 일어나더라도 추가되거나 장애가 발생한 캐시 서버의 데이터만 변동이 생길뿐 다른 서버의 변동은 생기지 않게 된다. (해시값이 자신보다 크면서 가까운 곳에 저장된다. 만약 자신보다 큰 값이 없다면 원으로 다시 돌아가서 제일 앞에 있는 값에 저장하게 된다.)
단점
인 메모리 기반의 시스템이므로 재부팅 시 데이터가 소멸하고, 이로 인해 영구적인 저장용 시스템으로 활용할 수 없다는 문제가 있다. 만약 영구 저장이 필요하다면 해당 데이터를 DB에 저장해 두고, 재부팅 시 DB로부터 데이터를 받아야 한다. (스냅샷 방식)
또한, 이러한 특징 때문에 메모리가 부족할 경우 일부 데이터를 삭제하여 메모리를 사용하게 된다.
특징
- 데이터 타입을 String만 지원한다.
- 멀티쓰레드를 지원한다.
- 캐시 용량은 Key는 250byte, Value는 1MB를 지원한다.
- 수평적 확장이 가능하다.
Redis
고성능 키-값 저장소로서 String, list, hash, set, sorted set 등의 자료 구조를 지원하는 In-Memory NoSQL이다. 성능은 Memecahced에 버금가면서 다양한 데이터 구조체를 지원함으로써 DB, Cache, Message Queue, Shared Memory 용도로 사용될 수 있다.
한편, Redis는 Remote Dictionary Server의 약자로 외부에서 사용 가능한 Key-Value 쌍의 해시 맵 형태의 서버라고 생각할 수 있따. 그래서 별도의 쿼리 없이 Key를 통해 빠르게 결과를 가져올 수 있다.
특징
- 영속성을 지원하는 In-Memory DB이다.
- 다양한 자료 구조를 지원한다.
- 싱글 스레드 방식으로 인해 연산을 원자적으로 수행 가능하다.
- 읽기 성능 증대를 위한 서버 측 리플리케이션을 지원한다.
- 쓰기 성능 증대를 위한 클라이언트 측 샤딩을 지원한다.
- 다양한 서비스에서 사용되며 검증된 기술이다.
- 캐시 용량은 Key와 Value 모두 512MB를 지원한다.
- Lua 익스텐션을 지원해서 로직을 만들어 적용시킬 수 있다.
- ACID를 유사하게 지원해서 트랜잭션을 걸 수 있다.
- Pub / Sub 메시징 용도로 사용할 수 있다.
Redis의 영속성
Redis는 영속성을 보장하기 위해 데이터를 디스크에 저장할 수 있다. 즉, 서버가 내려가더라도 디스크에 저장된 데이터를 읽어서 메모리에 로당하게 된다. 방식은 크게 두 가지가 있다.
- RDB(스냅샷) 방식
- 순간적으로 메모리에 있는 내용 전체를 디스크에 옮겨 담는 방식
- 반복적인 스냅샷을 통하여 디스크에 저장해 메모리의 여유 공간을 만들 수 있다.
- AOF(Append On File) 방식
- Redis의 모든 wirte/update 연산 자체를 모두 log 파일에 기록하는 형태
Redis 영속성의 문제점
하지만 Redis의 디스크 저장 기능이 사실 장애의 주된 원인이 되기도 한다. Redis의 경우 싱글스레드이기 때문에 RDB저장을 할려면 fork를 통해서 자신 프로세스를 생성한다. 즉, 자식 프로세스가 생성되면 현재 메모리 상태가 복제되므로, 이를 기반으로 데이터를 저장하는 것이다.
여기가 문제점인 것이다. 이전에는 운영체제가 자식 프로세스를 생성하면, 부모 프로세스의 메모리를 모두 자식 프로세스에 복사해야 했다. 예를 들어, 부모 프로세스가 10GB 메모리를 사용한다면 자식 프로세스를 생성할 때 10GB 메모리를 추가로 필요했다. 그러다 시간이 지나고 OS가 발전하며 COW(Copy On Write)라는 기술이 개발되었다. 이에 따라 fork 후, 자식 프로세스의 부모 프로세스의 메모리에서 실제로 변경이 발생한 부분만 차후에 복사하게 되었다.
하지만 Redis와 같은 솔루션을 사용하는 곳은 대부분 write가 많으므로 예전의 경우와 마찬가지로 메모리를 두배로 사용하는 경우가 생긴다.
- 최초에 fork로 자식 프로세스를 생성 시, 부모 프로세스와 자식 프로세스는 같은 메모리를 공유한다.
- 부모 프로세스에 write가 발생할 때마다, 공유하는 해당 데이터는 자식 프로세스에 복사된다.
- write작업이 많아져 부모 페이지에 있는 모든 페이지가 자식 페이지에 복사되면, 부모 프로세스와 자식 프로세스가 모두 같은 양의 메모리를 사용하게 되어 사용 메모리 양이 두배가 된다.
이렇게 메모리를 두 배를 사용하게 되면 메모리가 부족하게 되어 어떤 문제가 발생할 지 알 수 없게 된다. 그렇기 때문에 하나의 Redis 서버를 하나의 장치에서 사용하는 것보다는 멀티 코어를 활용하기 위해 여러 개의 Redis 서버를 한 서버에 띄우는 것이 좋다.
Redis의 컬렉션
Redis는 Memcached와 다르게 다양한 데이터 구조체를 지원한다. 이를 컬렉션이라고 부르며 아래와 같은 컬렉션을 지원하게 된다.
이렇게 다양한 자료 구조를 지원하게 되면서 개발의 편의성이 좋아지고 난이도가 낮아지는 장점이 있다.
예를 들어 랭킹 서버를 직접 구현한다고 가정했을 때 간단한 방법은 DB에 유저의 score를 저장하고 Score를 기준으로 Order by 정렬 후 읽어오는 것일 것이다. 하지만 이 방법은 개수가 많아지면 속도에 문제가 발생할 수 있다. 이 문제를 Redis의 Sorted Set을 사용하면 손쉽게 구현할 수 있게 된다.
https://comart.io/blog/realtime-ranking-with-redis-sorted-set
싱글 스레드를 사용하는 Redis
Redis의 자료구조는 싱글 스레드 특성상 기본적으로 원자적이기 때문에 Race Condition이 거의 발생하지 않는다. 예를 들어 유저의 친구리스트를 Key-Value 형태로 관리한다고 가정해보자.
123라는 유저가 존재하고 친구 Key는 friends:123이고 현재 친구 A가 존재한다. 해당 유저의 친구리스트에 B와 C를 추가하는 작업을 해보자.
각 트랜잭션이 순차적으로 실행될 경우 위와 같이 올바른 결과를 반환해줄 것이다.
하지만 각 트랜잭션이 동시에 실행된다면 어떤 결과를 반환할까?
두 트랜잭션이 동일한 최종 상태인 A를 자신의 메모리로 읽어 들이고, 그 상태에서 각자 B 또는 C를 추가하게 되면 최종 상태는 A, B 또는 A,C가 될 수 있다. 두 상태가 모두 나올 수 있는 이유는 컨텍스트 스위칭에 따라 두 트랜잭션 중 누가 먼저 끝날 지 모르기 때문이다. 물론 이러한 Race Condition을 해결하기 위해 격리 수준 등 여러 기법이 있지만, Redis는 싱글 스레드를 사용하므로 하나의 트랜잭션은 하나의 명령만 실행하게 되어 다수의 Race Condition을 해결할 수 있다. 하지만 모든이 아닌 다수라고 한 이유처럼 더블 클릭 이슈는 싱글 스레드만으로는 해결할 수 없다.
Redis VS Memcached
그렇다면 Redis와 Memcached 중 어떤 것을 사용해야할까?
편의성, 난이도(생산성)의 차이 : Collection
Redis를 통해서, 하나의 서버내에서만 공유했던 자료구조들에 대해, 자료구조를 지원하는 원격저장소로써 여러 서버에서 데이터를 효율적으로 공유하는 것이 가능하다. 이는 개발의 편의성과 난이도 측면에서, 많은 문제를 해결하여 개발시간을 단축시키기 때문에 비즈니스로직에 집중할 수 있는 것을 의미한다.
위에서 말했듯이 Redis는 대부분의 Race Condition을 피할 수 있고 다양한 컬렉션을 지원하기 때문에 Strings, Hash 를 통해 토큰 저장소로 사용하거나, 랭킹시스템을 구현할 때 Sorted Set을 사용하여 손쉽게 최적화되어 있는 시스템을 구현할 수 있다.
반면에 Memcached는 멀티 스레드 방식이므로 Race Condition에 대한 대책을 마련해야 하고 데이터 타입으로 String만 지원하기 때문에 Redis가 좀 더 편의성이나 난이도 측면에서 우수하다고 볼 수 있을 것 같다.
목적의 차이 : Persistent
위에 말했드이 Memcached와 다르게 Redis는 Persistent 기능을 제공한다.
이 때문에 Redis를 Key-Value 스토어로도 사용한다. 하지만 잘 모르고 사용하게 되면 운영중에 장애를 일으키는 원인이 되기도 한다.
그럼에도 불구하고, RDB와 AOF 두 방식을 적절히 함께 사용한다면, 장애가 발생하여 메모리의 데이터가 사라지더라도 복구할 수 있다.
결론
이외에도 Memcached에 비하여 Redis는 다양한 데이터 삭제 정책, 한 개의 키에 저장할 수 있는 크기 등에 이점이 있다. 반면, Memcached에 비해 응답속도가 균일하지 못하다는 단점이 있는데, 해당 문제는 Redis와 Memacached의 메모리 할당 구조가 다르기 때문에 발생하는 문제이다. Redis는 jemallo을 사용하는 데, 매번 malloc과 free를 통해서 메모리 할당이 이루어진다. 반면 Memcahced는 slab 할당자를 이용하여 내부적으로는 메모리 할당을 하지 않고 관리하는 형태를 취한다. 이로 인해 Redis는 메모리 프래그멘테이션 등이 발생하여 이 할당 비용 때문에 응답 속도가 느려지게 된다.다만 이는 극닥적으로 봤을 때 발생하는 일 치명적인 문제는 아니라고 한다.
강대명님의 Redis 운영 관리 책에는 다음과 같이 표현한다.
Memcached 는 캐시 솔루션이고, 이러한 Memcached 에 저장소의 개념이 추가된 것이 Redis 다
출처
그에 걸맞게 Redis가 Key-value Stores파트 중 1위를 하고 있다.
마무리
프로젝트에서 Redis를 사용해봤지만 사용법만 알았지 내부 기술에 대해서는 깊게 알지 못했던 것 같다.
이렇게 사용하다보면 Redis의 진가를 발휘하긴 커녕 분명 예기치 못한 장애와 마주칠 것이다.
그렇기 때문에 사용해봤다가 아닌 Redis의 내부 기술, 동작 흐름 등을 깊게 공부해보면서 정리해보고자 한다.
Reference
- memcached1. 기본개념
- Redis 운영 관리
- 우아한테크세미나 191121 우아한레디스 by 강대명님
- https://redis.io/
- In memory dictionary Redis 소개
- 레디스(Redis)란 무엇인가?
- https://github.com/alstjgg/cs-study/blob/main/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4/Redis%EB%9E%80.md
- https://github.com/2021-pick-git/2021-pick-git.github.io/blob/main/_posts/2021-10-19-spring-cache-with-redis.md
- 개발자를 위한 레디스 튜토리얼 01
'Back-End > Database' 카테고리의 다른 글
[왜 Redis를 사용했는가?] 02. In-Memory DB (1) | 2022.10.08 |
---|---|
[왜 Redis를 사용했는가?] 01. NoSQL (1) | 2022.09.23 |
댓글