Linux Internal
오늘은 오랜만에 리눅스에 대한 이야기를 해보려고 합니다. 그중에서도 I/O와 연관 깊은 dirty page에 대해서 살펴보겠습니다.
리눅스는 파일에 대한 읽기/쓰기 작업을 할 때 pagecache라는 것을 활용합니다. 읽기 작업을 한다고 해서 항상 디스크에 갔다 오는 것이 아니고 한 번 읽은 디스크의 내용을 pagecache에 저장해 놓고 해당 파일에 대한 읽기 작업이 다시 일어나면 pagecache에서 바로 꺼내서 제공합니다. 이를 통해 I/O 작업이 일어날 경우 불필요한 디스크 접근을 줄여서 성능 향상을 기대할 수 있습니다.
하지만 이렇게 pagecache에 저장된 디스크의 파일 내용에 변경이 생기면 어떻게 될까요? 이때 커널은 해당 pagecache의 메모리 영역에 대해 "이 페이지는 변경이 되어서 실제 디스크에 있는 내용과는 달라"라고 표시합니다. 바로 이런 메모리 영역을 dirty page라고 부릅니다.
위 그림은 dirty page의 생성 과정을 간략하게 나타낸 그림입니다. 맨 처음 pagecache에 있는 데이터와 디스크에 있는 데이터는 동일한 데이터를 유지하게 됩니다. 그러던 중 b라는 내용의 파일이 d라는 내용으로 변경이 된다고 가정해 보겠습니다. 이때 커널은 변경된 내용을 디스크에 바로 저장하지 않고 그와 매핑된 pagecache의 내용을 변경합니다. 그리고 해당 페이지는 ditry page가 됩니다. 그 후 일정한 조건이 되면 커널은 다양한 방법으로 dirty page의 내용을 실제 디스크에 쓰게 되는데요 이런 일련의 과정을 dirty page 동기화라고 합니다. 이렇게 쓰기 작업을 할 때도 디스크에 바로 쓰지 않고 pagecache를 이용함으로써 좀 더 빠른 I/O가 일어날 수 있게 해 줍니다.
하지만 dirty page를 실제 디스크에 쓰기 전까지는 메모리에 상주해 있는 데이터에 불과하기 때문에 이 상태에서 전원이 나가는 등의 이슈가 생기면 변경한 파일들의 내용이 디스크에 저장되지 못하는 결과를 초래합니다.
또한 동기화의 수준이 어느 정도냐에 따라서 심한 I/O 부하를 일으킬 수 있기 때문에 커널은 파라미터를 통해서 워크로드에 따라 이런 동기화 작업을 컨트롤할 수 있게 인터페이스를 제공해 주고 있습니다.
dirty와 관련된 커널 파라미터는 총 6개가 있습니다. 아래는 그 예제입니다.
vm.dirty_background_ratio = 10
vm.dirty_background_bytes = 0
vm.dirty_ratio = 20
vm.dirty_bytes = 0
vm.dirty_writeback_centisecs = 500
vm.dirty_expire_centisecs = 3000
첫 번째로 vm.dirty_background_ratio에 대해서 살펴보겠습니다. 이 값은 flush 데몬이 깨어나서 dirty page들을 싱크 시키는 조건 중에 하나입니다. ratio라는 단어가 의미하는 것처럼 비율을 의미하며 전체 메모리 대비 dirty page의 존재 비율입니다. 만약 전체 메모리가 8GB라고 하고 위 값이 10이라고 한다면 8GB의 10%인 800MB가 됩니다. dirty page의 크기가 800MB 이상이 된다면 flush 데몬이 깨어나서 dirty page들을 싱크 시키게 됩니다. 이 작업은 프로세스와는 무관하게 background 의 형태로 동작하게 됩니다. 이 때는 모든 dirty page들이 동기화됩니다.
두 번째로 vm.dirty_background_bytes입니다. vm.dirty_background_ratio와 유사하게 동작하지만 조건이 ratio 가 아니라 절대적인 bytes를 의미합니다. 기본 값은 0으로 놓고 disable 해서 시켜 놓습니다.
세 번째로 vm.dirty_ratio입니다. 이 값은 vm.dirty_background_ratio와 비슷하지만 background라는 단어가 빠진 것처럼 dirty page에 대한 싱크 작업을 background로 하지 않고 해당 프로세스의 쓰기 I/O를 블락시킨 후 싱크 작업을 합니다. 즉 dirty page가 너무 많으니 더 이상 쓰기 작업을 하지 말고 당장 디스크에 해당 내용들을 싱크 시키라는 의미입니다. ratio 단어가 지칭하는 것처럼 이 값도 전체 메모리 기준의 비율로 설정됩니다. 기본값은 10입니다.
네 번째로 vm.dirty_bytes입니다. vm.dirty_ratio와 마찬가지로 쓰기 I/O를 블락시키며, 비율이 아닌 bytes를 의미합니다. 기본값은 0으로 놓고 disable 해 놓습니다.
다섯 번째는 vm.dirty_writeback_centisecs입니다. 이 값은 flush 데몬이 background에서 깨어나는 기준이 되는 값입니다. 1/100 초를 기준으로 삼기 때문에 500은 5초를 의미합니다. 이 값은 커널 타이머에 등록되어서 타이머에 의해 flush 데몬이 깨어나게 됩니다. 기본값은 500이며 0으로 설정하면 주기적으로 깨어나지 않습니다.
여섯 번째는 vm.dirty_expire_centisecs입니다. 이 값은 flush 데몬이 깨어난 후 싱크 하게 된 dirty page의 기준값을 설정합니다. expire와 secs라는 단어가 의미하는 것처럼 일정 시간이 지난 dirty page가 싱크 됩니다. 기본값은 3000이며, 이는 생성된 지 30초 이상된 dirty page를 싱크 시키라는 의미가 됩니다.
여기까지 6개의 값들을 살펴봤는데요, 간단하게 테스트해 보겠습니다.
아주 간단한 I/O를 일으키는 프로그램을 만들어서 테스트해 보겠습니다. 아래 프로그램은 초당 1MB의 I/O를 일으키게 됩니다.
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#define MEGABYTE 1024*1024
int main() {
int output_fd;
char message[MEGABYTE] = "";
char file_name[] = "./test.dump";
int count = 0;
output_fd = open(file_name, O_CREAT | O_RDWR | O_TRUNC);
for( ; ; ){
count++;
write(output_fd, message, MEGABYTE);
printf("Write File - Current Size : %d KB\n", count*1024);
sleep(1);
}
return 0;
}
그리고 전체 메모리 8GB의 시스템에서 아래와 같이 vm.dirty_background_ratio와 vm.dirty_writeback_centisecs 값을 변경해 보겠습니다.
vm.dirty_background_ratio = 1
vm.dirty_writeback_centisecs = 0
dirty_background_ratio가 1이기 때문에 이 시스템은 dirty page가 80MB가 되면 동기화 작업을 시작하게 되며, dirty_writeback_centisec이 0이기 때문에 flush 데몬을 명시적으로 깨우지 않게 됩니다.
재밌는 결과를 볼 수 있습니다. 정말로 우리가 설정한 1% 값에 다다르자 dirty page들을 전부 반환하는 것을 볼 수 있습니다. 80MB는 그래도 제법 큰 양이기 때문에 한 번에 다 못 버리고 약 2초 정도 소요되는 것도 확인할 수 있습니다. 이 작업은 background로 이루어 지기 때문에 프로세스의 쓰기 I/O 작업 자체에는 영향을 끼치지 않습니다.
그럼 이번엔 flush 데몬을 명시적으로 깨어나게 해볼까요? 아래와 같이 바꿔 보겠습니다.
vm.dirty_writeback_centisecs = 500
vm.dirty_expire_centisecs = 1000
flush 데몬이 5초에 한 번 깨어나서 생성된 지 10초가 넘은 dirty page들을 동기화하게 됩니다. 정말 그렇게 동작할까요?
1초에 1메가씩 쓰기 작업을 하기 때문에 5초에 한 번 깨어나서 10초 된 dirty page들을 동기화시키면 10메가의 파일이 동기화됩니다. 그래서 dirty page의 전체 크기는 10메가를 넘기질 못합니다. 이렇게 되면 vm.dirty_background_ratio에 설정된 값까지 가기 전에 dirty page가 다 비워져 버리게 됩니다.
지금까지 간단한 테스트를 통해서 dirty page의 동기화에 대해 확인하고 테스트해 봤습니다. 정리하자면 아래와 같습니다.
1. vm.dirty_background_ratio는 전체 메모리 대비 dirty page의 비율을 의미하며 여기에 설정한 값을 넘기면 flush 데몬이 background 모드로 동작하면서 dirty page를 모두 비웁니다.
2. vm.dirty_writeback_centisec은 flush 데몬을 foreground에서 동작하게 하는 인터벌을 의미하며, 이 인터벌마다 깨어나서 생성 시간이 vm.dirty_expire_centisec 보다 오래된 dirty page들을 모두 비웁니다.
위 두 가지 동작은 똑같은 flush 데몬이 하는 역할이지만 첫 번째는 flush 데몬이 background 모드로 동작하고 두 번째는 foreground 모드로 동작한다는 차이점이 있습니다. 이에 대해서는 다음 글에서 ftrace 기능을 함께 활용해서 살짝 살펴보겠습니다.
감사합니다.