4 분 소요

Redis는 키-값 기반의 인메모리 데이터베이스로, Strings, Lists, Sets, Sorted Sets, Hashes, Streams, HyperLogLogs, Bitmaps와 같은 다양한 데이터 유형을 지원하며 스냅샷을 저장하는 RDB(Redis Database)와 변경사항을 로그에 추가하는 AOF(Append On File) 방식을 통해 데이터를 디스크에 저장하여 영속성을 보장합니다.

Redis는 여러 서버가 공유하는 인메모리 데이터 저장소, 도큐먼트 데이터베이스, 벡터 데이터베이스 등 다양한 용도로 활용됩니다. Redis는 독립적으로 사용할 수 있지만, 빠른 성능 덕분에 memcached와 유사하게 캐시 용도로도 자주 사용됩니다.

효율적인 작업 처리를 위해 Redis는 멀티스레드와 싱글 스레드 방식을 조화롭게 사용합니다. I/O 집약적인 작업인 RDB 스냅샷 생성, AOF 로그 기록, 네트워크 요청 처리 등에서는 멀티스레드를 활용하고, 명령어 처리는 싱글 스레드 기반의 이벤트 루프에서 순차적으로 처리됩니다.

싱글 스레드 방식을 사용하면 연산의 순차성이 보장되어 더티 읽기나 더티 쓰기와 같은 동시성 문제를 피할 수 있으며, 트랜잭션의 격리성을 쉽게 유지할 수 있습니다. 또한, 적은 수의 스레드로 작업을 처리하면 컨텍스트 스위칭이 줄어들고 캐시 효율도 향상됩니다. 이벤트 루프에서는 여러 스레드가 동일한 데이터에 접근하지 않기 때문에 락을 사용할 필요가 없어 동기화 오버헤드가 제거되고 성능이 개선됩니다. 다만, 시간이 오래 걸리는 연산이 처리되는 동안 다른 연산들이 대기해야 하는 단점이 있을 수 있습니다.

메모리는 높은 비용을 가진 제한된 자원이기 때문에, Redis는 메모리 효율을 위해 데이터를 메모리 효율적인 방식으로 인코딩하여 저장합니다. 그러나 많은 연산에서 인코딩된 데이터를 직접 처리할 수 없기 때문에 다시 디코딩해야 하며, 이로 인해 메모리를 절약하는 대신 더 많은 CPU 시간이 소모될 수 있습니다. 이 설정을 조정하여 CPU 사용량과 메모리 사용량을 균형 있게 조율할 수 있습니다.

새로운 메모리를 커널에 요청하는 작업은 많은 CPU 자원을 소모합니다. 이 과정에서는 유저 모드에서 커널 모드로 전환해야 하며, 현재 실행 중인 프로세스의 상태를 저장하고 복구하는 작업이 필요합니다. 또한, 모드 전환은 CPU 모드 변경과 메모리 보호 등의 추가 작업을 수반하며, CPU 캐시를 비울 수도 있습니다.

또한, 사용 가능한 메모리가 충분하더라도 작은 빈 공간이 여러 곳에 분산되어 실제로 큰 메모리를 할당할 수 없는 메모리 단편화 문제가 발생할 수 있습니다. 다중 스레드 환경에서는 여러 스레드가 동시에 메모리를 할당하고 해제하는 과정에서 잠금 경합이 발생하여 성능이 저하될 수 있습니다.

메모리 할당 성능을 최적화하기 위해 Redis는 jemalloc을 사용합니다. 이를 통해 각 스레드가 독립적으로 메모리를 할당받을 수 있도록 여러 개의 독립된 메모리 풀인 아레나를 사용하고, 스레드별 캐싱을 지원하여 다중 프로세서 환경에서 성능을 향상시키며 동시성 문제를 해결합니다.

서버와 클라이언트 간의 RTT(Round Trip Time)를 줄이기 위해 Redis는 파이프라이닝을 지원합니다. 여러 연산을 하나의 요청에 포함시켜 네트워크를 통한 TCP 패킷 수를 줄이고, 대량 삽입 및 업데이트 작업을 지원하여 단일 연산보다 빠르게 처리할 수 있습니다. 또한, 클라이언트 측 캐싱 기능을 통해 자주 액세스하는 키에 대한 읽기 작업을 네트워크에서 완전히 오프로드할 수 있습니다.

인메모리에만 데이터를 저장하는 것은 예상치 못한 장애에 매우 취약할 수 있습니다. 이를 해결하기 위해 데이터를 내구성 있는 저장소에 저장하여 지속성을 제공할 수 있습니다. Redis는 지속성을 보장하기 위해 RDB(Redis DataBase)와 AOF(Append Only File) 두 가지 방식을 제공합니다.

RDB는 지정된 간격으로 특정 시점의 스냅샷을 저장하며, AOF는 모든 쓰기 작업을 로그 파일에 기록합니다. RDB는 빠른 복구가 가능하고, 단일 파일로 관리되는 간결한 형태를 제공하지만, 스냅샷 생성 주기가 길어 데이터 손실 가능성이 상대적으로 큽니다. RDB는 자식 프로세스를 생성하여 디스크에 데이터를 저장하므로 데이터 양이 많을 경우 fork 연산에 시간이 소요되어 인스턴스가 잠시 중단될 수 있습니다.

AOF는 모든 쓰기 연산을 로그 파일에 계속 추가합니다. 이 방식은 RDB에 비해 데이터 손실 가능성이 적지만, WAL(Write-Ahead Logging)과 달리 지연을 최소화하기 위해 연산이 완료되고 메모리 내 데이터가 변경된 후에 로그를 작성하고, 주기적으로 디스크에 기록합니다. 따라서 여전히 손실 가능성이 존재합니다.

이러한 기능들은 메인 스레드를 최대한 중단시키지 않도록 설계되었지만, 여전히 오버헤드가 존재합니다. 애플리케이션 특성에 맞춰 이 설정을 조정하여 내구성과 지연 시간 간의 균형을 맞출 수 있습니다.

이러한 다양한 최적화 기법 덕분에 Redis는 디스크 기반 데이터베이스보다 빠른 성능을 제공합니다. 그러나 Redis와 디스크 기반 데이터베이스를 함께 사용할 경우, 이러한 구성의 효율성에 대한 몇 가지 의문이 있을 수 있습니다.

디스크 기반 데이터베이스는 모든 데이터를 디스크에만 저장하지 않고, 높은 성능을 보장하기 위해 일부 데이터를 메모리에 저장합니다.

B-트리 기반 데이터베이스에서는 쓰기 요청 시 데이터를 영구적으로 저장하기 위해 선행 기록 로그를 작성한 뒤, 디스크의 최소 작업 단위인 블록을 효과적으로 관리하기 위해 B-트리에 데이터를 저장합니다. 디스크 I/O 비용을 줄이기 위해 여러 블록을 결합한 페이지를 메모리에 저장했다가 특정 조건을 만족할 때 디스크로 플러시하여 저장합니다.

로그 구조 스토리지(Log-Structured Storage, LSM) 기반 데이터베이스는 불변 자료 구조를 사용하며, 정렬된 로그 파일을 주로 작성합니다. 이 방식에서는 메모리 기반 컴포넌트인 멤테이블(memtable)에 데이터를 저장한 뒤, 특정 조건에 따라 정렬된 파일로 데이터를 복사합니다.

이처럼 디스크 기반 데이터베이스도 성능 향상을 위해 일부 데이터를 메모리에 캐시합니다. 그러나 페이지 전체를 캐시하더라도 직렬화 및 데이터 레이아웃 관리에 따른 오버헤드가 발생하여 인메모리 기반 데이터베이스보다는 성능이 떨어질 수 있습니다.

디스크 기반 데이터베이스는 자체적인 캐시 계층을 가지고 있다고 볼 수 있습니다. 이때 Redis를 추가적인 캐시 계층으로 활용하면 캐시 히트가 발생하지 않을 경우 추가적인 네트워크 비용이 발생하여 성능이 저하될 수 있습니다. 또한, 디스크 기반 데이터베이스 자체에서도 데이터가 캐시되어 있는 경우, Redis를 통한 읽기 성능 향상이 쓰기 성능 감소나 관리 오버헤드를 상쇄하지 못할 가능성이 있습니다.

Redis를 추가적인 캐시 계층으로 사용할 때는 높은 캐시 히트를 통해 읽기 성능을 극대화하여 추가적인 오버헤드보다 높은 가치를 제공해야 합니다. 디스크 기반 데이터베이스의 메모리 캐시와는 달리, Redis는 도메인 정보를 기반으로 세분화된 캐시 정책을 구현할 수 있습니다. 예를 들어, 테이블별 데이터 크기, 접근 방식 등의 도메인 정보를 활용하여 보다 적합한 캐시 정책을 적용하고, 이를 통해 디스크 기반 데이터베이스에서 관리하기 어려운 테이블별 캐시 정책을 효과적으로 구현할 수 있습니다.

참고 문서

댓글남기기