OOM report의 memory 통계로 회수할 수 없는 부분 찾기

OOM report에는 global, node, zone, buddy allocator의 통계가 연달아 나옵니다. 비슷한 필드 이름이 반복되지만 범위와 단위가 같지는 않습니다. 각 묶음을 모두 더하면 memory 총량이 되는 표도 아닙니다.

Linux v6.12의 show_free_areas()와 show_mem()을 기준으로, 어떤 수치가 다음 질문으로 이어지는지 살펴보겠습니다. 실제 report와 다른 version의 설명을 비교하실 때는 필드와 출력 단위를 먼저 맞추어 주세요.

Global page count 묶음과 node·zone별 KiB 표기를 혼동하지 않는 것이 출발점입니다. Header와 출력 코드를 확인한 뒤 필요한 경우 base page 크기로 환산하시면 됩니다. SysRq의 m 요청으로 유사한 memory 진단 정보를 kernel log에 생성할 수도 있지만, 이는 단순한 파일 조회가 아니라 진단 출력을 발생시키는 trigger입니다.

Global LRU 통계는 backing과 상태를 함께 봅니다

active_anon과 inactive_anon은 anonymous 쪽 LRU, active_file과 inactive_file은 file 쪽 LRU에 연결됩니다. Active는 최근 사용과 reclaim 정책에 관한 분류이며 영구적으로 회수되지 않는다는 뜻은 아닙니다. Shmem은 file 형태라도 swap-backed 성격 때문에 일반 file cache와 같은 방식으로 분류되지 않습니다.

isolated_anon과 isolated_file은 reclaim이나 migration 등의 처리를 위해 LRU에서 떼어 놓은 pages를 살펴보는 데 도움이 됩니다. 값이 크다면 잠시 조사 중인 memory가 많은지와 진행이 지연되는지 확인할 단서가 됩니다. 수치 하나만으로 memory leak를 확정할 수는 없습니다.

unevictable과 mlocked는 memory locking과 연결되지만 항상 같은 순간에 같은 값이 되는 것은 아닙니다. Locking 상태와 회수 대상 분류의 전환 시점, 다른 unevictable 조건을 구분해야 합니다. 높은 locking 사용량은 전체 reclaim 가능량을 줄일 수 있습니다.

Dirty와 writeback을 서로 다른 상태로 추적합니다

dirty는 아직 저장할 변경분이 남은 file data, writeback은 writeback 작업 중인 pages를 이해하는 데 쓰입니다. Dirty가 많고 writeback 진행이 따라가지 못하면 allocation과 쓰기 지연을 함께 살펴보아야 합니다.

그렇지만 한 page가 writeback 중에 다시 수정될 수 있으므로 두 통계가 언제나 완전히 배타적이라고 가정하시면 안 됩니다. 출력 두 항목을 더하면 곧바로 서로 다른 physical pages의 개수가 된다는 보장도 없습니다. Kernel의 dirty/writeback 상태 전환과 counter 갱신을 기준으로 해석해야 합니다.

Dirty throttling의 기준은 단순히 설치된 RAM 전체의 일정 비율이라고만 설명할 수 없습니다. Dirtyable memory와 writeback domain의 계산이 관여합니다. OOM 시점의 단일 통계에서는 I/O가 완료되지 못한 이유까지 드러나지 않으므로, 저장 장치와 filesystem 상태도 함께 확인해야 합니다.

Slab과 page table의 비용도 확인합니다

slab_reclaimable은 shrinker 등을 통해 회수를 시도할 수 있는 slab 쪽 memory를, slab_unreclaimable은 그러한 방식으로 회수할 수 없는 부분을 살펴보는 출발점입니다. Reclaimable이라는 분류가 즉시 전량 회수 가능하다는 뜻은 아닙니다.

Unreclaimable slab이 유난히 크다면 어떤 cache가 증가하는지 /proc/slabinfo 등으로 좁힐 수 있습니다. Kernel object와 driver의 사용량이 관련될 수도 있지만, 큰 수치 자체만으로 특정 driver를 원인으로 지목해서는 안 됩니다. 정상적인 workload의 metadata 비용일 가능성도 함께 봐야 합니다.

pagetables는 user mapping을 관리하는 page table 자체의 비용입니다. 작은 data pages가 넓은 주소 범위에 퍼져 있거나 process가 많이 늘어나는 상황에서는 data RSS만 보는 것과 다른 그림이 나타날 수 있습니다. kernel_stack도 process와 thread의 kernel 측 비용을 볼 때 참고할 항목입니다.

Node 통계의 shmem_thp와 shmem_pmdmapped는 큰 allocation으로 존재하는 shmem과 실제 PMD mapping된 부분을 구분하는 데 쓰입니다. anon_thp도 anonymous huge page와 연결됩니다. 큰 backing을 확보했다는 사실과 현재 큰 page-table leaf로 mapping했다는 사실은 같지 않습니다. writeback_tmp는 FUSE 등의 임시 writeback buffer 관련 통계이며 일반적인 file dirty 값과 구별해야 합니다.

mapped는 file pages가 적어도 하나의 user page table에 mapping되어 있는지에 관한 통계이지, 모든 process의 RSS 합이 아닙니다. shmem, page cache, anonymous LRU의 관계에도 겹침이 있으므로 서로 독립적인 회계 항목처럼 더하지 않아야 합니다.

Free와 PCP는 집계 경로를 구분합니다

free는 free pages의 통계이며, free_pcp와 local_pcp는 per-CPU cache 쪽의 상태를 보여 줍니다. PCP는 각 CPU가 allocation과 free의 공유 lock 비용을 줄이는 데 사용하는 cache입니다. Buddy free list의 모양과 CPU별 cache의 상태는 다른 관찰 대상입니다.

Linux v6.12에서는 buddy에서 PCP로 가져오는 동안 NR_FREE_PAGES와 PCP의 count가 서로 다른 경로로 갱신됩니다. rmqueue_bulk()가 buddy에서 block을 꺼내면 free counter를 줄이고, 가져온 base pages 수를 PCP의 count에 더합니다. 그러므로 PCP pages가 항상 이미 NR_FREE_PAGES에 포함되어 있다고 설명해서는 안 됩니다.

반대로 local_pcp는 현재 CPU 쪽 PCP를 가리키므로 전체 free_pcp와 별개의 추가 pool이 아닙니다. 이 둘을 다시 더하면 중복할 수 있습니다. 전체 여유를 비교하려는 계산과 allocator가 zone watermark에 사용하는 counter도 구별해야 합니다. PCP 값을 더해 만든 자체 총합을 zone_watermark_ok()의 입력으로 대신 사용할 수는 없습니다. Counter의 동시 갱신과 snapshot 시점 차이도 남습니다.

free_cma는 CMA와 관련된 제약을 함께 봐야 하며, bounce는 장치의 주소 접근 제약을 처리하는 buffer와 연결됩니다. 특정 시스템에서 0이라고 해서 모든 architecture에서 의미 없는 항목은 아닙니다.

Zone의 크기와 사용할 수 있는 크기는 다릅니다

spanned는 zone이 걸치는 PFN 범위, present는 그 범위 중 실제 존재하는 memory, managed는 buddy allocator가 관리하는 pages를 이해하는 기준입니다. Hole과 예약 영역 때문에 서로 같지 않을 수 있습니다. 이미 allocation된 page도 managed 범위에 속하므로 managed를 곧 free로 읽으시면 안 됩니다.

Zone별 min, low, high, reserve와 boost를 실패한 GFP 요청에 맞추어 비교해야 합니다. Low 부근에서 background reclaim을 깨우고 high를 회복하는 흐름과, 요청이 slowpath에 들어가 direct reclaim을 수행하는 흐름은 구별됩니다. 단순한 세 단계 숫자만으로 모든 요청의 동작을 결정할 수는 없습니다.

reserved_highatomic은 높은 order의 atomic allocation을 위해 보호하는 자원과 연결됩니다. writepending은 zone의 dirty 및 writeback 관련 부담을 나타내는 항목입니다. 이런 값들은 현재 요청이 사용할 수 있는 free memory를 따질 때의 조건을 이해하는 데 쓰이며, 다른 모든 counter와 독립적으로 더하는 항목은 아닙니다.

all_unreclaimable? 같은 표시는 reclaim이 진행을 만들지 못했던 상태의 단서입니다. Linux v6.12에서는 node의 kswapd_failures와 retry 기준 등이 관련됩니다. 이것을 “앞으로 어떤 상황에서도 한 page도 회수할 수 없다”는 영구적인 선언으로 읽으시면 안 됩니다.

이 version의 출력은 kswapd_failures >= MAX_RECLAIM_RETRIES이면 yes를 쓰며, MAX_RECLAIM_RETRIES는 16입니다. 이 조건 자체가 그 순간 새 direct reclaim을 호출하는 명령은 아닙니다. 상태를 인쇄하는 코드와 이후 allocation/reclaim의 제어 흐름을 분리해야 합니다.

Report 마지막의 reserved도 구별해야 합니다. show_mem()의 이 항목은 present와 managed의 차이로 설명되는 예약 physical memory와 관련됩니다. totalreserve_pages, lowmem reserve와 이름이 비슷해도 같은 값은 아닙니다.

Buddy block의 크기와 migrate type을 읽습니다

Buddy 통계의 개수 * 크기 표기는 해당 order의 free block 분포입니다. Base page가 4 KiB라면 order n의 block 하나는 4 KiB * 2^n입니다. 높은 order의 block이 적으면 allocation의 연속성 요구가 문제가 되는지 살펴볼 수 있습니다.

이어지는 hugepages_total, hugepages_free, hugepages_surp, hugepages_size는 node의 HugeTLB pool과 크기를 구별해서 읽습니다. 앞의 세 값은 해당 크기의 huge page 개수이고 hugepages_size는 KiB 단위 크기입니다. free는 아직 allocation되지 않은 pool 부분, surp는 persistent 목표를 넘는 surplus와 연결됩니다. 이 pool을 THP 통계인 anon_thp와 동일시하거나, huge page 개수에 base page 크기만 곱해서는 안 됩니다. Linux v6.12 HugeTLB 문서

괄호의 U, M, E, H, C, I는 각각 Unmovable, Movable, Reclaimable, HighAtomic, CMA, Isolate 계열의 migrate type을 나타냅니다. Reclaimable object를 회수하는 것과 live object를 임의로 옮기는 것은 다르므로 E를 “어떤 object든 이동 가능”으로 해석해서는 안 됩니다.

Pageblock을 비슷한 migration 성격으로 묶는 이유는 오래 남는 unmovable allocation이 큰 연속 범위를 갈라놓는 일을 줄이기 위해서입니다. 다른 type에서 fallback할 수 있는 조건과 pageblock을 바꾸는 규칙에는 예외가 있으므로, 단순히 다른 열의 개수를 모두 요청 가능량에 더하면 안 됩니다. HighAtomic, CMA와 Isolate에도 각자의 보호 및 사용 조건이 있습니다.

Memory가 free되면 buddy 조건을 만족하는 인접 block들을 더 높은 order로 합칠 수 있습니다. 중간에 live page가 있으면 합칠 수 없지만, 그것이 movable이라면 migration으로 다른 곳에 옮길 여지가 있습니다. Unmovable allocation이 남아 있다면 그 지점이 연속 범위 확보를 막을 수 있습니다. Pageblock 분류는 이런 혼합을 줄이는 heuristic이지 fragmentation을 완전히 없애는 보장은 아닙니다.

Linux v6.12의 일반 fallback에서 can_steal_fallback()은 order >= pageblock_order이면 pageblock 단위 전환을 허용하는 쪽으로 판단합니다. 그보다 작아도 order >= pageblock_order / 2, 특정 unmovable/reclaimable 요청 또는 grouping 비활성화 조건 등이 관여합니다. 여기서 절반은 pageblock byte 크기의 절반이 아니라 order 값의 정수 나눗셈입니다. pageblock_order = 9이면 9 / 2 = 4가 됩니다. 이 판단 하나만으로 실제 모든 free pages를 가져올 수 있다는 뜻은 아니며, 뒤의 free-page 이동과 pageblock 전환 조건도 남습니다.

HighAtomic·CMA·Isolate를 일반 fallback 표의 보통 세 migrate type과 똑같이 취급할 수는 없습니다. 그렇다고 어떤 상황에서도 전혀 사용할 수 없는 memory라는 뜻도 아닙니다. 예를 들어 HighAtomic reserve를 해제하는 별도 경로와 CMA를 허용한 allocation의 별도 경로가 있습니다. 표의 문자를 보고 무조건 사용 가능 또는 영구 사용 불가로 나누기보다, 실제 요청의 allocation flags를 확인해야 합니다.

이 분포는 fragmentation의 단서를 제공합니다. 하지만 order 0조차 요청 조건을 만족시키지 못하는 상황이라면, 높은 order의 부족만 들여다보기보다 watermark, reserve, node 제한과 reclaim 진행부터 함께 확인하셔야 합니다.

OOM을 일으키지 않고 memory dump를 요청할 수 있습니다

관리 권한이 있고 SysRq가 build된 Linux 시험 환경에서는 다음처럼 m 진단을 요청한 뒤 kernel log를 읽을 수 있습니다. 이 글의 검증 과정에서 아래 명령을 실행한 것은 아닙니다. 단순 조회 파일을 읽는 예제와 달리 kernel log에 출력을 발생시키므로 관리 중인 환경의 허용 여부를 먼저 확인하셔야 합니다.

printf 'm' | sudo tee /proc/sysrq-trigger >/dev/null
sudo dmesg

m은 memory 정보를 출력하는 요청입니다. 실제 OOM 발생의 stack trace·victim 선택 결과까지 새로 만들어 주지는 않습니다. /proc/sys/kernel/sysrq의 bitmask가 keyboard invocation을 제어하는 것과, 관리 권한으로 /proc/sysrq-trigger를 사용하는 경로도 구분해야 합니다. 다른 command 문자는 reboot나 task 종료처럼 전혀 다른 동작을 하므로 바꾸어 실험하지 않는 편이 좋습니다. Linux v6.12 SysRq 문서

참고 자료