쿠버네티스가 쉬워지는 컨테이너 이야기 - cgroup, memory 리뷰

아티클 리뷰
이민석's avatar
Dec 05, 2024
쿠버네티스가 쉬워지는 컨테이너 이야기 - cgroup, memory 리뷰

본 게시글은 천강민님의 게시글[1]의 리뷰입니다.
여러번 정독하였음에도 기본 CS 지식의 부재로 해당 내용을 바로 이해하지 못하였고
부족한 CS 지식을 보완하면서 해당 내용을 조금이나마 이해할 수 있었습니다.

Overview

UNIX(Linux)는 제한 그룹*을 통해 프로세스가 사용할 리소스를 제한할 수 있습니다.
각 제한 그룹은 하나 이상의 하위 시스템과 연결되어 있으며, 하위 시스템은 제한 그룹이 가진 인터페이스 파일을 기반으로 각 리소스를 제한하는데 사용됩니다.

예를 들어,
memory.max는 제어그룹의 최대 Memory 사용량을 제한할 수 있습니다.
관련한 이벤트는 memory.events, memory.events.local, memory.current, memory.stats에서 확인할 수 있습니다.

전체적으로 디테일하고 딥한 부분들은 아래 각 챕터를 통해서 보완해야 합니다.
두세번 읽었는데도 100% 이해된다는 느낌이 없어서 여러 번 다시 읽으려 합니다.

  1. MEMORY Controller

  2. Linux RSS, Cadvisor RSS/WSS

  3. 커널 코드로 보는 totalreserve_pages 추측

Pre-requisites

쿠버네티스가 쉬워지는 컨테이너 이야기 - cgroup, cpu 리뷰 # cgroup 이란? [2]을 먼저 읽고 그 지식을 바탕으로 이 글을 읽어야 할 것 같습니다.

부끄럽지만 Linux, Linux kernel, Memory에 대한 지식이 얉아 사실 검증이 제대로 되지 않거나 불확실한 내용 혹은 잘못 이해한 내용이 있을 수 있는 글입니다. 반드시 가급적이면 인사이트를 얻게 해주신 원문 게시글[1]을 참조해주세요.

한 장으로 살펴보는 메모리

원문 게시글[1]에서는 메모리 관련 지표를 총 3가지 관점으로 분류하였습니다.
Linux 시스템의 메모리 체계와 Linux RSS, Cadvisor RSS,WSS 등을 이해하고자 합니다.

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

https://medium.com/@7424069/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4%EA%B0%80-%EC%89%AC%EC%9B%8C%EC%A7%80%EB%8A%94-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%9D%B4%EC%95%BC%EA%B8%B0-memory%ED%8E%B8-62cafabfd160
[그림 1] 한장으로 살펴보는 메모리

전체 글을 이해하기 위해서 RSS[d1], WSS[d2], free command를 한줄 소개합니다.

  • RSS[d1] : 프로세스가 물리적 메모리(RAM)에서 차지하고 있는 메모리 크기

  • WSS[d2] : 프로세스가 일정 기간 동안 실제로 사용한 메모리 크기

  • free command : 시스템 레벨의 프로세스 및 스왑 메모리 지표

Amazon Linux 2023에서는 USED = TOTAL - (FREE + BUFF/CACHE) 입니다.
하지만 일부 OS에서는 USED = TOTAL - AVAILABLE일 수 있어 주의가 필요합니다.

Linux가 바라보는 메모리

모든 실습은 기본적으로 Memory Swap을 비활성화하고 진행하고자 합니다.
하지만 일부 실습에서는 Memory Swap을 활성화할 수 있어, 각 지침을 따라주세요.

[ec2-user@... ~]$ swapon -a   # 활성화
[ec2-user@... ~]$ swapoff -a  # 비활성화

일반적으로 free command[3]를 사용하면 메모리 지표를 볼 수 있습니다.
직전에 메모리 스왑을 비활성화 하였으니 Swap 지표는 0으로 출력될 것입니다.

[ec2-user@... ~]# free -m₩
       total  used  free  shared  buff/cache  available
Mem:   3827   806   1120  153     1901        2570
Swap:  0      0       

[그림 1]을 참고하여 주요 지표에 대해서 설명하면 다음과 같습니다.

  • used : total - (free + buff/cache)

  • free : 실제 사용 가능 여부와 상관없이 비어 있는 메모리 (MemFree, SwapFree)

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

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

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

원문 게시글[1]과 [그림 1]을 참고하면 available을 신뢰할 값으로 말하고 있습니다.

서버의 메모리 가용량을 볼 때 보통은 available을 기준으로 보게됩니다.

별도 안건 - free > available 가능한가?

원문 게시글[1]에서는 available과 free의 차이를 다음과 같이 소개하고 있습니다.

이렇게 보면 available이 항상 free보다 클 것 같지만, 꼭 그렇지는 않습니다.
(시스템 예약 메모리 등은 free에는 포함되지만, available에는 포함되지 않음)
또한, 위와 같은 이유를 가지기 때문에 서버의 메모리 가용량을 볼 때 보통은 available을 기준으로 보게됩니다.

정확히 이해가 되지 않아서 free 공식 문서[3]를 살펴보았습니다.
free 공식 문서[3]에서는 free 지표는 MemFree, SwapFree 합임을 알 수 있습니다.

[ec2-user@... ~]$ cat /proc/meminfo | grep -E "(MemFree|SwapFree)"
MemFree:   1354800 kB
SwapFree:  0 kB

동 공식문서[3]에서는 available 지표가 Swap 없이 Page Cache를 고려해 새 어플리케이션을 시작하는데 사용되는 공간임을 알 수 있었습니다.

[ec2-user@... ~]$ cat /proc/meminfo | grep -E 'MemAvailable'
MemAvailable:    2766912 kB

부끄럽게도 원문 게시글[1]공식 문서[3]를 읽어도 명확하게 이해되지 않아서
참고 게시글[4]에서 실제 코드를 기반으로 시스템 예약 메모리를 이해하였습니다.

참고 게시글[4]에서는 다음과 같이 소개하고 있습니다.

free 메모리 라고 해서 모두 다 “유저가 할당 가능한 메모리” 인 것은 아니다…
available 메모리 계산할때 유저가 할당 불가능하다고 제외하는 영역은 시스템 예약 메모리(totalreserve_pages)가 있다.

free 공식 문서[3]에서는 free 지표는 MemFree, SwapFree 합임을 알 수 있습니다.

[ec2-user@... ~]$ cat /proc/meminfo | grep -E "(MemFree|SwapFree)"
MemFree:   1354800 kB
SwapFree:  0 kB

또한 동 공식문서[3]에서 available 지표는 Swap 없이 Page Cache를 고려하여 새 어플리케이션을 시작하는데 사용되는 공간임을 알 수 있었습니다.
(추가적으로 Recliamable Memory Slab도 실제 사용하고 있는 부분이 있을 경우, Cliam(회수)할 수 없는 상황이 올 수 있습니다.)

[ec2-user@... ~]$ cat /proc/meminfo | grep -E 'MemAvailable'
MemAvailable:    2766912 kB

따라서 SwapFree 값이 커지고 Page Cache 값이 작아지는 경우,
free 값이 available 값보다 커지는 역전 현상이 발생할 수 있습니다.

페이지 캐시(page cache)

페이지 캐시를 사용하면 파일의 입출력에 디스크가 아닌 메모리 활용할 수 있습니다.

페이지 캐시를 사용하면 파일의 입출력(I/O)에 디스크가 아닌 메모리를 쓸 수 있습니다.

스왑이 켜져있는 경우 페이지 캐시가 읽기/쓰기 모두 빠르다.
스왑이 꺼져있는 경우 페이지 캐시가 읽기/쓰기 모두 느리다.

swapon -a

  • 쓰기 작업

    [root@... ~]# dd if=/dev/zero of=/tmp/rex bs=100M count=1
    1+0 records in
    1+0 records out
    104857600 bytes (105 MB, 100 MiB) copied, 0.100176 s, 1.0 GB/s
    
    [root@... ~]# dd if=/dev/zero of=/home/ec2-user/rex bs=100M count=1
    1+0 records in
    1+0 records out
    104857600 bytes (105 MB, 100 MiB) copied, 0.304304 s, 345 MB/s
  • 읽기 작업

    [root@... /]# time cat /tmp/rex > /dev/null
    real    0m0.025s
    user    0m0.000s
    sys     0m0.019s
    
    [root@... /]# time cat /home/ec2-user/rex > /dev/null
    real    0m0.043s
    user    0m0.000s
    sys     0m0.022s

swapoff -a

  • 쓰기 작업

    [root@... ~]# dd if=/dev/zero of=/tmp/rex bs=100M count=1
    1+0 records in
    1+0 records out
    104857600 bytes (105 MB, 100 MiB) copied, 0.1431 s, 733 MB/s
    
    [root@... ~]# dd if=/dev/zero of=/home/ec2-user/rex bs=100M count=1
    1+0 records in
    1+0 records out
    104857600 bytes (105 MB, 100 MiB) copied, 0.100765 s, 1.0 GB/s
  • 읽기 작업

    [root@... /]# time cat /tmp/rex > /dev/null
    real    0m0.042s
    user    0m0.001s
    sys     0m0.023s
    
    [root@... /]# time cat /home/ec2-user/rex > /dev/null
    real    0m0.020s
    user    0m0.000s
    sys     0m0.017s

MEMORY Controller

MEMORY Controller의 주요 설정값인 memoery.events, … 등에 따라서
MEMORY Controller의 핵심 지표인 memory.stat 파일의 주요 지표들의 변경을 추적하고자 합니다.

Cgroups option

K8s option

정의

memory.max

spec.containers[].resources.limits.memory

MEM 사용 상한선

memory.current

memory.events

루트 제어그룹이 아닌 자식 제어 그룹에만 존재합니다.
읽기 전용으로 파일에 따라 보여줄 이벤트의 범위를 결정합니다.

  • memory.events.local : 현재 제어 그룹의 이벤트만 보여줄지

    [ec2-user@... B]$ cat memory.events
    
    low 0
    high 0
    max 0
    oom 0
    oom_kill 0
    oom_group_kill 0

  • memory.events : 자식 제어 글부을 포함한 모든 이벤트를 보여줄지

    [ec2-user@... B]$ cat memory.events.locals
    
    low 0
    high 0
    max 0
    oom 0
    oom_kill 0
    oom_group_kill 0

각 이벤트에 대한 자세한 옵션[13]은 다음과 같습니다.

  • low : 사용량이 낮은 경계(boundary) 구간임에도 cgroup이 메모리를 회수하는 횟수

  • high : 사용량이 높은 경계 구간을 넘어서서 cgroup이 메모리를 회수한 횟수

  • max : 사용량이 최대 경계 구간을 넘어서서 cgroup이 메모리를 회수한 횟수

  • oom : OOM

  • oom_kill : OOM Kill 발생한 횟수

  • oom_group_kill : 그룹 OOM Kill 발생한 횟수

memory.oom.group

루트 제어그룹이 아닌 자식 제어 그룹에만 존재합니다.
읽기/쓰기가 가능한 파일이며 0 또는 1만 입력가능하며 기본값은 0입니다.

sudo echo 1 | sudo tee memory.oom.group

sudo echo 0 | sudo tee memory.oom.group

설정값이 1이 되고 제어 그룹에서 OOM이 발생하고 OOM Kill이 발생하면
결과적으로 제어그룹에 속한 모든 프로세스를 종료시킵니다.
일부 oom_score_adj 값을 -1000으로 설정하면 종료되지 않습니다.

memory.max

아래와 같이 설정을 하고 python3을 실행하면 OOM Kill가 발생합니다.

sudo echo 1048576 | sudo tee memory.max

sudo echo $$ | sudo tee cgroup.prcos

python3

memory.events를 확인하면 oom, oom_kill 값이 올라간 것을 알 수 있습니다.

[ec2-user@... B]$ cat memory.events
low 0
high 0
max 495
oom 503
oom_kill 9
oom_group_kill 0

memory.current

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

[root@... B]# watch -n 1 'cat memory.current'
0 # KB...

memory.stat

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

[ec2-user@... B]$ cat memory.stat
......
pagetables 61440
......
shmem 0
......
inactive_anon 4427776
active_anon 4096
inactive_file 4349952
active_file 0
......

Linux RSS, Cadvisor RSS/WSS

[그림 1] 한장으로 살펴보는 메모리

Linux가 바라보는 RSS

Linux RSS는 FREE를 제외한 모든 영역에 존재하는 메모리를 나타낸다 볼 수 있다.
간단히 특정 루트 프로세스와 각 루트 프로세스를 추적할 수 있었습니다.

  • watch -n 1 “ps -efo ppid,pid,comm,rss”

    [ec2-user@... B] watch -n 1 "ps -efo ppid,pid,comm,rss"
       PPID     PID COMMAND           RSS
     581869  600294 bash             5196
     600294  600417  \_ python3      8992
     581869  596828 bash             5184
     596828  596901  \_ watch        3264
     581869  594381 bash             5068
     581869  593643 bash             5188
     581869  593470 bash             5328
     593470  600276  \_ watch        3604
     600276  601122      \_ watch    1744
     601122  601123          \_ ps   1420
  • cat /proc/593470/status | grep -E ‘(RSS|Rss)’

    [ec2-user@... B]$ cat /proc/600294/status | grep -E '(RSS|Rss)'
    VmRSS:      5196 kB
    RssAnon:            1636 kB
    RssFile:            3560 kB
    RssShmem:              0 kB

Cadvisor가 바라보는 RSS, WSS

원믄 게시글[1]은 RSS, WSS를 다음과 같이 소개하고 있습니다.

  1. RSS: 언제든 회수 당할 수 있는 메모리(file)를 제외하고, 순수하게 어플리케이션에서 할당 받아 사용하는 메모리(anon)를 표현. 즉, 운영체제와는 달리 “생존을 위한 메모리만을 보겠다!” 라고 해석할 수 있습니다. (다만, tmpfs, 공유메모리 등을 활용하는 경우 정상적인 사용량을 볼 수 없습니다.)

  2. WSS: RSS와는 달리 “사용하고 있는 메모리 중 효율적인 동작에 필요한 메모리를 보겠다!” 라고 볼 수 있습니다. 조금 더 자세히 설명하면, anon과 file을 모두 포함하지만, 자주 사용하지 않는 메모리(inactive_file)을 제외하고 보겠다는 뜻입니다. 이런 관점은, 1) 어플리케이션 동작의 일관성을 향상시키며, 2) 활발히 사용하는 캐시(active_file)를 포함하여 모니터링함으로써 같은 종류의 여러 어플리케이션 컨테이너에 대한 예측가능성을 높이게 됩니다.

커널 코드로 보는 totalreserve_pages 추측

시스템 예약 메모리는 정확히 어떤 값이며 어떤 방식으로 계산될까요?

참고 게시글[4]에 따르면 Available 지표는 아래 연산식으로 계산이 됩니다.

Available = Free 메모리 공간 - ( lowmem_reserve + watermark(high) )

각 계산식은 각각 lowmem_reserve(이하 시스템 예약 메모리값)watermark(high)(이하 높은 워터마크값)에 의해서 결정이 됩니다. 여기서는 시스템 예약 메모리에 대해서 자세히 보겠습니다.

시스템 예약 메모리는 calculate_totalreserve_pages(void)[5] 함수로 결정됩니다.
아래 코드는 시스템의 모든 활성 메모리 노드(pgdat)을 순회하며 각 존(zone)에서 관리되는 모든 페이를 가져와서 A + B를 더해서 totalreserve_pages로 정의합니다.

  • 현재 존과 상위 존에서 설정된 최대 사전 예약 메모리 값을 탐색 → A

  • 높은 워터마크 값을 예약 페이지로 간주 → B

lowmem_reserve 값은 lowmem_reserve_ratio 와 zone.protection으로 결정됩니다. 이 중 lowmem_reserve_ratio를 참조 게시글[4]에서 소개하고 있습니다.

lowmem_reserve_ratio 값이 256이라면, 1/256 으로 0.39%를 lowmem_reserve 용도로 사용할 수 있습니다.

[ec2-user@... ~] cat /proc/sys/vm/lowmem_reserve_ratio
256     256     32      0       0

해당 소개글을 읽다보면 3가지 궁금증이 생깁니다.

  1. 왜 숫자가 5개가 나오는 것일까?

  2. 0, 32, 256은 어떤 의미일까?

  3. 최종적으로 어떤 lowmem_reserve를 가지게될까?

첫 번째 답변 — 왜 lowmem_reserve_ratio가 5개 나올까?

첫 번째 답변은 sysctl_lowmem_reserve_ratio 선언식[7]에서 찾았습니다.
이는 순서대로 DMZ, DMZ32, Normal, HighMem, Moval Zone을 위한 값입니다.

static int sysctl_lowmem_reserve_ratio[MAX_NR_ZONES] = {
	[ZONE_DMA] = 256,
	[ZONE_DMA32] = 256,
	[ZONE_NORMAL] = 32,
	[ZONE_HIGHMEM] = 0,
	[ZONE_MOVABLE] = 0,
};

실제로 시스템에서 아래 명령어를 통해서 영역(zone) 목록을 조회할 수 있습니다.

[ec2-user@ip-172-31-10-203 ~]$ cat /proc/zoneinfo | grep "Node 0, zone"
Node 0, zone      DMA
Node 0, zone    DMA32
Node 0, zone   Normal
Node 0, zone  Movable
Node 0, zone   Device

이들의 짧은 설명을 Linux 커널 공식문서의 Physical Moemory[8]에서 찾았습니다.

  • ZONE_DMA, ZONE-DMA32 : 주소 지정이 가능한 모든 메모리에 엑세스할 수 없는 주변 장치에서 DMA에 적합한 메모리

  • ZONE_NORMAL : 커널이 항상 엑세스할 수 있는 일반 메모리로 항상 활성화되어 있으며, 주소 지정이 가능한 모든 메모리에 엑세스 가능한 DMA 장치가 있는 경우에는 ZONE_NORMAL의 페이지에서 ZONE_DMA 기능 수행이 가능합니다.

  • ZONE_HIGHMEM(optional) : 커널 페이지 테이블에서 영구 매핑이 적용되지 않는 물리적 메모리 부분

  • ZONE_MOVAL(optional) : ZONE_NORMAL과 동일하게 엑세스 가능한 일반 메모리지만, 지정된 물리적인 주소지를 변경할 수 있는 특성을 가짐

  • ZONE_DEVICE(optional) : PMEM 및 GPU같은 장치에 상주하는 메모리

두 번째 답변 — 0, 32, 256은 어떤 의미일까?

두 번째 답변은 Linux 커널 공식문서의 lowmem_reserve_ratio[9]에서 찾았습니다.
순서 대로 0, 1, N(32/256) 등의 기본 설정값을 가짐을 알 수 있었습니다.

  • 0 : 비활성화

  • N(≠ 0) : 활성화

    • 256 : 영역(zone[i])가 DMA, DMA32인 경우의 기본값

    • 32 : 영역(zone[i])가 기타인 경우의 기본값

    • 1 : 영역(zone[i]이 가질 수 있는 최댓값

lowmem_reserve_ratio는 시스템이 필요한 lowmem을 미리 예약(reserve)할지를 결정하기 위해서 사용되는 값입니다. 여기서 lowmem[10]이라는 개념이 나옵니다.

  • lowmem : logical adress가 존재하는 메모리 공간 (시스템 커널용)

  • highmem : logical address가 존재하지 않는 메모리 공간 (어플리케이션용)

고용량 메모리 시스템에서는 lowmem에 할당된 메모리가 mlock() 시스템 호출이나 swap 공간이 부족한 상황에서 고정(pinned)될 수 있습니다. 이렇게 고정된 lowmem이 많아지면 결과적으로 커널 및 프로세스가 사용할 메모리가 부족해질 수 있습니다.

따라서 메모리를 할당하는 Linux page allocator[10][11]는 최대한 lowmem 예약이 발생하지 않도록 하는 방어 전략을 가지고 있습니다. 따라서 영역(zone)과 저메모리 예약 비율은(lowmem_reserve_ratio) 다음과 같이 소개[9]되고 있습니다.

But, these values are not used directly. The kernel calculates # of protection pages for each zones from them. These are shown as array of protection pages in /proc/zoneinfo like the following. (This is an example of x86-64 box). Each zone has an array of protection pages like this:

As above expression, they are reciprocal number of ratio. 256 means 1/256. # of protection pages becomes about “0.39%” of total managed pages of higher zones on the node. [9]

즉 lowmem_reserve_ratio는 각 영역(zone)의 pages free, pages high, protections 값과의 비교 연산을 통해서 저메모리 예약(lowmem_reserve)를 결정합니다.

세 번째 답변 — 최종적으로 어떤 lowmem_reserve을 가질까?

lowmem_reserve 크기는 setup_per_zone_lowmem_reserve, Linux Kernel[13를 통해서 정의됩니다.

다음 쿼리를 통해서 존재하는 노드와 노드 별 페이지의 수를 알 수 있습니다.

[ec2-user@ip-172-31-10-203 vm]$ cat /proc/zoneinfo | grep -E "(Node 0, zone|managed)"
Node 0, zone      DMA
        managed  3840
Node 0, zone    DMA32
        managed  765026
Node 0, zone   Normal
        managed  211037
Node 0, zone  Movable
        managed  0
Node 0, zone   Device
        managed  0

또한 각 노드별 페이지의 숫자를 다음과 같이 확인했습니다.

[ec2-user@... ~] cat /proc/sys/vm/lowmem_reserve_ratio
256     256     32      0       0

시스템 예약 메모리의 페이지 숫자는 16,896 pages 입니다.

  • DMA : 14,424 pages

    • (765,026) / 256 = 2988.38 → 2988

    • (765,026 + 211,037) / 256 = 3812.74 → 3812

    • (765,026 + 211,037 + 0) / 256 = 3812.74 → 3812

    • (765,026 + 211,037 + 0 + 0) / 256 = 3812.74 → 3812

  • DMA32 : 2,472 pages

    • 211,037 / 256 = 824.36 → 824

    • 211,037 + 0/ 256 = 824.36 → 824

    • 211,037 + 0 + 0/ 256 = 824.36 → 824

  • ZONE_NORMAL, ZONE_MOVABLE, ZONE_DEVICE : 0 pages

Amazon Linux 2023에서는 페이지당 4 KB의 크기를 가집니다.

[ec2-user@... ~]$ getconf PAGE_SIZE
4096

즉, 시스템 예약 메모리의 페이지 용량은 66 MB으로 보입니다.

  • 16,896 page * 4 KB / page = 67,584 KB

  • 67,584 KB * 1024 MB / KB = 66 MB

Ref.

Tech. Ref.

Description. Ref.

  • [d1] RSS - Resident Set Size

  • [d2] WSS - Working Set Size

  • [d3] DMA - Direct Memory Access

Share article

Unchaptered