SE로서 시스템을 어떻게 더 공부해야 할지 갈피를 알 수 없는 과중에 새로운 challenge가 되는 책을 읽고 정리한 내용입니다. 좋은 책을 출판해주신 저자님께 감사드립니다 ):
http://www.yes24.com/Product/Goods/44376723
목차
- 더티 페이지와 sync 명령어
- 더티 페이지와 관련된 커널 파라미터
- 더티 페이지의 동기화와 I/O throttling
- 더티 페이지 설정과 iostat
- 결론
- Dirty Page와 sync 명령어
dirty page란 페이지 캐시 중 쓰기 작업이 이루어진 메모리입니다. 즉, 기존의 파일 I/O 작업을 통해 이미 페이지 캐시에 적재된 데이터에 어떠한 변화가 발생했지만, 이 수정 사항이 디스크까지 도달하지 못하여 페이지 캐시와 디스크의 데이터가 서로 맞지 않는 상태의 페이지 캐시가 바로 dirty page입니다.
리눅스를 다루면서 누구나 한 번 쯤은 'sync' 명령어를 입력해 보셨을거라는 생각을 합니다. 이 명령어는 바로 dirty page와 디스크를 명시적으로 동기화(Page Writeback)시키는 명령어입니다.
dirty page가 발생한다고 해서 디스크 sync를 자주 하게 되면 I/O 작업이 많아져서 시스템 성능이 저하될 수 있습니다. 그래서 커널은 몇 가지 조건을 만들고, 거기에 일치하는 dirty page와 디스크를 동기화시킵니다. dirty page를 언제 얼마나 동기화시키느냐가 바로 성능 튜닝의 요소입니다.
커널 버전마다 다르지만 pdflush, flush, bdflush 등의 스레드가 동기화 작업을 합니다.
* 더티 페이지와 관련된 커널 파라미터
root@compute-2-3:~# sysctl –a | grep –i dirty
vm.dirty_background_ratio = 10 # 백그라운드 동기화 임계치
vm.dirty_ratio = 20 # I/O Trottling 임계치
vm.dirty_background_bytes = 0 # byte와 ratio는 동시 적용 불가하며 하나만 사용 가능
vm.dirty_bytes = 0
vm.dirty_writeback_centisecs = 500 # flush 커널 스레드의 wakeup 주기(ms)
vm.dirty_expire_centisecs = 3000 # 동기화할 페이지의 interval 기준(ms)
- vm.dirty_background_ratio & vm.dirty_background_bytes
dirty page의 내용을 백그라운드로 동기화할 때 기준이 되는 비율입니다. ratio가 10인 경우 메모리의 10%를 의미합니다.
- vm.drity_ratio & vm.dirty_bytes
dirty page가 ratio 값을 초과하면 해당 프로세스의 I/O 작업을 sleep하고 I/O throttling을 수행합니다.
- vm.dirty_writeback_centisecs & vm.dirty_expire_centises
writeback_centisecs는 flush 커널 스레드(kworker 스레드)를 몇 초 간격으로 깨울 것인지 결정하는 값입니다. 10ms 단위로 동작하며, 값이 500이면 5초를 의미합니다. 그리고 expire_centisecs은 생성된지 얼마가 지난 더티 페이지를 동기화할 것인지 결정하는 기준이 됩니다.
위 값의 경우 커널 스레드가 5초마다 깨어나서 생성된지 30초가 지난 더티 페이지들을 동기화합니다.
- vm.dirty_background_ratio & vm.dirty_background_bytes
vm.dirty_writeback_centisecs로 깨어난 Flush Kernel 스레드가 디스크에 sync할 dirty page의 기준을 찾을때 이 값을 사용합니다. 마찬가지로 10ms 단위로 동작하며, 값이 3000이면 drity page로 분류된 페이지들 중 30초 동안 디스크에 동기화되지 않은 페이지들을 동기화합니다.
- 더티 페이지의 동기화와 I/O throttling
각 파라미터들이 어떻게 사용되어 background 동기화가 일어나는지 알아보기 위해 간단한 I/O 작업을 하는 스크립트를 동작시킨 뒤 ftrace를 통해 커널을 추적해 보도록 하겠습니다.
### ftrace 마운트
$ mount -t debugfs debugfs /sys/kernel/debug
$ cd /sys/kernel/debug/tracing
### 현재 사용가능한 tracer의 종류
$ cat available_tracers
hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop
### 특정 함수를 기준으로 추적
$ echo function > ./current_tracer
### function tracing으로 추적가능한 함수의 목록
$ cat available_filter_functions
### 추적할 함수 필터
$ echo *write* >> set_ftrace_filter
$ echo *dirty* >> set_ftrace_filter
$ echo *wb* >> set_ftrace_filter
$ cat trace
$ cat -v trace_pipe ### 실시간 확인
먼저, 주기적 동기화가 발생하지 않도록 vm.dirty_writeback_centisecs 값을 0으로 설정하고, dirty page가 8MB가 되었을 때 background 동기화가 발생하도록 vm.dirty_background_bytes 값을 아래처럼 설정해줍니다.
$ sysctl -w vm.dirty_writeback_centisecs=0
vm.dirty_writeback_centisecs = 0
$ sysctl -w vm.dirty_background_bytes=8388608
vm.dirty_background_bytes = 8388608
그리고 아래 코드를 실행하여 background 동기화를 발생시키면서 호출되는 함수를 살펴보겠습니다.
#!/usr/bin/python
import time
MEGABYTE = 1024*1024
DUMMY_STR = ' ' * MEGABYTE
i = 0
f = open("/io_test.txt", 'w')
while True:
i += 1
f.write(DUMMY_STR)
print("write File - Current Size: {} KB".format(i*1024))
time.sleep(1)
커널 트레이스의 결과처럼 아래의 순서대로 함수가 호출됩니다.
generic_perform_write() -> balance_dirty_pages_ratelimited() -> balance_dirty_pages()
그리고 wb_wakup() 함수에 의해 flush 커널 워커 스레드가 깨어나 writeback 하게 됩니다.
$ cat -v trace_pipe | grep 'balance_dirty_pages '
test.py-7782 [023] .... 13327571.607452: balance_dirty_pages_ratelimited <-generic_perform_write
test.py-7782 [023] .... 13327571.607453: balance_dirty_pages <-balance_dirty_pages_ratelimited
test.py-7782 [023] .... 13327571.607453: mem_cgroup_wb_domain <-balance_dirty_pages
test.py-7782 [023] .... 13327571.607454: domain_dirty_limits <-balance_dirty_pages
test.py-7782 [023] .... 13327571.607454: dirty_poll_interval.part.23 <-balance_dirty_pages
test.py-7782 [023] .... 13327571.607455: wb_start_background_writeback <-balance_dirty_pages
test.py-7782 [023] .... 13327571.607455: wb_wakeup <-wb_start_background_writeback
...
kworker/u98:1-3893 [015] .... 13327571.607493: wb_workfn <-process_one_work
kworker/u98:1-3893 [015] .... 13327571.607496: wb_over_bg_thresh <-wb_workfn
kworker/u98:1-3893 [015] .... 13327571.607498: mem_cgroup_wb_domain <-wb_over_bg_thresh
kworker/u98:1-3893 [015] .... 13327571.607499: domain_dirty_limits <-wb_over_bg_thresh
kworker/u98:1-3893 [015] .... 13327571.607499: wb_writeback <-wb_workfn
kworker/u98:1-3893 [015] .... 13327571.607500: wb_over_bg_thresh <-wb_writeback
kworker/u98:1-3893 [015] .... 13327571.607500: mem_cgroup_wb_domain <-wb_over_bg_thresh
kworker/u98:1-3893 [015] .... 13327571.607500: domain_dirty_limits <-wb_over_bg_thresh
kworker/u98:1-3893 [015] .... 13327571.607502: __writeback_inodes_wb <-wb_writeback
kworker/u98:1-3893 [015] .... 13327571.607502: writeback_sb_inodes <-__writeback_inodes_wb
kworker/u98:1-3893 [015] .... 13327571.607503: wbc_attach_and_unlock_inode <-writeback_sb_inodes
kworker/u98:1-3893 [015] .... 13327571.607504: __writeback_single_inode <-writeback_sb_inodes
kworker/u98:1-3893 [015] .... 13327571.607504: do_writepages <-__writeback_single_inode
kworker/u98:1-3893 [015] .... 13327571.607505: ext4_writepages <-do_writepages
kworker/u98:1-3893 [015] .... 13327571.607510: clear_page_dirty_for_io <-mpage_submit_page
kworker/u98:1-3893 [015] .... 13327571.607512: ext4_bio_write_page <-mpage_submit_page
kworker/u98:1-3893 [015] .... 13327571.607512: __test_set_page_writeback <-ext4_bio_write_page
kworker/u98:1-3893 [015] d... 13327571.607513: sb_mark_inode_writeback <-__test_set_page_writeback
kworker/u98:1-3893 [015] .... 13327571.607517: wbc_account_io <-ext4_bio_write_page
kworker/u98:1-3893 [015] .... 13327571.607517: clear_page_dirty_for_io <-mpage_submit_page
kworker/u98:1-3893 [015] .... 13327571.607518: ext4_bio_write_page <-mpage_submit_page
kworker/u98:1-3893 [015] .... 13327571.607518: __test_set_page_writeback <-ext4_bio_write_page
kworker/u98:1-3893 [015] .... 13327571.607519: wbc_account_io <-ext4_bio_write_page
generic_perform_write() 함수는 사용자 프로세스에 의해 buffered I/O가 발생하면 write() 시스템콜에 의해 호출됩니다. 그리고 I/O state 구조체인 iocb와 iov_iter(I/O vector iterator) 구조체를 인자로 받아 어플리케이션에서 쓰고자 하는 데이터를 모두 buffered write 할 때까지 아래의 순서로 반복문을 수행합니다.
1. 쓰기 작업을 진행할 page를 찾거나 새로 생성 - write_begin()
2. 사용자 영역의 데이터를 커널 영역의 데이터로 복사 - copy_page_from_iter_atomic()
3. 쓰기를 완료한 페이지를 dirty 상태로 변경하고 복사된 데이터의 양(copied)를 반환 - write_end()
4. 사용자 데이터가 페이지에 성공적으로 쓰여지면 balance_dirty_pages_ratelimited() 함수 호출
/* mm/filemap.c */
ssize_t generic_perform_write(struct kiocb *iocb, struct iov_iter *i) {
struct file *file = iocb->ki_filp;
loff_t pos = iocb->ki_pos;
struct address_space *mapping = file->f_mapping;
const struct address_space_operations *a_ops = mapping->a_ops;
long status = 0;
ssize_t written = 0;
do {
struct page *page;
unsigned long offset; /* Offset into pagecache page */
unsigned long bytes; /* Bytes to write to page */
size_t copied; /* Bytes copied from user */
void *fsdata = NULL;
offset = (pos & (PAGE_SIZE - 1));
bytes = min_t(unsigned long, PAGE_SIZE - offset, iov_iter_count(i));
again:
...
// 파일 포인터와 offset, length 등을 인자로 받아, 해당 파일의 offset에 쓰기가 가능한지 확인.
// 쓰기를 진행할 페이지(*pagep) 반환, Success 이면 return 0.
status = a_ops->write_begin(file, mapping, pos, bytes, &page, &fsdata);
...
// 사용자 프로세스가 쓴 User space의 데이터를 Kernel space의 페이지로 복사.
// copyin() + memcpy()
copied = copy_page_from_iter_atomic(page, offset, bytes, i);
flush_dcache_page(page);
// 쓰기를 완료한 페이지를 dirty로 변경.
// 실패 시 음수를 반환하고, 성공 시 복사된 데이터의 양(copied)를 반환.
status = a_ops->write_end(file, mapping, pos, bytes, copied, page, fsdata);
...
// 사용자 프로세스가 쓴 데이터의 양(bytes)와 페이지에 복사된 데이터의 양(copied)가 다르면
// again부터 다시 수행.
if (unlikely(status == 0)) {
if (copied)
bytes = copied;
goto again;
}
pos += status;
written += status;
// page의 dirty가 발생하였으므로 balance_dirty_pages_ratelimited() 함수 호출.
balance_dirty_pages_ratelimited(mapping);
} while (iov_iter_count(i));
return written ? written : status;
}
프로세스가 dirty page를 생성할 때 마다 동기화를 하는 것은 오버헤드가 크기 때문에, 일정 수치나 비율 이상인 경우에 동기화를 하도록 합니다.
generic_perform_write() 함수에 의해 호출된 balance_dirty_pages_ratelimited() 함수는 현재 실행중인 프로세스의 dirty page 개수(current->nr_dirtied)가 threshold 인 current->nr_dirtied_pause 값보다 큰 경우 balance_dirty_pages() 함수를 호출하여 더티 페이지를 동기화합니다.
여기서 pause 값은 CPU가 I/O 요청을 처리하는 속도에 비해 느린 저장 장치의 쓰기 속도를 고려하는 것입니다. 그리고 이것은 더티 페이지가 vm.dirty_ratio(Hard limit)를 초과하여 I/O throttling에 동작하는 경우 pause 값은 write() 시스템 콜이 얼마만큼 sleep할지 결정하는 period 값(20ms ~ 200ms)으로 사용하여 I/O 속도를 조절한다는 것입니다.
/* mm/page-writeback.c */
int balance_dirty_pages_ratelimited_flags(struct address_space *mapping, unsigned int flags) {
struct inode *inode = mapping->host;
struct backing_dev_info *bdi = inode_to_bdi(inode);
struct bdi_writeback *wb = NULL;
int ratelimit;
int ret = 0;
int *p;
// Device의 용량이 부족한지, Device dirver가 cache writeback을 지원하는지 확인.
if (!(bdi->capabilities & BDI_CAP_WRITEBACK))
return ret;
if (inode_cgwb_enabled(inode))
wb = wb_get_create_current(bdi, GFP_KERNEL);
if (!wb)
wb = &bdi->wb;
// 현재 실행중인 프로세스의 task_struct 구조체 내 nr_dirtied_pause 값을 ratelimit로 받음.
ratelimit = current->nr_dirtied_pause;
if (wb->dirty_exceeded)
ratelimit = min(ratelimit, 32 >> (PAGE_SHIFT - 10));
// 커널 스케줄러에 의한 인터럽트 Disable
preempt_disable();
// per CPU 변수 bdp_ratelimits의 값 초기화.
// 과도한 task로 한 개의 CPU core에 너무 많은 nr_dirtied가 쌓이는 것을 방지.
p = this_cpu_ptr(&bdp_ratelimits);
if (unlikely(current->nr_dirtied >= ratelimit))
*p = 0;
else if (unlikely(*p >= ratelimit_pages)) {
*p = 0;
ratelimit = 0;
}
// life cycle이 짧은 task의 페이지를 nr_dirted에 반영(?)
p = this_cpu_ptr(&dirty_throttle_leaks);
if (*p > 0 && current->nr_dirtied < ratelimit) {
unsigned long nr_pages_dirtied;
nr_pages_dirtied = min(*p, ratelimit - current->nr_dirtied);
*p -= nr_pages_dirtied;
current->nr_dirtied += nr_pages_dirtied;
}
preempt_enable();
// 현재 프로세스의 dirty page의 개수가 ratelimit 이상이라면 balance_dirty_pages() 함수 호출.
if (unlikely(current->nr_dirtied >= ratelimit))
ret = balance_dirty_pages(wb, current->nr_dirtied, flags);
wb_put(wb);
return ret;
}
/* likely()와 unlikely()는 분기 예측으로 조건문이 주로 참인지 거짓인지에 따라
* 컴파일러에게 힌트를 주어 최적화를 할 수 있습니다.
* likely()는 주로 참이 오는 경우이고,
* 반대로 unlikely()는 주로 거짓이 오는 경우에 사용합니다. */
balance_dirty_pages() 함수는 인자로 받은 bdi 구조체의 'CGROUP_WRITEBACK' 플래그에 따라 글로벌 페이지와 cgroup에서 격리된 페이지를 구분하여 실행합니다. 먼저, 시스템 전체 더티 페이지의 개수와 더티 페이로 사용가능한 글로벌 페이지의 개수, 커널 파리미터의 threshold 값을 dirty_throttle_control(dtc) 구조체에 업데이트합니다. 만약 writeback을 하려는 device가 HDD나 JBOD, USB 등의 느린 매체라면, 개별 device에 맞는 I/O bandwidth로 조정하고 그 값을 dtc 구조체에 업데이트합니다.
/* mm/page-writeback.c */
static int balance_dirty_pages(struct bdi_writeback *wb, unsigned long pages_dirtied, unsigned int flags) {
struct dirty_throttle_control gdtc_stor = { GDTC_INIT(wb) };
struct dirty_throttle_control mdtc_stor = { MDTC_INIT(wb, &gdtc_stor) };
struct dirty_throttle_control * const gdtc = &gdtc_stor;
struct dirty_throttle_control * const mdtc = mdtc_valid(&mdtc_stor) ? &mdtc_stor : NULL;
struct dirty_throttle_control *sdtc;
unsigned long nr_reclaimable; /* = file_dirty */
...
bool dirty_exceeded = false;
unsigned long task_ratelimit;
unsigned long dirty_ratelimit;
struct backing_dev_info *bdi = wb->bdi;
bool strictlimit = bdi->capabilities & BDI_CAP_STRICTLIMIT;
unsigned long start_time = jiffies;
int ret = 0;
for (;;) {
...
// 시스템 전체 더티 페이지의 개수
nr_reclaimable = global_node_page_state(NR_FILE_DIRTY);
// 더티페이지로 사용가능한 global 페이지 개수.
// (NR_FREE_PAGES + NR_INACTIVE_FILE + NR_ACTIVE_FILE – reserved pages – HIGH Memory)
gdtc->avail = global_dirtyable_memory();
gdtc->dirty = nr_reclaimable + global_node_page_state(NR_WRITEBACK);
// vm_dirty_{bytes|ratio}와 dirty_background_{bytes|ratio} 값을
// dirty_throttle_control 구조체에 반영.
domain_dirty_limits(gdtc);
// HDD, JBOD, USB 등 상대적으로 느린 저장장치의 Bandwidth에 맞게 조정.
if (unlikely(strictlimit)) {
wb_dirty_limits(gdtc);
// 시스템 전체 dirty가 아닌 개별 저장장치에 대한 dirty(wb_dirty) 기준 적용.
dirty = gdtc->wb_dirty;
thresh = gdtc->wb_thresh;
bg_thresh = gdtc->wb_bg_thresh;
} else {
dirty = gdtc->dirty;
thresh = gdtc->thresh;
bg_thresh = gdtc->bg_thresh;
}
...
domain_dirty_limists() 함수는 '/proc/sys/vm' 경로에 선언된 커널 파라미터 값을 받아 dtc구조체에 업데이트합니다.
/* mm/page-writeback.c */
static void domain_dirty_limits(struct dirty_throttle_control *dtc) {
const unsigned long available_memory = dtc->avail; // 전체 메모리의 페이지 개수
struct dirty_throttle_control *gdtc = mdtc_gdtc(dtc);
unsigned long bytes = vm_dirty_bytes;
unsigned long bg_bytes = dirty_background_bytes;
// PAGE_SIZE는 page의 크기와 같은 4KB.
unsigned long ratio = (vm_dirty_ratio * PAGE_SIZE) / 100;
unsigned long bg_ratio = (dirty_background_ratio * PAGE_SIZE) / 100;
unsigned long thresh;
unsigned long bg_thresh;
struct task_struct *tsk;
...
// vm_dirty_{background}_bytes와 vm_dirty_{background}_ratio 중 1가지 Thres만 설정 가능.
if (bytes)
thresh = DIV_ROUND_UP(bytes, PAGE_SIZE);
else
thresh = (ratio * available_memory) / PAGE_SIZE;
if (bg_bytes)
bg_thresh = DIV_ROUND_UP(bg_bytes, PAGE_SIZE);
else
bg_thresh = (bg_ratio * available_memory) / PAGE_SIZE;
// background threshold가 Hard limit보다 크다면, dirty 값의 절반으로 적용.
if (bg_thresh >= thresh)
bg_thresh = thresh / 2;
...
// 계산된 thresh 값을 dirty_throttle_control 구조체에 업데이트.
dtc->thresh = thresh;
dtc->bg_thresh = bg_thresh;
...
그리고 각 변수들의 값이 업데이트되면 nr_reclaimable(시스템 전체 더티페이지 개수) 값이 background threshold(bg_thres)보다 크다면 wb_start_background_writeback() 함수를 호출하며 백그라운드 동기화를 실행합니다.
static int balance_dirty_pages(struct bdi_writeback *wb, unsigned long pages_dirtied, unsigned int flags) {
...
// 더티 페이지로 사용 가능한 페이지의 수가 vm.dirty_background_ratio의 값보다 크다면
// wb_start_background_writeback() 함수로 해당 블록 디바이스에 해당하는 inode들의
// 더티 페이지를 writeback.
if (!laptop_mode && nr_reclaimable > gdtc->bg_thresh && !writeback_in_progress(wb))
wb_start_background_writeback(wb);
// laptop_mode = vm.laptop_mode (절전모드?)
...
balance_dirty_pages() 함수에서 호출된 wb_start_background_writeback() 함수는 flush 커널 워커 스레드를 깨우고 bdi 구조체를 인자로 넘겨줍니다.
/* mm/fs-writeback.c */
void wb_start_background_writeback(struct bdi_writeback *wb) {
...
// flush 커널 스레드를 깨움.
wb_wakeup(wb);
}
그리고 깨어난 kworker 스레드는 wb_workfn() 함수를 시작으로 wb_writeback() 함수를 통해 writeback을 수행합니다.
/* fs/fs-writeback.c */
void wb_workfn(struct work_struct *work) {
struct bdi_writeback *wb = container_of(to_delayed_work(work), struct bdi_writeback, dwork);
long pages_written;
set_worker_desc("flush-%s", bdi_dev_name(wb->bdi));
if (likely(!current_is_workqueue_rescuer() || !test_bit(WB_registered, &wb->state))) {
do {
pages_written = wb_do_writeback(wb);
...
}
static long wb_do_writeback(struct bdi_writeback *wb) {
struct wb_writeback_work *work;
long wrote = 0;
set_bit(WB_writeback_running, &wb->state);
// bdi_writeback 구조체에 연결된 wb_writeback_work 리스트를 순회하며 writeback.
while ((work = get_next_work_item(wb)) != NULL) {
trace_writeback_exec(wb, work);
wrote += wb_writeback(wb, work); // 명시적 동기화 수행. (sync)
finish_writeback_work(wb, work);
}
wrote += wb_check_start_all(wb);
wrote += wb_check_old_data_flush(wb); // 주기적 동기화 수행.
wrote += wb_check_background_flush(wb); // 백그라운드 동기화 수행.
clear_bit(WB_writeback_running, &wb->state);
return wrote;
}
struct wb_writeback_work {
long nr_pages;
struct super_block *sb;
enum writeback_sync_modes sync_mode;
unsigned int tagged_writepages:1;
unsigned int for_kupdate:1;
unsigned int range_cyclic:1;
unsigned int for_background:1;
unsigned int for_sync:1; /* sync(2) WB_SYNC_ALL writeback */
unsigned int auto_free:1; /* free on completion */
enum wb_reason reason; /* why was writeback initiated? */
struct list_head list; /* pending work list */
struct wb_completion *done; /* set if the caller waits */
};
wb_do_writeback() 함수는 bdi_writeback 구조체를 받아 아래 그림처럼 연결리스트인 wb_writeback_work 구조체를 순회하며 각 item을 동기화합니다. 이때 wb_writeback() 함수는 wb_writeback_work 구조체에 선언된 flag에 따라 그에 맞는 동기화 작업을 수행합니다.
다시 balacne_dirty_pages() 함수로 돌아오겠습니다. 백그라운드 동기화가 진행된 이후 더티 페이지의 개수가 '(thresh + bg_thresh) / 2' 값보다 작다면 현재 프로세스의 task 구조체 내 더티 페이지 개수를 초기화하고, dirty_poll_interval() 함수를 통해 ((log₂(thresh – dirty) / 2)만큼 pause값을 업데이트하고 종료합니다.
반대로 더티 페이지의 개수가 해당 값보다 크다면 pause 값은 balance_dirty_pages() 함수가 인자로 받은 글로벌 더티 페이지의 개수와 bdi 구조체에 선언된 개별 저장 장치의 dirty_ratelimit을 기준으로 계산된 period 값으로 적용됩니다. 만약 pause 값이 balance_dirty_pages() 함수를 호출할 만큼 크지만 min_pause보다 작은 경우라면 balance_dirty_pages() 함수의 잦은 호출로 인한 I/O 성능저하로 이어질 수 있으므로 break합니다.
위 조건에 해당되지 않는다면 동기화로 writeback하는 속도보다 더티 페이지를 생성하는 속도가 더 빠른 경우로 간주하여 I/O throttling을 수행하여 해당 시간만큼 write()를 sleep합니다.
/* mm/page-writeback.c */
static int balance_dirty_pages(struct bdi_writeback *wb, unsigned long pages_dirtied, unsigned int flags) {
...
// 더티 페이지가 해당 값 이하라면 I/O throttling을 진행하지 않고 종료.
// dirty_freerun_ceiling(thresh, bg_thresh) = (thresh + bg_thresh) / 2
if (dirty <= dirty_freerun_ceiling(thresh, bg_thresh) && (!mdtc || m_dirty <= dirty_freerun_ceiling(m_thresh, m_bg_thresh))) {
unsigned long intv;
unsigned long m_intv;
free_running:
intv = dirty_poll_interval(dirty, thresh); // ((log₂(thresh – dirty)) / 2)
m_intv = ULONG_MAX;
current->dirty_paused_when = now;
current->nr_dirtied = 0;
if (mdtc)
m_intv = dirty_poll_interval(m_dirty, m_thresh);
// global과 cgroup의 interval 중 작은 값을 pause로 업데이트.
current->nr_dirtied_pause = min(intv, m_intv);
break;
}
...
dirty_exceeded = (gdtc->wb_dirty > gdtc->wb_thresh) && ((gdtc->dirty > gdtc->thresh) || strictlimit);
wb_position_ratio(gdtc);
sdtc = gdtc;
...
// bdi 구조체에 dirty_exceeded flag 반영
if (dirty_exceeded != wb->dirty_exceeded)
wb->dirty_exceeded = dirty_exceeded;
...
dirty_ratelimit = READ_ONCE(wb->dirty_ratelimit);
task_ratelimit = ((u64)dirty_ratelimit * sdtc->pos_ratio) >> RATELIMIT_CALC_SHIFT;
max_pause = wb_max_pause(wb, sdtc->wb_dirty);
min_pause = wb_min_pause(wb, max_pause, task_ratelimit, dirty_ratelimit, &nr_dirtied_pause);
...
// 인자로 받은 글로벌 더티 페이지 개수와 스토리지의 dirty rate를 기준으로 pause 값 업데이트.
period = HZ * pages_dirtied / task_ratelimit;
pause = period;
if (current->dirty_paused_when)
pause -= now - current->dirty_paused_when;
// pause 값이 작아서 balacing이 자주 발생하면 I/O 성능 저하로 이어질 수 있음.
if (pause < min_pause) {
...
if (pause < -HZ) {
current->dirty_paused_when = now;
current->nr_dirtied = 0;
} else if (period) {
current->dirty_paused_when += period;
current->nr_dirtied = 0;
} else if (current->nr_dirtied_pause <= pages_dirtied)
current->nr_dirtied_pause += pages_dirtied;
break;
}
if (unlikely(pause > max_pause)) {
now += min(pause - max_pause, max_pause);
pause = max_pause;
}
pause:
...
wb->dirty_sleep = now;
io_schedule_timeout(pause); // pause만큼 더티 페이지 생성 제한.
current->dirty_paused_when = now + pause;
current->nr_dirtied = 0;
current->nr_dirtied_pause = nr_dirtied_pause;
...
}
return ret;
}
- 더티 페이지 동기화와 iostat
아래는 초당 1GB씩 쓰기 작업을 하면서 iostat을 확인한 통계입니다. flush 커널 스레드가 깨어나기 전까지 %util 값이 낮은 상태를 유지하다가 writeback이 발생하면 %util 값이 순간적으로 증가하게 됩니다.
# iostat -mx -p sdb 1
avg-cpu: %user %nice %system %iowait %steal %idle
3.81 0.00 0.89 94.09 0.00 1.22
Device r/s w/s rMB/s wMB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sdb 1.00 69.00 0.50 0.42 0.00 0.00 0.00 0.00 2.00 0.06 0.01 512.00 6.27 1.14 8.00
avg-cpu: %user %nice %system %iowait %steal %idle
3.28 0.00 2.03 93.47 0.00 1.22
Device r/s w/s rMB/s wMB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sdb 0.00 193.00 0.00 111.73 0.00 4.00 0.00 2.03 0.00 5.94 1.15 0.00 592.80 1.60 30.80
avg-cpu: %user %nice %system %iowait %steal %idle
3.10 0.00 1.33 94.33 0.00 1.24
Device r/s w/s rMB/s wMB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sdb 0.00 561.00 0.00 499.09 0.00 3.00 0.00 0.53 0.00 9.29 5.21 0.00 911.00 1.78 100.00
avg-cpu: %user %nice %system %iowait %steal %idle
5.92 0.00 1.43 91.49 0.00 1.17
Device r/s w/s rMB/s wMB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sdb 0.00 554.00 0.00 498.58 0.00 2.00 0.00 0.36 0.00 9.15 5.07 0.00 921.56 1.81 100.00
- 결론
threshold 또는 cemtosecs의 값이 작으면 flush 커널 스레드가 너무 자주 꺠어나서 스케줄링에 대한 오버헤드가 발생할 수 있습니다. 반대로 값이 크면 I/O %util 값이 높아집니다.
- Reference
https://medium.com/@hyoje420/linux-ftrace-%EC%82%AC%EC%9A%A9%EB%B2%95-31b4dc7ac93c
https://www.xml.com/ldd/chapter/book/ch02.html
https://kkikyul.tistory.com/90
https://oslab.kaist.ac.kr/wp-content/uploads/esos_files/publication/conferences/korean/WC_ojt.pdf
https://www.quora.com/Where-does-VFS-lie-in-the-Linux-Kernel
https://www.slideshare.net/AdrianHuang/linux-kernel-virtual-file-system
https://students.mimuw.edu.pl/ZSO/Wyklady/08_VFS1/VFS-1.pdf
https://lwn.net/Articles/404439/
https://junsoolee.gitbook.io/linux-insides-ko/summary/concepts/linux-cpu-1
https://www.kernel.org/doc/Documentation/this_cpu_ops.txt
https://lwn.net/Articles/648292/
https://zhuanlan.zhihu.com/p/93480009
https://blog.csdn.net/weixin_33963189/article/details/92562519
'System Engineering > Linux' 카테고리의 다른 글
[커널이야기] TCP handshake와 TIME_WAIT 소켓 (0) | 2023.09.23 |
---|---|
[Linux] NTPv4(RFC5905)와 chrony 그리고 timex (0) | 2023.07.21 |
[커널이야기] 리눅스 메모리 1 - 메모리를 확인하는 방법과 slab/swap 메모리 (2) | 2023.06.15 |
[커널이야기] Load Average로 시스템 콜 추적하기 (0) | 2023.06.09 |
폐쇄망 환경에서 휴대폰 테더링으로 yum/apt 사용하기 (0) | 2023.04.20 |