AOF Internal

<< Persistence Introduction AOF Recorded Commands >>

시작

소개 Introduce

레디스는 데이터를 메모리(RAM)에 저장한다.   레디스를 중지(shutdown) 하거나 서버에 전원공급이 중지되면 레디스의 데이터는 날아간다.   레디스는 데이터를 영구 보존하기 위해 두 가지 방법을 제공하는데 그중 하나가 AOF이다.

AOF에 저장 여부는 redis.conf 파일의   "appendonly   yes/no"로 정한다.

AOF는 AppendOnlyFile의 약자로, 쓰기 명령이 실행될 때마다 텍스트(text)로 저장되고 편집 가능하다.   쓰기 명령은 입력, 수정, 삭제를 의미한다.   예를 들면 입력은 SET, 수정은 APPEND, 삭제는 DEL 같은 명령이다.   GET 같은 읽기(조회) 명령은 데이터를 변경시키지 않으므로 AOF에 기록하지 않는다.

명령이 실행되면 레디스는 메모리에 있는 데이터는 데이터 자체를 변경을 시키지만,   AOF의 기본 저장 방식은 추가만 하는 것이다.   예를 들면,

    • 첫 번째로 set key Hello 명령이 실행되면, 메모리에 key->Hello 형태로 저장되고, AOF에는 간략하게 표현해서 set key Hello 형태로 저장된다.  
    • Memory: key->Hello
      AOF: set key Hello
    • 두 번째로 append key Redis 명령이 실행되면, 메모리에는 기존 데이터를 수정해서 key->HelloRedis 형태로 저장되는데,   AOF에는 이미 저장된 데이터는 그대로 두고 append key Redis를 추가로 저장한다.
    • Memory: key->HelloRedis
      AOF: set key Hello
               append key Redis
    • 세 번째로 del key 명령이 실행되면, 메모리에는 데이터를 지워서 없어졌지만,   AOF에는 del key를 추가로 저장한다.
    • Memory:
      AOF: set key Hello
               append key Redis
               del key

AOF의 기본적인 동작 방식은 이해했다.   그럼 이제 위해서 간략히 표현한 AppendOnlyFile에 저장 형태를 알아보자.

AppendOnlyFile에 저장된 형태

위에서 실행한 세 개의 명령(set key value, append key Redis, del key)이 저장된 형태를 보면 다음과 같다.

    *3
    $3
    set
    $3
    key
    $5
    value
    *3
    $6
    append
    $3
    key
    $5
    Redis
    *2
    $3
    del
    $3
    key
    • 시작은 항상 '*'로 한다.   '*'는 명령의 시작을 나타내고, '*' 다음 숫자는 명령을 구성하고 있는 요소(단어) 개수이다.   첫 번째 명령이 set, key, value 세 요소로 이루어져 있으므로 3이다.
    • 두 번째 줄 '$'는 요소를 나타내며, 다음 숫자는 요소의 바이트 수이다.
    • 세 번째 줄은 요소(명령) 자체가 들어있다.

AppendOnlyFile에는 위와 같은 방식으로 명령마다 같은 형태로 반복되어 저장된다.
자, 그럼 이제 AOF의 내부로 들어가서 어디에서 이와 같은 저장 형태를 만드는지 알아보자.


AOF 준비와 저장

AOF 준비

aof prepare
    그림 1-1   AOF 준비 흐름도
  • 클라이언트에서 명령이 들어오면, processCommand()가 수행되어, call()을 호출한다. call()은 c->cmd->proc(c)를 호출해서 명령을 수행한다.
    명령을 수행한 다음, propagate()가 수행된다.   propagate()는 feedAppendOnlyFile()을 호출한다.   feedAppendOnlyFile()에서 실제로 AOF File에 저장될 형태로 명령을 만든다.
  • 이제 명령을 저장 형태로 준비하는 과정을 살펴보자.   세 가지 경우로 구분되어 처리된다.
  • Case 1:   EXPIRE/PEXPIRE/EXPIREAT 명령이면 catAppendOnlyExpireAtCommand()가 호출되어 PEXPIREAT 명령으로 변환되어 buf에 저장된다.   이때 *, $가 추가된 형태로 만들어진다.
  • Case 2:   SETEX/PSETEX 명령이면 catAppendOnlyGenericCommand()와 catAppendOnlyExpireAtCommand()가 호출되어 SET과 PEXPIREAT 두 개의 명령으로 변환되어 buf에 저장된다.   이때도 역시 *, $가 추가된 형태로 만들어진다.
  • Case 3:   위 두 가지 경우가 아니면 명령을 catAppendOnlyGenericCommand()를 수행해서 *, $가 추가된 형태로 만들어 buf에 저장한다.
  • Case 1, 2 이외에 명령이 바뀌어서 저장되는 것이 있는데  (예를 들어 incrbyfloat는 SET으로 바뀐다),   이런 경우는 명령을 수행하는 function에서 직접 바꾼 것이다.
  • 마지막으로 sdscatlen()으로 buf에 저장된 내용을 server.aof_buf에 추가한다.
  • 이제, server.aof_buf의 내용을 AOF file에 저장하면 된다.   하지만 이 과정이 간단하지 않다.
    하나씩 차례대로 살펴보자.

저장(write)과 디스크 쓰기(fsync)

우선, 이 과정을 요약해서 설명한다.   데이터를 디스크에 완전하게 저장하기 위해서는 write()와 fsync()가 수행되어야 한다.   Write()는 데이터를 커널 메모리에 저장하는 것이고,   fsync()는 커널 메모리에 저장된 데이터를 디스크에 쓰는 것이다.
Write()는 명령 실행 시 마다 수행되지만,   fsync()는 appendfsync 옵션에 따라, always 일 때는 write()와 같이 수행되고, everysec 일 때는 1초마다 수행된다.   No 일 때는 레디스 서버에서는 fsync()를 수행하지 않고 리눅스에 맡겨진다.
이제 아래 그림을 보면서, 하나씩 설명한다.

aof write() and fsync()
    그림 1-2   AOF Write() and Fsync() functions
  • beforeSleep()에서 flushAppendOnlyFile()을 호출한다.
  • flushAppendOnlyFile()에서 Write()를 호출한다.
  • Write()가 성공적으로 수행되었으면 server.aof_buf를 비운다.   그림에서는 sdsfree() 하나만 표시했지만 aof_buf 크기가 4000 바이트 미만이면 sdsclear()가 호출되어 메모리를 초기화하여 재활용하고, 4000 바이트 이상이면 free() 했다가 재 할당한다.
    만약 write()가 실패했다면, always 일 때는 메시지를 내보내고 중지한다.   Everysec나 No 일 때는 aof_buf를 free() 하지 않은 상태에서 메시지를 내보내고 일정 시간 후에 재시도 한다.
  • appendfsync 옵션이 always 일 때는 aof_fsync()가 수행된다.   aof_fsync()는 운영체제가 리눅스일 때는 fdatasync()가 수행되고, 기타 운영체제에서는 fsync()가 수행된다.
  • fsync()와 fdatasync() 차이점은, fdatasync()는 데이터만 디스크에 쓰고, file access time, modify time, file size 등 metadata는 저장하지 않는다.   그러므로 fdatasync()가 fsync()보다 속도가 빠르다.
  • appendonly.aof 파일에 데이터가 계속 저장되는데 시간과 사이즈가 변하지 않는 경우를 발견할 수 있는데,   이것은 위해서 설명한 fdatasync()를 사용하기 때문이다.   시간과 사이즈는 Linux 데몬에서 일정 간격(디폴트 30초)마다 sync()로 업데이트해준다.
  • 레디스는 fsync() 하는 시점을 세 가지로 정하고, 이를 redis.conf에서 설정할 수 있게 했다.
    • appendfsync Always는 매 write()마다 fsync() 실행
    • appendfsync Everysec는 1초마다 fsync() 실행
    • appendfsync No는 fsync() 하는 시점을 운영체제(OS)에 맡기는 것이다.

Always

  • 레디스에서 완전한 데이터 보존을 위한 최선의 선택은 always이다.   하지만 성능이 심각하게 떨어져 디스크 기반의 일반 DBMS를 사용하는 것과 비슷해진다.
  • 다음은 flushAppendOnlyFile()에서 fsync()를 수행하는 소스 코드이다. 이것은 beforeSleep()에서 호출되고 beforeSleep()은 명령이 끝날 때 마다 호출된다.
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
  /* aof_fsync is defined as fdatasync() for Linux in order to avoid flushing metadata. */
  latencyStartMonitor(latency);
  aof_fsync(server.aof_fd);   /* Let's try to get this data on the disk */
  latencyEndMonitor(latency);
  latencyAddSampleIfNeeded("aof-fsync-always",latency);
  server.aof_last_fsync = server.unixtime;
}
  • aof_fsync() 앞, 뒤 부분에 있는 latencyStartMonitor(), latencyEndMonitor(), latencyAddSampleIfNeeded()는 응답시간 모니터를 위해서 필요한 경우 fsync() 수행 소요시간을 저장하는 역할을 한다.   이는 latency 명령으로 확인할 수 있다.
  • fsync() 수행 후 server.aof_last_fsync에 현재 시간이 저장된다.

Everysec : 성능과 데이터 보존 사이에 최선의 선택

appendfsync 옵션이 everysec 일 때 fsync()는 별도의 쓰레드에서 처리한다.   bio.c에 있는 bioInit()에서 pthread_create()로 AOF fsync() 용 쓰레드를 생성한다.   이 쓰레드는 pthread_cond_wait()에서 signal이 오기를 대기하고 있다.

aof appendfsync everysec fsync()
    그림 1-3   AOF fsync() 쓰레드
  • 그림 1-2에서, Appendfsync가 everysec이고 마지막 fsync() 수행후 1초가 지났으면, 다음 fsync()를 하기 위해 aof_background_fsync()를 호출한다.  
  • aof_background_fsync()는 bioCreateBackgroundJob()을 호출하여, 대기 중인 AOF 쓰레드에 signal을 보낸다.   위로 올라가는 점선을 보라.   그러면 pthread_cond_wait()으로 대기 중이던 쓰레드가 신호를 받아 fsync()를 수행한다.
  • 데이터를 입력하면서 appendonly.aof 파일을 확인해 보면, everysec 옵션인데도 불구하고 1초 이내에 데이터가 저장되는 것을 발견할 수 있다.   이런 현상은, 그림 1-2에 있는 것과 같이 everysec 일 때는 aof_background_fsync()를 호출해서 fsync()를 수행하는데, aof_background_fsync() 수행 조건이 디폴트로 100ms마다 체크하는데, 같은 초내에 이미 수행되었으면 수행되지 않는다.   하지만 같은 초 내에 수행된 적이 없으면 fsync()가 100ms 내에 수행되는 것이다.   그래서 1초 이내에 AppendOnlyFile에 데이터가 저장되는 것을 볼 수도 있다.
  • 일반적인 경우 데이터가 계속 들어온다면 매초 fsync()가 수행된다.
  • 다음은 flushAppendOnlyFile()에서 aof_background_fsync()를 수행하는 소스 코드이다.
    server.unixtime > server.aof_last_fsync가 같은 초 내에 이미 fsync()가 수행되었는지 확인하는 부분이다.   이전에 수행되었던 시간보다 현재 시간(최소 단위: 초)이 커야 aof_background_fsync()가 수행된다.
if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
      server.unixtime > server.aof_last_fsync)) {
  if (!sync_in_progress) aof_background_fsync(server.aof_fd);
  server.aof_last_fsync = server.unixtime;
}

No : fsync()를 OS에게 맡긴다

  • Fsync()를 OS가 하므로 이 경우 레디스 서버에서는 fsync() 하는 부분이 없다.   리눅스는 30초마다 sync()가 수행되어 저장하기 때문에 No 일 때도 최대 30초 이내로 디스크에 저장된다.



<< Persistence AOF Internal AOF Recorded Commands >>

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

Email 返事がかかってなれば、メールでお知らせします。