Redis와 I/O Multiplexing

Jisu·2024년 11월 26일
1
post-thumbnail

배경

사내에서 ElastiCache 담당자가 되면서, 수많은 redis들을 만들고 운영했다. 그런데 막상 내가 redis에 대해서 깊게 모른다는 생각이 들어 이번 기회에 좀 정리를 해보고자한다.


Single Thread

Redis는 싱글 스레드이다. 왜 싱글 스레드로 만들었을까?

Single Thread Event Loop

싱글 스레드 이벤트 루프는 하나의 스레드로 모든 작업을 처리하는 프로그래밍 모델로, 입력/출력(I/O) 작업과 같은 비동기 작업을 효율적으로 처리할 수 있도록 설계된 구조이다.

하나의 쓰레드로 처리한다고 하면 뭔가 안좋아보이는데 왜 싱글 스레드로 설계했을까? 그걸 이해하려면 스레드부터 뜯어봐야한다

쓰레드는 추상적으로 그냥 프로세스의 작업 단위로 알고 있는데, 사실 TCB라고 불리는 특별한 데이터 타입이다. 여기에는 컴퓨터가 연산을 수행하기 위해 필요한 프로그램 카운터, 레지스터, 스택포인터 등이 담겨있는 것이다.

ThreadControlBlock tcb1 = { 
    .thread_id = 1, 
    .program_counter = 0x1000, 
    // 쓰레드가 어디까지 실행되었는지를 저장.
    // 문맥 전환(Context Switching) 시, 중단된 위치에서 작업을 재개하기 위해 필요.
    .registers = {0, 1, 2, 3}, 
    // 문맥 전환 시, 쓰레드의 현재 연산 상태를 보존.
    .stack_pointer = (void*)0x2000, 
    // 함수 호출 시 호출 정보(리턴 주소, 매개변수, 로컬 변수 등)를 스택에 저장.
    .priority = 5, 
    .state = READY, 
    .shared_memory = (void*)0x3000 
	// 프로세스 내 다른 쓰레드와 공유하는 메모리 영역.
};

Multi-Thread의 race condition

저기 주목할 점은 컨텍스트 스위칭을 위해 문맥을 저장하는 것과 shard memory!

멀티 쓰레드를 사용한다면 shared memory에 여러 쓰레드가 동시 접근하면서 발생하는 데이터 정합성 문제가 발생한다!

그래서 뮤텍스 (공유 자원에 접근하기 전에 락을 걸어 한 번에 하나의 쓰레드만 접근 가능) 세마포어 (제한된 개수의 쓰레드만 공유 자원에 접근 가능)를 사용하게 되는 것이다.

또한 CPU가 연산을 처리하면서 멀티 쓰레드를 사용한다면 컨텍스트 스위칭을 하게 되는데..

쓰레드가 사용 중인 프로그램 카운터(PC), 레지스터 값, 스택 포인터 등 작업 상태를 TCB(Thread Control Block)에 저장하고.. 현재 쓰레드가 더 이상 CPU를 점유하지 않도록 조정하고.. 스케줄러가 다음에 실행할 쓰레드를 선택하고..
해당 쓰레드의 TCB에서 저장된 상태(PC, 레지스터, 스택 등)를 CPU에 복원하고.. 뭐 이런 과정이 반복되기 때문에 오버헤드가 상당하다.


멀티 쓰레드는 이러한 문제점들을 가지고 있고 차라리 속편하게 싱글 쓰레드로 만들어버린것이다.


Redis의 Single Thread

Redis는 이벤트큐 없이 어떻게 대량 트래픽을 처리할까?

동시성 문제에서 벗어났지만 싱글쓰레드는 한번에 한개의 작업을 처리하는 한계를 가지고 있으므로 기본적으로는 Blocking I/O 이다. 이를 극복하기위해 Node.js는 이벤트큐를 통해 Non-Blocking I/O를 실현하였다. (메인 쓰레드의 이벤트 루프가 이벤트 큐를 계속 감시하며 비동기 작업이 발생하면 해당 작업은 이벤트 큐에 추가하는 구조)

I/O 작업은 커널이 수행하기 때문에 Non-Blocking I/O의 핵심은 어떻게 프로세스가 I/O가 끝났는지 확인하는 것이다. Node.js는 libuv 라이브러리를 사용하여 각 운영체제의 이벤트 통지 메커니즘을 추상화했다. 그렇다면 Redis는 내부적으로 어떻게 이걸 구현했을까?


Linux의 이벤트 알림 시스템

Redis는 리눅스 자체의 이벤트 알람 메커니즘을 사용한다. 여기서 말하는 이벤트란 memory I/O 요청의 발생과 종료이다.

epoll/kqueue

리눅스에는 epoll, 유닉스 계열에는 kqueue라는 기능이 있다. 이 기능을 통해 시스템의 I/O 이벤트를 감지하고 알려주게 된다. 주로 다음과 같은 I/O 이벤트를 모니터링한다.

  • 네트워크 I/O
    소켓의 파일 디스크립터를 통해 네트워크 통신 모니터링
    연결 수립, 데이터 수신/송신 등의 이벤트 감지

  • 디스크 I/O
    파일시스템의 파일 디스크립터를 통해 디스크 작업 모니터링
    파일 읽기/쓰기 완료 이벤트 감지

** 파일 디스크립터(File Descriptor)
Unix/Linux 시스템에서 파일이나 I/O 리소스를 식별하는 정수값
프로세스가 파일, 소켓 등의 리소스를 열면 OS는 해당 리소스를 고유한 FD로 관리함


I/O Multiplexing

멀티플렉싱이란 하나의 자원으로 여러 작업을 동시에 처리하는 기술이다. 정확히 말하면 동시에 처리하는 것처럼 보이는..기술이다

Single Thread로 여러 클라이언트의 연결을 Non-Blocking I/O로 거의 동시에 처리하는 방법은 아래와 같다.

  1. 클라이언트와 Redis가 통신할 때마다, Redis 호스트의 OS에는 소켓이 생성되고 각 소켓은 고유한 파일 디스크립터(FD)로 구분된다. (소켓은 os에서 네트워크 엔드포인트이므로)

  2. 클라이언트 연결 수락시 소켓을 epoll의 관찰 목록에 등록한다. 그리고 해당 소켓에 대한 읽기/쓰기 이벤트 모니터링을 시작한다. (물론 FD로 구분한다,)

  3. Redis의 메인 쓰레드에서 이벤트루프가 아래 절차로 수행된다.

    • epoll_wait()로 이벤트 발생 감지
    • 이벤트가 발생한 소켓들에 대해 차례로 처리

Conclusion

Redis는 싱글 쓰레드지만, OS의 이벤트 추적 탐지 기능인 (epoll/kqueue)을 활용하여 I/O Multiplexing을 구현하였다. 따라서 멀티스레드의 복잡성을 피하면서도 고성능으로 다중 요청을 처리할 수 있다.

profile
기술 공유를 즐기는 DevOps Engineer 장지수입니다.

0개의 댓글