6 minute read

OOM

사전적인 의미는 모든 가용가능한 모든 메모리(스왑, 메모리, 캐시 …)가 할당 되어 프로세스가 필요한 메모리를 줄수 없는 상태를 의미한다.

예전에 메모리가 적은 시절엔 흔한일이었다고 한다. 그래서 옛날 프로그램의 경우 처음에 모든 필요한 메모리 공간이 있는지 체크한후 없으면 그냥 크래시를 내버리는 구조로 작성되었다.

최근의 프로그램의 경우 메모리 단가하락/용량증가, 스왑 사용, 메모리 오버커밋등의 힘으로 메모리 공간이 충분하다는 가정하에 주로 작성되어있다고 한다. 이로 인해 실제 메모리가 부족한 상황이 발생할수 있어 OOM은 현대에도 풀리지 않은 숙제다.

OOM 이해에 필요한 배경지식

페이징

우선 주소공간을 논리주소, 물리주소로 나눈다. 그후 메모리를 일정한 사이즈로 잘게 쪼개는데, 쪼갠 조작을 논리주소공간에서 페이지, 물리주소공간에선 프레임 이라고 부른다

이를 통해 프로그램은 논리공간에서 일정한 주소로 실행되므로 recompile을 막을수 있다. 또한 메모리를 조각내서 할당함으로써 외부 단편화 현상을 막는다.

페이지 폴트

논리주소 공간을 물리주소 공간으로 변환하는 과정에서 페이지 테이블에 물리주소 mapping이 안되있는 상황을 말한다. locality의 원리로 보조저장장치(swap)에 가있거나 더이상 할당할 공간이 없는 OOM 상황일수 있다.

메모리 오버커밋

분리된 물리/논리 공간을 통하여 실제 물리메모리의 용량보다 더 많은 공간을 프로세스에 할당할수 있는 기술이다. 순간적으로 많은 양의 메모리를 필요로 하는 fork 같은 system call에 유용하다. 왜냐하면 exec을 통해 다른 프로그램의 문맥으로 교체하는 경우가 많아 복사해온 메모리 공간이 대부분 불필요 하기 때문이다.

그리고 locality에 의해 프로세스는 메모리의 일부분의 공간만 재참조할 확률이 높기때문에 오버커밋은 유효한 전략이 될수 있다.

memory overcommit이 활성화된 상태에서 메모리공간이 프로세스에 할당 되면 논리주소가 map되지만 물리주소가 map되지 않은 memory commit 상태가 된다.

그리고 실제로 메모리가 필요할때 물리주소공간이 할당되는데 이를 위해 COW 기술이 활용되기도 한다.

overcommit은 물리메모리가 부족한 상황인 out of memory 상황을 야기할 위험이있다.

  • fork 시스템콜 예제

    7GB 의 물리 메모리를 가지고 있는 서버에서 4GB 의 프로세스가 돌고 있다고 가정한다. 프로세스가 fork()를 통해 자식 프로세스를 만들고, exec()으로 특정 작업을 하는 프로세스로 스위칭하는 상황이다.

    프로세스가 fork()를 하게 되면 똑같은 4GB 의 메모리 공간이 더 필요하다. 원칙적으로 용량이 부족하기 때문에 oom 상황에 빠질수도 있다.

    하지만 overcommit이 활성화 되어있다면 4GB의 추가 공간은 실제로는 memory commit 상태가 된다. 이를 통해 여러 메모리공간을 많이 먹는 프로세스가 동시에 fork 한다 해도 오류없이 진행이 가능한것이다.

  • overcommit rule (always, guess, disable)

      // https://github.com/torvalds/linux/blob/master/mm/util.c
      int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin)
      {
      	long allowed;
        
      	vm_acct_memory(pages);
        
      	/**
         * OVERCOMMIT_ALWAYS
         *
         * 조건들을 검사하지 않고 무조건 일단 논리공간을 할당해준다
         */
      	if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
      		return 0;
        
        /**
         * OVERCOMMIT_GUESS
         * 
         * 주어진 페이지수가 물리메모리 + 스왑용량 보다 작으면 논리공간 할당이 가능하다.
         */
      	if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
      		if (pages > totalram_pages() + total_swap_pages)
      			goto error;
      		return 0;
      	}
        
        /**
         * OVERCOMMIT_DISABLED
         *
         * overcommit 이 비활성화된 경우 캐시, 스왑, 메모리등의 모든 요소들을 고려하여
         * 허용된 페이지수를 반환하게 된다.
         */
        allowed = vm_commit_limit();
    

커널 패닉

사전적의미는 복구할수 없는 에러/원인 으로 인해 커널의 동작이 멈추는 현상을 말한다. 이때는 시스템 리부팅이 시스템을 회복시키는 유일한 수단이라고 한다.

AT&T 파생 및 BSD 유닉스 계열 소스코드에선 panic()으로 알려진 패닉을 처리하는 커널 루틴이 있다. 심각한 오류 (OOM killer가 kill할 프로세스를 찾을수 없는 상황) 가 발생하면 panic() 함수가 호출되어 일반적으로 콘솔에 오류 메시지를 출력하고, 사후 디버깅을 위해 커널 메모리 이미지를 디스크에 덤프한 다음 시스템이 수동으로 재부팅될 때까지 기다리거나 자동 재부팅을 시작하도록 설계되어있다고 한다.

리눅스에선 관리자가 panic() 시스템콜을 통해 리눅스 커널을 강제로 패닉상태로 집어넣을수도 있다.

oom_score

  • 프로세스마다 oom score가 주기적으로 계산되어 저장된다
    • ex) **cat /proc/<pid>/oom_score**
  • score가 높을수록 oom_kill() 에 의해 kill될 가능성이 높다.
  • 메모리양은 많지만 할당된 시간이 적을수록 score가 높을수 있다.
  • 관리자 권한의 프로세스는 score 가 작을수 있다

oom_score_adj

  • oom_score 와 함께 kill 될 프로세스를 결정하는 요소
    • ex) **cat /proc/$$/oom_score**
  • -1000 ~ 1000 범위의 숫자 할당
  • 관리자가 튜닝하여 프로세스의 특정 adj을 낮추면 kill될 가능성을 낮출수 있음

__vm_enough_memory()

주어진 페이지의 수만큼 가상 주소 공간 할당이 가능한지 검사하는 커널 내부 함수이다.

overcommit rule 과 여러 여건들(캐시, 스왑, 메모리 여유공간)을 참고하여 할당이 가능하면 0을 반환, 아니면 -ENOMEM 을 반환한다.

__vm_enough_memory()out_of_memory() 콜의 발생을 예방하는 효과가 있다.

리눅스 커널 소스 코드에서 함수 이름이 두 개의 밑줄(__)로 시작하는 경우, 이는 주로 내부적으로 사용되는 함수임을 나타낸다. 이러한 함수들은 주로 해당 커널의 핵심 부분에서 사용되거나 특정 아키텍처나 하드웨어와 관련된 부분에서 구현된 함수들이다.

그래서 API을 통해 프로세스들이 직접 호출하지 않고 프로세스로부터 fork(), mmep(), shmem()등으로 메모리 요청시 함수 내부적으로 __vm_enough_memory() 을 호출하여 OOM을 예방하는 동작과정으로 추측된다.

간략 동작과정

  • overcommit rule 체크
  • 여유페이지, 캐시, 스왑 용량 체크
  • 루트 권한 프로세스가 아니면 총 여유공간에 루트 전용 reserved space 를 빼준다.
  • 총 여유공간보다 요청된 페이지수가 적은지 체
// https://github.com/torvalds/linux/blob/master/mm/util.c
int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin)
{

	long allowed;

	vm_acct_memory(pages);

	/**
   * check overcommit rule
   */
	if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
		return 0;
	if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
		if (pages > totalram_pages() + total_swap_pages)
			goto error;
		return 0;
	}

 /**
   * 여유 페이지, 캐시, 물리 메모리 용량 체크
   */
	allowed = vm_commit_limit();

	/*
   * 총 메모리 여유공간은 루트 예약 공간을 뺀후 계산된다.
   * 할당하고 싶은 프로세스의 권한이 루트인 경우 그냥 통과
	 */
	if (!cap_sys_admin)
		allowed -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);

	/*
	 * Don't let a single process grow so big a user can't recover
	 */
	if (mm) {
		long reserve = sysctl_user_reserve_kbytes >> (PAGE_SHIFT - 10);

		allowed -= min_t(long, mm->total_vm / 32, reserve);
	}

	if (percpu_counter_read_positive(&vm_committed_as) < allowed)
		return 0;
error:
	pr_warn_ratelimited("%s: pid: %d, comm: %s, not enough memory for the allocation\n",
			    __func__, current->pid, current->comm);
	vm_unacct_memory(pages);

	return -ENOMEM;
}

out_of_memory()

out_of_memory() 는 페이지 폴트 상황에서 더이상 할당해줄 물리공간이 없는 global oom 상황, cgroup의 memory subsystem에 정한 규칙을 넘어가서 발생하는 cgroup oom 상황에서 호출되는것으로 추정된다.

out_of_memory() 는 메모리가 진짜 OOM 상태인지 검증을 거친뒤 충분한 메모리를 확보하기 위해 oom_kill 시스템콜 (oom_killer) 을 진행한다.

간략 동작 구조

  • oom_killer가 비활성화 되어있다면 그냥 빠져나온다.
  • 진짜 out of memory 인지 여러 사항들을 체크
    • 이전 호출로 메모리가 회복되었는가? → OOM 아님
    • 현재시간 기준으로 9, 11 시그널(강제종료, 종료)을 수신한 프로세스가 있는가 → OOM 아님
    • …….
  • select_bad_process() 를 호출하여 kill할 프로세스 선정
    • kill 할 프로세스를 못찾으면 데드락(경합상태)로 간주하여 panic() 호출 → 커널 패닉!

Global OOM

논리주소와 물리주소는 페이지 테이블을 통해 매핑된다. 하지만 overcommit, swap공간의 힘으로 항상 논리주소와 물리주소가 map되어있는것은 아니다.

이때 논리주소로 메모리공간 접근 물리주소가 map 안되어있으면 pagefault 가 발생한다. 그제서야 커널은 물리주소 mapping을 위한 작업을 진행한다.

그런데 mapping할 물리메모리 공간이 부족하게 되면 프로세스들은 기아상태와 교착상태에 빠질가능성이 높아지는 글로벌 OOM 상태가 된다.

글로벌 OOM으로 인해 oom_killer가 실행되었는데 프로세스 kill에 실패하면 커널 패닉이 된다.

주요 설정 파라미터

  • vm.panic_on_oom
    • OOM이 발생할 때 시스템을 강제로 패닉 상태로 만들 것인지를 결정한다.
    • 값: 0 또는 1 (기본값: 0)
  • vm.oom_kill_allocating_task
    • OOM 이벤트가 발생할 때, 어떤 프로세스를 종료할지 결정한다. 1로 설정하면 메모리를 할당하고 있는 프로세스를 종료한다.
    • 값: 0 또는 1 (기본값: 0)
  • vm.oom_dump_tasks
    • 설명: OOM 이벤트가 발생할 때 현재 메모리를 많이 사용하는 프로세스들의 정보를 덤프로 저장할지 여부를 결정한다.
    • 값: 0 또는 1 (기본값: 0)
  • vm.overcommit_memory
    • 오버커밋 설정
    • 값: 0(TURN_OFF), 1(OVERCOMMIT_ALWAYS), 2 (OVERCOMMIT_GUESS)
  • vm.overcommit_ratio:
    • 이 값은 vm.overcommit_memory가 2일 때 사용
    • atio/100*메모리공간 만큼은 overcommit 할수 있게 한다.
    • 값: 0~100 (기본값: 50)
  • vm.oom_kill
    • oom_killer 활성화/비활성화
    • 비활성화후 자원부족시 프로세스간 경합으로 인한 데드락 가능성 있음

Cgroup OOM

cgroup은 리눅스에서 프로세스 그룹을 관리하기 위한 메커니즘으로, CPU, 메모리, 네트워크 등의 리소스를 제어하고 제한하는 데 사용된다

cgroup에서의 OOM은 특정 cgroup 내에서 메모리 서브시스템을 통해 설정한 값들을 체크하여, 메모리 부족 상태에 도달했을 때 발생한다. 각 cgroup은 자체적으로 메모리 제한(limit(을 가질 수 있으며, 이 제한을 초과하면 해당 cgroup에서 여유메모리 확보 시도한후 OOM을 간주하여 out_of_memory() 가 호출될수 있다.

cgroup에서의 OOM은 전체 시스템이 아닌 특정 cgroup에만 영향을 미치므로, 다른 cgroup에 속한 프로세스들은 정상적으로 동작할 수 있다. cgroup에서의 OOM 이벤트가 발생하면, 커널은 해당 cgroup 내의 프로세스 중 어떤 것을 kill 할지 결정하여 메모리를 확보하려고 시도한다.

cgroup들에 대해 개별적으로 OOM Killer를 비활성화할수 있다. 이를 통해 특정 프로세스 그룹에 대해서만 OOM Killer를 제어할 수 있게 된다.

주요 파라미터

  • memory.limit_in_bytes
    • 해당 cgroup의 모든 프로세스가 사용할 수 있는 최대 물리 메모리(+ 캐시) 를 바이트 단위로 지정
    • echo 4G > /sys/fs/cgroup/memory/my_cgroup/memory.limit_in_bytes
  • memory.memsw.limit_in_bytes
    • 스왑 공간을 포함한 전체 가용 메모리 양을 지정한다. 이 값은 메모리 및 스왑의 총합이다.
    • echo 5G > /sys/fs/cgroup/memory/my_cgroup/memory.memsw.limit_in_bytes
  • memory.soft_limit_in_bytes
    • 프로세스간 메모리 경합, 메모리 부족시 최선을 다해 보장해줄 메모리의 양
    • echo 3G > /sys/fs/cgroup/memory/my_cgroup/memory.soft_limit_in_bytes
  • memory.oom_control
    • 0이면 cgroup에 대해 oom killer 작동, 1이면 비활성화
  • memory.kmem.limit_in_bytes
    • 커널 메모리(Kernel Memory)에 대한 제한을 설정
    • echo 500M > /sys/fs/cgroup/memory/my_cgroup/memory.kmem.limit_in_bytes
  • memory.usage_in_bytes
    • 현재 cgroup에서 사용 중인 메모리 양을 나타낸다. (read only)
    • /sys/fs/cgroup/memory/my_cgroup/memory.usage_in_bytes
  • memory.max_usage_in_bytes
    • cgroup의 모든 시간 동안 사용된 최대 메모리 양을 나타낸다 (read only)
    • /sys/fs/cgroup/memory/my_cgroup/memory.max_usage_in_bytes
  • memory.failcnt
    • cgroup의 메모리 할당 실패 횟수를 나타낸다.
    • /sys/fs/cgroup/memory/my_cgroup/memory.failcnt

OOM의 예방 과 소중한 프로세스 지키기

  • 매트릭 분석을 통해 누수 지점 확인
  • 넉넉한 자원 할당 (메모리, 스왑)
  • 메모리 오버커밋 비활성화 (global level)
    • 데드락 상태나, 커널 패닉이 일어나는 일은 방지할수 있다.
    • 관리자 전용 메모리 공간을 활용하여 장애조치를 할수 있는 가능성이 생긴다.
    • 프로세스가 실행되지 못하며 시스템의 불안정성이 높아질수 있다.
  • 컨테이너화 한후 오케스트레이션 툴을 사용하여 self-healing 시켜 복구시간 최소화
  • oom_score_adj 튜닝
  • oom_kill 비활성화 (global level: deadlock 가능성 있음, cgroup level)

      bool out_of_memory(struct oom_control *oc)
      {
        /**
         * killer를 꺼버리면 out_of_memory 하위로직이 실행되지 않음)
         * 메모리가 부족해지면 프로세스간의 경합으로 인한 교착상태 가능성이 커질수 있다.
         */
      	if (oom_killer_disabled)
      		return false;
        
      	......
    

참고자료

Leave a comment