일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 프로그래머스
- redis
- codewars
- programmers
- 파이썬
- Python
- 튜토리얼
- scala
- DP
- 스칼라
- 알고리즘
- docker
- go
- 동적프로그래밍
- 코드워
- HBase
- 리눅스
- Go언어
- boj
- gradle
- zookeeper
- Java
- 주키퍼
- 자바
- Linux
- 문제풀이
- leetcode
- golang
- OOM
- dynamic programming
- Today
- Total
파이문
[Linux] oom_score 알아보기 본문
😀 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) 하는 순서가 다음과 같기 때문이다.
- select_bad_process
- oom_evaluate_task
- 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 를 호출하는 순서가 다음과 같다 했다.
- select_bad_process
- oom_evaluate_task
- oom_badness
3은 위에서 알아봤고 1,2 는 oom_control 이라는 구조체에 oom_badness 에서 나온 point 를 할당하고 chosen 이라는 값에 해당 task 를 넣는 정도 밖에 없었다. (oc 는 oom_control 구조체 변수 이름이다.)
oc->chosen = task;
oc->chosen_points = points;
함수 순서는 이렇게 된다.
- oom_kill_process (__oom_kill_process)
- out_of_memory
- select_bad_process
oc 값이 유효하면 최종적으로 oom_kill_process 가 oc->chosen 를 정리하게 된다. (이를 코드 내에선 victim 이라고 변수명을 지었더라.)
🙄 마치며
이 글도 언젠간 레거시가 되어서, 안 맞는 날이 올 수도...
'TIL > 리눅스 Linux' 카테고리의 다른 글
[Linux] Memory Commit 이란? (0) | 2021.02.19 |
---|---|
[Linux] 메모리 부족 시 어떤 일이 일어나는걸까? (0) | 2020.10.20 |
[Linux] 리눅스에서 쓰레드 최대 갯수 (1) | 2020.10.20 |