Posts Redis 동작 원리
Post
Cancel

Redis 동작 원리

Redis는 실무에서 캐싱, 동시성 문제 해결을 위한 분산 락, Rate Limit, 이벤트 Pub/Sub 모델 기반 브로드캐스팅 등 다양하게 활용되고 있다.

인메모리(RAM) 기반의 key-value 구조로 하드디스크로부터 조회하는 것에 비해 뛰어난 조회 성능을 가지고 있다. 또한, 싱글 스레드 기반으로 하나의 명령씩 실행이 가능하기에 동시성 문제를 해결하는데 효과적이다.

그렇다면 레디스는 내부적으로 어떻게 동작하며, 싱글 스레드 기반으로 높은 성능을 보장할 수 있는 것인지 알아보자.

Redis 내부 동작 메커니즘

Image 출처: ByteByteGo

레디스의 전체적인 동작 메커니즘은 다음과 같다.

1. Redis 기동 과정

  • Redis가 처음 기동하면 초기화 작업(설정 파일 로드, 메모리 할당, 데이터 구조 초기화 등)을 수행한다.
  • 메인 스레드에서 이벤트 루프(Event Loop)를 생성후 초기화한다.
  • 네트워크 소켓을 생성하고 지정된 포트(기본 6379)에서 클라이언트 연결을 대기한다.

2. 클라이언트 요청 처리

  • 클라이언트가 Redis 서버의 IP와 포트로 TCP연결을 요청한다.
  • Redis 서버는 accept()를 통해 새로운 클라이언트 연결을 수락한다.
  • 운영체제 커널을 통해 TCP 3-way handshake를 수행한다.
  • 새로운 커넥션을 수립되면 클라이언트 소켓(소켓 디스크럽터)이 생성되고 I/O Multiplexing의 관찰 목록에 등록된다.
  • 레디스는 I/O Multiplexing 을 통해 여러 파일 디스크럽트(소켓 디스크럽터)들의 이벤트 발생을 동시에 모니터링하며 발생된 이벤트들을 싱글 스레드로 처리한다.
  • 싱글 스레드는 발생되는 이벤트(명령어)를 순차적으로 실행하며 결과를 클라이언트의 소켓에 응답을 씀(write)으로써 클라이언트에게 응답한다.

조금 더 상세한 내용들을 아래에서 살펴보자.

소켓(Socket)

먼저 소켓과 관련된 자세한 내용은 아래 포스팅에 잘 정리되어있으니 참고하면 좋다.

소켓은 네트워크를 경유하는 프로세스 간 통신의 종착점이다. 전송 계층과 응용 프로그램 사이의 인터페이스 역할로써, 두 호스트를 연결해주는 역할을 한다.

파일 디스크럽터(File Descriptor)

파일 디스크럽터는 Linux, Unix 계열 시스템의 프로세스가 파일에 접근하기 위해 구분해놓은 정수값이다. 쉽게 표현하면 해시테이블에서 특정 파일에 접근하기 위한 키값과 같은 개념인 것이다.

프로세스별로 관리되며 기본적으로 0, 1, 2는 프로세스가 시작될때 기본적으로 할당되어 있다. 즉 새로운 파일 스크립터가 생성될때 3이상의 값부터 가지게 된다.

  • fd 0 : 표준입력 (Standard Input)
  • fd 1 : 표준출력 (Standard Output)
  • fd 2 : 표준에러 (Standard Error)

저수준 I/O 함수에게 파일 디스크립터를 전달하면, 해당 파일에 입출력을 진행한다.

보다 더 자세한 내용은 여기에 자세히 정리되어 있다.

소켓(Socket)과 파일 디스크럽터(File Descriptor)

모든 것이 파일이다(Everything is a file)“는 유닉스/리눅스 철학의 핵심 개념 중 하나이다.

Socket도 서버와 클라이언트의 IP 주소, 포트 번호, 연결 상태 등을 기록해놓은 일종의 파일(File)이며, File Descriptor(FD, 파일 디스크립터)로 관리된다.

하지만 더 정확히는 소켓 디스크립터는 실제 물리 파일이 아니다.

소켓 디스크립터는 커널의 메모리(RAM) 영역에서 관리되는 프로세스(Redis 등)가 네트워크 연결을 참조할 수 있도록 하는 정수값 형태의 식별자이다. “모든 것이 파일이다” 라는 리눅스 철학을 바탕으로 파일 디스크럽터 형태로 해당 식별자가 관리되는 것일 뿐이다.

Redis가 이 소켓 디스크립터를 사용해 클라이언트와 통신할 때, 실제로는 RAM에 있는 이 데이터 구조를 통해 커널의 네트워크 스택과 상호작용하는 것이다.

따라서 저수준 I/O 함수를 기반으로 클라이언트/서버 간의 데이터 송·수신이 가능하게 되는 것이다.(ex. open(), close(), write(), read(), …)

I/O Multiplexing(멀티플랙싱)

먼저 멀티플랙싱은 하나의 통신 채널로 둘 이상의 데이터를 동시에 전송하는 네트워크상에서의 기술이다.

I/O 멀티플렉싱이란 싱글 스레드가 여러 I/O 작업을 동시에 모니터링 할 수 있도록 해주는 기술이다. 즉, 여러 파일 디스크립터(File Descriptor)의 I/O 상태를 하나의 호출로 확인할 수 있도록 해주는 것이다.

epoll(linux), kqueue(unix 계열), iocp(windows) 등의 운영체제의 시스템콜을 통해 구현된다.

Redis의 I/O Multiplexing(멀티플랙싱)

Redis가 싱글 스레드로 여러 클라이언트의 연결을 Non-Blocking I/O로 거의 동시에 처리하는 방법은 아래와 같다.

  • 클라이언트와 Redis가 통신할 때마다, Redis 호스트의 OS에는 소켓이 생성되고 각 소켓은 고유한 파일 디스크립터(FD)로 구분된다.(소켓은 os에서 네트워크 엔드포인트이므로)
  • 클라이언트 연결 수락시 소켓을 epoll의 관찰 목록에 등록한다. 그리고 해당 소켓에 대한 읽기/쓰기 이벤트 모니터링을 시작한다.(물론 FD로 구분한다)
  • Redis의 메인 쓰레드에서 이벤트루프가 아래 절차로 수행된다.
    • 1)epoll_wait()로 이벤트 발생 감지
    • 2)이벤트가 발생한 소켓들에 대해 차례로 처리

Redis의 이벤트 루프

Redis는 이벤트 처리를 위해 ae 라이브러리(Asynchronous Event library)를 사용한다.

Redis의 이벤트 루프 관련해서는 여기에 잘 정리되어 있으며 아래 내용들은 전체적인 수준에서 이해하기 쉽도록 정리하였다.

Image 출처: https://velog.io/@ohjinseo/Redis가-싱글-스레드-모델임에도-높은-성능을-보장하는-이유-IO-Multiplexing#-redis-event-loop

이벤트 루프에서의 처리 과정은 다음과 같다.

1) 이벤트 루프 시작

aeMain()함수를 통해 이벤트 루프가 무한 루프로 시작하며, 특정 플래그(stop)가 설정될 때까지 무한히 실행된다.

2) 이벤트 폴링

aeProcessEvents()는 현재 발생할 수 있는 모든 이벤트(Time Events, File Events)를 처리하기 위해 호출된다. aeProcessEvents 함수는 aeApiPoll 함수를 호출하여 이벤트 폴링(epoll_wait[linux])을 수행하여 이벤트를 기다린다.

3) 이벤트 발생

aeApiPoll()에서 이벤트가 감지되면, 해당 이벤트에 연결된 fd와 이벤트 유형(AE_READABLE, AE_WRITABLE)가 eventLoop->fired 배열에 저장된다.

4) 이벤트 처리

그런 다음, aeProcessEvents()는 fired 배열을 순회하며 각 이벤트를 처리한다. 만약 발생한 이벤트가 AE_READABLE(클라이언트로부터 데이터가 도착)이라면 fe->rfileProc() 읽기 핸들러를 호출시켜 데이터를 읽고, AE_WRITABLE(데이터를 클라이언트에게 전송할 준비)이라면, fe->wfileProc() 쓰기 핸들러를 호출시켜 데이터를 클라이언트에게 전송한다.

실제 아래 aeApiPoll()에서 호출하는 epoll() (I/O Multiplexing 기술) 시스템 콜 덕분에, 싱글 스레드임에도 불구하고 다수의 연결과 요청을 비동기적으로 처리할 수 있게 되는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);
    if (retval > 0) {
        int j;

        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    } else if (retval == -1 && errno != EINTR) {
        panic("aeApiPoll: epoll_wait, %s", strerror(errno));
    }

    return numevents;
}

정리

Redis의 세부적인 동작 메커니즘을 딥다이브 해보았다. 또한 I/O Multiplexing과 이벤트 루프를 통해 다수의 연결과 요청을 비동기적으로 처리함으로써 높은 처리 성능을 보장할 수 있다는 것을 확인할 수 있었다. 대용량 트래픽 처리에서 자주 사용되는 Redis의 이러한 동작 메커니즘을 한 번쯤은 깊이 있게 이해해보면 좋을 것 같다 :)

Reference

This post is licensed under CC BY 4.0 by the author.

MSA 환경에서의 분산 트랜잭션 관리

Redis 특징