본문 바로가기
Linux

[Linux] 페이지 캐시 (Page Cache)

by A6K 2024. 2. 2.

리눅스 커널은 파일에 대한 접근 속도 개선을 위해 '페이지 캐시(Page Cache)'를 이용한다. 이번 포스트에서는 리눅스 커널의 페이지 캐시에 대해 알아보고 페이지 캐시를 다루고 모니터링하는 방법에 대해 정리해본다.

목차

    페이지 캐시(Page Cache)

    리눅스의 메모리 관리 시스템은 최대한 메인 메모리를 잘 활용하도록 만들어져 있다. 사용자의 프로세스만 메인 메모리에 데이터를 저장하는 것이 아니고 리눅스 커널도 적극적으로 메인 메모리를 활용한다. 그 중에 '페이지 캐시(Page Cache)'라는 것도 있다. 리눅스는 파일의 입출력 성능 향상을 위해 페이지 캐시(Page Cache)라는 메모리 영역을 만들어 사용한다.

    데이터를 저장하고 있는 파일은 HDD 혹은 SSD에 저장된다. 이런 저장장치들은 메인메모리(RAM)에 비해 매우 느린 속도를 가지고 있다. 만약 CPU 위에서 동작하는 프로그램이 파일에 저장되어 있는 데이터를 매번 읽고 쓴다면 대부분의 시간동안 속도가 느린  HDD나 SSD가 데이터를 읽거나 쓰기를 기다리고 있을 것이다. 이는 굉장한 자원 낭비다.

    리눅스는 매번 저장장치에 읽기와 쓰기 요청을 보내는 대신 메인 메모리의 한 영역에 캐싱을 한다. 쓰기의 경우 새로운 파일 컨텐츠가 메인 메모리에 있는 페이지 캐시에 먼저 반영된 이후 적당한 시간에 디스크로 전달된다. 따라서 시스템은 파일의 컨텐츠에 대한 수정을 메모리 위에서 할 수 있고, 이런 수정들을 모아서 한번의 I/O 연산으로 디스크에 쓸 수 있게 된다. 비슷하게 읽기에 대해서도 한번 읽어서 메모리에 로드한 다음 이후 요청되는 읽기 연산은 디스크에서 읽지 않고 메모리에서 읽어 전달할 수 있게 된다.

    리눅스는 이렇게 입출력을 최적화하기 위해 메인 메모리의 한 공간을 사용하는데 이 곳의 이름이 '페이지 캐시(Page Cache)'다. 파일에 대한 입출력이 요청되면 파일의 데이터는 페이지 캐시 영역에 로드되어 관리된다. 페이지 캐시는 사용자가 명시적으로 페이지를 정리하라고 요청을 하거나 새로운 메모리 요청이 도착할 때까지 메모리 위에 존재한다.

    페이지 캐시와 유사한 개념으로 버퍼링이 있다. 버퍼링은 프로세스가 버퍼라고 하는 메모리 영역에 데이터를 미리 모아두는 것을 의미한다. 버퍼 메모리는 두 프로세스 혹은 디바이스 사이에 데이터를 전송하기 전에 미리 데이터를 모아두는 메인메모리 상의 공간을 의미한다. 주로 전송속도가 다른 두 디바이스나 네트워크 상의 프로세스 사이에 데이터를 주고 받을 때 사용한다. 버퍼링은 데이터의 전송과 재조립 과정에서 유용하게 사용된다. 디스크에 쓰기 연산을 하거나 소켓을 통해 데이터를 전송할 때, CPU에서 나오는 데이터를 모아서 한번에 보내기 위한 목적으로 사용하는 메모리 공간을 버퍼(Buffer)라고 한다.

    페이지 캐시 조회

    free 명령을 이용하면 페이지 캐시 정보를 조회할 수 있다.

    $ free -h --si
                  total        used        free      shared  buff/cache   available
    Mem:           131G         14G        113G        2.7M        3.7G        116G
    Swap:            0B          0B          0B

    free 명령이 출력하는 정보를 보면 현재 시스템에서 3.7 GB를 버퍼와 페이지 캐시로 활용하고 있음을 알 수 있다.

    top 명령을 이용해도 유사한 정보를 확인할 수 있다. top 명령을 실행하면 화면 상단에 다음과 같은 정보가 출력된다.

    %Cpu(s):  0.0 us,  0.0 sy,  0.0 ni, 99.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    KiB Mem : 13142759+total, 11324227+free, 14449808 used,  3735508 buff/cache
    KiB Swap:        0 total,        0 free,        0 used. 11652606+avail Mem

    KiB Mem으로 시작하는 라인을 보면 buff/cache 항목에서 용량을 확인할 수 있다.

    리눅스는 시스템의 다양한 정보를 확인할 수 있는 특수 파일들을 제공한다. 페이지 캐시 정보는 /proc/meminfo 파일을 통해 조회할 수 있다.

    $ cat /proc/meminfo | grep ^Cached
    Cached:          3167416 kB

    페이지 캐시 드랍

    리눅스 서버를 운영하다보면 메모리 상황이 빡빡하게 돌아가는 경우가 있다. 이 경우 커널에서 성능향상을 위해 사용하고 있는 페이지 캐시 영역을 드랍해서 가용 메모리를 확보해야 하는 경우가 가끔있다. 혹은 서버의 입출력 성능 테스트를 하는 경우 페이지 캐시를 드랍하는 경우도 있다.

    리눅스 시스템에서는 /proc/sys/vm/drop_caches 파일을 통해 페이지 캐시를 드랍할 수 있다. 이 파일에 숫자 1을 쓰면 페이지 캐시를 드랍하게 된다.

    $ echo 1 | sudo tee /proc/sys/vm/drop_caches > /dev/null

    숫자 2를 쓰면 디렉토리 엔트리(dentries)와 inode들을 드랍한다.

    $ echo 2 | sudo tee /proc/sys/vm/drop_caches > /dev/null

    숫자 3을 쓰면 페이지 캐시와 디렉토리 엔트리, inode를 모두 드랍한다.

    $ echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null

    주의할 점은 페이지 캐시 드랍요청은 '더티 페이지(Dirty Page)'에는 적용되지 않는다는 점이다. 더티 페이지란 페이지 캐시에는 수정되어있지만 아직 디스크에는 반영되지 않은 페이지를 의미한다.

    이 때문에 파일의 컨텐츠 수정이 빈번한 경우 캐시를 드랍하는 명령이 효과적으로 동작하지 않을 수 있다. 이럴 경우 sync  명령을 이용해 페이지 캐시의 데이터를 디스크로 쓰도록 요청한 다음 페이지 캐시를 드랍하면 된다.

    $ sudo bash -c 'sync; echo 3 > /proc/sys/vm/drop_caches'

    추가로 메모리 락도 고려해야 한다. 리눅스 시스템은 특정 파일의 컨텐츠가 메모리에 락 될 수 있는 기능을 제공한다. 메모리에 락되어 있는 페이지는 드랍되지 않는다.

    예를 들어보자. vmtouch 명령을 이용해 파일의 데이터를 메모리에 캐싱하고 락을 할 수 있다.

    $ vmtouch -tl test.dat
    LOCKED 492712 pages (1G)

    이후 페이지 캐시를 드랍하도록 명령을 실행해도 큰 효과가 없는 것을 확인할 수 있다.

    $ cat /proc/meminfo | grep ^Cached &&
    sudo bash -c 'sync; echo 3 > /proc/sys/vm/drop_caches' &&
    cat /proc/meminfo | grep ^Cached
    
    Cached:          3167416 kB
    Cached:          3000548 kB

    만약 락되어 있는 메모리 영역을 강제로 회수하고 싶다면, 메모리에 락을 확보하고 있는 PID를 찾아서 kill하고 페이지 캐시를 드랍해야한다.

    다행히 리눅스 시스템에는 특정 프로세스가 얻을 수 있는 메모리 락의 용량 제한을 할 수 있다.

    $ ulimit -Ha | grep locked
    max locked memory           (kbytes, -l) 2033684

    페이지 캐시 모니터링

    리눅스 커널이 디스크에 있는 파일 컨텐츠를 메모리의 페이지 캐시에 로드하고 삭제하는 과정을 모니터링할 수 있다. 시스템을 프로파일링 할 수 있는 perf 명령을 이용하면 된다.

    $ sudo perf record -a -e filemap:mm_filemap_add_to_page_cache,filemap:mm_filemap_delete_from_page_cache sleep 120

    perf 명령을 이용해서 mm_filemap_add_to_page_cache, mm_filemap_delete_from_page_cache 이벤트를 모니터링할 수 있다. 전자는 페이지 캐시에 데이터가 새로 캐싱되는 이벤트고, 후자는 페이지 캐시에서 특정 페이지가 삭제(evict)되는 이벤트다. 마지막 sleep 120은 120초 동안 이벤트를 기록하겠다는 의미다.

    시스템에서 디스크 입출력을 하는 자바 프로세스가 실행되는 도중에 perf 명령을 실행해봤다. 그리고 perf 명령을 이용해 프로파일링 된 이벤트 정보들을 확인할 수 있다.

    $ sudo perf script
    ...
                java 8423 [022] 13771.892867:      filemap:mm_filemap_add_to_page_cache: dev 8:4 ino 8e0042 page=0xffffdee17f6b1b40 pfn=18446707658819574592 ofs=0
                java 8423 [022] 13771.892874:      filemap:mm_filemap_add_to_page_cache: dev 8:4 ino 8e0042 page=0xffffdee181c90fc0 pfn=18446707658859286464 ofs=4096
                java 7932 [016] 13771.979079: filemap:mm_filemap_delete_from_page_cache: dev 8:4 ino 8e0067 page=0xffffdee17f6b1b40 pfn=18446707658819574592 ofs=0
                java 7932 [016] 13771.979082: filemap:mm_filemap_delete_from_page_cache: dev 8:4 ino 8e0067 page=0xffffdee181c90fc0 pfn=18446707658859286464 ofs=4096
    ...

    perf script로 확인할 수 있는 정보는 다음과 같다. 8423 PID의 java 프로세스가 동작했으며, 8e0042 inode의 파일의 0 offset에 해당하는 페이지가 18446707658819574592 페이지에 새롭게 매핑되었다. 4096 offset에 해당하는 페이지는 18446707658859286464 페이지에 매핑되었다.

    결론

    리눅스 시스템의 메모리가 모자라는 상황에서 페이지 캐시를 드랍하는 것이 당장 문제를 해결할 수 있는 방법인 경우가 많다. 하지만 커널은 적극적으로 페이지 캐시를 사용하고, 회수해서 재사용하기 때문에 시스템에서 페이지 캐시로 활용되는 메모리가 증가하는 현상이 문제가 있음을 의미하지는 않는다.

    만약 페이지 캐시가 비정상적으로 커지고 드랍도 안되는 경우라면 어떤 프로세스에서 비정상적으로 메모리를 사용하는 설정이 있는지 확인해봐야 한다.

    댓글