소프티어 프로젝트 최종 평가 당일 면접관 분께 사용한 기술에 대해 말씀드리게 되었다. Redis의 Geospatial Index 부분에서 여러 질문과 답이 오가던 중 Redis 동작 방식에 대해 여쭤보셨다. 당시 싱글스레드라는 것은 알고 있어서 싱글스레드 방식으로 동작한다라고 말씀드렸더니 ‘왜 싱글스레드 일까요?’라고 물어보셨다. 조금 당황하다가 컨텍스트 스위칭 비용 때문에 싱글스레드를 사용하는 것 같다고 말씀드리면서 넘어갔지만 이에 대해 정확하게 알고 있지는 못한 상태였다. 따라서 더 자세히 공부한 과정을 남겨본다.
1. Redis는 왜 빠른가요?
A: Redis는 인메모리 데이터베이스라 빨라요.
B: 근데 인메모리면 왜 빨라요??
가장 기본적인 CS 내용이다. 이에 대해 잘 모르겠다면 빠르게 알아보고 넘어가자.
👊🏻 RAM VS SSD VS HDD
1. 메모리 (RAM)
- 특징
- CPU가 직접 접근하여 데이터를 읽고 쓸 수 있는 휘발성(전원이 꺼지면 데이터가 사라짐) 기억장치
- 매우 빠른 접근 시간(나노초 단위)
- CPU가 실행 중인 프로그램과 데이터를 임시로 저장하여 빠른 처리 속도를 지원
- 구조 및 작동 원리
- DRAM
각 메모리 셀은 트랜지스터와 축전기로 구성되며, 전하를 저장하여 데이터를 유지한다. 이 전하는 주기적으로 새로 고침(refresh)되어야 하며, 이 과정이 DRAM의 동작 특성을 결정한다. - SRAM
플립플롭(flip-flop) 구조를 사용하여 데이터를 저장하며, DRAM보다 빠르지만 비용과 전력 소모가 커서 주로 CPU 캐시 메모리로 사용된다.
- DRAM
2. SSD (Solid State Drive)
- 특징
- NAND 플래시 메모리를 기반으로 하는 비휘발성 저장 장치
- 읽기/쓰기 속도는 HDD에 비해 매우 빠르지만, DRAM보다는 느림(마이크로초 단위)
- 운영체제, 애플리케이션, 대용량 파일 저장 등 지속적 데이터 저장
- 구조 및 작동 원리
- 플래시 메모리 셀에 전하를 저장하여 데이터를 유지하며, 이동 부품이 없어 HDD보다 기계적 지연이 없다.
- 데이터 접근은 전자적으로 이루어지나, 메모리 컨트롤러와 인터페이스(SATA, NVMe 등)를 거치므로 메모리보다는 접근 속도가 느리다.
3. HDD (Hard Disk Drive)
- 특징
- 기계적으로 움직이는 회전하는 자기 디스크에 데이터를 저장하는 비휘발성 저장 장치
- 수 밀리초(ms) 단위의 지연이 발생(메모리나 SSD에 비해 상대적으로 매우 느림)
- 대용량 데이터 저장, 백업 및 비용 효율적인 저장소 제공
- 구조 및 작동 원리
- 디스크(플래터)가 회전하고, 읽기/쓰기 헤드가 해당 위치로 이동하여 데이터를 읽고 쓴다.
- 이 물리적 이동 때문에 데이터 접근에 시간이 소요된다.
🤔 그래서 메모리가 빠른 이유?
1. 전자적 접근 및 낮은 지연 시간
- 반도체 기반
- 메모리는 반도체 칩에 구현되므로, 모든 데이터 접근이 전자적으로 이루어진다.
- 이로 인해 기계적인 이동이나 회전이 전혀 필요하지 않아 극히 낮은 지연(나노초 단위)을 제공한다.
- 직접 연결
- CPU와 메모리는 메모리 컨트롤러를 통해 직접 연결되어 있다.
- 고속 메모리 버스(DDR4, DDR5 등)를 사용하여 매우 빠른 데이터 전송이 가능하다.
2. 병렬 처리 및 최적화
- 병렬 인터페이스
- 메모리는 다수의 데이터 버스를 통해 동시에 여러 비트의 데이터를 전송할 수 있도록 설계되어 있어, 고대역폭 처리가 가능하다.
- 캐시 계층 구조
- CPU 내부에는 L1, L2, L3 캐시가 있어 메모리 접근 시간을 더욱 줄이고, 자주 사용되는 데이터에 대한 빠른 접근을 보장한다.
3. 설계 최적화
- 전용 목적
- 메모리는 데이터의 임시 저장 및 빠른 읽기/쓰기를 위해 설계되었기 때문에, 그 목적에 맞게 회로와 전력 관리, 주기적인 리프레시(refresh) 기능 등이 최적화되어 있다.
- 낮은 접근 시간
- DRAM 셀의 구조는 간단한 트랜지스터-축전기 조합으로, 상대적으로 적은 회로 지연을 가져와 빠른 데이터 접근을 가능하게 한다.
2. Redis는 싱글 스레드인가요?
반은 맞고 반은 틀리다. Redis는 I/O에서는 멀티 스레드를 사용하고 명령 처리에서는 싱글 스레드를 사용한다.
1. 근데 왜 싱글 스레드를 사용할까?
① 락(Lock) 오버헤드 제거: 동시에 여러 데이터 구조에 접근할 때 발생할 수 있는 동기화 이슈가 없다
② 일관성(Consistency) 보장: 하나의 명령이 원자적으로 실행되므로 ACID에서 A(Atomicity)에 가깝게 동작
③ 성능 최적화: 고속 메모리 액세스와 네트워크 I/O가 결합되어 있을 때, 여러 스레드 간 문맥 교환(context switch) 비용 없이 빠른 처리가 가능
2. 주요 과정 설명
① 클라이언트 요청 접속 (TCP 소켓)
- Redis는 클라이언트와 통신할 때 TCP 프로토콜을 사용한다.
- 클라이언트는 TCP/IP 소켓을 통해 Redis 서버와 연결을 수립한다.
- 명령은 RESP(Redis Serialization Protocol)을 통해 주고받는다.
② I/O Multiplexing
- Redis가 하나의 스레드로 많은 클라이언트 연결을 동시에 처리할 수 있는 핵심 원리이다.
- Redis는 하나의 스레드로 여러 소켓을 등록하여 모니터링한다.
- OS(epoll)가 등록된 소켓들 중 데이터가 도착하면 Redis의 메인 이벤트 루프를 깨운다.
- Redis의 싱글 스레드 메인 루프는 이 이벤트를 감지하여, 이벤트가 발생한 소켓만 읽거나 데이터를 쓰는 작업을 수행한다.
③ I/O 스레드(최신 Redis, 6.0+)
- Redis 6.0부터는 I/O 스레드를 통해 네트워크 송수신 작업을 멀티스레드로 병렬화할 수 있게 되었다.
- 다만, 내부적인 명령 실행(데이터 수정, 삭제 등)은 여전히 싱글 스레드로 동작한다.
내부 동작
- 클라이언트가 소켓으로 데이터 전송
- OS의 TCP 네트워크 스택(커널 레벨)에서 데이터 수신 (커널 내부 TCP 버퍼에 임시 저장)
- Redis의 I/O 스레드가 소켓의 데이터를 커널에서 유저스페이스(Redis 프로세스 메모리)의 입력 버퍼로 복사해오는 작업 수행
- Redis 입력 버퍼(query buffer, SDS)에 즉시 저장
④ 이벤트 루프(Event Loop)
- 이벤트 루프는 Redis가 클라이언트 연결로부터 들어오는 이벤트(읽기 및 쓰기)를 감지하고 처리하는 중심부이다.
- 단일 이벤트 루프(single-threaded event loop) 모델로 설계되었다.
- Redis는 모든 클라이언트 소켓과의 이벤트(읽기 가능, 쓰기 가능, 에러 등)를 감지하는 역할을 이벤트 루프에서 처리한다.
- OS의 시스템 호출인
epoll(Linux)
,kqueue(BSD/macOS)
같은 I/O 다중화(multiplexing) 메커니즘을 사용하여 여러 연결을 효율적으로 관리한다.
왜 epoll/kqueue를 사용하는가?
다수의 소켓 중 어떤 소켓이 이벤트가 발생했는지 효율적으로 찾기 위함.
많은 클라이언트를 관리하면서 CPU와 메모리 자원을 적게 소모하여 효율적으로 작동한다.
3. I/O 스레드 동작 흐름 총정리
Multiplexing과 멀티스레드는 다르다. 헷갈리지 말자!
① 클라이언트가 소켓으로 데이터를 전송
- 클라이언트가 Redis에게 명령(예:
SET key value
)을 요청한다. - 데이터는 네트워크를 통해 Redis 서버에 전달된다.
② OS의 TCP 네트워크 스택(커널)이 데이터를 수신
- 클라이언트로부터 전송된 데이터는 일단 OS의 커널 내 TCP 버퍼에 임시로 저장된다.
- 아직 Redis는 이 데이터를 읽지 않은 상태!
③ Redis의 메인 이벤트 루프(싱글 스레드)가 I/O 이벤트(epoll)를 감지
- 메인 이벤트 루프는 항상 OS(epoll 시스템콜)를 통해 여러 소켓을 모니터링한다.
- OS(epoll)는 데이터가 도착한 소켓을 감지하여 메인 이벤트 루프에 알림을 준다.
- 메인 이벤트 루프는 데이터가 도착한 소켓 목록을 확인
④ Redis의 메인 이벤트 루프가 읽기 작업을 I/O 스레드에게 할당
- 메인 이벤트 루프는 데이터가 도착한 소켓들을 직접 읽지 않고, 멀티스레드로 처리하기 위해 I/O 스레드들에게 읽기 작업을 분배한다.
⑤ I/O 스레드들이 데이터를 읽어서 입력 버퍼(query buffer)에 저장
- 각 I/O 스레드는 커널의 TCP 버퍼에서 Redis 프로세스의 유저스페이스 메모리로 데이터를 복사하는 작업을 수행한다.
- 복사한 데이터는 Redis의 클라이언트 객체가 가진 입력 버퍼(query buffer, SDS 구조)에 즉시 저장된다.
⑥ 메인 이벤트 루프가 입력 버퍼에서 데이터를 읽어 명령 파싱
- 모든 읽기 작업이 완료된 후, Redis의 메인 이벤트 루프가 각 클라이언트의 입력 버퍼에서 데이터를 가져온다.
- 메인 스레드는 이 데이터를 해석(파싱)하여 실제로 어떤 명령인지 파악한다.
⑦ 메인 이벤트 루프가 명령을 처리 (싱글 스레드로 동작)
- 메인 이벤트 루프가 실제 Redis 명령(
SET
,GET
,DEL
등)을 실행한다. - 데이터의 변경, 삭제 등 실제 로직 처리는 이 단계에서만 이루어지며 싱글 스레드로 처리!
⑧ 메인 이벤트 루프가 처리한 결과 데이터를 출력 버퍼에 저장
- 명령을 처리한 후, 결과 데이터는 Redis의 출력 버퍼(output buffer)에 저장한다.
⑨ 메인 이벤트 루프가 쓰기 작업을 다시 I/O 스레드에게 할당
- 메인 이벤트 루프는 응답을 보내는 작업(소켓에 데이터를 쓰는 작업)을 다시 I/O 스레드에게 할당한다.
⑩ I/O 스레드들이 클라이언트 소켓에 데이터를 전송
- I/O 스레드들은 Redis 출력 버퍼의 데이터를 클라이언트의 소켓을 통해 다시 전송(write)한다.
- 클라이언트는 응답 데이터를 수신
3. TTL 처리는 어떤 방식으로 이루어지나요?
JWT 토큰 관리를 위해 TTL처리를 통해 메모리를 관리하면서 내부적으로 Redis가 TTL 처리를 어떤 방식을 통해 하는지 궁금해졌다. 공식 문서에 따르면 다음 2가지 방식에 의해 처리되는 것을 알 수 있다.
수동적 만료 (Passive expiration)
- Redis는 키에 접근할 때 해당 키의 만료 여부를 확인한다.
- 클라이언트가 만료된 키를 요청하면 Redis는 즉시 해당 키를 삭제한다.
능동적 만료 (Active expiration)
- Redis는 주기적으로 백그라운드에서 만료된 키를 확인하고 삭제한다. 과정은 다음과 같다.
- 100ms마다 실행된다.
- 무작위로 키를 샘플링하여 만료된 키를 제거한다.
- 만료된 키의 비율이 높을 경우, 다시 한번 같은 과정을 진행하며 이를 반복한다.