Skip to main content

dirty page가 I/O에 끼치는 영향

아래 내용은 DevOps와 SE를 위한 리눅스 커널 이야기 10장 dirty page가 I/O에 끼치는 영향을 읽고 정리한 내용입니다.

devops-se-ch-10-1

리눅스에서 파일 I/O가 일어날 때 커널은 PageCache를 이용해서 디스크에 있는 파일의 내용을 메모리에 잠시 저장하고 필요할 때마다 메모리에 접근해서 사용한다. dirty page는 PageCache에 있는 페이지 중에서도 쓰기 작업이 이루어진 메모리이다. 하지만 dirty page로 표시된 메모리들을 dirty page가 생성될 때마다 디스크에 쓰면 이 또한 상당량의 쓰기 I/O를 일으켜서 성능이 저하된다.

그래서 커널은 몇가지 조건을 만들어서 해당 조건을 만족시키면 dirty page를 디스크로 동기화한다. 이 과정을 page writeback이라고 하고 dirgy page 동기화라고도 한다.

커널 버전에 따라서 다르겠지만 보통 flush라는 단어가 들어간 커널 스레드(pdflush, flush, bdflush)가 이 작업을 진행한다. 그래서 I/O가 많이 발생하는 서버에서는 dirty page를 언제 얼마나 동기화시키느냐가 중요한 성능 튜닝의 요소가 된다.

dirty page 관련 파라미터

[root@ip-172-31-35-50 ~]$ sysctl -a | grep -i dirty
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 500
vm.dirty_expire_centisecs = 3000
vm.dirtytime_expire_seconds = 43200
  • vm.dirty_background_ratio
  • vm.dirty_background_bytes
  • vm.dirty_ratio
  • vm.dirty_bytes
  • vm.dirty_writeback_centisecs
  • vm.dirty_expire_centisecs

dirty page 동기화

  • 백그라운드 동기화: 애플리케이션 dirty page를 생성할 때마다 현재까지 생성된 dirty page와 전체 메모리의 비율을 바탕으로 진행된다.
  • 주기적인 동기화: dirty page를 동기화하기 위해 필요한 flush 데몬을 깨우는 주기와 깨웠을 때 동기화시키는 dirty page의 기준을 설정할 수 있다.
  • 명시적인 동기화: sync, fsync 등의 명령을 이용하면 현재 생성되어 있는 dirty page를 명시적으로 디스크에 쓰는데, 이런 작업을 명시적인 동기화라고 표현한다.
[root@ip-172-31-35-50 tracing]$ mount -t debugfs debugfs /sys/kernel/debug
[root@ip-172-31-35-50 tracing]$ pwd
/sys/kernel/debug/tracing
[root@ip-172-31-35-50 tracing]$ echo function < ./current_tracer

# trace_pipe 파일을 통해 커널 함수 호출 확인-------------------------------------
[root@ip-172-31-35-50 tracing]$ cat -v ./trace_pipe

# 커널 파리미터 조정----------------------------------------------------------
# 주기적인 동기화 제거
[root@ip-172-31-35-50 tracing]$ sysctl -w vm.dirty_writeback_centisecs=0
vm.dirty_writeback_centisecs = 0
# 빠른 백그라운드 동기화
[root@ip-172-31-35-50 tracing]$ sysctl -w vm.dirty_background_ratio=1
vm.dirty_background_ratio = 1
초당 1MB 쓰기 작업
#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;
}
dirty page 변화 확인
#!/bin/bash

while true
do
cat /proc/meminfo | grep -i dirty
sleep 1
done
# 전체 메모리 크기 0.9GB
[root@ip-172-31-35-50 ~]$ cat /proc/meminfo
MemTotal: 987700 kB

# vm.dirty_background_ratio가 1이므로
# 전체 메모리 크기의 약 1% 공간동안 dirty page가 쌓일 경우 초기화
[root@ip-172-31-35-50 ~]$ sysctl -a | grep -i vm.dirty_background_ratio
vm.dirty_background_ratio = 1

# 약 7MB 수준의 dirty page가 쌓이면 백그라운드 동기화 시작
[root@ip-172-31-35-50 ~]$ sh show_dirty.sh
Dirty: 5104 kB
Dirty: 6144 kB
Dirty: 7152 kB
Dirty: 0 kB
Dirty: 1008 kB
Dirty: 2048 kB
Dirty: 3056 kB
Dirty: 4096 kB
Dirty: 5104 kB
Dirty: 6144 kB
Dirty: 7152 kB
Dirty: 0 kB
ftrace 결과 확인하기
[root@ip-172-31-35-50 tracing]$ pwd
/sys/kernel/debug/tracing

[root@ip-172-31-35-50 tracing]$ cat -v ./trace_pipe | grep -i balance_dirty
dirty_page-7645 [000] .... 164783.682984: balance_dirty_pages_ratelimited <-iomap_write_actor
dirty_page-7645 [000] .... 164783.682992: balance_dirty_pages_ratelimited <-iomap_write_actor
dirty_page-7645 [000] .... 164783.683400: balance_dirty_pages_ratelimited <-iomap_write_actor
dirty_page-7645 [000] .... 164783.683714: balance_dirty_pages_ratelimited <-iomap_write_actor
dirty_page-7645 [000] .... 164783.683721: balance_dirty_pages_ratelimited <-iomap_write_actor
dirty_page-7645 [000] .... 164783.683721: balance_dirty_pages <-balance_dirty_pages_ratelimited
dirty_page-7645 [000] .... 164783.683722: mem_cgroup_wb_domain <-balance_dirty_pages
dirty_page-7645 [000] .... 164783.683722: domain_dirty_limits <-balance_dirty_pages
dirty_page-7645 [000] .... 164783.683722: dirty_poll_interval.part.0 <-balance_dirty_pages

dirty page가 생성될 때마다 시스템의 모든 dirty page를 검사하고 확인하는 과정을 거치면 오버헤드가 꽤 크기 때문에 일정 수준 이상이 되었을 때만 확인하는 과정을 거치도록 비율을 이용해 제한을 주는 함수가 balance_dirty_pages_ratelimited 함수이다.

그래서 ratelimit라는 변수의 값을 이용해 해당 프로세스가 생성하는 dirty page의 크기가 일정 수준을 넘어서면 그때서야 비로소 balance_dirty_pages() 함수를 호출해서 시스템의 모든 dirty page의 크기를 바탕으로 동기화가 필요한지 여부를 확인한다. 아주 적은 양의 dirty page를 생성했는데 전체 시스템의 dirty page 크기를 계산해서 비교하게 되면 그것은 그것대로 시스템의 부하를 일으킬 수 있기 때문이다.

이후로 본문에서는 각 함수들의 소스 코드를 살펴보면서 방어 로직 구성을 파악하는데 이는 vm.dirty_background_ratiovm.dirty_ratio 값들의 활용 방법이 중요하기 때문이다.

[root@ip-172-31-35-50 tracing]$ sysctl -w vm.dirty_background_ratio=20
vm.dirty_background_ratio = 20

[root@ip-172-31-35-50 tracing]$ sysctl -w vm.dirty_ratio=40
vm.dirty_ratio = 40

# 5초에 한 번 flush 커널 스레드 깨우기
[root@ip-172-31-35-50 tracing]$ sysctl -w vm.dirty_writeback_centisecs=500
vm.dirty_writeback_centisecs = 500

# 생성된지 10초가 넘은 dirty page 동기화
[root@ip-172-31-35-50 tracing]$ sysctl -w vm.dirty_expire_centisecs=1000
vm.dirty_expire_centisecs = 1000


[root@ip-172-31-35-50 ~]$ sh show_dirty.sh
Dirty: 11272 kB
Dirty: 12280 kB
Dirty: 13320 kB
Dirty: 14328 kB
Dirty: 15368 kB
Dirty: 1016 kB
Dirty: 2068 kB

1초에 1MB씩 쓰기 작업을 하기 때문에 flush 커널 스레드가 깨어나는 타이밍과 맞을 경우 10~15MB 사이에서 dirty page가 유지된다.

dirty page 설정과 I/O 패턴

dirty page 관련 커널 파라미터 설정
[root@ip-172-31-35-50 tracing]$ sysctl -a | grep -i dirty
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 500
vm.dirtytime_expire_seconds = 43200
for문 코드 수정
#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);

if (count%1000 == 0) {
output_fd = open(file_name, O_CREAT | O_RDWR | O_TRUNC);
}

if (count >= 5000)
break;
}
return 0;
}
I/O 사용률 확인
[root@ip-172-31-35-50 ~]$ iostat -x
Linux 5.10.109-104.500.amzn2.x86_64 (ip-172-31-35-50.ap-northeast-2.compute.internal) 10/05/2023 _x86_64_ (1 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
0.26 0.00 0.17 0.10 0.02 99.44

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvda 0.00 0.21 0.18 1.31 4.18 129.42 179.07 0.05 30.26 0.78 34.27 1.51 0.22

위 테스트를 통해서 dirty page 관련 커널 파라미터 설정을 변경하면서 flush 커널 스레드가 깨어나는 주기를 바꿔가며 그때마다 iostat 명령으로 살펴본 util(%)이 얼마나 높아지는지 확인할 수 있다.


dirty page 동기화와 관련해서 가장 중요한 부분은 flush 커널 스레드를 얼마나 자주 깨울 것인지, 깨울 때 어느 정도의 양을 동기화할지를 설정하는 것이다.

자주 깨어나면 ioutil(%)이 비교적 적지만 flash 커널 스레드가 자주 깨어나는 단점이 있고 늦게 꺠우면 flush 커널 스레드는 자주 깨어나지 않지만 io util이 높아지는 단점이 있다. flush 커널 스레드가 너무 자주 깨어나면 스케줄링에 대한 오버헤드가 발생할 수 있으며 멀티 스레드 환경의 애플리케이션의 경우 불필요하게 자주 깨어나는 flush 커널 스레드에 cpu 리소스를 빼앗길 수 있기 때문에 성능 저하가 발생할 수 있다.

같은 애플리케이션을 사용하더라도 운영하고 있는 시스템에 따라 dirty page 동기화는 다른 전략을 취해야 하며 다양한 값을 설정해 가면서 모니터링해 최적의 값을 찾는 방법이 가장 좋다고 저자는 말한다.