파이문

[Linux] oom_score 알아보기 본문

TIL/리눅스 Linux

[Linux] oom_score 알아보기

민Z 2021. 2. 25. 17:19
728x90

😀 0. 프로세스가 죽었다

서버의 프로세스가 메모리 부족으로 kill 되었다는 알람이 왔다.

 kernel: Out of memory: Kill process <pid> (<process_type>) score <score> or sacrifice child

최근 서버에 서비스 몇 개를 더 띄웠는데, 그것 때문에 일어난 영향으로 보였다.

 

짧은 식견으로는 새로 올린 서비스가 메모리를 많이 잡아먹었고, 그렇기 때문에 새로 올린 서비스가 죽어야 한다고 생각했다. 그러나 실제로 죽은 프로세스는  HBase 의 RegionServer 였다.

 

확인해보니 HBase RegionServer 의 oom_score 가 더 높았었기 때문이었다.

 

oom_score 는 여기서 짧게 작성한 적이 있는데, 쉽게 말하면 서버의 메모리가 부족할 때 oom_score 가 높은 프로세스가 oom killer 에 의해 죽을 확률이 높다는 것이다.

 

⚠️ 틀린 내용 있을 수 있음 / BFS 기반의 공부임  ⚠️

🤔 1. oom_score 계산은 어떻게 하는걸까?

죽은 RegionServer 를 다시 살리고 해당 서버에 띄워져있는 각 서비스의 oom_score를 확인해보았다.

cat /proc/<pid>/oom_score

서비스 / 프로세스 명을 자세히 밝힐 수는 없지만 의외로 RegionServer 가 가장 크지는 않았다 (처음 죽었을 때 나왔던 알람의 score 근처도 가지 않았었다.) 만약 현재 상황에서 또 다시 메모리 장애가 일어난다면 RegionServer 가 아닌 다른 프로세스가 죽게 될 것이다.

 

그래서 oom_score 가 어떻게 계산 되는 건지, kill 은 어떤 식으로 일어나는 건지 자세히 살펴보기로 하였다.

😵 2. oom_badness 살펴보기

리눅스에는 oom_badness 라는 함수가 있다.

컨셉은 kill 했을 때 가장 높은 메모리를 확보할 수 있는 task 인 경우, 높은 점수를 return 한다는 개념이다.

 

이 함수를 살펴보는 이유는 메모리 관련해서 리눅스에서 실행(kill) 하는 순서가 다음과 같기 때문이다.

  1. select_bad_process
  2. oom_evaluate_task
  3. oom_badness

여기서 oom_badness() 라는 함수가 리턴해주는 점수가 가장 높은 task 가 bad_process 로 선정 되어 죽게 된다.

 

oom_badness 는 이렇게 생겼다.

long oom_badness(struct task_struct *p, unsigned long totalpages)
{
	... 생략
	// 대충 죽일 필요가 없으면 LONG_MIN 을 리턴한다는 얘기
    
	/*
	 * Do not even consider tasks which are explicitly marked oom
	 * unkillable or have been already oom reaped or the are in
	 * the middle of vfork
	 */
	adj = (long)p->signal->oom_score_adj;
	if (adj == OOM_SCORE_ADJ_MIN ||
			test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
			in_vfork(p)) {
		task_unlock(p);
		return LONG_MIN;
	}
    
	... 생략
}

이 중 oom_score_adj 라는 값을 oom_badness 에서 LONG_MIN 을 리턴하기 위해 사용하고 있다. (LONG_MIN 을 리턴한다는 개념은 낮은 점수를 줘서 kill 하지 못하게 하겠다는 의미다. 참고로 도커 데몬의 기본 OOM 점수는 -999이다.)

 

oom_score_adj 는 커널 파라미터에서도 찾아볼 수 있다.

/proc/<pid>/oom_adj
/proc/<pid>/oom_score
/proc/<pid>/oom_score_adj

프로세스가 OOM Killer 에 의해 죽지 않길 원한다면 oom_score 를 조정하는게 아니라 oom_score_adj 값을 변경해야한다.

 

아래처럼 Negative 값을 설정하면 된다.

sudo echo -1000 > /proc/<pid>/oom_score_adj

낮은 점수를 리턴하는 계산 식이 끝나고 나면 이제 oom_kill 프로세스는 point 를 계산한다. (oom_score)

long oom_badness(struct task_struct *p, unsigned long totalpages)
{
	// 생략
    
	/*
	 * The baseline for the badness score is the proportion of RAM that each
	 * task's rss, pagetable and swap space use.
	 */
	points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
		mm_pgtables_bytes(p->mm) / PAGE_SIZE;
	task_unlock(p);

	/* Normalize to oom_score_adj units */
	adj *= totalpages / 1000;
	points += adj;

	return points;
}

RSS (프로세스가 사용하고 있는 물리 메모리) + 프로세스의 스왑 메모리 + (프로세스의 pagetable / page_size) 의 값이 프로세스 (task) 의 점수 (point) 가 된다.

 

이 코드만 보면 결국 프로세스가 점유하고 있는 메모리가 클 경우 score 가 높아진다고 이해할 수 있을 것 같다.

 

참고로

get_mm_rss 가 내부적으로 get_mm_counter 를 쓰고 있고

static inline unsigned long get_mm_rss(struct mm_struct *mm)
{
	return get_mm_counter(mm, MM_FILEPAGES) +
		get_mm_counter(mm, MM_ANONPAGES) +
		get_mm_counter(mm, MM_SHMEMPAGES);
}

get_mm_counter 는 이렇게 생겼다.

/*
 * per-process(per-mm_struct) statistics.
 */
static inline unsigned long get_mm_counter(struct mm_struct *mm, int member)
{
	long val = atomic_long_read(&mm->rss_stat.count[member]);
 	// 생략
	return val
}

🥺 3. 이게 다일까?

함수를 살펴보긴 했으나 뭔가 이게 다가 아닌것 같은 생각이 들었다. (c 도 몰라서 이해하기도 어렵고...) 그래서 더 찾아 보기로 했다.

 

medium.com/@EJSohn/out-of-memory-killer-%ED%9A%8C%ED%94%BC%ED%95%98%EA%B8%B0-9efc65f88c92 와 dreamlog.tistory.com/307 블로그 글을 발견하게 되었다.

 

해당 블로그들에서는 points 값은 프로세스의 나이스 점수 (niceness) 등을 사용하여 계산이 된다고 하던데, 나는 리눅스 oom_kill.c 코드에서 이 부분을 찾을 수가 없었다. 😢

 

(nice 는 top 명령어에서도 확인할 수 있는 지표로 프로세스의 실행 우선순위를 결정하는데도 사용한다. 이때 top 명령어에선 NI 라고 표기된다.)

 

심지어 linux-mm.org/OOM_Killer 여기에도 설명이 있는데...

 

패치 되기 이 전에 방식인가 싶어서, 가장 오래된 블로그 글로 보이던 시점의 커밋까지 살펴보았고... 결국 찾아냈다. 2010년의 커밋이다. github.com/torvalds/linux/commit/a63d83f427fbce97a6cea0db2e64b0eb8435cd10#diff-268fe084429e2dda106503d80d590ac28f341bcf5969eaed6c09891eea0ca466

 

위의 커밋에서 task_nice 에 대한 계산식은 빠졌고 해당 패치에 대한 아티클은 lwn.net/Articles/396552/ 여기서 볼 수 있었다. 이 패치에서 niceness 값이 빠진 대신 (옛날에는 이 값이 kill 우선순위를 낮추는데 사용하였던듯?) oom_score_adj 라는 게 생겼다.

 

왜 바꿨냐 하면, 어차피 badness 는 휴리스틱한 기반이기도 하고, 좀 더 심플하게 만드는게 목표다 라는 것 같다. (??)

 

(리눅스라는게 배포판도 여러개고, 버젼도 여러개다 보니까 블로그 글들이나 인터넷 글들이 잘 갱신이 안되는 듯)

😊 결론 ?!

oom_score 는 결국 프로세스가 사용하고 있는 메모리 기반으로 정해지는 게 맞는 것 같다.

 

죽었다가 다시 살렸기 때문에 다른 서버의 RegionServer 보다 oom_score 가 상대적으로 낮았던 것이고, 동일한 서버의 다른 서비스들 보다 (DataNode 나 ResourceManager) 도 oom_score 가 낮았던 것이다.

 

지금 당장 메모리 이슈가 발생한다면 RegionServer 가 아닌 다른 서비스가 죽을 거라는 것도 맞는 얘기 인듯 (?!)

 

(죄다 문장 끝이 ~ 듯 하다. ~ 같다 로 끝나는 건 내 착각일까..?)

🧐 찾아본 김에 더 찾아봄

초반에 oom_badness 를 호출하는 순서가 다음과 같다 했다.

  1. select_bad_process
  2. oom_evaluate_task
  3. oom_badness

3은 위에서 알아봤고 1,2 는 oom_control 이라는 구조체에 oom_badness 에서 나온 point 를 할당하고 chosen 이라는 값에 해당 task 를 넣는 정도 밖에 없었다. (oc 는 oom_control 구조체 변수 이름이다.)

oc->chosen = task;
oc->chosen_points = points;

함수 순서는 이렇게 된다.

  1. oom_kill_process (__oom_kill_process)
  2. out_of_memory
  3. select_bad_process

oc 값이 유효하면 최종적으로 oom_kill_process 가 oc->chosen 를 정리하게 된다. (이를 코드 내에선 victim 이라고 변수명을 지었더라.)

🙄 마치며

이 글도 언젠간 레거시가 되어서, 안 맞는 날이 올 수도...

 

- github.com/torvalds/linux/blob/master/Documentation/filesystems/proc.rst#chapter-3-per-process-parameters

- github.com/torvalds/linux/blob/master/include/linux/mm.h

- github.com/torvalds/linux/blob/master/mm/oom_kill.c

Comments