AOF Rewrite

<< AOF Recorded Commands AOF Loading >>

주요 흐름

3 Steps

이 글은 레디스 Version 3.2.0 기준으로 작성되었습니다.

AOF Rewrite는 Child Background Process에 의해서 수행된다.   bgrewriteaof 명령이 수행되면, rewriteAppendOnlyFileBackground()가 호출된다.   이 function은 Child Process를 생성(fork) 하고, Child Process는 rewriteAppendOnlyFile()를 수행해서 Rewrite를 한다.

AOF Rewrite 과정은 크게 3 Step으로 나누어 볼 수 있다.

  • Step 1: 자식 프로세스는 메모리의 데이터를 6개의 명령(데이터 타입별 하나, 그리고 pexpireat 명령)으로 임시 AOF 파일에 쓰면서, 부모 프로세스에 추가된 데이터를 받는(읽어오는) 과정.
    이 과정이 완료되면 fork() 시점의 데이터를 모두 임시 AOF 파일에 쓰고(write), fsync까지 완료한 상태가 된다.
  • Step 2: 자식 프로세스는 부모 프로세스에 추가된 데이터가 있을 경우 최대 1초, 없을 경우 20ms를 대기하면서 부모 데이터를 받고(읽어오고), 이 시간이 지나면 자식 프로세스는 부모 프로세스에게 "Stop sending" 신호를 보내고 "Ack"를 받는다.
    마지막으로 부모 데이터를 읽어와 쓰고(write), fsync 하고, 임시 AOF 파일을 close 하는 과정
  • Step 3: 부모 프로세스는 100ms마다 실행되는 serverCron()에서 임시 AOF 파일을 다시 open 하고,   Step 2 이후에 추가된 데이터를 쓰고(write), 파일명을 원래 AOF 파일명으로 이름을 변경하고 최종 완료하는 과정.

Step 1

Rewrite 할 때는 각 데이터 타입별로 한 가지의 명령만 사용하여 저장된다.   SET, RPUSH, SADD, ZADD, HMSET 그리고 PEXPIREAT 명령이다.   PEXPIREAT 명령은 키에 expire time이 설정되어 있으면 각 쓰기 명령 기록 후 추가로 기록된다.


aof bgrewriteaof
    그림 1-1   AOF Rewrite Step 1 흐름도
  • bgrewriteaodCommand()는 rewriteAppendOnlyFileBackground()를 호출한다.
    rewriteAppendOnlyFileBackground()는 자식 프로세스를 fork() 하기 전에 aofCreatePipes()를 호출해서 생성될 자식 프로세스와 통신하기 위해서 pipe()를 생성한다.
  • fork() 해서 자식 프로세스를 생성한다.   Fork() 하면 부모 프로세스에게는 생성된 자식 프로세스의 Process id가 리턴되고, 자식 프로세스에게는 0을 리턴한다.   그러므로 리턴된 값에 따라 분기해서 자식 프로세스가 할 일을 수행하면 된다.
  • 자식 프로세스는 Rewrite 하는 메인 function인 rewriteAppendOnlyFile()를 호출한다.
  • 수행 순서는 DB 0부터 15까지 하나씩 진행한다. 물론 DB 개수는 redis.conf의 databases 16 파라미터에 의한다.
  • 처음 명령은 select DBnum이다.   그 DB 안에 키가 없으면 다음 DB로 넘어간다.
  • 이제, While 루프에서 dictNext()으로 진행하면서 dictGetKey(), dictGetVal() function으로 키를 하나씩을 가져와서 데이터 타입을 비교해서, 그 데이터 타입에 맞는 function을 호출해서 저장한다.
    그림 1-1에 잘 나타나 있다.
  • getExpire()로 Expire time를 가져와서 설정되어 있으면 PEXPIREAT 명령이 추가로 저장된다.
  • 키 하나에 여러 개의 데이터가 있는 컬렉션은 한 명령에 최대 64개까지 기록된다.
    예를 들어 Lists의 key 하나에 value가 150일 경우
    RPUSH key value1, value2, ..., value64
    RPUSH key value65, value66, ..., value128
    RPUSH key value129, value130, ..., value150
    이렇게 기록된다.
    이는 server.h에 정의되어 있다.
    #define AOF_REWRITE_ITEMS_PER_CMD 64
  • 마지막으로, Loop를 한 번 돌 때마다, 기록(write)한 데이터의 양이 10kb 이상이면 aofReadDiffFromParent()를 호출하여, 자식 프로세스가 rewrite 하는 동안 부모 프로세스에서 추가로 입력된 데이터를 읽어서 server.aof_child_diff에 저장한다.
    여기서 Step 2로 가기 전에 부모 프로세스에서는 자식 프로세스가 있을 때 어떻게 처리하는지 알아보자.

부모 프로세스에서 처리 방법: 리스트와 쓰기 이벤트

AOF ON 일때 feedAppendOnlyFile()에서 catAppendOnlyGenericCommand()를 호출해서 AOF 파일에 저장한다.   다음, AOF 자식 프로세스가 실행 중이면, 이 데이터를 자식 프로세스에게 보내 주여야 하므로, aofRewriteBufferAppend()를 호출해서, 만들어진 AOF 저장 형태를 server.aof_rewrite_buf_blocks에 저장한다.

aof bgrewriteaof write event
    그림 1-2   AOF Rewrite: Write Event
  • server.aof_rewrite_buf_blocks는 List로 구성되어 있고, listNode는 aofrwblock를 가리킨다.
    aofrwblock는 10MB 데이터 buf와 사용량을 기록하는 used, 남은 바이트 수를 기록하는 free 필드로 구성된 구조체이다.
  • 레디스는 리눅스의 epoll을 이용해서 aofrwblock에 데이터가 쓰이면, 이벤트를 발생시켜 aofChildWriteDiffData()가 호출되도록 했다.
    aofChildWriteDiffData()는 이 데이터를 읽어서 자식 프로세스에 연결된 파이프에 쓴다.
  • 부모 자식 간 통신에 대해서 더 살펴보자.

부모와 자식 프로세스 통신

레디스에서는 리눅스의 epoll을 이용해서 프로세스 간 통신(Inter-Process Communication)을 한다.
epoll은 세 가지 function이 사용되는데, epoll_create()는 준비단계이고, epoll_ctl()이 이벤트를 등록, 수정, 삭제하고, epoll_wait()이 등록된 이벤트를 기다리는 function이다.

aof bgrewriteaof ipc(inter-process communication)
    그림 1-3   AOF Rewrite: IPC와 Pipe
  • 부모 프로세스는 server.aof_rewrite_buf_blocks에 쓰기(write)가 있으면 aofChildWriteDiffData()를 실행하도록 epoll에 이벤트를 등록한다.   그러면 aof_rewrite_buf_blocks에 쓰일 때마다, aofChildWriteDiffData()는 그 데이터를 읽어서 자식 프로세스에 연결된 파이프(server.aof_pipe_write_data_to_child)에 쓴다.
  • 부모 프로세스는 자식 프로세스에서 "Stop Sending" 시그널이 올 때까지 이 작업을 반복한다.
  • 자식 프로세스는 10kb마다, aofReadDiffFromParent()를 수행해서 부모 프로세스가 파이프에 쓴 데이터를 읽어서 server.aof_child_diff에 저장한다.

Step 1 마무리

위 과정이 완료되면, 자식 프로세스는 fflush(), fsync()를 실행해서 디스크에 쓰고, 1 단계를 완료한다.

Step 2

스텝 2에서 데이터가 없으면 20ms 동안, 데이터가 계속 있으면 최대 1초 동안 데이터를 읽어온다.   이 부분은 소스 코드로 설명한다.

int nodata = 0;
mstime_t start = mstime();
while(mstime()-start < 1000 && nodata < 20) {
   if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0) {
      nodata++;
      continue;
    }
    nodata = 0;
    aofReadDiffFromParent();
}
  • Line 1,2: 변수를 초기화한다.
  • While: 시작한 지 1초 이내이고 nodata가 20보다 작을 때
    aeWait()으로 부모 프로세스로부터 데이터가 들어오는지 1ms를 기다린다.
    데이터가 들어오면 nodata = 0; 하고 aofReadDiffFromParent()로 데이터를 읽어온다.
  • 데이터가 없으면 nodata를 1 증가하고, Loop를 계속 돈다.
  • 데이터가 계속 있으면 1초 동안 계속 데이터를 읽어오고,
    데이터가 20ms 동안 없으면 Loop를 종료한다.

Step 2 마무리

데이터을 읽어온 후 자식 프로세스와 부모 프로세스는 시그널을 주고받은 다음, 데이터를 저장하고 자식 프로세스는 종료한다.

aof bgrewriteaof ipc(inter-process communication)
    그림 1-4   AOF Rewrite: IPC와 Pipe
  • 자식 프로세스는 부모 프로세스에게 "Stop Sending" 시그널을 보낸다.
    그림에서 점선은 데이터의 흐름이다.
  • 부모 프로세스는 시그널을 받고, server.aof_stop_sending_diff = 1 세팅해서 더 이상 자식 프로세스에게 데이터를 보내지 않게 하고,   자식 프로세스에서 "Ack"를 보낸다.
  • 자식 프로세스는 "Ack"를 받고, 마지막으로 aofReadDiffFromParent()를 호출해서 데이터를 읽어온다.
  • server.aof_child_diff 데이터를 쓰고(rioWrite)하고, 차레대로 다음 function 들을 수행한다.
    fflush(), fsync(), fclose()
  • 마지막으로 rename()으로 파일명을 변경한다.   임시 파일명은 temp-rewriteaof-child_pid.aof으로 생성된다.   여기서 child_pid는 자식 프로세스 ID이다.
    여기서 rename은 원래 AOF 파일명이 아니고, 또 다른 임시 파일명 temp-rewriteaof-bg-child_pid.aof으로 바꾸는 것이다.
  • 이제 자식 프로세스는 종료한다.
  • 부모 프로세스는 계속 데이터를 AOF 파일에 저장하고, 아직 자식 프로세스가 종료된 것을 업데이트하지 않았기 때문에 aof_rewrite_buf_blocks에 저장한다.

Step 3

AOF 자식 프로세스가 종료된 후, 첫 번째 실행되는 serverCron에서 backgroundRewriteDoneHandler()를 호출하면서 스텝 3은 시작된다.
serverCron은 디폴트로 100ms마다 실행된다.
이는 redis.conf의 hz 파라미터 값에 영향을 받는다.
hz가 10이면 1000/10 = 100ms, 즉 1초에 10번,
hz가 20이면 1000/20 = 50ms, 즉 1초에 20번 수행된다.

aof bgrewriteaof serverCron backgroundRewriteDoneHandler
    그림 1-5   AOF Rewrite: serverCron backgroundRewriteDoneHandler
  • 자식 프로세스에서 데이터를 저장한 temp-rewriteaof-bg-child_pid.aof 파일을 open() 한다.
  • 자식 프로세스가 "Stop Sending" 시그널을 보내서, 더 이상 자식 프로세스에게 데이터를 보내지 못 해서 쌓여 있는 데이터를 마지막으로 aofRewriteBufferWrite()를 수행해서 저장한다.
  • 이제 tmpfile을 원래 AOF filename으로 rename() 한다.
  • 마지막으로 시작할 때 open 했던 pipe를 aocClosePipes()를 수행해서 닫는다.
    이제 정말로 Background AOF Rewrite 작업이 완료되었다.

로그로 본 Step 1,2,3

bgrewriteaof 명령을 실행하면 9개의 로그 메시지가 남는다.   위에서 살펴본 Step 1,2,3이 메시지의 어느 부분에서 출력되는지 알아보자.

메시지를 카톡형식으로 표시했다.   M은 Parent, C는 Child Process를 나타낸다.   메시지위에 메시지를 출력하는 function name을 표시했다.   같은 function이 계속 출력할 경우에는 function name을 한 번만 표시했다.   메시지 옆에 시분초와 밀리초를 표시했다.


aof bgrewriteaof Log
    그림 1-6   AOF Rewrite 로그(메시지)

  • ① 61818:M 02:21:44.181 * Background append only file rewriting started by pid 61849
    bgrewriteaof 명령이 실행되면 맨 먼저 찍히는 메시지이다.   자식 프로세스를 fork() 한 직후, 부모 프로세스에서 출력한 것이다.
    이 로그를 남긴 후 클라이언트에게 "Background append only file rewriting started" 이 메시지를 내보낸다.
  • 이 후 Step 1, 2가 연속해서 실행된다.
  • ② 61818:M 02:21:49.533 * AOF rewrite child asks to stop sending diffs.
    이 메시지는 Step 2에서 자식 프로세스가 부모 프로세스로부터 데이터를 받은 직 후, 더 이상 보내지 말라는 시그널을 부모 프로세스에게 보냈고,   부모 프로세스가 시그널을 받은 직 후 남긴 메시지이다.
    시간을 보면, 약 700MB를 쓰는데 5.352초가 걸렸다.
  • ③ 61849:C 02:21:49.533 * Parent agreed to stop sending diffs. Finalizing AOF...
    이 메시지는 자식 프로세스가 부모 프로세스로부터 "Ack"를 받고 남긴 것이다.
    이후 aofReadDiffFromParent()로 부모로부터 데이터를 읽어온다.
  • ④ 61849:C 02:21:49.533 * Concatenating 19.34 MB of AOF diff received from parent.
    Step 2에서 추가된 데이터가 19.34MB이고, 이 메시지 직후 write()를 한다.   그리고, fflush(), fsync(), fclose(), rename()을 한다.
  • ⑤ 61849:C 02:21:49.760 * SYNC append only file rewrite performed
    rename()이 성공하면 이 메시지를 출력한다.
  • ⑥ 61849:C 02:21:49.766 * AOF rewrite: 42 MB of memory used by copy-on-write
    부모 프로세스가 자식 프로세스를 생성하면, 데이터 영역을 공동 사용한다.   자식 프로세스는 공동 사용하는 데이터 영역에서 키와 값을 읽어 rewrite한다.   그런데 부모 프로세스가 데이터를 변경하면 해당 페이지를 복사하는데, 이것을 copy-on-write라고 한다.
    실제 추가된 메모리는 ④번에 있는 19.34MB 지만, copy-on-write는 페이지 단위로 관리되므로 42MB인 것이다.
    실제로 이 값은 /proc/self/smaps 파일에서 Private_Dirty 필드의 값을 읽어온 것이다.
    ⑥ 번 메시지를 출력하고 자식 프로세스는 종료한다.
  • ⑦ 61818:M 02:21:49.870 * Background AOF rewrite terminated with success
    이 메시지는 부모 프로세스의 backgroundRewriteDoneHandler()가 출력한 것이다.
    이 function은 디폴트로 100ms마다 실행되는 serverCron()에서 호출한다.   그러므로 자식 프로세스 종료 후 이 function이 호출될 때까지는 최대 100ms의 시간이 경과한 것이다.   이는 그동안 AOF 버퍼에 새로운 데이터가 쌓였다는 것을 의미한다.
    이제 Step 3을 처리한다.
  • ⑧ 61818:M 02:21:49.870 * Residual parent diff successfully flushed to the rewritten AOF (1.47 MB)
    임시 AOF 파일을 open() 하고, 데이터를 쓰고, 원래 AOF 파일명으로 rename() 하고, 이 메시지를 출력한 것이다.
    자식 프로세스가 종료되고 추가된 데이터가 1.47MB이고, 이것을 AOF 파일에 썼다는 메시지다.
  • ⑨ 61818:M 02:21:49.870 * Background AOF rewrite finished successfully
    server.aof_fd, aof_lastbgrewrite_status 등 서버 구조체 변수 등을 정리하고, 이 메시지를 출력한다.
    메시지 출력 후, aofClosePipes()를 호출해서 Pipe를 close 하고, server.aof_child_pid = -1 같은 작업을 하고 AOF Rewriete는 완료된다.

추가 정보

AOF Rewrite는 여러 곳에서 수행된다.

지금까지 AOF Background Rewrite에 대해서 살펴보았다.   이 기능은 bgrewriteaof 명령을 실행했을 때뿐만 아니고, serverCron이나 다른 명령에서도 수행된다.


aof bgrewriteaof Start
    그림 2-1   여러 곳에서 수행되는 AOF Background Rewrite
  • bgrewriteaof 명령을 수행했을 때, 두 가지를 확인하고 통과되면 AOF Rewrite를 실행한다.
  • 첫 번째로 확인하는 것이 AOF Rewrite Child Process가 이미 실행중인지이다.   이미 실행 중이면, 클라이언트에게 "Background append only file rewriting already in progress" 메시지를 내보내고 실행하지 않는다.
    AOF Rewrite Child Process가 이미 실행 중인지는 server.aof_child_pid 값을 확인해서 알아본다.   이 변수에는 AOF 자식 프로세스가 실행 중이면 자식 프로세스 id가 들어있고, 실행 중이 아니면 -1이 들어있다.
    위에서 Step 1,2,3으로 구분해서 rewrite를 알아보았는데, Step 2가 완료되면 AOF 자식 프로세스는 종료된다.   하지만 aof_child_pid 값은 Step 3이 완료되는 마지막 순간에 -1로 변경된다.
  • 두 번째로 확인하는 것은 RDB Save Child Process가 실행중인지 이다.
    레디스 서버는 자식 프로세스 두 개가 동시에 수행되지 않도록 설계했다.   그래서 RDB Save Child Process가 실행 중이면 AOF Rewrite를 예약(server.aof_rewrite_scheduled = 1)하고, 클라이언트에게 관련 메시지("Background append only file rewriting scheduled")를 내보내고 리턴한다.
  • serverCron()에서도 두 가지를 확인하고 실행한다.
  • 첫 번째는 위에서 설명한 예약 여부이다.   Child Process가 실행 중이 아니고, 예약(server.aof_rewrite_scheduled == 1)되어 있다면 AOF Child Process를 실행한다.
  • 두 번째는 Child Process가 실행 중이 아니고, AOF File size가 설정값 이상으로 증가했으면 AOF Child Process를 실행한다.   클라이언트에 "Starting automatic rewriting of AOF on 100% growth" 메시지를 내보낸다.
    설정값이란 redis.conf에 있는 auto-aof-rewrite-percentage 파라미터 값을 의미한다.   디폴트로 100이 설정되어 있는데, 100은 현재 AOF 파일 크기가 직전 rewrite 후 파일 크기보다 2 배로 증가했다는 것을 의미한다.
    레디스 서버 시작 후에는 시작 시 AOF 파일 크기를 직전 파일 크기로 간주한다.   그런데 레디스 서버 시작 시 AOF 파일 크기가 0이면 어떻게 할까?   이런 경우를 처리하기 위해서 redis.conf에 auto-aof-rewrite-min-size 파라미터가 있다.   디폴트로 64mb인데, 이는 Rewrite하려면 현재 AOF 파일 크기가 64mb 이상은 되어야 한다는 것을 의미한다.
  • 마지막으로 config set appendonly yes 명령을 수행해도 AOF Background Child Process가 실행된다.
    이 명령은 AOF를 ON(시작)시키는 기능이므로 현재 AOF OFF 상태에서만 가능하다.   새로 AOF를 ON하면 일단 전체 데이터를 Rewrite하고 AppendOnlyFIle 쓰기를 시작한다.
    config set 명령의 여기에 설명되어 있으니 참고하세요.
  • 이상 세 곳에서 AOF Background Rewrite를 시작하는 것을 알아보았다.
    RDB Save는 Background와 Sync(Foreground) 두 가지 모드로 실행할 수 있는데,   AOF Rewrite는 Background 모드만 가능하다.

configset 명령과 AOF Rewrite와의 관계

AOF가 OFF 상태일 때 config set appendonly yes를 하면, AOF 상태를 WAIT_REWRITE로 변경하고, 자식 프로세스를 생성해서 rewrite 한 다음, AOF를 ON 한다.   그런데, 앞서 수행한 자식 프로세스가 종료되기 전에, config set appendonly no 하고 다시 yes를 하면, 자식 프로세스가 2개 생기는지 궁금해졌다.

aof bgrewriteaof configset
    그림 2-2   configset 명령과 AOF Rewrite 관계
  • config set appendonly no 하면, 현재 AOF 상태가 OFF가 아닌지 검사한다.   AOF 상태가 ON이거나 WAIT_REWRITE 이면, stopAppendOnly()를 수행해서, AOF 데이터를 모두 쓴다.   그리고 AOF 자식 프로세스가 수행 중이면 kill 시키고, AOF 버퍼를 비우고, AOF 임시파일을 지운다.
  • 그러므로, yes -> no -> yes를 빠른 시간 내에 반복해도 AOF 자식 프로세스가 2개 생기는 일은 발생하지 않는다.

epoll function 관계도

리눅스에서 이벤트 관리는 주로 epoll을 사용한다.   이벤트 생성은 epoll_create(), 이벤드 등록, 수정, 삭제는 epoll_ctl(), 이벤트 대기는 epoll_wait()을 사용한다.
아래 그램은 ae.c에 있는 이벤트 관리 function과 epoll function과의 관계도이다.

aof bgrewriteaof epoll
    그림 2-3   epoll function 관계도
  • 이벤트 생성, 등록, 삭제, 대기를 레디스 소스에서 뽑았다.
  • 이벤트 생성: redis.c에 있다.
    server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);
    maxclients는 기본값이 10000이고, redis.conf의 maxclients 파라미터로 변경할 수 있다.
    REDIS_EVENTLOOP_FDSET_INCR는 128이다.
  • 이벤트 등록: aof.c 등 여러 곳에서 사용된다.
    aeCreateFileEvent(server.el, fd, AE_WRITABLE, aofChildWriteDiffData, NULL);
    fd에 write()가 발생하면 aofChildWriteDiffData() function을 실행한다.   리눅스/유닉스에서는 pipe도 fd(file descriptor)이다.
  • 이벤트 삭제: aof.c 등 여러 곳에서 사용된다.
    aeDeleteFileEvent(server.el, fd, AE_WRITABLE);
    등록한 이벤트를 삭제한다.
  • 이벤트 대기: ae.c에 있다.
    aeProcessEvents(eventLoop, AE_ALL_EVENTS);

eventLoop data structure diagram

aof bgrewriteaof eventloop data structure diagram
    그림 2-4   eventLoop data structure diagram

부모 자식 간 Pipe file descriptor

aofCreatePipes()에서 Pipe를 생성한다.

  • server.aof_pipe_write_data_to_child = fds[1];
    부모 프로세스: 자식에게 보낼 이 fds에 데이터를 쓴다.
  • server.aof_pipe_read_ack_from_child = fds[2];
    부모 프로세스: 자식으로부터 온 "Stop Sending" 시그널을 이 fds에서 읽는다.
  • server.aof_pipe_write_ack_to_child = fds[5];
    부모 프로세스: 자식에게 "Stop Sending"을 잘 받았다고 "Ack"를 이 fds에 쓴다.
  • server.aof_pipe_read_data_from_parent = fds[0];
    자식 프로세스: 부모가 보낸 데이터를 이 fds에서 읽는다.
  • server.aof_pipe_write_ack_to_parent = fds[3];
    자식 프로세스: 부모에게 "Stop Sending" 시그널을 이 fds에 쓴다.
  • server.aof_pipe_read_ack_from_parent = fds[4];
    자식 프로세스: 부모로부터 온 "Ack"를 이 fds에서 읽는다.
  • 여기서 "Stop Sending"과 "Ack"는 실제로 느낌표 한 문자('!')을 주고받는 것이다.

정리

AppendOnlyFile Rewrite 과정이 생각보다 복잡했습니다.   특히, diff data를 한 번에 받지 않고 중간에 계속 받는다는 것과, 마무리는 부모 프로세스에서 한다는 것이 특이했습니다.

AOF Rewrite는 부모와 자식 프로세스 간에 통신을 하므로 이것을 이해하는데 IPC(Inter-Process Communication)과 PIPE 개념을 이해해야 하고, 이벤트 처리하는 epoll도 어느 정도 알아야 합니다.

긴 글 끝까지 읽어 주셔서 고맙습니다.




<< AOF Recorded Commands AOF Rewrite AOF Loading >>

질문하거나 댓글을 보려면 클릭하세요.  댓글수 :    조회수 :

Email 답글이 올라오면 이메일로 알려드리겠습니다.