Monday, March 31, 2008

단순한 디자인

추가할 것이 더 이상 없을 때가 아니라 제거할 것이 더이상 없을 때, 디자이너는 완벽함에 도달했다는 것을 알게 된다.

Antoine de Saint-Exupery, 프랑스 소설가, 항공기 디자이너

Optimizing I/O Performance

간단하다. User space에서 할 수 있는 최적화란, 물리적인 블럭 순으로 I/O 작업을 요청하는 것이다. 이렇게 하기 위해 세가지 방법이 있는데,
  • 파일 경로를 보고 정렬해주는 방법 (단순하나, 별로...)
  • inode 번호를 보고 정렬해주기 (fragmentation 경우 별로이나, 대부분 이 방식을 쓴다.)
  • physical block 순으로 정렬하기 (다 좋은데 root 권한이 있어야 한다.)

inode 번호 대로 정렬하는 것은 stat() system call을 통해 사용할 수 있다.



#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int get_inode(int fd)
{
struct stat buf;
int ret;

ret = fstat(fd, &buf);

if (ret < 0) {
perror("stat");
return -1;
}

return buf.st_ino;
}

physical block 순으로 정렬하기 위해서는 ioctl() system call을 사용한다. 아래처럼 FIBMAP을 주면 해당하는 논리 블럭의 physical block 번호를 block에 넣어준다.


#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/fs.h>

int get_inode(int fd)
{
struct stat buf;
int ret;

ret = fstat(fd, &buf);

if (ret < 0) {
perror("stat");
return -1;
}

return buf.st_blocks;
}

int get_block(int fd, int logical_block)
{
if (ioctl(fd, FIBMAP, &logical_block) < 0) {
perror ("ioctl"); return -1;
}

return logical_block;
}

앞서 봤던 inode 값을 리턴하는 함수에서 buf.st_ino를 buf.st_blocks로 바꾸면 논리 블럭 갯수를 반환한다. 예를 들어 어떤 파일이 8개의 논리 블로으로 구성되어 8을 리턴했으면 이 파일은 0~7번의 논리 블럭을 가지고 있는 것이다. 그래서 이 0~7번을 ioctl 함수를 통해 각각에 대한 physical block의 번호를 받을 수 있고 이 값을 가지고 I/O 요청을 최적화할 수 있다.

I/O scheduler 고르기

/sys/block/device/queue/scheduler 에 아래와 같이 사용하고픈 scheduler를 등록하면 된다.

> echo cfq > /sys/block/hda/queue/scheduler

각 scheduler 들의 tuning 값들은 /sys/block/device/queue/iosched에 있다.

Saturday, March 29, 2008

I/O Schedulers

하드 디스크는 느리다. 프로세서는 빠르다. 그래서 느린 I/O가 bottle neck이 되지 않게 하기 위한 스케줄러가 필요하다. 리눅스에서 사용해온 스케줄러는 아래와 같다.
  • Linus Elevator
  • Deadline I/O Scheduler
  • The Anticipatory I/O Scheduler
  • The CFQ I/O Scheduler

CHS Addressing 은 Cylinder, Header, Sector 를 의미하는 것으로 하드 디스크의 데이터를 접근할 때 사용하는 물리 주소이다. 오늘 날의 디스크 들은 이러한 물리적 주소가 physical/device block address로 연결되어 있다. (몰라도 된다는 ...)

I/O Scheduler의 기능 은 두가지다. I/O 요청들의 mergingsorting.

Writes-starving-reads 2.4커널 이전에 있었던 현상인데, 연속적인 데이터를 write혹은 read한다고 치자. write 는 일단 버퍼 캐시에 쓰고 바로 리턴하기 때문에 실제로 연속적인 데이터 write가 일어난다. 하지만 read는 어떨까 write를 하고 있는 동안 연속적인 데이터의 read가 일어난다고 해보자. I/O 스케줄러가 들어온 요청 순서대로 일을 처리한다고 하면, read는 연속적으로 이루어지지 않는다. 한번의 요청에 일부분의 데이터를 읽어오고 또 다른 요청에 일부를 읽어오고,. 하는 식으로 동작하기 때문에 매우 느려진다. 요게 writes-starving-read 란다. (아,. 확실하진 않음 ㅡ.ㅡ;)

Linus Elevator 가 그래서 등장했다. 좀 Heuristic한 방법이긴 한데, 큐에서 요청을 (block number 순으로) Insertion sort 하다가 age가 오래 된 요청이 있으면 그거부터 처리하는 방식이다. 단순한 만큼 개선점이 많아 2.6커널 부터는 사라졌다.

Deadline I/O Scheduler 는 앞의 리누스 엘리베이터 에 작업 큐외에 별도의 read/write 큐를 둔다. 작업 큐에는 요청이 block 번호 순서로 정렬되어 있고, read/write큐는 FIFO로 (요청 들어온 순서대로) 들어간다. I/O Scheduler는 작업 큐에서 정렬된 요청들을 가지고 작업을 하다가 expiration time에 넘은 요청이 발견되면 그 요청을 가지고 있는 read(혹은 write)큐에가서 작업을 진행한다. 일반적으로 read 큐는 expiration time이 500ms로 짧고 write 큐는 5초정도로 준다.

Anticipatory I/O Scheduler 한번의 read 작업이 끝나고 다음 read작업이 일어나는데 방금 전 읽은 부분의 다음 부분이라고 가정해 보자. 디스크의 헤더는 이미 다른 위치로 이동해 있는 상태에서 다시 이전의 위치로 이동하여 작업을 수행하게 되는 낭비가 있다. 때문에 연속적인 read를 위해 한번의 read가 마쳐지면 최대 6ms 까지 아무일도 수행하지 않고 (헤드의 위치를 이동하지 않고) 대기해서 연속적인 read작업시 시간을 아끼는 방식의 스케쥴링을 anticipatory I/O scheduler라고 한다.

CFQ I/O Scheduler 가장 성능이 좋은 스케쥴러 인것으로 알고 있다. Complete Fair Queuing 의 약자 인데, 각 프로세스 마다 작업 큐를 가지고 있고 이것들이 round robin방식으로 돌며 정해진 time slice 내에서 작업을 수행하게 된다. time slice안에 작업을 모두 끝내게 되어도 10ms 정도를 추가로 기다리며 혹시나 있을 I/O 작업을 대기하다가 안들어오면 다른 프로세스의 큐로 이동한다. 각 큐에서는 synchronous 요청이 asynchronous보다 우선순위를 가지고 진행되어 writes-starving-reads 문제를 해결한다.

Noop I/O Scheduler 블럭 번호 순으로 정렬은 하지 않고 merging만 하는 스케쥴러 이다. storage종류에 따라 정렬이 필요하지 않은 경우가 있는데 이따 사용한다.

Thursday, March 27, 2008

When you build your own asynchronous I/O

직접 비동기 I/O를 구현해야하는 경우는 성능이나 특별히 수행해야하는 작업이 있을 때 인데, 참 고루한 작업이 되겠지만, 왠만하면 안하는게 좋겠지만, 어쩔 수 없이 해야한다면 아래와 같은 순서로 하자
  1. 모든 I/O 를 처리할 worker thread를 만든다.
  2. work queue에 I/O 작업을 할당할 수 있는 interface를 만든다.
  3. 각각의 thread가 큐에서 작업을 가져와 수행할 수 있도록 해준다.
  4. 작업이 마쳐지면 result queue에 결과를 얹어 놓게 한다.
  5. result queue에서 상태를 가져와 요청 루틴에 결과값을 반환하는 interface를 만든다.

synchronous, asynchronous, synchronized and nonsynchronized

미국사람들도 혼동스러워하는 위 네 단어의 뜻을 짚고 넘어가자. synchronous, asynchronous는 kernel buffer cache에 대해 동기화가 되냐 안되냐의 얘기이고, synchronized, nonsynchronized는 disk에 대해 동기화가 되냐 안되냐의 얘기이다.

read의 경우는 항상 synchronized이다(읽어야 보여주니까..). 그래서 read의 경우에는 buffer cache에 올라온 상태에서 리턴하냐 아니냐 (block이냐 non block이냐)에 따라서 synchronous, asynchronous 두가지 상태로 나눌 수 있다.

posix_fadvise(), readahead() - Advice for File I/O

File I/O 에 대해서는 posix_fadvise()를 통해 r/w 매커니즘을 효율적으로 사용하도록 커널에 hint를 줄 수 있다.

#include <fcntl.h>

int posix_fadvice (int fd, off_t offset, off_t len, int advice);

대부분 madvise()의 flag과 비슷하나 몇가지가 살짝 다른데,

  • POSIX_FADV_NORMAL

  • POSIX_FADV_RANDOM

  • POSIX_FADV_SEQUENTIAL : from low to high address

  • POSIX_FADV_WILLNEED

  • POSIX_FADV_NOREUSE : 한번만 쓰인다는 얘긴데, 지금은 WILLNEED와 동일하게 동작한다.

  • POSIX_FADV_DONTNEED


이 함수는 커널 2.6이후에 도입되었고 사실 리눅스에서는 그전에 readahead()로 같은 기능을 제공하고 있었다(이 함수는 POSIX_FADV_WILLNEED와 같은 기능을 한다).

#include <fcntl.h>

size_t readahead(int fd, off64_t offset, ssize_t count);

stream video 같은 경우에는 한번 display한 데이터는 캐시가 안되도 되기 때문에 POSIX_FADV_DONTNEED를 주면 좋다.