OSTEP 21장 Beyond Physical Memory: Mechanisms는 지금까지 미뤄 둔 가정 하나를 깨면서 시작합니다. 18장부터 20장까지는 모든 프로세스의 주소 공간이 물리 메모리 안에 다 들어간다고 가정했습니다. 21장은 메모리에 다 들어가지 않는 주소 공간을 운영체제가 디스크의 swap space 로 어떻게 지원하는지 설명합니다.

작은 EC2 인스턴스에 Spring Boot 애플리케이션과 데이터베이스를 함께 올리면 메모리가 금방 부족해집니다. Linux 서버 운영 자료를 찾아보면 “메모리가 부족하면 swap 을 추가해 보세요”라는 조언을 자주 만납니다. 실제로 swap 을 켜면 메모리 부족으로 종료되던 프로세스가 계속 실행되기도 합니다. 그래서 21장을 읽으며 다음 질문이 생겼습니다.

swap 을 추가하면 OOM(Out Of Memory)을 막을 수 있을까요? 막을 수 있다면 왜 어떤 서버에서는 swap 을 끌까요? Docker 와 Kubernetes 에서는 swap 이 어떻게 동작할까요?

이 글에서는 21장의 swap 메커니즘을 먼저 정리하고 22장의 thrashing 으로 swap 이 성능을 떨어뜨리는 이유를 설명한 뒤 Linux, Docker, Kubernetes 가 swap 을 어떻게 다루는지 순서대로 연결합니다. OSTEP 과 Linux 커널, Docker, Kubernetes 공식 문서를 읽고 정리한 내용이며 직접 실행한 실험이나 측정 결과는 포함하지 않았습니다. cgroup 은 v2 를 기준으로, Kubernetes 는 1.37 문서를 기준으로 설명합니다.

1. 프로세스는 어떻게 RAM 보다 큰 메모리를 쓸까요?

운영체제는 프로세스마다 독립된 가상 주소 공간을 줍니다. 프로세스는 자기 메모리가 연속해 있다고 여기지만 실제 데이터는 페이지 단위로 나뉘어 물리 메모리의 여러 프레임에 흩어져 있습니다. 가상 페이지 번호(VPN)를 물리 프레임 번호(PFN)로 바꾸는 일은 페이지 테이블이 맡습니다.

연속된 가상 페이지가 물리 메모리에서는 떨어진 프레임에 놓입니다. 프레임 번호는 설명을 위해 정한 값입니다.

이 그림에서는 모든 가상 페이지가 어떤 물리 프레임에 매핑되어 있습니다. 여러 프로세스가 쓰는 메모리의 합이 물리 메모리보다 커지면 모든 페이지에 프레임을 줄 수 없습니다.

21장에서 운영체제는 디스크에 swap space 를 마련해 이 문제를 해결합니다. 당장 많이 쓰지 않는 페이지를 swap space 로 내보내고(swap out) 다시 필요해지면 메모리로 읽어 옵니다(swap in). 읽고 쓰는 단위는 페이지이고 운영체제는 내보낸 페이지가 디스크의 어느 주소에 있는지 기억해야 합니다. OSTEP 은 swap space 의 크기가 한 시스템이 동시에 쓸 수 있는 메모리 페이지 수의 상한을 정한다고 설명합니다.

swap 을 켠다고 RAM 이 늘어나지는 않습니다. RAM 2GB, swap 2GB 인 서버를 예로 들면 물리 메모리는 여전히 2GB 입니다. 메모리에서 내보낸 페이지를 보관할 디스크 공간 2GB 만 새로 생겼습니다. 단순하게 계산하면 페이지를 둘 수 있는 곳이 4GB 로 늘어나지만 그중 RAM 에 들어가지 못한 페이지에 접근할 때마다 디스크를 읽어야 합니다. 커널이 쓰는 메모리와 할당 정책은 이 계산에서 뺐습니다.

swap space 만 페이지를 보관하는 디스크 위치는 아닙니다. ls 같은 프로그램의 코드 페이지는 처음부터 디스크의 실행 파일에 있습니다. 운영체제는 메모리가 필요하면 이 코드 페이지가 쓰던 프레임을 다른 용도로 쓰고 나중에 실행 파일에서 다시 읽어 올 수 있습니다. 이 구분은 5절에서 Linux 가 회수할 페이지를 고르는 기준으로 다시 나옵니다.

2. 페이지가 디스크에 있는지는 어떻게 알까요?

페이지 테이블 엔트리(PTE)에는 present bit 가 있습니다. 1이면 페이지가 물리 메모리에 있고 0이면 디스크 어딘가에 있습니다.

valid bitpresent bit의미
0상관없음프로세스의 주소 공간에 속하지 않는 페이지
11물리 메모리에 있는 페이지
10주소 공간에 속하지만 지금은 디스크에 있는 페이지

valid bit 는 그 가상 페이지가 프로세스가 써도 되는 주소인지를 나타내고 present bit 는 그 페이지가 지금 물리 메모리에 있는지를 나타냅니다. 그래서 유효한 페이지도 present bit 가 0일 수 있습니다.

TLB miss 가 나면 하드웨어는 페이지 테이블에서 PTE 를 읽습니다. valid bit 가 0이면 잘못된 접근이므로 운영체제가 프로세스를 종료할 가능성이 큽니다. valid bit 가 1이고 present bit 가 0이면 하드웨어는 page fault 를 일으키고 운영체제의 page fault handler 가 실행됩니다. handler 가 하는 일은 OSTEP 21장의 다음 의사 코드와 같습니다.

PFN = FindFreePhysicalPage()
if (PFN == -1)                      // no free page found
    PFN = EvictPage()               // replacement algorithm
DiskRead(PTE.DiskAddr, PFN) // sleep (wait for I/O)
PTE.present = True                  // update page table:
PTE.PFN        = PFN                // (present/translation)
RetryInstruction()                  // retry instruction

handler 는 빈 프레임을 찾고 없으면 다른 페이지를 내보내 프레임을 만듭니다. 그다음 PTE 에 적힌 디스크 주소에서 페이지를 읽고 present bit 와 PFN 을 고친 뒤 멈췄던 명령어를 다시 실행합니다. OSTEP 의 handler 는 PTE 만 고치고 TLB 는 고치지 않아서 다시 실행한 명령어는 TLB miss 를 한 번 더 거친 뒤 TLB hit 로 데이터를 읽습니다.

page fault 라는 이름 때문에 잘못된 접근처럼 들리지만 이 경우는 프로세스가 정상적인 주소에 접근한 것입니다. OSTEP 도 이 상황은 page miss 라고 부르는 쪽이 맞다고 덧붙입니다. OSTEP 의 저자들은 하드웨어가 처리할 수 없는 일이 생겨 운영체제로 제어를 넘긴다는 점이 잘못된 접근과 같아서 fault 라는 이름이 붙었을 것으로 추정합니다.

디스크를 읽는 동안 그 프로세스는 blocked 상태가 되고 운영체제는 그동안 준비된 다른 프로세스를 실행합니다. 준비된 다른 프로세스가 있으면 한 프로세스의 I/O 와 다른 프로세스의 실행이 겹쳐 CPU 가 쉬지 않습니다. 모든 프로세스가 디스크를 기다리는 4절의 thrashing 에서는 이 겹침도 일어나지 않습니다. 기다리는 프로세스의 입장에서는 메모리를 한 번 읽는 데 디스크 I/O 시간이 걸립니다.

3. 메모리가 가득 차면 어떤 일이 생길까요?

page fault handler 의 첫 줄에서 빈 프레임을 찾지 못하면 운영체제는 다른 페이지를 내보내야 합니다. 어떤 페이지를 내보낼지 정하는 규칙이 page replacement policy 입니다. OSTEP 은 잘못 고르면 프로그램이 메모리 속도 대신 디스크 속도로 실행되고 이는 10,000배에서 100,000배 느려지는 것이라고 경고합니다.

운영체제는 메모리가 완전히 찰 때까지 기다리지 않고 미리 페이지를 내보냅니다. 21장은 high watermark(HW)와 low watermark(LW)를 설명합니다. 사용 가능한 페이지가 LW 아래로 내려가면 백그라운드 스레드가 깨어나 HW 만큼 빈 페이지가 생길 때까지 페이지를 내보내고 다시 잠듭니다. 이 스레드를 swap daemon 이나 page daemon 이라고 부릅니다. 여러 페이지를 한꺼번에 내보내면 디스크에 묶어서 쓸 수 있어 쓰기 효율도 올라갑니다.

21장의 요약은 이 모든 과정이 프로세스에게 투명하게 일어난다는 점을 강조합니다. 프로세스는 자기만의 연속된 가상 메모리에 접근할 뿐이고 페이지가 디스크에 있는지는 알지 못합니다. 같은 요약에서 OSTEP 은 최악의 경우 명령어 하나를 실행하는 데 수 밀리초가 걸릴 수 있다고도 덧붙입니다. 프로세스가 알지 못하는 사이에 메모리 접근 비용이 디스크 I/O 비용으로 바뀝니다.

4. swap 이 있으면 왜 느려질 수 있을까요?

swap 공간을 넉넉히 두면 할당은 실패하지 않을 수 있습니다. 대신 swap out 된 페이지에 접근할 때마다 page fault 와 디스크 읽기가 따라옵니다. 오래 쓰지 않을 페이지가 swap 에 있다면 비용이 거의 없습니다. 애플리케이션이 반복해서 쓰는 페이지가 swap 에 있다면 그 접근마다 응답 시간이 늘어납니다.

가장 나쁜 경우는 22장 Beyond Physical Memory: Policies에서 다루는 thrashing 입니다. 실행 중인 프로세스들이 자주 쓰는 페이지의 합이 물리 메모리보다 크면, 운영체제는 페이지를 읽어 오자마자 곧 다시 필요할 다른 페이지를 내보내야 합니다. 시스템은 실제 작업보다 페이지를 디스크와 주고받는 데 시간을 더 씁니다.

프레임 하나가 모자라면 어떻게 될까요?

프로세스가 페이지 A, B, C, D 를 차례로 두 번 읽습니다. 처음에는 네 페이지가 모두 swap space 에 있습니다. 프레임 수를 고르고 실행해 보세요. 두 번째 바퀴에서는 몇 번이나 RAM 에서 바로 읽을까요?

RAM 에서 읽음 디스크 읽기를 기다림 page fault 0/8 RAM 에서 바로 읽음 0/8

RAM 을 프레임 3개, 4개로 줄인 모형입니다. 빈 프레임이 없으면 가장 오래전에 쓴 페이지를 내보내는 LRU 로 그렸고, 페이지 테이블 조회, TLB, 명령어 재실행은 생략했습니다. 내보낸 페이지는 swap space 로 옮겨지는 것으로 그렸지만, swap 에 같은 내용이 이미 있고 수정하지 않은 페이지는 다시 쓰지 않고 버릴 수 있습니다. 시간 막대의 길이는 축척을 맞추지 않았습니다. OSTEP 21장에 따르면 디스크 속도로 실행되는 프로그램은 메모리 속도보다 10,000배에서 100,000배 느려질 수 있습니다.

데모의 프레임 3개 실행을 표로 옮기면 다음과 같습니다. 운영체제는 가장 오래전에 쓴 페이지를 내보냅니다(LRU).

접근접근 후 메모리에 있는 페이지결과
AAmiss
BA, Bmiss
CA, B, Cmiss
DB, C, D (A 를 내보냄)miss
AC, D, A (B 를 내보냄)miss
BD, A, B (C 를 내보냄)miss
CA, B, C (D 를 내보냄)miss
DB, C, D (A 를 내보냄)miss

네 번째 접근부터는 매번 곧 다시 쓸 페이지를 내보내니 모든 접근이 miss 가 됩니다. 프레임이 4개이면 처음 네 번만 miss 이고 나머지는 모두 hit 입니다. swap 이 있으면 밀려난 페이지를 내보낼 곳이 있어 프로세스는 종료되지 않고 계속 실행됩니다. 프로세스는 살아 있지만 접근할 때마다 디스크를 기다리니 요청을 제때 처리하지 못합니다.

이런 상황에서 운영체제가 쓰는 방법도 22장에 나옵니다. admission control 은 일부 프로세스를 실행하지 않고 나머지 프로세스의 페이지가 메모리에 들어가게 합니다. Linux 처럼 메모리를 많이 쓰는 프로세스를 골라 종료하는 OOM killer 를 실행하는 방법도 있습니다. 둘 다 모든 프로세스를 느리게 실행하는 대신 일부를 멈추거나 종료합니다.

5. Linux 는 언제 swap 을 쓸까요?

Linux 도 메모리가 완전히 찬 뒤에야 swap 을 쓰지는 않습니다. 21장의 watermark 처럼, 빈 메모리가 기준 아래로 내려가면 백그라운드에서 페이지를 회수합니다. Linux 에서 이 일을 하는 커널 스레드가 kswapd 입니다.

회수할 페이지는 크게 두 종류입니다.

  • file-backed page: 파일 내용을 담은 페이지로, 1절의 실행 파일 코드 페이지와 page cache 가 여기에 속합니다. page cache 는 커널이 파일을 읽고 쓸 때 그 내용을 메모리에 남겨 둔 것입니다. 수정되지 않은 페이지는 파일에서 다시 읽으면 되므로 바로 버릴 수 있고 수정된 페이지는 파일에 먼저 써야 버릴 수 있습니다.
  • anonymous page: 힙과 스택처럼 대응하는 파일이 없는 페이지입니다. 내용을 보존한 채 메모리에서 내보내려면 swap space 에 써야 합니다. swap 이 없으면 anonymous page 는 회수할 수 없습니다.

swap 이 없는 서버에서도 메모리가 부족해지면 커널은 page cache 를 버려 메모리를 확보합니다. swap 이 있으면 anonymous page 도 내보낼 수 있으니 커널이 어느 쪽을 얼마나 회수할지 정해야 합니다. 이 비율을 조정하는 설정이 vm.swappiness 입니다.

cat /proc/sys/vm/swappiness
# 또는
sysctl vm.swappiness

Linux 커널 문서에 따르면 swappiness 는 swap 과 파일 시스템 페이징의 상대적인 I/O 비용을 0부터 200 사이의 값으로 나타내고, 기본값은 60입니다. 여기서 파일 시스템 페이징은 위의 file-backed page 를 버렸다가 다시 읽는 일입니다.

값커널이 가정하는 비용
100보다 작은 값swap I/O 가 파일 시스템 I/O 보다 비쌉니다. page cache 를 더 많이 회수합니다.
100두 I/O 의 비용이 같습니다. page cache 와 anonymous page 에 같은 압력을 줍니다.
100보다 큰 값swap I/O 가 더 쌉니다. zram, zswap 처럼 메모리 안에 swap 을 두거나 swap 장치가 더 빠를 때 고려합니다.
sudo sysctl vm.swappiness=10

swappiness=10 은 swap 을 금지하지 않습니다. 회수할 때 anonymous page 보다 file-backed page 를 더 많이 고르게 할 뿐입니다. 이 값을 “메모리 사용률이 90%를 넘으면 swap 을 쓴다”로 읽는 설명도 보이는데, 커널 문서의 정의는 I/O 비용의 비율이라 그런 threshold 는 없습니다. 0으로 설정해도 한 zone 의 빈 페이지와 file-backed page 의 합이 그 zone 의 high watermark 아래로 내려가면 커널은 swap 을 씁니다. 이 high watermark 는 3절에서 본 HW 에 해당하는 Linux 쪽 기준값입니다.

커널 문서는 메모리 압박 상황에서 파일 시스템 I/O 가 swap 의 random I/O 보다 효율적인 경향이 있다는 점도 언급합니다. 최적값은 저장장치와 워크로드에 따라 달라서 실험으로 정해야 합니다.

6. swap 이 있어도 왜 OOM 이 날까요?

Linux 는 메모리를 할당해야 하는데 회수로도 공간을 만들 수 없으면 OOM killer 로 프로세스를 골라 종료합니다. swap 은 회수할 수 있는 페이지를 늘려 줄 뿐이라 다음 경우에는 swap 이 있어도 OOM 이 납니다.

  • RAM 과 swap space 가 모두 찼을 때. 1절에서 본 것처럼 swap space 크기가 쓸 수 있는 페이지 수의 상한을 정하기 때문입니다.
  • 커널 메모리처럼 swap 으로 내보낼 수 없는 메모리가 대부분을 차지할 때.
  • 프로세스가 속한 cgroup 이 메모리 한도에 닿았는데, swap 한도까지 다 썼거나 swap 을 허용하지 않아 더 줄일 수 없을 때.

컨테이너 환경에서는 세 번째 경우를 호스트의 메모리 부족과 구분해야 합니다. cgroup v2 는 메모리 한도 memory.max 와 swap 한도 memory.swap.max 를 따로 둡니다. cgroup 의 메모리 사용량이 memory.max 에 도달하면 커널은 먼저 회수를 시도하고 swap 을 허용한 cgroup 이면 memory.swap.max 에 닿을 때까지 anonymous page 를 swap 으로 내보냅니다. 그래도 줄일 수 없으면 커널은 그 cgroup 안에서 OOM killer 를 실행합니다. 호스트 전체에 RAM 과 swap 이 남아 있어도 컨테이너는 자기 한도에 걸려 종료될 수 있습니다. 힙 밖의 메모리가 이 한도에 어떻게 포함되는지는 Java 힙은 여유로운데 컨테이너는 왜 종료될까?에서 정리했습니다.

OOM killer 는 회수가 더 이상 진행되지 않을 때 실행됩니다. 회수가 느리게나마 진행되는 동안에는 OOM 이 늦어지고, 그사이 4절의 thrashing 이 이어질 수 있습니다. swap 이 있으면 anonymous page 를, swap 이 없으면 page cache 와 코드 페이지를 내보냈다 다시 읽으며 같은 일이 생길 수 있습니다. Linux 커널의 메모리 관리와 cgroup v2 를 개발하는 Chris Down 은 In defence of swap에서 swap 이 OOM kill 을 늦출 수 있고 swap 이 없으면 그 디스크 I/O 가 자주 쓰는 page cache 를 버리는 쪽으로 옮겨 갈 뿐이라고 설명합니다. 그래서 프로세스가 종료되지 않았다는 사실만으로는 서비스가 정상이라고 말하기 어렵습니다.

7. Docker 는 컨테이너의 swap 을 어떻게 제한할까요?

Docker 는 cgroup 으로 컨테이너의 메모리와 swap 을 제한합니다. --memory 는 메모리 한도이고 --memory-swap 은 메모리와 swap 을 합친 한도입니다.

docker run \
  --memory=512m \
  --memory-swap=1g \
  my-app

이 설정에서 컨테이너는 메모리 512MB 와 swap 512MB 를 쓸 수 있습니다. --memory-swap 이라는 이름 때문에 swap 크기로 읽기 쉽지만 합계입니다. cgroup v2 가 두 한도를 따로 두는 것과도 다릅니다.

Docker 문서는 두 옵션의 조합별 동작을 다음과 같이 정합니다. 표의 swap 은 호스트에 swap 이 구성되어 있을 때 쓸 수 있는 양입니다.

설정컨테이너가 쓸 수 있는 swap
--memory=512m 만 지정--memory 와 같은 512MB. 합계 한도는 1GB 가 됩니다.
--memory=512m --memory-swap=1g1GB 에서 512MB 를 뺀 512MB
--memory=512m --memory-swap=512m0. swap 을 쓰지 않습니다.
--memory=512m --memory-swap=-1호스트의 swap 이 허락하는 만큼. 상한이 없습니다.

--memory 만 지정하면 swap 이 꺼진다고 생각하기 쉬운데, 호스트에 swap 이 있으면 컨테이너는 메모리 한도만큼 swap 을 더 씁니다. swap 을 쓰지 않게 하려면 두 값을 같게 지정해야 합니다. -1 은 swap 상한만 없앱니다. 메모리 한도 512MB 는 그대로입니다. 커널에서 swap 한도 기능이 꺼져 있으면 docker info 출력에 WARNING: No swap limit support 가 나올 수 있습니다. 이 경고가 보이면 --memory-swap 이 의도대로 적용되는지 따로 확인해야 합니다.

컨테이너마다 한도를 두는 이유는 한 호스트의 컨테이너들이 RAM 과 swap 장치를 함께 쓰기 때문입니다.

컨테이너별 한도는 RAM 과 swap 의 양을 나눕니다. 디스크는 나뉘지 않아서 한 컨테이너의 swap I/O 가 다른 컨테이너의 I/O 와 겹칩니다.

Container C 가 swap 을 많이 쓰며 디스크 I/O 를 일으키면 같은 디스크를 쓰는 PostgreSQL 의 I/O 도 느려질 수 있습니다. 컨테이너별 --memory 와 --memory-swap 은 한 컨테이너가 쓸 수 있는 swap 의 양을 제한합니다. 한도 안에서 일어나는 swap I/O 는 여전히 같은 디스크의 다른 컨테이너를 느리게 할 수 있으니, 호스트에 swap 을 켤 때는 swap 을 어느 디스크에 둘지도 함께 정해야 합니다.

8. Kubernetes 는 왜 swap 을 막아 왔을까요?

Kubernetes 는 Pod 의 memory request 를 기준으로 노드에 Pod 를 배치하고 memory limit 로 사용량을 제한합니다. swap 을 쓰는 Pod 는 limit 안에 있어도 응답 시간이 크게 달라질 수 있고 1.37 문서 기준으로 스케줄러는 Pod 를 배치할 때 swap 사용량을 계산에 넣지 않습니다. 그리고 Linux 노드에서 swap 이 켜져 있으면 kubelet 은 기본 설정으로 시작조차 하지 않습니다.

swap 을 켠 노드를 지원하는 기능은 여러 버전에 걸쳐 확장되었고 Kubernetes v1.34 에서 Linux 노드의 swap 지원이 GA 가 되었습니다. 노드에서 swap 을 쓰려면 kubelet 설정에서 failSwapOn 을 끄고 swapBehavior 를 정합니다.

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration

failSwapOn: false

memorySwap:
  swapBehavior: LimitedSwap
swapBehavior동작
NoSwap (기본값)Pod 는 swap 을 쓰지 않습니다. kubelet 같은 시스템 데몬은 swap 을 쓸 수 있습니다.
LimitedSwap조건을 만족하는 Pod 만 정해진 만큼 swap 을 씁니다.

LimitedSwap 을 켜도 모든 Pod 가 swap 을 쓰지는 않습니다. 아래 조건에 나오는 QoS class 는 request 와 limit 를 지정한 방식으로 정해집니다. 모든 컨테이너에 CPU 와 memory 의 request 와 limit 를 같게 지정하면 Guaranteed, 아무것도 지정하지 않으면 BestEffort, 그 사이는 Burstable 입니다. Kubernetes 문서에 따르면 조건은 다음과 같습니다.

  • Burstable QoS Pod 만 swap 을 씁니다. BestEffort 와 Guaranteed Pod 는 쓰지 않습니다.
  • 우선순위(PriorityClass)가 높은 Pod 는 메모리가 늘 RAM 에 있도록 swap 을 쓰지 않습니다.
  • Burstable Pod 안에서도 memory request 와 limit 를 같게 지정한 컨테이너는 swap 을 쓰지 않습니다.

컨테이너가 쓸 수 있는 swap 은 Pod 들이 쓸 수 있는 노드의 swap 총량(totalPodsSwapAvailable)에, 노드 전체 메모리 중 그 컨테이너의 memory request 가 차지하는 비율을 곱한 값입니다.

(containerMemoryRequest / nodeTotalMemory) × totalPodsSwapAvailable

kubelet 은 이 값을 cgroup v2 의 memory.swap.max 로 컨테이너 런타임에 전달합니다. 같은 문서는 swap 을 켠 노드에서 swap 을 암호화하고, 가능하면 swap 전용 디스크를 쓰고, 시스템 데몬의 성능이 떨어지면 그 cgroup 에 memory.swap.max=0 을 지정해 swap 을 막으라고 권합니다. 적용하기 전에 Kubernetes 버전, 노드의 cgroup 버전, 컨테이너 런타임의 지원 여부도 확인해야 합니다.

Kubernetes 가 swap 을 지원하게 되었다고 모든 워크로드에 swap 을 켜야 하는 것도 아닙니다. 아래 표는 앞의 내용을 바탕으로 제가 정리한 기준입니다.

swap 을 고려할 만한 워크로드swap 에 신중해야 하는 워크로드
메모리 사용량이 잠깐씩 튀는 워크로드응답 지연에 민감한 API 서버
지연에 덜 민감한 배치 작업메모리 전체를 자주 읽는 워크로드
오래 쓰지 않는 페이지가 많고 빠른 저장장치가 있는 환경swap I/O 가 같은 노드의 다른 Pod 에 영향을 줄 수 있는 환경

9. swap 이 문제를 일으키는지 어떻게 확인할까요?

swap 을 켠 서버에서는 swap 사용량과 swap I/O 를 나눠서 읽어야 합니다. 이미 swap out 된 페이지를 다시 읽지 않는다면 swap 사용량이 커도 성능에는 영향이 거의 없습니다.

free -h          # 전체 메모리, swap 사용량, available
swapon --show    # 활성화된 swap 영역과 크기
vmstat 1         # 1초마다 메모리, swap, I/O, CPU 통계

free -h 의 available 은 swap 없이 새 프로그램에 줄 수 있는 메모리를 추정한 값입니다. 이 값은 회수할 수 있는 page cache 도 포함합니다.

OSTEP 21장의 숙제도 vmstat 으로 swap 을 관찰하게 합니다. vmstat 에서 읽을 열은 다음과 같고 si 와 so 의 기본 단위는 KiB/s 입니다.

열의미
si초당 디스크에서 swap in 된 메모리 양
so초당 디스크로 swap out 된 메모리 양
free아무 데도 쓰지 않는 메모리. page cache 는 포함하지 않습니다.
waCPU 가 I/O 를 기다린 시간의 비율

free 열이 작다고 메모리가 부족한 것은 아닙니다. Linux 는 남는 메모리를 page cache 로 쓰기 때문입니다. si 와 so 가 계속 0보다 크고 wa 가 함께 오른다면 4절의 상황처럼 자주 쓰는 페이지를 swap 과 주고받고 있을 가능성이 큽니다.

작업이 메모리 때문에 실제로 얼마나 기다렸는지는 PSI(Pressure Stall Information)로 읽습니다.

cat /proc/pressure/memory
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0

커널 문서에 따르면 some 은 적어도 한 작업이 메모리를 기다리며 멈춘 시간의 비율이고, full 은 유휴 상태가 아닌 모든 작업이 동시에 멈춘 시간의 비율입니다. avg10, avg60, avg300 은 10초, 60초, 300초 평균입니다. cgroup v2 에서는 cgroup 디렉터리마다 같은 형식의 memory.pressure 파일이 있어서 컨테이너 단위로도 읽을 수 있습니다. 메모리 사용률이 높아도 some 이 0에 가깝다면 작업은 기다리지 않고 있습니다.

10. 처음 질문으로 돌아가면

swap 을 추가하면 OOM(Out Of Memory)을 막을 수 있을까요? 막을 수 있다면 왜 어떤 서버에서는 swap 을 끌까요? Docker 와 Kubernetes 에서는 swap 이 어떻게 동작할까요?

질문답
swap 은 어떻게 메모리 부족을 완화할까요?쓰지 않는 anonymous page 를 디스크로 내보내 프레임을 만듭니다. present bit 가 0인 페이지에 접근하면 page fault handler 가 디스크에서 다시 읽어 옵니다.
swap 이 있으면 OOM 이 나지 않을까요?RAM 과 swap 이 모두 차거나, cgroup 이 memory.max 에 닿았는데 swap 한도까지 써서 더 줄일 수 없으면 OOM 이 납니다. swap 은 OOM 을 늦추거나 막을 수 있지만 그동안 thrashing 이 이어질 수 있습니다.
왜 swap 을 끄는 서버가 있을까요?자주 쓰는 페이지가 swap 에 있으면 메모리 접근이 디스크 I/O 로 바뀌어 응답 시간이 크게 늘어납니다. 이런 서버는 느린 상태로 버티는 대신 일찍 종료되는 쪽을 택하기도 합니다. 다만 swap 이 없어도 page cache 를 두고 thrashing 이 날 수 있어서 swap 을 끈다고 느려지는 문제가 모두 사라지지는 않습니다.
Docker 에서는 어떻게 될까요?--memory-swap 은 메모리와 swap 의 합계입니다. --memory 만 지정하면 같은 양의 swap 을 쓰고, 두 값을 같게 지정해야 swap 을 막습니다.
Kubernetes 에서는 어떻게 될까요?v1.34 에서 GA 가 되었고 기본값은 NoSwap 입니다. LimitedSwap 에서도 Burstable Pod 의 컨테이너만, 그중 우선순위가 높거나 request 와 limit 가 같은 경우를 빼고 request 비율만큼 swap 을 씁니다.

OSTEP 21장의 요약처럼 프로세스는 자기 페이지가 RAM 에 있는지 디스크에 있는지 모릅니다. 운영체제가 페이지를 RAM 과 디스크 사이에서 옮기는 일을 맡기 때문에 프로세스는 물리 메모리보다 큰 주소 공간을 쓸 수 있습니다. 서버를 운영하는 쪽에서는 RAM 과 디스크의 접근 비용 차이가 응답 시간으로 드러납니다. 오래 쓰지 않는 페이지만 swap 에 있으면 비용이 거의 없이 RAM 을 다른 곳에 쓸 수 있습니다. 자주 쓰는 페이지까지 밀려나는 메모리 부족 상황에서는 swap 이 프로세스를 일찍 종료할지, 느린 상태로 계속 실행할지를 가릅니다. 어느 쪽이 나은지는 워크로드에 따라 다릅니다.

메모리 사용량이 계속 늘어나는 경우라면 swap 은 종료 시점을 미룰 뿐입니다. 이때는 메모리 누수, 캐시 크기, 동시 요청 수, JVM 힙 설정처럼 메모리를 쓰는 원인을 찾아야 합니다.

직접 실험해 확인하고 싶은 질문도 남았습니다. RAM 2GB 인 Ubuntu 서버에서 메모리를 점점 더 할당하는 프로그램을 실행해 다음을 비교해 보려고 합니다.

  • swap 을 끈 경우와 2GB 를 켠 경우에 OOM 이 나는 시점은 얼마나 달라질까요? 그 사이 si, so 와 PSI 의 some 은 어떻게 변할까요?
  • swap 을 켜서 프로세스가 살아남았을 때 응답 시간과 처리량은 얼마나 떨어질까요?
  • 호스트에 swap 이 있을 때 컨테이너의 --memory-swap 설정에 따라 cgroup OOM 시점과 swap 사용량은 어떻게 달라질까요?

학습 자료: Remzi H. Arpaci-Dusseau, Andrea C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces, Chapter 21 Beyond Physical Memory: Mechanisms, Chapter 22 Beyond Physical Memory: Policies. Linux Kernel Documentation, Documentation for /proc/sys/vm/, Control Group v2, PSI - Pressure Stall Information. Docker Docs, Resource constraints. Kubernetes Documentation, Swap memory management, Linux Node Swap Behaviors. Chris Down, In defence of swap: common misconceptions.