레디스의 스레드 모델

레디스가 Single Thread?

Main thread

레디스는 CPU-Intensive하지 않은 작업을 위해 디자인되었다. 대부분의 레디스 명령어는 메모리 내에서 단순한 데이터 조작을 수반하며, CPU 사용량이 비교적 낮다. 레디스의 주요 병목은 일반적으로 CPU 사용량보다는 메모리와 네트워크 대역폭에서 발생한다. 이러한 이유로 레디스는 단일 스레드 모델이 충분하다.

The speed Redis is famous for is mostly due to the fact that Redis stores and serves data entirely from RAM instead of disk, as most other databases do. Another contributing factor is its predominantly single-threaded nature: single-threading avoids race conditions and CPU-heavy context switching associated with threads.

출처 : redis.io - Redis Server Overview

공식 페이지에도 언급되어있듯 레디스는 싱글 스레드로 동작한다. 이로 인해 race condition을 피하고, CPU-heavy한 context switching을 피할 수 있다.

Multi Threads 이용

위의 그림처럼 레디스는 메인 스레드로 요청들을 처리하지만, 주요 스레드를 느리게 만들 수 있는 작업을 분리하여 전체 성능을 극대화한다. 아래와 같은 최적화는 Redis가 단일 스레드 모델에 엄격하게 고수하지 않는다는 것을 보여준다.

비동기 메모리 해제

Background thread free memory

(Redis 4.0부터) 대용량 키를 삭제할 때 메모리를 비동기적으로 해제하는 lazy-free 메커니즘을 도입했다. Redis는 메인 스레드가 이 시간이 많이 걸리는 작업에 의해 차단되는 것을 방지하기 위해 메모리 해제를 백그라운드 스레드에서 수행할 수 있다. 이렇게 하면 메인 스레드는 큰 임팩트 없이 큰 키에 대한 삭제 요청을 처리할 수 있다.

프로토콜 파싱

Redis 6.0에서는 요청 데이터의 프로토콜 파싱을 다중 스레드로 처리하기 시작했다. 특히 고동시성 시나리오에서. 이는 수신된 요청의 처리 부담을 줄여 성능을 향상시킨다. 그러나 실제 명령어 처리와 데이터 조작은 여전히 단일 스레드로 이루어진다.

Single Thread 모델로 인한 특징

레디스가 단일 스레드 기반으로 동작하기 때문에 아래와 같은 특성을 갖고 있다.

Sequential Execution

이벤트 룹 내에서 한 번에 하나의 명령어만 실행된다. 이는 서로 다른 클라이언트 연결로부터의 명령어가 서로 간섭하지 않고 일관되고 결정적인 방식으로 처리되도록 한다.

Atomicity

레디스의 명령어들은 atomic하게 실행된다. 이벤트 룹이 클라이언트 요청을 순차적으로 처리하더라도, 각 명령어는 원자적으로 실행되어 데이터 무결성과 일관성을 유지한다.

I/O Multiplexing

Redis도 epoll 기반의 I/O 멀티플렉싱을 사용한다. Redis는 네트워크 입출력을 효율적으로 관리하기 위하여 사용되는 기법으로, 단일 스레드 모델에서도 많은 클라이언트 요청을 처리할 수 있다.

Execution flow

Amazon ElastiCache for Redis

Enhanced I/O Multiplexing Thread는 클라이언트 명령어를 수집하고 멀티플렉싱하여 단 하나의 배치로 메인 스레드 엔진에 전달한다.

레이턴시 이슈

느린 커맨드

싱글 스레드 모델에서는 느린 명령어가 실행되면 다른 요청도 지연된다. GET, SET 등은 문제없지만, SORT, LREM, SUNION 같이 많은 CPU 자원을 사용하는 명령어는 성능 문제를 유발할 수 있다.

KEYS 명령어는 실제 환경에서 사용하지 말고, SCAN 계열 명령어를 사용해야 한다. 한 번에 많은 키를 반환하는 명령어는 성능에 영향을 줄 수 있다.

fork()1 호출

Redis persistence를 참고한다. RDB 파일을 백그라운드에서 생성하거나, AOF(Append Only File) 지속성이 활성화된 경우 AOF을 다시 작성하려면 Redis는 백그라운드 프로세스를 포크해야 한다. 이 포크 작업은 메인 스레드에서 실행되며, 자체적으로 지연을 유발할 수 있다.

대부분의 유닉스 계열 시스템에서는 포크가 비용이 많이 드는 작업이다. 이는 프로세스와 연결된 많은 객체를 복사해야 하기 때문이다. 특히 가상 메모리 시스템에서 사용되는 페이지 테이블 복사가 성능에 큰 영향을 미친다.

예를 들어, Linux/AMD64 시스템에서는 메모리가 4KB 단위의 페이지로 나뉜다. 가상 주소를 물리적 주소로 변환하기 위해 각 프로세스는 페이지 테이블을 저장하는데, 이는 트리 구조로 표현되며 프로세스 주소 공간의 각 페이지마다 최소한 하나의 포인터를 포함한다.

즉, 24GB 크기의 Redis 인스턴스는 다음과 같은 크기의 페이지 테이블을 필요로 한다. 백그라운드 저장이 실행되면 이 인스턴스를 포크해야 하므로 48MB2의 메모리를 할당하고 복사해야 한다. 이 과정은 시간이 걸리고 CPU 리소스를 소비하며, 특히 가상 머신에서는 대규모 메모리 할당 및 초기화가 더욱 부담될 수 있다.