안녕하세요. 알티스토리에 처음으로 글을 남기에 되었네요.
저는 알티베이스에서 개발자로 일하고 있습니다. 알티스토리에 ‘flow’라는 필명으로 글을 올릴 예정입니다. 글에 오류가 있거나, 이해가 안가시는 부분이 있으면 자유롭게 코멘트에 남겨주시면 감사하겠습니다.
리눅스에서 Direct IO와 Synchronous IO를 정확하게 이해할 수 있는 재밌는(?) 실험을 했던 것이 기억나 블로그를 통해 공유할까 합니다. 먼저 간단히 정리하면 Direct IO는 IO가 버퍼를 거치느냐 여부를 결정하는 정책을 의미하고, Synchronous IO는 IO가 즉시 반영되는지 여부를 결정하는 정책을 의미합니다. 둘은 분명히 다르지만, 운영체제에서 Synchronous IO를 지원하는 것 자체가 버퍼가 있다는 가정으로 특정한 경우 이를 조작하기 위한 것이므로 Direct IO는 당연히 Synchronous IO 입니다. 이 부분이 조금 헷갈릴 수 있는데, 아래의 실험을 살펴보시면 도움이 되리라 생각합니다.
자, 시작합니다!
Direct IO란 무엇인가요? Direct IO란 운영체제의 버퍼를 거치지 않고(bypass), 직접 IO를 수행하는 것입니다. 혹자는 파일 시스템 버퍼라도고 하는데, 저는 그냥 운영체제가 관리하는 버퍼라고 하겠습니다. Direct IO와 반대되는 용어를 굳이 정의한다면, 반대로 일반적인 IO를 Buffered IO라 할 수 있습니다. (참고로 fopen()을 이용하여 파일을 여는 경우, 응용프로그램 단계에서 IO를 버퍼링하기도 합니다. 이 이슈는 이 글에서 다루지 않습니다.)
동기화의 관점에서 IO에는 Synchronous IO (동기 IO)와 Asynchronous IO (비동기 IO)가 있습니다. 이는 쓰기에 대한 IO를 수행할때 IO가 발생한 즉시 디스크에 반영하느냐 아니냐의 정책을 결정하는 것입니다.
자 이제 프로그래머의 관점에서 생각해봅시다. 위에서 설명한 IO 정책을 내가 짠 프로그램에서 반영하려면 어떻게 해야 할까요? IO를 수행하려면 가장 먼저 open() 시스템 콜을 호출해야 합니다. 추가로 원하는 정책을 적용시키려면 적절한 플래그를 지정하면 됩니다.
Synchronous IO는 어떻게 수행할 수 있나요? 동기화에 관련된 플래그를 찾아보면 O_DSYNC, O_RSYNC, O_SYNC가 나오고, 이것들은 POSIX.1 표준입니다. 간단히 이 플래그를 open()의 플래그에 적용하시면 됩니다. (혹은 fsync() 등으로도 가능합니다.)
Direct IO는 어떻게 수행할 수 있나요? (제가 알기로) POSIX 표준은 Direct IO에 대한 플래그는 정의하지 않고 있습니다. 다행히(?) 리눅스는 (제 기억으로) 2.4 커널 일부에서 O_DIRECT라는 플래그를 제공합니다. (아마 2.6 커널에서는 모두 지원이 될 것 입니다.)
자! 그럼 이러한 플래그가 실제로 어떻게 반영되는지 눈으로 확인할 수 있는 방법은 없을까요?
만약 리눅스를 사용하신다면?
blktrace라는 툴이 있습니다.
간략하게 blktrace에 대해 설명해드리면, 리눅스의 요청 큐(request queue)의 연산을 관찰할 수 있도록 하는 툴입니다.
이제 리눅스에 ‘blktrace’를 이용하면 Direct IO를 Synchronous IO의 동작(behavior)를 관찰할 수 있습니다. (참고로 blktrace은 낮은 버전의 커널 패치가 필요한데, 제가 실험했던 2.6.22 커널에서는 별다른 패치 없이 패키지 설치만을 통해 가능했습니다. 2.6.18이상이라면 별도의 커널패치가 필요 없습니다.)
일반적으로 배포판에서 제공하는 패키지로 설치할 수도 있고, 아래의 주소의 저장소에서도 구할 수 있습니다.
git://git.kernel.org/pub/scm/linux/kernel/git/axboe/blktrace.git bt
blktrace를 이용하여 아래와 같이 완료(complete)된 요청에 대한 결과만을 출력하여 IO가 실제로 발생하는 상황을 살펴볼 수 있습니다. (실제로 더 많은 상태에 대한 정보들이 있는데, 이 실험에서는 실제로 장치에 IO가 요청되었는지만 관심이 있으므로 아래와 같은 옵션으로 필터링한 것 입니다.)
blktrace [블록장치경로] -a complete -o - | blkparse -f “%d,%S,%n\n” -i -
주의: 이 실험을 재현하는 경우 쓰기연산을 수행하는 디스크 혹은 파티션은 운영체제가 설치된 시스템과 같이 중요한 데이터를 포함하지 않아야 합니다. 이 부분을 제대로 이해하지 못하고 실험을 재현하다 생기는 문제는 책임지지 않습니다. 매우 유용한 방법이 있는데, 하드디스크에 적당한 파티션이 없는 분은 사용하지 않는 (각종 행사에서 하나씩은 얻으셨을) ‘USB 드라이브’를 이용하시면 좋습니다.
실험 순서는 아래와 같습니다.
- blktrace 실행
- IO 발생
- blktrace를 이용하여 실시간으로 결과 관찰
기본 옵션 (버퍼링 + 지연된 쓰기 IO 발생)
아래의 소스코드를 컴파일하여 수행합니다.
/* default.c */
#define _GNU_SOURCE // O_DIRECT
#define IOUNIT 128*1024
#include
#include
#include
#include
int main()
{
char *nBuf; // not aligned buffer
char *aBuf; // aligned buffer
int fd; // file descriptor
// 1MB buffer (not aligned yet)
nBuf = (char*) malloc(IOUNIT+getpagesize());
// aligned buffer 'aBuf'
aBuf = (char*)((unsigned)(nBuf+getpagesize()-1)/getpagesize()*getpagesize());
printf("OPEN FD: %d\n", fd=open("[블록장치경로]", O_RDWR ) );
printf("WRITE: %d\n", write( fd, aBuf, IOUNIT ) );
printf("READ: %d\n", read( fd, aBuf, IOUNIT ) );
close(fd);
free(nBuf);
}
위의 프로그램은 오픈한 블록 장치(block device)의 0번 주소에 대해 128KB 단위의 쓰기 연산을 한번 수행하고, 그 다음 128KB위치의 해당하는 주소에 128KB크기의 읽기를 한번 수행하는 아주 간단한 프로그램입니다.
위의 프로그램 수행 직 후 blktrace를 이용하여 관찰된 IO는 아래와 같습니다.
R,256,256
분명 쓰기 연산도 수행했는데, 반영되지 않고 있습니다. !!!
몇 초가 흐르면 아래의 IO가 발생합니다.
W,0,256
첫 번째 IO는 쓰기 요청임에도 실제로 READ 요청이 먼저 발생하였습니다. 실제로는 “W,0,256“이 발생하여야 하지만, 일단 버퍼에 요청된 쓰기를 수행하고, 디스크에는 반영하지 않았음을 의미합니다. 실제 IO는 몇 초가 지난 후에야 비로소 발생합니다.
만약 실제 쓰기에 대한 IO가 발생하기 이전에 컴퓨터가 비정상적으로 전원이 나간다거나 하는 상황이 발생하면 어떻할까요? 이러한 이유로 데이터베이스의 트랜잭션 로그의 경우, 위와 같은 코드르 사용하면 안됩니다.
추가로 프로그램을 반복하여 수행하여 봅시다. 실제로 IO가 요청되지 않습니다. 프로그램에게 버퍼에 저장되어 있는 값을 전해주기 때문입니다.
O_SYNC (쓰기 IO 즉시 발생)
처음 작성한 default.c를 아래의 osync.c와 같이 수정합니다. 실제로 다른 부분은 모두 같고, O_SYNC 플래그만이 추가된 것입니다.
/* osync.c */
#define _GNU_SOURCE // O_DIRECT
#define IOUNIT 128*1024
#include
#include
#include
#include
int main()
{
char *nBuf; // not aligned buffer
char *aBuf; // aligned buffer
int fd; // file descriptor
// 1MB buffer (not aligned yet)
nBuf = (char*) malloc(IOUNIT+getpagesize());
// aligned buffer 'aBuf'
aBuf = (char*)((unsigned)(nBuf+getpagesize()-1)/getpagesize()*getpagesize());
printf("OPEN FD: %d\n", fd=open("[블록장치경로]", O_RDWR | O_SYNC ) );
printf("WRITE: %d\n", write( fd, aBuf, IOUNIT ) );
printf("READ: %d\n", read( fd, aBuf, IOUNIT ) );
close(fd);
free(nBuf);
}
같은 방법으로 blktrace를 시작하고, 위의 프로그램을 수행하면 아래와 같은 결과를 확인하실 수 있습니다.
W,0,256
R,256,256
위의 프로그램을 반복하면, 읽기는 발생하지 않고 쓰기만 계속 발생합니다. 쓰기 연산에 대한 동기화 플래그가 O_SYNC로 설정되어 있기 때문입니다. 참고로 O_SYNC 플래그를 이용하여 파일을 열지 않더라도 fsync()를 이용하여 쓰기 연산을 장치에 실제로 반영하도록 할 수 도 있습니다.
O_DIRECT (버퍼링 하지 않음)
default.c 파일을 아래의 odirect.c와 같이 수정합니다. (O_DIRECT 플래그 사용)
/* odirect.c */
#define _GNU_SOURCE // O_DIRECT
#define IOUNIT 128*1024
#include
#include
#include
#include
int main()
{
char *nBuf; // not aligned buffer
char *aBuf; // aligned buffer
int fd; // file descriptor
// 1MB buffer (not aligned yet)
nBuf = (char*) malloc(IOUNIT+getpagesize());
// aligned buffer 'aBuf'
aBuf = (char*)((unsigned)(nBuf+getpagesize()-1)/getpagesize()*getpagesize());
printf("OPEN FD: %d\n", fd=open("[블록장치경로]", O_RDWR | O_DIRECT ) );
printf("WRITE: %d\n", write( fd, aBuf, IOUNIT ) );
printf("READ: %d\n", read( fd, aBuf, IOUNIT ) );
close(fd);
free(nBuf);
}
blktrace를 다시 시작하고, 위의 프로그램을 실행하면 아래의 결과를 얻을 수 있습니다.
W,0,256
R,256,256
버퍼링 효과가 적용되지 않아 즉각적으로 읽기와 쓰기가 발생하였습니다. 프로그램을 반복하여 수행하여도 정직하게 반복합니다.
버퍼링과 prefetch의 관계
버퍼링은 단순히 IO가 반복하여 발생하는 경우에만 적용되는 것은 아닙니다. 위의 경우와 같이 한번 IO와 발생한 주소에서 다시 발생할 확률이 높다고 가정하는 것을 “temporal locality”라고 합니다. 하지만 데이터를 연속적으로 저장되고 처리될 확률이 높기 때문에 A라는 주소에서 IO가 발생했다면, A+1주소에서 짧은 시간안에 다시 IO가 발생할 확률 역시 높습니다. 이러한 것은 “spatial locality”라고 합니다.
Spatial locality를 이용하여 성능을 높이기 위한 대표적인 방법은 IO의 단위를 크게 하는 것입니다. 사용자가 1바이트를 읽어도 기본 IO의 단위가 8KB라면, 이후 주소에 대한 데이터까지 상위 단계의 메모리로 함께 복사해오겠죠. 이에 더불어 prefetch라는 기술을 이용할 수도 있습니다. 단순히 A주소에 대한 IO가 발생하면 A+1주소에 대한 IO까지 미리 수행하여 버퍼에 저장하는 것입니다.
Prefetch효과를 확인하기 위해, 아래의 default_prefetch.c 파일을 생성합니다. default.c파일에서 쓰기 연산을 제거하고 읽기만 수행한 것입니다.
/* default_prefetch.c */
#define _GNU_SOURCE // O_DIRECT
#define IOUNIT 128*1024
#include
#include
#include
#include
int main()
{
char *nBuf; // not aligned buffer
char *aBuf; // aligned buffer
int fd; // file descriptor
// 1MB buffer (not aligned yet)
nBuf = (char*) malloc(IOUNIT+getpagesize());
// aligned buffer 'aBuf'
aBuf = (char*)((unsigned)(nBuf+getpagesize()-1)/getpagesize()*getpagesize());
printf("OPEN FD: %d\n", fd=open("[블록장치경로]", O_RDWR ) );
printf("READ: %d\n", read( fd, aBuf, IOUNIT ) );
close(fd);
free(nBuf);
}
blktrace를 시작하고 위의 소스코드를 컴파일하여 수행하면, 아래와 같은 결과를 얻을 수 있습니다.
R,0,256
R,256,256
흥미로운 것이 실제로는 128KB크기의 읽기를 한번만 요청하였지만, 연속된 128KB에 대해서도 읽기를 수행했다는 것입니다. 연속된 읽기가 발생할 수 있는 상황을 가정하여 데이터를 미리 읽어오도록 한 것입니다. 리눅스에서 어떤 상황에서 prefetch를 결정하는지는 잘 모르겠습니다. 더욱 재밌는건 처음의 쓰기 후 읽기 패턴에서는 prefetch를 수행하지 않다가 지금의 읽기만 있는 패턴에서는 prefetch를 결정했다는 것입니다.
한가지 당연한 진실은 prefetch라는 것은 버퍼링이 수행된다는 가정에서만 가능하다는 것입니다. 따라서 Direct IO에서는 별 의미가 없습니다. 바보 같지만 실제로 같은 실험을 O_DIRECT플래그를 주고 실험해 보겠습니다.
/* odirect_prefetch.c */
#define _GNU_SOURCE // O_DIRECT
#define IOUNIT 128*1024
#include
#include
#include
#include
int main()
{
char *nBuf; // not aligned buffer
char *aBuf; // aligned buffer
int fd; // file descriptor
// 1MB buffer (not aligned yet)
nBuf = (char*) malloc(IOUNIT+getpagesize());
// aligned buffer 'aBuf'
aBuf = (char*)((unsigned)(nBuf+getpagesize()-1)/getpagesize()*getpagesize());
printf("OPEN FD: %d\n", fd=open("[블록장치경로]", O_RDWR | O_DIRECT ) );
printf("READ: %d\n", read( fd, aBuf, IOUNIT ) );
close(fd);
free(nBuf);
}
성실하게 위의 프로그램을 수행하면 아래의 결과를 확인할 수 있습니다.
R,0,256
버퍼링을 하지 않으므로 prefetch 효과도 발생하지 않습니다. (참고로) 프로그램을 반복 수행하여도 읽기 IO를 정직하게 실제로 계속 수행합니다.
결론
정리하면 다음과 같습니다.일반적으로 운영체제에서는 IO발생시 IO요청을 버퍼링하여 성능을 향상시키려 합니다. 이로 인해 사용자가 쓰기 연산을 수행하고, 사용자에게는 연산을 완료했다고 리턴하면서도 장치에는 즉각 반영되지 않는 일이 발생합니다.
사용자 입장에서는 당황스러울 수 있지만, 다음과 같은 IO 성능 향상의 기회를 얻을 수 있습니다.
- 같은 번지수의 IO가 짧은 시간내에 반복될 경우, 한번만 반영할 수도 있습니다.
- 혹은 연속된 주소의 다수의 IO가 발생하면 이를 모아서 처리할 수 있습니다.
- 혹은 (디스크의 경우) IO요청을 블록 주소로 정렬하여 반영하면 성능이 향상될 수 있습니다.
- 그외에도 IO를 지연시킴으로써 다른 최적화 할 수 있는 기회를 얻을 수 있습니다.
하지만 다음과 같은 이유로 버퍼링을 부분적으로 제한하거나 아예 버퍼를 무시하고자 합니다.
- Synchronous IO: 응용프로그램의 특성에 따라 사용자는 자신의 요청을 즉각적으로 반영하길 원합니다. 이 경우 O_SYNC플래그를 이용하거나 fsync()와 같은 함수를 이용할 수 있습니다. 알티베이스와 같은 DBMS 소프트웨어의 경우에는 트랜젝션 로그를 반영하는 일들이 그에 해당될 수 있습니다.
- Direct IO: 일부 자신만만한(?) 응용프로그램(DBMS가 대표적입니다)은 운영체제의 버퍼 자체가 없었으면 하기도 합니다. 이 경우 리눅스에서는(!) 근래의(?) 커널에서
O_DIRECT플래그를 이용할 수 있습니다.
DBMS에서 Direct IO를 사용하는 이유는 DBMS자체가 내부적으로 버퍼를 관리하고, 이게 알고리즘이 운영체제에서 하는 것보다 우수하다고 믿기 때문입니다. 운영체제에서 IO를 버퍼링하게 되면 같은 디스크 블록에 대해 중복으로 버퍼링하므로 낭비이고, 이게 성능을 저하시킨다는 것입니다. 추가로 확인하신 prefetch의 효과 또한 DBMS에게는 낭비일 수 있습니다. DBMS는 이미 자신이 연속된 블록을 읽을지 임의의 블록을 읽을지 알 수 있어 스스로 IO의 단위를 조정할 수 있기 때문입니다.
그렇다고 항상 Direct IO가 성능이 우수한 것은 아닌 모양입니다. 최근 엑셈의 조동욱님께서 관련된 글을 찾아서 블로그에 링크시켜 놓으신게 있습니다. 글을 읽어보시면, DBMS 버퍼의 크기를 너무 작게 설정하여 빈번한 IO가 발생하는 경우, 운영체제 버퍼를 이용하는 것이 더 좋은 성능을 보인다는 어쩌면 당연한 이야기입니다.
실험 코드를 포함하다 보니 생각보다 글이 길어졌군요. (지루하지 않으셨길 바랍니다.)
모두 좋은 한주 보내세요. 다음에 뵙겠습니다.

Visits today: 245
3월 2nd, 2009 at 오후 7시45분
좋은 정보 감사합니다. 좋은 글 계속 부탁드립니다.
3월 2nd, 2009 at 오후 8시17분
성능의 강점을 갖고 있는 하이브리드 DBMS 개발의 기수답게 성능 향상과 관련한 좋은 글을 올려 주셨군요.. 개발자가 아니어서 완전 다 이해하지는 못하지만, 개발자들에게 유용한 글이다라는 확신은 100%인데요? ㅎㅎ 그동안 flow님의 걱정은 기우였군요.. 아님 심한 엄살? ㅎㅎㅎ 다른 개발자들에게도 피가 되고 살이 되는 좋은 포스팅 완전 기대하겠습니당..^^
3월 4th, 2009 at 오후 12시15분
이 테스트를 할려면 CONFIG_BLK_DEV_IO_TRACE 옵션으로 커널빌드를 해야 하나요? blktrace를 실행하니 “Faild to start trace…” 에러가 나네요~
3월 4th, 2009 at 오후 6시17분
예, 그렇습니다. 2.6.17.rc1 미만 버전에서는 별도의 패치도 필요하다고 합니다. 실험할 때, 데비안 2.6.22 커널에서 별도의 설정없이 되어서 다른 배포되는 커널 이미지에도 기본 포함일 것이라 가정했네요.
3월 5th, 2009 at 오후 11시45분
너무 좋은 정보 감사합니다.. 제가 지금 write()함수를 호출 하였을 때, 실제 physical disk에 write되는 시간을 측정하고 있는데요..어려움을 겪고 있었습니다..처음에는 단순히 strace로 측정 가능할 줄 알았는데, 측정 값이 마이크로 세컨드 단위로 나오는 것으로봐서는 버퍼에 쓰여지는 시간으로 추정되구요, 그래서, 현재 kprobes를 이용해서 측정 할 수 있지 않을까? 해서 공부하고 있습니다..그런데, 포스팅하신 내용을 보니 왠지 blktrace로 제가 원하는 결과를 얻을 수 있지않을까 하는 희망이 보입니다..저도 당연히 blktrace를 공부해야하지만, 염치 없이 blktrace를 이용해서 physical disk에 write하는 시간을 구할 수 있는 방법이나 옵션 좀 알려주실수 있을까요?? 아니면, 정보를 얻을 수 있는 싸이트 등 좀 알려주실수 있나요?? strace 등에 비해서 정보가 많지 않은 것 같습니다…
3월 7th, 2009 at 오후 9시49분
plzMe님:
저도 blktrace에 대해 잘 모릅니다.
google에서 ‘blktrace user guide’라고 검색하면 매뉴얼 문서를 찾으실 수 있을 겁니다. 옵션에 대한 설명이 잘 되어 있습니다.
언듯 생각하기로 issue되는 시간과 complete되는 시간을 측정하면 되지 않을까 싶네요. 그리고 timestamp는 ns(nano second)단위로 기록되는데, 내부적으로 사용하는 시간 측정 방법의 정확성은 잘 모르겠네요.
원하시는 결과를 얻으셨으면 좋겠네요.
3월 31st, 2009 at 오전 10시21분
안녕하세요. 좋은 포스트 정말 감사합니다 ^^
현재 USB 속도 이슈 때문에 고생중이라
mount 옵션을 조절하여 테스트를 하는 과정에서
DIO와 Sync/Async의 차이점이 애매했었는데요.
이 포스트를 보니 이해가 되네요.
1월 19th, 2010 at 오전 11시25분
blktrace같은 경우, request가 dispatch or complete 되는 time, location, offset등을 표현하기 때문에 D ~ C 의 시간을 측정하면 특정 request에 대한 소비시간을
측정할 수 있습니다. D 와 C 는
1월 27th, 2010 at 오전 11시53분
이 포스트를 읽는게 세 번째군요 ^^;;
요즘 DB를 공부하고 있는데, 올 때 마다 매번 느낌이 다릅니다 ^^;;
항상 잘 배우고 갑니다 ^^