Linux Internal
지난번 글을 통해서 dirty page가 무엇인지 그리고 각각의 파라미터가 의미하는 것은 무엇인지 살펴봤습니다. dirty page의 동기화에는 크게 백그라운드 동기화, 주기적인 동기화, 명시적인 동기화 세 가지 방법이 있습니다.
첫 번째로 백그라운드 동기화는 dirty page의 생성량이 일정 수준을 넘으면 백그라운드로 flush 커널 스레드가 동작하면서 동기화하는 방식입니다. 이전 글에서 얘기했던 것처럼 vm.dirty_background_ratio(bytes), vm.dirty_ratio(bytes)로 동작을 조절할 수 있습니다.
두 번째로 주기적인 동기화는 일정 시간을 주기로 flush 커널 스레드가 깨어나서 특정 조건에 해당하는 dirty page를 동기화하는 방식입니다. vm.dirty_expire_centisecs, vm.dirty_writeback_centisecs로 동작을 조절할 수 있습니다.
마지막으로 명시적인 동기화는 sync 등의 명령어를 통해 명시적으로 동기화시키는 방식입니다.
이번 글에서는 이 중에서도 백그라운드와 주기적인 동기화에 대해 이야기해 보겠습니다.
ftrace를 켜고 io_test 프로그램을 실행시킵니다. 실행시킨 후에는 해당 프로세스 이름으로 trace_pipe 파일을 지켜보겠습니다.
cat -v /sys/kernel/debug/tracing/trace_pipe | grep io_test > ~/dirty_trace
dirty page가 생성되는 것을 확인한 후 몇 초 후에 덤프 파일을 확인해 보면 여러 가지 함수들이 보이는데요 그중에서 아래와 같은 함수가 보이는 것을 확인할 수 있습니다.
뭔가 냄새가 나는 함수입니다. 이름에서도 느껴지듯이 dirty page들의 생성량을 측정하는 듯한 느낌이 납니다. balance라는 단어가 있으니까요. 그럼 해당 함수의 소스 코드를 살펴볼까요?
가운데 if 문을 보면 nr_pages_dirtied 된 값들이 ratelimit라는 값보다 크면 balance_dirty_pages() 함수를 호출하도록 되어 있습니다. 그럼 ratelimit 값은 어떻게 결정이 되는 걸까요? ratelimit_pages가 어떤 값으로 설정되는지를 먼저 살펴보겠습니다.
전체 메모리를 기반으로 계산된 페이지의 수를 CPU 값과 32라는 특별한 값으로 나눠서 설정하고 있습니다. 메모리가 많은 서버일수록 vm_total_pages의 수가 클 거고, CPU가 많은 서버일수록 num_online_cpus()의 값이 커질 겁니다. 이렇게 계산된 값이 4096 * 1024보다 크다면 값이 조정이 됩니다.
그래서 대부분의 시스템은 ratelimit_pages가 1024가 됩니다.
PAGE_CACHE_SIZE의 값도 대부분 4096이기 때문입니다.
다시 balance_dirty_pages_ratelimited_nr() 함수로 돌아와서, ratelimit 값이 1024가 되기 때문에 결국 1024개의 페이지에 쓰기 작업이 발생했을 때, 즉 최소한 4MB 정도의 쓰기 작업이 이루어졌을 때에야 비로소 balance_dirty_pages() 함수를 호출하게 됩니다. 이 함수는 쓰기 작업이 발생할 때마다 계속 호출되며, 간단한 비교 로직만 있기 때문에 성능에 큰 영향을 주지 않습니다. 만약 이렇게 조절하지 않고 쓰기 작업이 있을 때마다 전 시스템의 dirty page 값을 확인하게 된다면 시스템의 성능에 영향을 줄 수 있기 때문에 이렇게 한 번 정도 거를 수 있는 함수를 만든 것으로 보입니다.
그럼 이번엔 balance_dirty_pages() 함수를 살펴볼까요? 실질적인 dirty page 동기화의 핵심이라고 부를 수 있는 함수입니다.
backing_dev_info는 파일 쓰기가 일어나는 디바이스에 대한 포인터입니다. 그리고 밑에 보면 get_dirty_limits() 함수가 보이고 우리에게 낯익은 background라는 단어와 dirty라는 단어가 보입니다. 저 함수를 한 번 따라가 보겠습니다.
우리가 설정한 커널 파라미터 값이 실제로는 이 함수에서 활용이 되는 것을 볼 수 있습니다. 한데 재밌는 로직이 있습니다. vm_dirty_bytes와 dirty_background_bytes 값이 설정되어 있다면 각각 ratio와 관련된 값은 무시되는 것을 볼 수 있습니다. 즉, bytes로 설정하든 아니면 ratio로 설정하든 둘 중에 하나밖에 설정할 수 없다는 것을 볼 수 있습니다. 그리고 dirty_ratio의 최솟값은 5라는 것도 확인할 수 있습니다. vm.dirty_ratio를 5보다 작게 설정한다면 5로 맞춰지게 되는 것입니다. 마지막으로 background_ratio가 dirty_ratio보다 크면 어떻게 될까 라고 궁금하신 분들을 위한 로직입니다. background_ratio가 dirty_ratio보다 크다면 dirty_ratio의 절반에 해당하는 값으로 자동 조절되는 로직입니다. 위 함수를 통해 우리는 커널 파라미터와 관련된 몇 가지 방어 로직을 확인할 수 있습니다.
다시 balance_dirty_pages() 함수로 돌아와 보겠습니다.
함수가 너무 길어서 중간 부분을 잘랐습니다. if 문에 설정된 조건들을 모두 통과해서, 즉 dirty pages의 크기가 일정량 이상으로 유지된다면 해당 프로세스를 잠시 재우는 것을 볼 수 있습니다. 너무 많은 dirty pages가 있으니 잠시 쉬고 그동안 flush 커널 스레드가 dirty page를 동기화시키는 시간을 벌어주는 로직입니다. 이것을 I/O Throttling 기법이라고 하며, 성능 저하의 가장 큰 원인이 되는 부분입니다. 백그라운드에서 dirty page를 비우는 속도보다 dirty page의 생성 속도가 커진다면 I/O 때문에 성능이 저하될 수 있다는 것을 알 수 있습니다.
이제 balance_dirty_pages() 함수의 마지막 부분입니다. 시스템의 전체 dirty page의 크기가 background ratio로 계산된 값보다 크다면 백그라운드에서 dirty page를 비울 flush 커널 스레드를 깨우는 코드입니다. bdi_start_background_writeback() 함수를 살펴보겠습니다.
내부적으로 __bdi_start_writeback() 함수를 호출해서 dirty page 동기화 작업을 시작하게 됩니다.
이 함수는 진행해야 할 동기화 작업에 대한 몇 가지 구조체의 값을 채우고 bdi_queue_work() 함수를 통해 작업을 등록합니다.
그리고 bdi_queue_work() 함수는 결과적으로 wake_up_process() 함수를 이용해서 해당 디바이스에 등록된 동기화 작업 커널 스레드를 깨우게 됩니다. 그럼 이때 깨우는 flush 커널 스레드의 시작점은 어떻게 알 수 있을까요? 무식한 방법이지만 kthread_run과 flush라는 문자열로 커널 코드를 grep을 통해 살펴 보면 bdi_forker_task() 라는 함수를 만날 수 있습니다.
kthread_run() 함수를 통해 flush- 라는 문자열로 시작하는 커널 쓰레드를 등록하는 과정과 해당 쓰레드가 실행하게 될 함수를 정의한 부분을 확인할 수 있습니다. 그리고 bdi_start_fn() 함수를 찾아보면 아래와 같은 로직을 만날 수 있습니다.
즉, 결과적으로는 wb_do_writeback() 함수가 호출되어 dirty page 동기화 작업이 진행된다는 것을 확인할 수 있습니다. wb_do_writeback() 함수를 잘 기억해 주세요.
이번엔 주기적인 동기화입니다. vm.dirty_writeback_centisecs에 설정한 값에 맞춰서 flush 데몬이 잘 깨어나는지 그리고 깨어나면 어떤 함수들을 호출해 나가는지 살펴보겠습니다. 이번에도 ftrace를 설정하고 아래와 같이 grep 구문으로 잡아 보겠습니다.
cat -v /sys/kernel/debug/tracing/trace_pipe | grep "flush-" > flush_trace
그러면 곧 아래와 같은 함수의 호출 과정을 확인할 수 있습니다.
보시면 다시 실행되는데 5초가 소요되는 것을 확인할 수 있습니다. (101677 -> 101682) 그리고 스케쥴러에 의해 깨어난 후 bdi_writeback_task()라는 함수가 호출되고 연이어 wb_do_writeback() 함수가 호출되고 있습니다. 그런데 저 함수는 위에서 백그라운드 동기화에서 호출되는 flush 커널 스레드가 호출하는 함수입니다.
결국 백그라운드 동기화나 주기적인 동기화나 모두 flush 커널 스레드가 호출되고 wb_do_writeback() 함수가 호출된다는 것을 알 수 있습니다.
두 경우 깨어나는 조건이 다를 뿐 결국 같은 커널 스레드가 호출되고 같은 일을 한다는 것을 확인할 수 있습니다.
그리고 wb_do_writeback() 함수는 wb_writeback() 함수와 wb_check_old_data_flush() 함수를 각각 호출해서 dirty page 동기화를 진행한다는 것도 알 수 있습니다. 이는 다시 생각해 보면 백그라운드 동기화로 깨어난 flush 커널 스레드가 background ratio에 맞게 dirty page를 동기화하는 것뿐 아니라 생성된 지 특정 시간 이상이 된 dirty page 동기화도 함께 진행한다는 것을 알 수 있습니다.
마지막으로 wb_check_old_data_flush() 함수를 살펴보겠습니다.
dirty_writeback_interval 값, 커널 파라미터로 vm.dirty_writeback_centisecs에 해당하는 그 값이 0이 되면 wb_check_old_data_flush()는 아무 작업도 하지 않습니다. 그래서 주기적인 동기화를 중지 시키려면 해당 값을 0으로 설정하면 됩니다.
지금까지 커널 소스 코드를 살펴보면서 우리가 설정한 커널 파라미터들이 코드에 어떻게 적용이 되어 있고 어떤 역할들을 하는지 살펴봤습니다. 몇 가지 기억해야 할 것들은 아래와 같습니다.
vm.dirty_backgound_ratio가 vm.dirty_ratio보다 크다면 vm.dirty_ratio의 절반으로 변경됩니다.
dirty bytes와 관련된 커널 파라미터가 설정된다면 dirty ratio와 관련된 커널 파라미터는 무시됩니다.
vm.dirty_ratio의 최솟값은 5입니다.
vm.dirty_ratio 이상의 값으로 dirty page가 생성된다면 커널은 해당 프로세스를 잠시 중지시키고 dirty page 동기화 작업을 시킵니다. 여기서 성능 저하가 일어나게 됩니다.
vm.dirty_writeback_centisecs가 0이면 주기적인 동기화를 하지 않습니다.
백그라운드 동기화와 주기적인 동기화는 완전 별개로 동작하지 않고 상호 보완적으로 동작하게 됩니다.
dirty page 동기화 과정을 알고 커널 파라미터가 어떤 영향을 끼치는지 알고 있는 것은 성능 튜닝에 큰 도움이 됩니다. 주기적으로 IO가 튀는 현상이 발견된다면 우선적으로 dirty page 동기화 과정에 대해 의심할 수 있을 것이고 커널 파라미터의 변경을 통해서 변화하게 되는 동작을 예상하고 이 동작이 성능을 향상하는데 도움이 될 수 있을지 없을지를 판단할 수 있기 때문입니다.
의도치 않게 긴 글이 되었는데요, 이 글이 많은 분들께 도움이 되었으면 좋겠습니다. ^^