DB에서 자주 조회해야 하는 데이터가 있다고 생각해보자. 매 요청마다 해당 데이터를 DB에서 가져가 준다면 엄청난 성능 저하가 발생할 것이다.
만약, 게임 사이트 메인 페이지에서 레벨이 높은 100명의 유저 정보를 보여준다고 생각해보자. 유저 정보는 변동성이 상대적으로 적을 것이다. 이렇다면 데이터를 창고(DB)에서 미리 꺼내놓고 요청마다 꺼내놓은 데이터를 그대로 준다면 응답 속도가 훨씬 좋아질 것이다.
이처럼 자주 조회하면서 변동성이 적은 데이터를 담아둘 수 있는 공간은 필수적이다. 캐시(Cache)를 활용한다면 이를 구현할 수 있다.
대표적으로 많이 사용하는 캐시 시스템으로 Redis와 MemCached가 있다. 이를 살펴보고 장/단점을 알아보자.
인메모리 구조란 RAM에 데이터를 올려 사용하는 방법을 말한다. Redis는 인메모리 구조를 사용하여 상대적으로 속도가 빠르나 용량이 적다.
Dictionary 라는 말에서 알 수 있듯 Redis는 사전(Key-Value) 형태의 데이터 저장소이다.
다양한 형태의 데이터 구조를 지원하긴 하나 복잡한 데이터를 다루기엔 적합하지 않다.
위 2가지 이유로 인해 캐시 데이터베이스 서버에 적합하다. 앞서 언급한 게임 사이트의 상위 100명 유저를 응답하는 트래픽 처리에 매우 유리하다.
Redis는 메모리에 데이터를 저장하므로 물리적인 메모리(RAM) 용량보다 더 많은 용량을 사용하면 문제가 발생한다. 즉, 메모리 관리가 매우 중요하다
RAM 용량이 부족한 경우 디스크에 SWAP 공간을 만들어 임시 저장하는 방식이다.
보통 Redis가 느려진 상황이라면 RAM 공간이 부족하여 메모리 SWAP이 발생한 경우다. 만약 사용중인 Redis에서 SWAP이 발생했다면 계속해서 SWAP이 발생한다. 이 경우 프로세스를 재실행하여 해결이 가능하다.
RAM에서 사용 가능한 메모리가 충분히 존재하지만 메모리 공간이 작은 조각으로 나뉘어 할당이 불가능한 상태를 말한다.
Redis는 jmalloc
을 사용하여 메모리를 할당하는데 jmalloc
의 Memory-page-size 를 4096 byte로 잡으면 1 byte만 사용해도 4096 byte 만큼을 할당한다. 이는 결국 메모리 낭비로 이어지고 정확한 메모리 사용량을 파악하기 힘들다는 단점이 있다.
따라서, 다양한 크기의 데이터 사용을 자제하고 유사한 크키의 데이터를 사용하자. (데이터 크키의 통일성이 필요)
Redis의 가장 큰 특징이다. 동시에 처리할 수 있는 명령어의 개수가 한 번에 하나다. (Redis가 C언어로 작성되있어 그런거 같음...) 덕분에, 데이터 손실없이 수평으로 스케일링할 수 있다.
참고
실제 명령어 동작이 패킷(packet)단위로 전달되고 이게 쌓여 command(명령)가 완성되면 명령을 실행한다.
이러한 특징 때문에 O(n)
관련 명령어를 가급적이면 지양하는게 좋다. 꼭 필요한 경우가 아니라면 사용하지 말자.
참고) Redis에서 대표적인
O(n)
명령어
keys
delete collection
get all collection
flushAll
,flushDB
MemCached와 차이점이라 볼 수 있다. 특정 데이터를 디스크에 영구 저장할 수 있다. AOF, RDB Snapshot 2가지 방식이 존재한다.
메모리만 사용한다면 정합성을 맞추기가 어려운데 디스크 영속화를 통해 이를 보장해준다. 그러나, 성능은 메모리만 사용할 때가 훨씬 좋다.
Redis 서버는 영속성과 고가용성을 높이기 위해 클러스터링 방식으로 많이 사용한다. Master-Replica 형태로 구성되며, Master의 데이터를 Replica로 복제한다.
영속성
- 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터 특성
고가용성
- 서버, 네트워크, 프로그램 등의 정보 시스템이 상당히 오랜 기간동안 지속적으로 정상 운영이 가능한 성질
클러스터링
- 유사한 성격을 가진 개체를 묶어 그룹으로 구성하는 것
비동기 복제(Async Replication)으로 인해 복제 시 동시성 문제가 발생하여 렉이 발생할 수 있다.
추가로 Replicaof
명령어 사용시 fork가 발생하므로 메모리 여유 공간을 반드시 확인해야 한다. (만약, Master 메모리 공간이 5GB이면 순간적으로 10GB가 필요)
MemCache와 달리 Redis는 트랜잭션을 지원한다. MULTI
커맨드를 통해서 트랜잭션을 시작하며 EXEC
로 추가 명령어를 실행한다. WATCH
를 통해서 트랜잭션을 종료할 수 있다.
MemCached 또한 인메모리 구조를 사용하여 상대적으로 속도가 빠르나 용량이 적다.
key-value 형태의 스토리지다. 단, NoSQL은 아니다.
오직 String
(문자열) 데이터 타입만 지원한다. 오로지 읽기 전용으로 사용되며 더 이상 가공할 필요가 없어진다. 따라서, 데이터를 저장하는데 유리하다.
내부 메모리 관리가 Redis에 비해 상대적으로 복잡하지 않아 비교적 적은 메모리를 사용한. 따라서, HTML 코드 부분같이 작고 변하지 않는 데이터를 캐싱하기 유리하다. 따라서, DB/API 통신을 줄이기 위해 데이터를 캐싱할 때 유리하다.
Redis에 비해 slab
할당자를 사용하여 메모리 할당을 수행한다. 이는 내부 메모리를 다시 할당하지 않고 사용할 수 있다. (Redis는 jmalloc
이라 사용마다 메모리를 할당한다.)
따라서, 상대적으로 메모리 할당이 잦지 않으며 트래픽이 몰려도 응답속도가 안정적이다.
멀티 스레드로 동작하여 컴퓨팅 자원을 추가함으로 스케일 업을 할 수 있다. 이로인해 캐싱된 데이터가 유실될 가능성도 존재한다.
이 둘의 공통점 및 차이점을 비교해보고 상대적인 장/단점에 대해 알아보자.
페이지 교체 알고리즘
String
), 해시(hash
), 리스트(list
) 등 다양한 자료형 지원String
)만 지원어떤 프레임워크를 사용하는 이유를 명확히 알고 사용하는 것과 모르고 사용하는 것은 천치차이다. 프로젝트에 캐시 시스템을 도입하고자 할 때, 둘 중 뭐를 사용하는게 더 효율적일지 생각해보자.
MemCached에 비해 다양한 데이터 구조를 지원한다. 복잡한 데이터를 저장하기에 유리하다.
SnapShot
- 특정 시간에 데이터 저장 장치의 상태를 별도의 파일이나 이미지로 저장하는 기술
- 이를 통해 유실된 데이터 복원과 일정 시점의 상태로 데이터 복원 가능
Redis는 특정 시점의 데이터를 Disk에 저장할 수 있으며, 장애 상황시 데이터 복구가 필요한 경우 이를 사용할 수 있다.
<단점>
단, 이로 인해 메모리를 2배 사용하는 단점이 있다. Shapshot을 뜰 때 마다 자식 프로세스를 만들고 서로 변경된 메모리 페이지를 복사해서 사용한다. Copy and Write 방식을 사용하여 자식 프로세스는 실제 메모리 양 이상의 메모리를 복사한다. 따라서, 실제로 필요한 메모리 양보다 더 많은 메모리를 사용하기에 주의해야 한다.
Copy and Write
작성 시 이전의 내용을 복사하는 기법 (e.g. Java의String
)String x = "Hello"; String y = x; // 같은 레퍼런스 y += " World"; // 기존 x가 참조하는 공간(버퍼)는 그대로 두고 // 해당 공간의 값을 새로운 공간 y에 복사 후 쓴다 (copy and write) // x는 영향을 받지 않는다.
기존
"Hello"
를 저장하는 버퍼가 수정되는 것이 아니라 이를 복사한 새로운 공간이 할당된다. Redis에서도 이러한 방식으로 자식 프로세스를 만들기에 조심해야 한다.cf) Java에서도 이로 인한 가비지(Gabage)가 생기는 것을 방지하기 위해
StringBuilder
등의 유틸 클래스를 지원한다.
Mater - Replica 형태로 구성이 되어 Master의 데이터를 Replica로 복제한다. 따라서, 영속성과 고가용성을 높이기 위해 클러스터링 방식으로 많이 사용한다.
<단점>
Replicaof
명령 시 fork가 발생하여 메모리 부족으로 이어질 수 있다. (e.g. Master 메모리 공간이 5GB이면 순간적으로 10GB 필요)
트랜잭션을 지원하여 ACID을 보장한다.
ACID
- Atomicity(원자성) : 트랜잭션의 연산은 데이터베이스에 모두 반영되든지 아니면 전혀 반영되지 않아야 한다.
- Consistency(일관성) : 시스템이 가지고 있는 고정요소는 트랜잭션 수행 전과 완료 후의 상태가 같아야 한다.
- Isolation(독립성) : 둘 이상의 트랜잭션이 동시에 병행 실행되는 경우 어느 하나의 트랜잭션 실행 중에 다른 트랜잭션의 연산이 끼어들 수 없다.
- Durablility(영속성)
성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.
내부적으로 slab
명령어를 사용하므로 Redis에 비해 메모리 할당이 빈번하지 않다.
메모리 할당이 잦지 않으므로 대용량 트래픽을 처리할 때 유리하다.
정적인 데이터를 캐싱할 때 내부 메무리 관리가 Redis만큼 복잡하지 않아 비교적 적은 메모리를 사용한다.
멀티 스레드를 사용하여 멀티 프로레스 코어를 사용할 수 있다. 이로 인해 스케일업을 통해 더 많은 작업을 처리할 수 있다.