요구 페이징과 스래싱 — 메모리가 모자랄 때 벌어지는 일
한 줄 요약
페이지를 필요할 때만 올리면 물리 메모리보다 큰 프로그램도 돌릴 수 있지만, 폴트 한 번의 비용이 메모리 접근보다 수만 배 비싸므로 폴트율이 조금만 올라도 시스템이 무너진다.
왜 이게 필요했나
프로그램 전체를 메모리에 올릴 필요는 없다. 오류 처리 코드처럼 거의 실행되지 않는 부분도 많고, 큰 배열의 일부만 실제로 쓰이는 경우도 흔하다. 필요한 시점에만 올리면(요구 페이징) 더 많은 프로세스를 동시에 돌릴 수 있고, 프로그램 시작도 빨라진다.
유효 비트가 꺼진 페이지를 건드린 순간부터 그 명령어가 다시 실행되기까지를 세 토막으로 줄였다. 비싼 것은 가운데 토막이다.
- 트랩과 판정커널로 들어가 주소 범위 밖의 잘못된 접근인지, 아직 올리지 않은 페이지인지 가른다. 잘못된 접근이면 여기서 끝난다.
- 프레임 마련과 디스크 읽기빈 프레임이 없으면 희생 페이지를 고르고, 수정된 페이지라면 먼저 디스크에 쓴다. 그다음 필요한 페이지를 읽어 온다. 밀리초 단위의 비용이 여기서 나온다.
- 표 갱신과 재실행페이지 테이블을 고치고 폴트를 일으킨 명령어부터 다시 실행한다. 프로그램은 아무 일도 없었던 것처럼 이어 간다.
여기서 구분할 것 나노초 단위의 메모리 접근과 밀리초 단위의 폴트 처리는 수만 배 차이가 난다. 그래서 폴트율은 퍼센트가 아니라 몇십만 번에 한 번이라는 단위로 따진다.
잠깐, 예측해 보세요 메모리 접근 200ns, 폴트 처리 8ms 인 시스템에서 천 번에 한 번 폴트가 난다. 평균 접근 시간은 0.1% 쯤 느려질까?
설명 확인 · 채점 없는 자가 점검
아니다. 실효 접근 시간이 약 8,200ns 로 40배 느려진다. 폴트 한 번이 메모리 접근 4만 번의 값을 치르므로 드문 폴트가 평균을 지배한다.
어떻게 동작하나
페이지 테이블 항목에 유효 비트를 둔다. 유효하지 않은 페이지에 접근하면 페이지 폴트가 발생하고 다음 순서로 처리된다.
- 트랩이 걸려 커널로 진입한다.
- 잘못된 접근인지(주소 범위 밖) 아니면 아직 안 올린 것인지 판단한다.
- 빈 프레임을 찾는다. 없으면 희생 페이지를 골라 내보낸다. 그 페이지가 수정된 상태(dirty)면 먼저 디스크에 기록한다.
- 디스크에서 페이지를 읽어 프레임에 올린다.
- 페이지 테이블을 갱신하고, 폴트를 일으킨 명령어부터 다시 실행한다.
비용을 계산해 보면 무섭다. 메모리 접근이 200ns 이고 페이지 폴트 처리가 8ms 라고 하자. 폴트 확률이 p 일 때 실효 접근 시간은 (1 빼기 p) 곱하기 200 더하기 p 곱하기 8,000,000 이다.
- p = 0.001, 즉 천 번에 한 번 폴트가 나면 실효 접근 시간은 약 8,200ns 로, 폴트가 없을 때보다 40배 느리다.
- 성능 저하를 10퍼센트 이내로 묶으려면 p 가 약 0.0000025 이하여야 한다. 40만 번에 한 번이다.
페이지 교체 알고리즘의 성격도 알아 둘 만하다. 참조열 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1 에 프레임 3개를 주면 FIFO 는 폴트 15회, LRU 는 12회, 이론적 최적인 OPT 는 9회다. FIFO 에는 프레임을 늘렸는데 폴트가 늘어나는 벨래디의 이상현상까지 있다. 실무 시스템은 순수 LRU 대신 참조 비트를 이용한 클럭(second-chance) 알고리즘을 쓴다. LRU 에 가까운 성능을 훨씬 싼 비용으로 얻기 때문이다.
스래싱은 프레임이 부족해 폴트가 폭증하고, 그 때문에 CPU 가 놀고, 운영체제가 "한가하니 프로세스를 더 올리자"고 판단해 상황이 더 나빠지는 악순환이다. 대응은 워킹 셋(최근 일정 구간에서 실제로 참조된 페이지 집합)을 추적해 그만큼의 프레임을 보장하거나, 폴트율을 직접 모니터링해 프레임을 조절하는 것이다.
현장에서 만나는 모습
컨테이너에 메모리 제한을 걸었을 때 두 가지 결말이 있다. 스왑이 꺼져 있으면 OOM Killer 가 프로세스를 죽인다. 갑작스럽지만 원인은 명확하다. 스왑이 켜져 있으면 대신 스래싱이 시작된다. 프로세스는 살아 있는데 응답이 수십 배 느려지고, CPU 사용률은 낮은데 iowait 만 치솟는다. 죽는 편이 차라리 진단하기 쉽다는 말이 나오는 이유다. 쿠버네티스가 오랫동안 스왑을 끄도록 요구했던 배경에도 이 예측 불가능성이 있다.
컨테이너가 메모리 한도에 닿은 같은 상황이다. 스왑이 있느냐에 따라 증상이 전혀 다르게 나타난다.
- 스왑이 꺼져 있다되찾을 페이지가 바닥나면 OOM Killer 가 프로세스를 죽인다. 갑작스럽지만 종료 기록이 남아 원인이 곧바로 드러난다.
- 스왑이 켜져 있다죽지 않고 페이지를 디스크로 내보냈다 다시 읽기를 되풀이한다. 프로세스는 살아 있는데 응답이 수십 배 느려지고 iowait 만 치솟는다.
여기서 구분할 것 CPU 사용률이 낮다고 한가한 것이 아니다. 스래싱 중인 시스템은 디스크를 기다리느라 CPU 가 논다. 이것을 여유로 보고 프로세스를 더 올리면 악순환이 된다.
잠깐, 예측해 보세요 응답이 갑자기 느려졌는데 CPU 사용률은 낮고 iowait 가 높다. 워커 프로세스를 더 띄우면 나아질까?
설명 확인 · 채점 없는 자가 점검
더 나빠진다. 스래싱이라면 모자란 것은 CPU 가 아니라 프레임이다. 프로세스를 늘리면 필요한 워킹 셋이 커져 폴트가 더 늘어난다. 줄이거나 메모리를 보장해야 한다.
이어지는 퀴즈에서 확인할 것
페이지 폴트 비용 계산을 직접 할 수 있는지, 스래싱이 왜 스스로를 강화하는 악순환인지 설명할 수 있는지 확인한다.