데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      쿠버네티스가 쉬워지는 컨테이너 이야기 — memory편

      천강민 24.11.14
      3,078 10 1
      DEVOTEE 요약
      쿠버네티스의 메모리 관리에 대한 글에서는 리눅스와 cadvisor 관점의 메모리 차이, 페이지 캐시의 역할, OOM 상황에서의 처리 방식 등을 다룹니다. 메모리 사용량을 이해하는 데 도움이 되는 다양한 질문을 탐구하며, cadvisor가 사용하는 RSS와 WSS의 정의와 관련된 메모리 측정 방법을 설명합니다. 또한 OOM Killer의 동작 과정과 메모리 관리의 세부 사항을 통해 쿠버네티스 환경에서의 메모리 관리 전략을 고려합니다.
      DEVOTEE 추천 블로그

      이전 글

      들어가며,

      이전 글에선 cgroup, cpu에 대해서 알아 봤습니다. 이번 글은 memory에 대한 내용인데요.

      memory 컨트롤러에서 제공하는 기능이나 통계, 그리고 OOM Killer를 제대로 이해하려면 리눅스가 바라보는 메모리와 cadvisor가 바라보는 메모리의 차이를 이해해야 합니다.

      그렇기에 이번 글은 조금 길지만, 끝까지 보고나면 k8s를 활용하는데 있어 디테일이 달라질거라고 확신합니다.


      이번 글의 대상이 아닌 독자

      1. RSS(Resident Set Size)는 WSS(Working Set Size)보다 클까? 작을까? 관점(?)에 따라 다른가?

      2. 파일시스템을 통해 파일을 읽는건 tmpfs에서 읽는 것보다 항상 느린가?

      3. 메모리는 정말 압축 불가능한가?

      4. OOM Killer와 관련하여 memory.max 값은 왜 필요한가?

      5. 메모리 1 GiB 서버에서 324 MiB, 700 MiB를 사용중인 어플리케이션이 있을 때, OOM Killer는 어떤 것을 선택하는가?

      6. 어플리케이션이 메모리를 많이 사용하지 않아도 OOM이 발생할 수 있는가?

      7. k8s에도 OOM Killer가 존재한다는 것은 사실일까? 아니면 예방하기 위해 노력할까?

      8. 메모리까지 봤을 때 k8s Guaranteed QoS는 (당신의 환경에) 필요한가? 그렇지 않은가?

      9. 메모리 관점에서 어떤 유형의 어플리케이션이 컨테이너와 어울리지 않는가? 어울리지 않는다고 생각하는 어플리케이션들은 이를 어떻게 극복하고 있는가?

      10. 메모리 X% 도달 시 알람을 거는 이유가 무엇인가? 오늘 살펴 본 내용 외에 또 무엇이 문제가 될까?

      11. k8s에서 메모리와 관련된 추가될만한 기능 또는 이미 존재하는 기능은 무엇이 있을까?

      1 ~ 7번에 대한 답과, 8 ~ 11번에 대한 개인적인 생각이 있다면 이번 글은 읽지 않으셔도 됩니다.

      * 참고 * 이번 글은 AWS EC2에서 진행됩니다.


      한 장으로 살펴보는 메모리


      실선으로 표현한 부분은 free 명령을 입력했을 때 나오는 결과이고, 점선으로 표현한 부분은 프로세스가 사용 중인 메모리를 리눅스와 cadvisor가 우리에게 알려주는 범위를 나타냈습니다.

      그림만으로 벌써 1번 질문에 대한 답을 할 수 있겠죠? (자세한 내용은 차차 설명드릴 예정입니다.)

      *주의* free 명령의 결과는 사용 중인 시스템에 따라 표현하는 방식이 다를 수 있습니다. 예를 들어, 어떤 시스템에서는 1) used = total - available이고, 다른 시스템에서는 2) used = total - free - buff/cache로 표현합니다. 오늘 글에서 표현된건 2)번의 형태입니다.


      리눅스가 바라보는 메모리

      [root@ip-172-31-12-125 ec2-user]# free -k               total        used        free      shared  buff/cache   availableMem:          972320      211784      554396         632      206140      621516Swap:              0           0           0

      리눅스를 사용 중이라면 흔히 볼 수 있는 free 명령의 결과입니다.

      예측가능성을 높이기 위해 swapoff -a를 수행한 상태이고, -k 플래그를 통해 KiB 단위로 출력하고 있는 것을 볼 수 있습니다.

      각 항목을 간단히 설명하면 아래와 같습니다.

      1. total: 전체 메모리 용량

      2. used: total - free - buff/cache

      3. free: 실제 사용 가능 여부와 상관없이 비어 있는 메모리

      4. shared: (거의 대부분) tmpfs에서 사용 중인 메모리

      5. buff/cache: 버퍼 캐시, 페이지 캐시가 이용 중인 메모리

      6. available: 실제 사용 가능한 메모리. 이때, 해제하여 회수 가능한(Reclaimable) 메모리(캐시 등)도 포함됨.

      이렇게 보면 available이 항상 free보다 클 것 같지만, 꼭 그렇지는 않습니다. (시스템 예약 메모리 등은 free에는 포함되지만, available에는 포함되지 않음)

      또한, 위와 같은 의미를 가지기 때문에 서버의 메모리 가용량을 볼 때 보통은 available을 기준으로 보게 됩니다.

      # node-exporter 메트릭# HELP node_memory_MemAvailable_bytes Memory information field MemAvailable_bytes.# TYPE node_memory_MemAvailable_bytes gaugenode_memory_MemAvailable_bytes 6.0356608e+08

      다른 항목도 살펴봐야겠죠? 사실 used나 shared는 별도의 설명이 없어도 이해하는데 크게 무리가 없습니다.

      예를 들어, 프로세스가 메모리를 할당 받아서 실제로 사용하고 있다면 used가 오를 것이고, tmpfs가 마운트된 디렉토리에 파일을 쓰고 있다면 shared가 오를 겁니다.

      (당연히 공유메모리를 사용할 때에도 오릅니다.)

      남은건 buff/cache 뿐인데요. cadvisor가 제공하는 WSS와 RSS를 이해하기 위해선 buff/cache 중에서도 **페이지 캐시(Page Cache)**에 대해 알아야 합니다.

      한 번 자세히 살펴 보겠습니다.


      페이지 캐시 (Page Cache)

      페이지 캐시는 쉽게 말해서 파일시스템을 통해 파일을 읽거나 쓸 때, 효율성 향상을 위해 해당 파일의 내용을 메모리에 저장해두는 것을 의미합니다.

      이를 통해, 항상 디스크에 접근하는게 아닌 메모리에서 직접 데이터를 가져오거나 쓸 수 있어 속도가 매우 크게 향상되게 됩니다.

      테스트를 위해 tmpfs(/tmp/rex) 경로와 루트(/rex)에 rex라는 이름의 100 MiB 파일을 생성합니다.

      [root@ip-172-31-12-125 /]# dd if=/dev/zero of=/tmp/rex bs=100M count=11+0 records in1+0 records out104857600 bytes (105 MB, 100 MiB) copied, 0.101319 s, 1.0 GB/s[root@ip-172-31-12-125 /]# dd if=/dev/zero of=/rex bs=100M count=11+0 records in1+0 records out104857600 bytes (105 MB, 100 MiB) copied, 0.248434 s, 422 MB/s# 페이지 캐시 삭제[root@ip-172-31-12-125 /]# echo 1 > /proc/sys/vm/drop_caches

      파일을 쓰는 행위조차 차이가 나는 것을 볼 수 있습니다. (0.1초 vs 0.24초) 이후, 정확한 결과를 살펴보기 위해 페이지 캐시를 초기화 해줍니다.

      자 이제 cat 명령을 통해 파일을 읽어 보겠습니다.

      [root@ip-172-31-12-125 /]# time cat /tmp/rex > /dev/nullreal    0m0.019suser    0m0.000ssys     0m0.019s[root@ip-172-31-12-125 /]# time cat /rex > /dev/nullreal    0m0.641s # 30배가 넘는 차이...user    0m0.000ssys     0m0.024s

      tmpfs에서 읽는 것에 비해 (시스템이나 상황에 따라 차이는 있겠지만) 30배가 넘는 시간이 걸린 것을 볼 수 있습니다. 다시 한 번 읽어볼까요?

      [root@ip-172-31-12-125 /]# time cat /tmp/rex > /dev/nullreal    0m0.019suser    0m0.000ssys     0m0.019s[root@ip-172-31-12-125 /]# time cat /rex > /dev/nullreal    0m0.018suser    0m0.000ssys     0m0.018s

      오! 이제는 거의 동일한 수치가 나오는 것을 볼 수 있습니다. 이처럼 리눅스는 디스크와 메모리의 속도 차이를 줄이기 위한 캐시를 제공하고 있습니다.

      이러한 동작방식의 결과를 free 명령으로 살펴보면 아래와 같습니다.

      [root@ip-172-31-12-125 /]# free -m               total ...<SNIP>... buff/cache   availableMem:             949 ...<SNIP>...    319        453##### 파일 읽음 ######[root@ip-172-31-12-125 /]# free -m               total ...<SNIP>... buff/cache   availableMem:             949 ...<SNIP>...    419         453

      buff/cache가 늘어났고, 그럼에도 불구하고 available은 동일한 것을 볼 수 있습니다.

      앞서 말씀드렸듯이 이러한 방식을 페이지 캐시라고 부르며, 다른 말로는 “필요할 때 해제하여 회수 가능한(재사용 가능한) 메모리” 라고도 부를 수 있습니다.

      (다만, 항상 회수 가능한 것은 아님)

      * 페이지 캐시가 중요한 시스템이라면? *

      available 보단 free를 통해 시스템의 상태를 모니터링 해야합니다. 하지만 이는 너무 피곤한 일이므로 웬만하면 페이지 캐시에 의존하는 도구의 사용이나 개발을 지양하는 것이 좋습니다.

      이렇게 제어그룹의 memory 컨트롤러를 살펴보기 전 알고 있어야 하는 내용을 알아 봤습니다. 이를 통해 2, 3번의 질문과 9, 10번의 고민에 대해 얘기할 수 있게 되었습니다.


      MEMORY 컨트롤러

      메모리는 CPU와 더불어 가장 중요한 자원 중 하나입니다. 그런만큼 다양한 파일들이 있는데요. 이번 글에선 개인적으로 중요하다고 생각되는 부분들만 살펴보도록 하겠습니다.

      memory.events*

      루트 제어그룹이 아닌 자식 제어그룹에만 존재하며,

      읽기 전용으로 파일에 따라 현재 제어그룹의 이벤트(memory.events.local)만 보여줄지 아니면 자식 제어그룹들을 모두 포함한 이벤트(memory.events)를 보여줄지가 결정됩니다.

      간단히 살펴볼까요?

      [root@ip-172-31-12-125 init.scope]# cat memory.eventslow 0high 0max 0oom 0oom_kill 0oom_group_kill 0[root@ip-172-31-12-125 init.scope]# cat memory.events.locallow 0high 0max 0oom 0oom_kill 0oom_group_kill 0

      여러가지 내용들이 있지만, (제가 생각하기에) 중요한건 1) oom_kill이 발생했는지 알수 있다, 2) oom_group_kill은 뭘까? 정도인 것 같습니다.

      다른 필드들은 직접 살펴보시기 바랍니다.

      memory.oom.group

      memory.oom.group은 자식 제어그룹에만 존재하며, 읽기/쓰기가 가능한 파일 입니다. 0 또는 1만 입력 가능하고, 기본 값은 0입니다.

      [root@ip-172-31-12-125 init.scope]# cat memory.oom.group 0[root@ip-172-31-12-125 init.scope]# echo 1 > memory.oom.group[root@ip-172-31-12-125 init.scope]# cat memory.oom.group 1

      위와 같이 값을 변경(1)하게 되면, OOM 발생 시, OOM Killer가 해당 제어그룹 에 속한 모든 프로세스들을 종료시킵니다.

      다만, 해당 제어그룹에 속하더라도 특정 프로세스의 oom_score_adj 값을 -1000으로 설정하면 이러한 설정과 관계없이 절대 종료되지 않습니다.

      (OOM 관련 내용은 뒤에서 자세히 살펴 보겠습니다.)

      memory.max

      자식 제어그룹에 존재하는 읽기/쓰기 가능 파일로, 최대 메모리 사용량을 제어하는 파일입니다.

      k8s에서는 spec.containers[].resources.limits.memory 값을 통해 이러한 설정을 지원하고 있습니다. 값을 한 번 살펴보면,

      [root@ip-172-31-12-125 init.scope]# cat memory.maxmax

      max라고 나옵니다. 이 파일의 기본 값은 max이며, max로 설정된 경우 메모리 제어그룹이 아닌 운영체제가 할당 가능한 메모리 만큼 자유롭게 사용이 가능합니다.

      그럼 이제 memory.max의 값을 1 MiB로 설정해주고, 특정 PID를 해당 제어그룹의 cgroup.procs에 추가한 후에 python3를 실행해보겠습니다. 어떤 결과가 나올까요?

      [root@ip-172-31-12-125 memory]# echo 1048576 > memory.max[root@ip-172-31-12-125 memory]# echo $$ > cgroup.procs[root@ip-172-31-12-125 memory]# python3Killed

      memory.max 값은 바이트 단위로 설정해줘야 하기 때문에 1024 * 1024의 값인 1048576(= 1 MiB)으로 설정 했습니다.

      이후 python3를 실행시키니 OOM Killer에 의해 프로세스가 종료된 것을 확인할 수 있습니다. 그리고 아까는 심심해 보였던 memory.events 파일도 아래와 같이 변경되게 됩니다.

      [root@ip-172-31-12-125 memory]# cat memory.eventslow 0high 0max 10873oom 1oom_kill 1oom_group_kill 0

      memory.current

      자식 제어그룹에 존재하는 읽기 가능 파일로, 현재 제어그룹과 하위 제어 그룹에서 현재 사용 중인 총 메모리 양을 의미합니다.

      [root@ip-172-31-12-125 memory]# cat memory.current204800 # 200 KiB 사용 중

      memory.stat

      자식 제어그룹에 존재하는 읽기 가능 파일로, 현재 사용 중인 메모리를 유형별로 얼마나 사용하고 있는지를 알려주는 파일입니다.

      [root@ip-172-31-12-125 memory]# cat memory.statanon 4431872file 4349952......pagetables 61440......shmem 0......inactive_anon 4427776active_anon 4096inactive_file 4349952active_file 0......

      위 결과는 개인적으로 앞으로 작성할 내용에 필요하다 생각하는 부분만 추린 내용입니다. 간단히 한 번 살펴보면,

      1. anon: 익명(anonymous)으로 사용 중인 메모리의 양입니다.

        여기서 **“익명"**이란, 앞서 살펴본 특정 파일의 내용을 메모리에 올리는 것과 같이 특정 이름이 있는게 아닌 malloc, mmap 등으로 할당받아 사용 중인 메모리를 의미합니다.

      2. file: 파일시스템의 데이터를 캐시하는데 사용된 메모리를 의미(페이지 캐시)하며, tmpfs 및 공유 메모리를 사용하는 경우도 포함됩니다.

      3. pgtables: 현재 사용 중인 페이지 테이블의 크기입니다.

      4. shmem: tmpfs, 공유 메모리 등의 사용량입니다.

      5. active/inactive_*: 각 유형(anon, file)의 메모리에서 활발히(active) 사용되는 메모리와 그렇지 않은(inactive) 메모리의 양을 나타냅니다.

        주의할 점은 active_A + inactive_A = A가 아닐 수도 있다는 겁니다. 예를 들면 아래와 같습니다.

      [root@ip-172-31-12-125 memory]# cat memory.statanon 159744file 10559488shmem 10485760inactive_anon 10645504

      tmpfs에 10 MiB 파일을 만들었을 때, file과 shmem에 기록되어 active_file 또는 inactive_file의 메모리가 올라갈 것으로 예상하지만, 실제로는 inactive_anon에 존재합니다.

      이러한 현상이 발생하는 이유는 각각을 계산하는데 사용되는 방식이 다르기 때문입니다.

      (간단히 생각해보면, inactive_file은 파일시스템에서 읽긴 했지만 오랫동안 사용되지 않은(LRU) 메모리이기에 언제든 회수가능하다는 관점인데,

      tmpfs를 해당 값에 포함시키면 안되겠죠? 결국 리눅스 내부 정책과 설계 시 관점의 차이라고 볼 수 있습니다.)

      이것으로 서론, 즉 쉬운(?) 부분은 끝났습니다. 이제까지 알아본 내용을 바탕으로 조금 더 깊이 들어가 보겠습니다.


      Linux RSS, cadvisor RSS/WSS

      앞에서 기본적인 내용들은 파악했으니, 그림에서 설명했던 RSS, WSS를 살펴 볼 차례입니다.


      이제는 조금 이해가 되는 것 같기도…?


      Linux가 바라보는 메모리

      운영체제 입장에서는 특정 프로세스가 사용하는 메모리가 회수 가능하냐 가능하지 않냐는 중요하지 않습니다.

      어쨋든 사용하고 있는 것으로 생각하는거죠. 그렇기에 해당 프로세스가 사용 중인 모든(anon, file, shmem) 종류의 메모리 사용량을 합하여 계산합니다.

      *참고* 정확히는, 사용(참조) 중인 페이지 수를 기준으로 사용 중인 메모리 사용량을 계산합니다. 이렇다 보니 메모리가 부족해지는 경우 모든 프로세스의 RSS를 합하면 실제 사용 가능한 물리 메모리보다 크게 나타나기도 합니다.

      참고 사례: https://stackoverflow.com/questions/62926652/the-java-zgc-garbage-collector-uses-a-lot-of-memory

      그렇기에 그림에서 표현한 것처럼 Linux의 RSS는 FREE를 제외한 모든 영역에 존재하는 메모리를 나타낸다고 볼 수 있습니다.

      간단히 특정 프로세스의 RSS는 아래와 같이 확인 가능합니다. (kB지만 KiB 단위입니다.)

      [root@ip-172-31-12-125 ec2-user]# ps -efo ppid,pid,comm,rss   PPID     PID COMMAND           RSS  56446   57230 sudo             7896...<SNIP>...[root@ip-172-31-12-125 ec2-user]# cat /proc/57230/status...<SNIP>...VmRSS:       7896 kBRssAnon:     1304 kBRssFile:     6592 kBRssShmem:       0 kB


      Cadvisor가 바라보는 메모리

      이제 cadvisor가 제공하는 메모리에 대해 살펴보겠습니다. 실제 데이터를 보기 전에 cadvisor가 어떤 관점으로 메모리를 바라보고 계산하는지를 알아야 합니다.

      간단히 RSS, WSS로 구분해서 보면,

      1. RSS: 언제든 회수 당할 수 있는 메모리(file)를 제외하고, 순수하게 어플리케이션에서 할당 받아 사용하는 메모리(anon)를 표현.

        즉, 운영체제와는 달리 “생존을 위한 메모리만을 보겠다!” 라고 해석할 수 있습니다. (다만, tmpfs, 공유메모리 등을 활용하는 경우 정상적인 사용량을 볼 수 없습니다.)

      2. WSS: RSS와는 달리 “사용하고 있는 메모리 중 효율적인 동작에 필요한 메모리를 보겠다!” 라고 볼 수 있습니다.

        조금 더 자세히 설명하면, anon과 file을 모두 포함하지만, 자주 사용하지 않는 메모리(inactive_file)을 제외하고 보겠다는 뜻입니다.

        이런 관점은, 1) 어플리케이션 동작의 일관성을 향상시키며, 2) 활발히 사용하는 캐시(active_file)를 포함하여 모니터링함으로써 같은 종류의 여러 어플리케이션 컨테이너에 대한 예측가능성을 높이게 됩니다.

      *참고* 사실 anon과 file 외 다른 항목(kernel)도 WSS 계산에 포함되지만, 우리가 제어할 수 없거나, 보편적으로 잘 건드리지 않는 부분은 제외하였습니다.

      지금까지 살펴 본 내용들을 그림으로 표현하면 대략 아래와 같습니다.


      지금까지 열심히 보셨다면, 이 그림이 정확하진 않다는걸 아실겁니다.

      자, 그럼 이제 실제 데이터를 살펴보겠습니다.

      [root@ip-172-31-12-125 memory]# cat memory.current10645504[root@ip-172-31-12-125 memory]# cat memory.statanon 86016...<SNIP>...inactive_file 4096[root@ip-172-31-12-125 memory]# curl -s http://localhost:8080/api/v1.3/containers/memory | jq ".stats[3].memory"{  "usage": 10645504,...<SNIP>...  "rss": 86016,...<SNIP>...  "working_set": 10641408,...<SNIP>...}
      1. usage = memory.current

      2. rss = memory.stat anon

      3. working_set(wss) = memory.current - inactive_file

      결론적으로, k8s 환경에서 다양한 노드에 있는 같은 종류의 컨테이너가 일관되고 예측가능하게 동작하는지 볼 때에는 WSS가 좋겠구나! 라고 생각할 수 있습니다.

      (1번 질문이 여기까지와서야 해결이 됐군요!)


      Out of Memory (OOM)

      이제는 우리 모두 OOM Killer를 똑바로 마주할 준비가 됐습니다.

      (솔직히 cgroup, cpu편도 작성할 때 되게 힘들다 생각했는데 memory가 리얼이었네요..) 시작해보시죠!


      OOM과 OOM Killer

      숨가쁘게 달려왔으니, 간단한 내용부터 살펴 보겠습니다. OOM과 OOM Killer가 뭘까요? 너무 간단하죠?

      1. OOM: 할당 받아 사용 가능한 메모리가 부족한 상황

      2. OOM Killer: 1번의 상황을 해결하기 위해 특정 기준에 따라 프로세스를 종료시키는 커널의 기능

      뭔지 아는 것도 중요하지만, 실제 동작 방식과 거기서 무엇을 알 수 있을지가 더 중요하겠죠? 더욱 깊이 들어가 보시죠.


      OOM 동작 과정 한 눈에 보기


      커널 소스코드의 표현을 그대로 사용한 것은 아니지만, 실제 동작방식을 이해하는데는 전혀 문제가 없을 플로우 차트를 그려 봤습니다.

      아마 “제거 불가능?” 부분까지는 많이들 알고 계실 것 같습니다. 하지만, memory.max에 따라 무언가 바뀐다는 것은 처음 보시는 분들이 많을 것 같습니다.

      한 눈에 살펴봤으니 이제는 조금 더 깊게 들어가 보겠습니다.

      *참고* 위 플로우 차트의 특정 과정마다 memory.events*에 max, oom, high등이 기록됩니다.


      oom_score 계산식

      뜬금 없이 oom_score 계산식? 이라고 생각하시는 분들이 계실겁니다.

      하지만, 실제 계산되는 방식을 보고 나면 “그래서 그 때 어플리케이션이 죽었던거구나...” 또는 “우리도 당할 뻔 했구나…” 라고 생각하게 될겁니다.

      oom_score는 아래와 같이 계산됩니다.

      badness = RSS_PAGE_COUNT + SWAP_PAGE_COUNT + PAGE_TABLE_BYTES/PAGE_SIZE(4096)adj = adj * TOTAL_MEMORY_PAGE_COUNT / 1000badness = badness + adj# 최종적으로 oom_score의 범위는 [0, 2000]oom_score = (1000 + badness * 1000 / TOTAL_MEMORY_PAGE_COUNT) * 2 / 3

      이 글은 k8s 환경을 기준으로 살펴보고 있기 때문에 SWAP 관련 내용은 무시할 수가 있습니다. 그럼 RSS는 어떤걸 말하는걸까요?

      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);}

      시리즈를 진행하면서 웬만하면 커널 코드를 가져오지 않으려 했지만, RSS를 계산하는 코드는 너무나 간단하기에 넣어 봤습니다.

      그저 anon, file, shmem 메모리들에 대한 페이지의 갯수를 반환하는 것이 끝입니다. RSS 계산식을 봤을 때 아래와 같은 생각이 들었다면 잘 따라오고 계신겁니다.

      ‘file은 크게 의미가 없지 않나?’

      거의 대부분의 경우에는 맞는 얘기입니다. 하지만 file도 회수가 불가능한 경우가 있기 때문에 포함되어 있다고 보시면 됩니다.

      지금까지 살펴 본 것을 기반으로 생각해보면 anon이 계산에 들어가는 것은 당연한 일이라고 생각되실 겁니다. 그럼 shmem은 어떨까요? 예시를 만들어서 살펴보겠습니다.

      [root@ip-172-31-12-125 memory]# echo 104857600 > memory.max[root@ip-172-31-12-125 memory]# echo $$ > /sys/fs/cgroup/memory/cgroup.procs[root@ip-172-31-12-125 memory]# dd if=/dev/zero of=/tmp/rex bs=10M count=88+0 records in8+0 records out83886080 bytes (84 MB, 80 MiB) copied, 0.0445064 s, 1.9 GB/s[root@ip-172-31-12-125 memory]# cat memory.statanon 110592 # <- RSSfile 83886080shmem 83886080inactive_anon 83996672active_anon 0inactive_file 0active_file 0

      아래와 같은 상황을 생각해보죠.

      1. memory.max는 100 MiB

      2. tmpfs를 통해 80 MiB를 사용 중

      3. RSS를 기준으로 모니터링 중

      4. OOM 발생에 대한 모니터링 중

      이제 어플리케이션이 동작하면서 메모리를 할당받는다고 가정해보겠습니다.

      [root@ip-172-31-12-125 memory]# python3Python 3.9.16 (main, Apr 24 2024, 00:00:00) [GCC 11.4.1 20230605 (Red Hat 11.4.1-2)] on linuxType "help", "copyright", "credits" or "license" for more information.>>> a = []>>> a += [i for i in range(100000)] # 4.5 MiB>>> a += [i for i in range(100000)] # 4.5 MiB>>> a += [i for i in range(100000)] # 4.5 MiB>>> a += [i for i in range(100000)] # 4.5 MiBKilled

      뭔가 이상합니다. OOM 발생 알람이 와서 확인해봤는데 RSS 기준으론 이상이 없습니다. 커널이 발생시킨 메세지를 보니,

      [38660.448132] Memory cgroup out of memory: Killed process 72772 (python3) \total-vm:245280kB, anon-rss:19596kB, file-rss:1420kB, shmem-rss:0kB, \UID:0 pgtables:100kB oom_score_adj:0

      메세지 자체도 크게 문제가 없어보입니다. 특히 anon은 약 19 MiB 밖에 안썻음에도 OOM으로 죽는 상황이 발생한겁니다. 이후에 다양한 도구를 활용해서 분석해보고 소스코드에 버그가 있나? 싶어 살펴봐도 이상한 점을 찾을 수가 없습니다. 점점 미궁 속으로 빠져듭니다…

      물론 위와 같은 상황은 너무 극단적이라고 볼 수 있습니다. 그리고 일부러 쉽게 보여드리기 위해 그렇게 표현한 부분도 있구요.

      또한, 커널 메세지를 자세히 살펴보면 특정(shmem) 데이터가 튀는 것도 발견할 수 있을 겁니다.

      다만, 실제 상황에서는 이렇게 극단적이기 보단 미세하게 문제를 일으키는 경우가 많기 때문에 알아둘 필요는 있습니다.

      * oom_score를 계산해볼까요? *

      /proc/[PID]/oom_score_adj, /proc/[PID]/statm, /proc/meminfo, memory.stat 파일들을 참고해서 oom_score를 계산하는 툴을 만들어 보세요. 각 파일들에 대한 이해도를 올리고, OOM에 대해서도 더욱 친숙해질겁니다.


      메모리를 적게 사용해도 OOM Killer의 대상이 될 수 있다?

      맞습니다. 여러분이 예상하신 것 처럼 oom_score_adj를 활용해볼 겁니다. 먼저, 첫번째 어플리케이션에 상태입니다.

      1. 메모리 상대적으로 많이 사용

      2. oom_score_adj = -997

      3. memory.max = max

      ### 1번 어플리케이션 #### 1번 터미널[root@ip-172-31-12-125 memory]# python3>>> a = []>>> a += [i for i in range(10000000)] # 약 430 MiB>>> import os>>> os.getpid()75116# 2번 터미널[root@ip-172-31-12-125 memory]# echo -997 > /proc/75116/oom_score_adj

      다음으로, 두번째 어플리케이션 상태입니다.

      1. 메모리 상대적으로 적게 사용

      2. oom_score_adj = 0

      3. memory.max = 100 MiB

      ### 2번 어플리케이션 ###[root@ip-172-31-12-125 memory]# python3>>> # 아무것도 하지 않을겁니다.

      이때 첫번째 어플리케이션의 메모리 사용율을 계속 늘리면 어떻게 될까요?

      ### 1번 어플리케이션 ###>>> a += [i for i in range(1000000)] # 약 44 MiB...<반복>...### 2번 어플리케이션 ###[root@ip-172-31-12-125 memory]# python3>>> Killed # <- 사실 이 정도 상황이되면 Killed라는 메세지도 나오지 않습니다.# 다만 아래와 같이 dmesg를 통해 확인이 가능합니다.[40865.125535] Memory cgroup out of memory: Killed process 75373 (python3) \total-vm:245440kB, anon-rss:19340kB, file-rss:5168kB, shmem-rss:0kB, \UID:0 pgtables:104kB oom_score_adj:0

      위에서 설명드린 것 처럼 memory.max가 max로 설정된 제어그룹에 속한 프로세스가 OOM을 유발하는 경우,

      전체 프로세스 대상으로 oom_score를 계산하여 종료할 프로세스를 정하기 때문에 예상치 못한 상황이 발생할 수 있습니다.

      * 익숙한 숫자 -997? *

      k8s는 QoS가 Guaranteed인 경우 oom_score_adj를 -997로 설정합니다. 여기까지 봤을 때, 아직도 cpu.max는 불필요할까요? 고민해봅시다. (많이 고민해볼수록 정답이 없다는 결론에 가까워질 확률이 높습니다.)


      메모리를 많이 사용해도 OOM Killer의 대상이 아닐 수 있다?

      그렇겠죠? 결국 OOM Killer는 OOM을 유발한 프로세스를 기준으로 동작하기 때문에 아래와 같은 상황이면 더 적은 메모리를 사용하더라도 프로세스가 종료 될 수 있습니다.

      1. 두 프로세스 모두 oom_score_adj = 0

      2. memory.max가 max인 제어그룹에 속한 프로세스 메모리: 700 MiB

      3. memory.max가 max가 아닌 제어그룹에 속한 프로세스 메모리: 324 MiB

      4. 2번 프로세스가 추가 메모리 할당을 요청하는 경우 2번 제어그룹 내의 프로세스가 종료됨.


      k8s의 OOM Killer?

      간단히 얘기해서 k8s는 자체 OOM Killer를 보유하고 있지 않습니다.

      다만, 노드 압력(Node-pressure) 상태일 때 OOM Killer가 호출되지 않도록(= 예측가능성을 유지하기 위해) 최선의 노력을 할 뿐입니다.

      이것으로 memory에 대한 이야기가 끝이 났습니다.

      사실 OOM과 관련해서는 또 다른 고려대상이 있는데 그 부분은 컨테이너의 네트워크 이야기를 할 때에 설명드리려 합니다.

      다음 글은 cpuset에 대해 알아보려 합니다.

      읽어주셔서 감사합니다.


      참고

      1. https://docs.kernel.org/admin-guide/cgroup-v2.html

      2. https://github.com/google/cadvisor

      3. https://itnext.io/from-rss-to-wss-navigating-the-depths-of-kubernetes-memory-metrics-4d7d77d8fdcb

      4. https://github.com/prometheus/node_exporter

      5. https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#node-out-of-memory-behavior

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      천강민 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기